feat(WEB): QMS 검사 모달 개선, 전자결재/생산대시보드/템플릿 기능 수정
- QMS: InspectionModal/InspectionModalV2 개선, mockData 정리 - 전자결재: DocumentCreate 기능 수정 - 생산대시보드: ProductionDashboard 개선 - 템플릿: IntegratedDetailTemplate/UniversalListPage 기능 수정 - 문서: i18n 가이드 업데이트, 문서뷰어 아키텍처 계획 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
import { ZoomIn, ZoomOut, Download, Printer, AlertCircle, Maximize2 } from 'lucide-react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Document, DocumentItem } from '../types';
|
||||
import { MOCK_ORDER_DATA, MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||||
import { MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||||
|
||||
// 기존 문서 컴포넌트 import
|
||||
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
|
||||
@@ -73,9 +73,9 @@ const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: D
|
||||
);
|
||||
};
|
||||
|
||||
// 수주서 문서 컴포넌트 (간소화 버전)
|
||||
// 수주서 문서 컴포넌트 (간소화 버전 - deprecated, V2 사용)
|
||||
const OrderDocument = () => {
|
||||
const data = MOCK_ORDER_DATA;
|
||||
const data = { lotNumber: '', orderDate: '', client: '', siteName: '', manager: '', managerContact: '', deliveryRequestDate: '', expectedShipDate: '', deliveryMethod: '', address: '', items: [] as { id: string; name: string; specification: string; unit: string; quantity: number; unitPrice?: number; amount?: number }[], subtotal: 0, discountRate: 0, totalAmount: 0, remarks: '' };
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm">
|
||||
|
||||
@@ -6,19 +6,28 @@ import { DocumentViewer } from '@/components/document-system';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { Document, DocumentItem } from '../types';
|
||||
import { MOCK_ORDER_DATA, MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||||
import { MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||||
|
||||
// 기존 문서 컴포넌트 import
|
||||
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
|
||||
import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip';
|
||||
|
||||
// 수주서 문서 컴포넌트 import
|
||||
import { SalesOrderDocument } from '@/components/orders/documents/SalesOrderDocument';
|
||||
import type { ProductInfo } from '@/components/orders/documents/OrderDocumentModal';
|
||||
import type { OrderItem } from '@/components/orders/actions';
|
||||
|
||||
// 품질검사 문서 컴포넌트 import
|
||||
import {
|
||||
ImportInspectionDocument,
|
||||
ProductInspectionDocument,
|
||||
JointbarInspectionDocument,
|
||||
QualityDocumentUploader,
|
||||
} from './documents';
|
||||
|
||||
// 제품검사 성적서 (신규 양식) import
|
||||
import { InspectionReportDocument } from '@/components/quality/InspectionManagement/documents/InspectionReportDocument';
|
||||
import { mockReportInspectionItems } from '@/components/quality/InspectionManagement/mockData';
|
||||
import type { InspectionReportDocument as InspectionReportDocumentType } from '@/components/quality/InspectionManagement/types';
|
||||
import type { ImportInspectionTemplate, ImportInspectionRef } from './documents/ImportInspectionDocument';
|
||||
|
||||
// 작업일지 + 중간검사 성적서 문서 컴포넌트 import (공정별 신규 버전)
|
||||
@@ -44,6 +53,8 @@ interface InspectionModalV2Props {
|
||||
itemName?: string;
|
||||
specification?: string;
|
||||
supplier?: string;
|
||||
// 읽기 전용 모드 (QMS 심사 확인용)
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
// 문서 타입별 정보
|
||||
@@ -85,132 +96,38 @@ const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: D
|
||||
);
|
||||
};
|
||||
|
||||
// 수주서 문서 컴포넌트 (간소화 버전)
|
||||
const OrderDocument = () => {
|
||||
const data = MOCK_ORDER_DATA;
|
||||
// QMS용 수주서 Mock 데이터
|
||||
const QMS_MOCK_PRODUCTS: ProductInfo[] = [
|
||||
{ productName: '방화 스크린 셔터 (표준형)', productCategory: '스크린', openWidth: '3000', openHeight: '2500', quantity: 5, floor: '1F', code: 'FSS-01' },
|
||||
{ productName: '방화 스크린 셔터 (방화형)', productCategory: '스크린', openWidth: '3000', openHeight: '2500', quantity: 3, floor: '2F', code: 'FSS-02' },
|
||||
];
|
||||
const QMS_MOCK_ORDER_ITEMS: OrderItem[] = [
|
||||
{ id: 'mt-1', itemCode: 'MT-001', itemName: '모터(380V 단상)', specification: '150K', type: '모터', quantity: 8, unit: 'EA', unitPrice: 120000, supplyAmount: 960000, taxAmount: 96000, totalAmount: 1056000, sortOrder: 1 },
|
||||
{ id: 'br-1', itemCode: 'BR-001', itemName: '브라켓트', specification: '380X180 [2-4"]', type: '브라켓', quantity: 16, unit: 'EA', unitPrice: 15000, supplyAmount: 240000, taxAmount: 24000, totalAmount: 264000, sortOrder: 2 },
|
||||
{ id: 'gr-1', itemCode: 'GR-001', itemName: '가이드레일 백면형 (120X70)', specification: 'EGI 1.5ST', type: '가이드레일', quantity: 16, unit: 'EA', unitPrice: 25000, supplyAmount: 400000, taxAmount: 40000, totalAmount: 440000, width: 120, height: 2500, sortOrder: 3 },
|
||||
{ id: 'cs-1', itemCode: 'CS-001', itemName: '케이스(셔터박스)', specification: 'EGI 1.5ST 380X180', type: '케이스', quantity: 8, unit: 'EA', unitPrice: 35000, supplyAmount: 280000, taxAmount: 28000, totalAmount: 308000, width: 380, height: 180, sortOrder: 4 },
|
||||
{ id: 'bf-1', itemCode: 'BF-001', itemName: '하단마감재', specification: 'EGI 1.5ST', type: '하단마감재', quantity: 8, unit: 'EA', unitPrice: 18000, supplyAmount: 144000, taxAmount: 14400, totalAmount: 158400, sortOrder: 5 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs">경동기업</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-[0.5rem]">수 주 서</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-gray-100" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">작성</td>
|
||||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">검토</td>
|
||||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 h-10"></td>
|
||||
<td className="border px-2 py-1 h-10"></td>
|
||||
<td className="border px-2 py-1 h-10"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center bg-gray-50">판매</td>
|
||||
<td className="border px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 기본 정보 */}
|
||||
<table className="w-full border-collapse mb-6 text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-3 py-2 bg-gray-100 w-24">LOT NO.</td>
|
||||
<td className="border px-3 py-2">{data.lotNumber}</td>
|
||||
<td className="border px-3 py-2 bg-gray-100 w-24">수주일</td>
|
||||
<td className="border px-3 py-2">{data.orderDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-3 py-2 bg-gray-100">발주처</td>
|
||||
<td className="border px-3 py-2">{data.client}</td>
|
||||
<td className="border px-3 py-2 bg-gray-100">현장명</td>
|
||||
<td className="border px-3 py-2">{data.siteName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-3 py-2 bg-gray-100">담당자</td>
|
||||
<td className="border px-3 py-2">{data.manager}</td>
|
||||
<td className="border px-3 py-2 bg-gray-100">연락처</td>
|
||||
<td className="border px-3 py-2">{data.managerContact}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-3 py-2 bg-gray-100">납기요청일</td>
|
||||
<td className="border px-3 py-2">{data.deliveryRequestDate}</td>
|
||||
<td className="border px-3 py-2 bg-gray-100">출고예정일</td>
|
||||
<td className="border px-3 py-2">{data.expectedShipDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-3 py-2 bg-gray-100">배송방법</td>
|
||||
<td className="border px-3 py-2">{data.deliveryMethod}</td>
|
||||
<td className="border px-3 py-2 bg-gray-100">배송지</td>
|
||||
<td className="border px-3 py-2">{data.address}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 품목 테이블 */}
|
||||
<table className="w-full border-collapse mb-6 text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border px-2 py-2 w-10">No</th>
|
||||
<th className="border px-2 py-2">품목명</th>
|
||||
<th className="border px-2 py-2 w-24">규격</th>
|
||||
<th className="border px-2 py-2 w-12">단위</th>
|
||||
<th className="border px-2 py-2 w-12">수량</th>
|
||||
<th className="border px-2 py-2 w-20">단가</th>
|
||||
<th className="border px-2 py-2 w-24">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((item, index) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border px-2 py-2 text-center">{index + 1}</td>
|
||||
<td className="border px-2 py-2">{item.name}</td>
|
||||
<td className="border px-2 py-2 text-center">{item.specification}</td>
|
||||
<td className="border px-2 py-2 text-center">{item.unit}</td>
|
||||
<td className="border px-2 py-2 text-center">{item.quantity}</td>
|
||||
<td className="border px-2 py-2 text-right">{item.unitPrice?.toLocaleString()}</td>
|
||||
<td className="border px-2 py-2 text-right">{item.amount?.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50">소계</td>
|
||||
<td colSpan={2} className="border px-2 py-2 text-right">{data.subtotal.toLocaleString()}원</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50">할인 ({data.discountRate}%)</td>
|
||||
<td colSpan={2} className="border px-2 py-2 text-right text-red-600">-{(data.subtotal * data.discountRate / 100).toLocaleString()}원</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-100 font-bold">총액</td>
|
||||
<td colSpan={2} className="border px-2 py-2 text-right font-bold text-blue-600">{data.totalAmount.toLocaleString()}원</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{/* 비고 */}
|
||||
{data.remarks && (
|
||||
<div className="border p-4">
|
||||
<h3 className="font-medium mb-2 text-xs">비고</h3>
|
||||
<p className="text-xs text-gray-600">{data.remarks}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
// QMS용 제품검사 성적서 Mock 데이터
|
||||
const QMS_MOCK_REPORT_DATA: InspectionReportDocumentType = {
|
||||
documentNumber: 'RPT-KD-SS-2024-530',
|
||||
createdDate: '2024-09-24',
|
||||
approvalLine: [
|
||||
{ role: '작성', name: '김검사', department: '품질관리부' },
|
||||
{ role: '승인', name: '박승인', department: '품질관리부' },
|
||||
],
|
||||
productName: '방화스크린',
|
||||
productLotNo: 'KD-SS-240924-19',
|
||||
productCode: 'WY-SC780',
|
||||
lotSize: '8',
|
||||
client: '삼성물산(주)',
|
||||
inspectionDate: '2024-09-26',
|
||||
siteName: '강남 아파트 단지',
|
||||
inspector: '김검사',
|
||||
inspectionItems: mockReportInspectionItems,
|
||||
specialNotes: '',
|
||||
finalJudgment: '합격',
|
||||
};
|
||||
|
||||
// QMS용 작업일지 Mock WorkOrder 생성
|
||||
@@ -289,6 +206,7 @@ export const InspectionModalV2 = ({
|
||||
itemName,
|
||||
specification,
|
||||
supplier,
|
||||
readOnly = false,
|
||||
}: InspectionModalV2Props) => {
|
||||
// 수입검사 템플릿 상태
|
||||
const [importTemplate, setImportTemplate] = useState<ImportInspectionTemplate | null>(null);
|
||||
@@ -339,6 +257,23 @@ export const InspectionModalV2 = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 수입검사 저장 핸들러 (hooks는 early return 전에 호출해야 함)
|
||||
const handleImportSave = useCallback(async () => {
|
||||
if (!importDocRef.current) return;
|
||||
|
||||
const data = importDocRef.current.getInspectionData();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// TODO: 실제 저장 API 연동
|
||||
console.log('[InspectionModalV2] 수입검사 저장 데이터:', data);
|
||||
toast.success('검사 데이터가 저장되었습니다.');
|
||||
} catch {
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!doc) return null;
|
||||
|
||||
const docInfo = DOCUMENT_INFO[doc.type] || { label: doc.title, hasTemplate: false, color: 'text-gray-600' };
|
||||
@@ -391,23 +326,6 @@ export const InspectionModalV2 = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 수입검사 저장 핸들러
|
||||
const handleImportSave = useCallback(async () => {
|
||||
if (!importDocRef.current) return;
|
||||
|
||||
const data = importDocRef.current.getInspectionData();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// TODO: 실제 저장 API 연동
|
||||
console.log('[InspectionModalV2] 수입검사 저장 데이터:', data);
|
||||
toast.success('검사 데이터가 저장되었습니다.');
|
||||
} catch {
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 수입검사 문서 렌더링 (Lazy Loading)
|
||||
const renderImportInspectionDocument = () => {
|
||||
if (isLoadingTemplate) {
|
||||
@@ -419,14 +337,35 @@ export const InspectionModalV2 = ({
|
||||
}
|
||||
|
||||
// 템플릿이 로드되면 전달, 아니면 기본 템플릿 사용
|
||||
return <ImportInspectionDocument ref={importDocRef} template={importTemplate || undefined} />;
|
||||
return <ImportInspectionDocument ref={importDocRef} template={importTemplate || undefined} readOnly={readOnly} />;
|
||||
};
|
||||
|
||||
// 문서 타입에 따른 컨텐츠 렌더링
|
||||
const renderDocumentContent = () => {
|
||||
switch (doc.type) {
|
||||
case 'order':
|
||||
return <OrderDocument />;
|
||||
return (
|
||||
<SalesOrderDocument
|
||||
orderNumber="KD-SS-240924-19"
|
||||
documentNumber="KD-SS-240924-19"
|
||||
certificationNumber="KD-SS-240924-19"
|
||||
orderDate="2024-09-24"
|
||||
client="삼성물산(주)"
|
||||
siteName="강남 아파트 단지"
|
||||
manager="김담당"
|
||||
managerContact="010-1234-5678"
|
||||
deliveryRequestDate="2024-10-05"
|
||||
expectedShipDate="2024-10-04"
|
||||
deliveryMethod="직접배차"
|
||||
address="서울시 강남구 테헤란로 123"
|
||||
recipientName="김인수"
|
||||
recipientContact="010-9876-5432"
|
||||
shutterCount={8}
|
||||
products={QMS_MOCK_PRODUCTS}
|
||||
items={QMS_MOCK_ORDER_ITEMS}
|
||||
remarks="납기일 엄수 요청"
|
||||
/>
|
||||
);
|
||||
case 'log':
|
||||
return renderWorkLogDocument();
|
||||
case 'confirmation':
|
||||
@@ -436,7 +375,7 @@ export const InspectionModalV2 = ({
|
||||
case 'import':
|
||||
return renderImportInspectionDocument();
|
||||
case 'product':
|
||||
return <ProductInspectionDocument />;
|
||||
return <InspectionReportDocument data={QMS_MOCK_REPORT_DATA} />;
|
||||
case 'report':
|
||||
return renderReportDocument();
|
||||
case 'quality':
|
||||
@@ -444,6 +383,7 @@ export const InspectionModalV2 = ({
|
||||
<QualityDocumentUploader
|
||||
onFileUpload={handleQualityFileUpload}
|
||||
onFileDelete={handleQualityFileDelete}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -456,8 +396,8 @@ export const InspectionModalV2 = ({
|
||||
console.log('[InspectionModalV2] 다운로드 요청:', doc.type);
|
||||
};
|
||||
|
||||
// 수입검사 저장 버튼 (toolbarExtra)
|
||||
const importToolbarExtra = doc.type === 'import' ? (
|
||||
// 수입검사 저장 버튼 (toolbarExtra) - readOnly일 때 숨김
|
||||
const importToolbarExtra = doc.type === 'import' && !readOnly ? (
|
||||
<Button onClick={handleImportSave} disabled={isSaving} size="sm">
|
||||
{isSaving ? (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
|
||||
@@ -2,59 +2,6 @@ import { InspectionReport, RouteItem, Document, ChecklistCategory, StandardDocum
|
||||
import type { WorkOrder } from '@/components/production/ProductionDashboard/types';
|
||||
import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types';
|
||||
|
||||
// 품질검사용 수주서 아이템 타입 (자체 정의)
|
||||
interface QualityOrderItem {
|
||||
id: string;
|
||||
name: string;
|
||||
specification: string;
|
||||
unit: string;
|
||||
quantity: number;
|
||||
unitPrice?: number;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
// 품질검사용 수주서 데이터 타입 (자체 정의)
|
||||
export interface QualityOrderData {
|
||||
lotNumber: string;
|
||||
orderDate: string;
|
||||
client: string;
|
||||
siteName: string;
|
||||
manager: string;
|
||||
managerContact: string;
|
||||
deliveryRequestDate: string;
|
||||
expectedShipDate: string;
|
||||
deliveryMethod: string;
|
||||
address: string;
|
||||
items: QualityOrderItem[];
|
||||
subtotal: number;
|
||||
discountRate: number;
|
||||
totalAmount: number;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
// 수주서 샘플 데이터
|
||||
export const MOCK_ORDER_DATA: QualityOrderData = {
|
||||
lotNumber: 'KD-SS-240924-19',
|
||||
orderDate: '2024-09-24',
|
||||
client: '삼성물산(주)',
|
||||
siteName: '강남 아파트 단지',
|
||||
manager: '김담당',
|
||||
managerContact: '010-1234-5678',
|
||||
deliveryRequestDate: '2024-10-05',
|
||||
expectedShipDate: '2024-10-04',
|
||||
deliveryMethod: '직접배차',
|
||||
address: '서울시 강남구 테헤란로 123',
|
||||
items: [
|
||||
{ id: '1', name: '스크린 셔터 (표준형)', specification: '3000×2500', unit: 'SET', quantity: 5, unitPrice: 1200000, amount: 6000000 },
|
||||
{ id: '2', name: '스크린 셔터 (방화형)', specification: '3000×2500', unit: 'SET', quantity: 3, unitPrice: 1500000, amount: 4500000 },
|
||||
{ id: '3', name: '슬랫 패널', specification: '1000×500', unit: 'EA', quantity: 20, unitPrice: 50000, amount: 1000000 },
|
||||
],
|
||||
subtotal: 11500000,
|
||||
discountRate: 5,
|
||||
totalAmount: 10925000,
|
||||
remarks: '납기일 엄수 요청',
|
||||
};
|
||||
|
||||
// 작업일지 샘플 데이터
|
||||
export const MOCK_WORK_ORDER: WorkOrder = {
|
||||
id: 'wo-1',
|
||||
|
||||
@@ -369,6 +369,7 @@ export default function QualityInspectionPage() {
|
||||
onClose={() => setModalOpen(false)}
|
||||
document={selectedDoc}
|
||||
documentItem={selectedDocItem}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { format } from 'date-fns';
|
||||
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -87,6 +88,7 @@ export function DocumentCreate() {
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { currentUser } = useAuth();
|
||||
const { canCreate, canDelete } = usePermission();
|
||||
|
||||
// 수정 모드 / 복제 모드 상태
|
||||
const documentId = searchParams.get('id');
|
||||
@@ -546,20 +548,22 @@ export function DocumentCreate() {
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
미리보기
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={isPending}
|
||||
disabled={isPending || !canCreate}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
@@ -583,7 +587,7 @@ export function DocumentCreate() {
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
|
||||
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode, canCreate, canDelete]);
|
||||
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { Factory, Clock, PlayCircle, CheckCircle2, AlertTriangle, Timer, Users } from 'lucide-react';
|
||||
import { StatCardGridSkeleton } from '@/components/ui/skeleton';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -27,6 +28,8 @@ import { STATUS_LABELS } from './types';
|
||||
|
||||
export default function ProductionDashboard() {
|
||||
const router = useRouter();
|
||||
const screenPerm = usePermission('/production/screen');
|
||||
const workOrderPerm = usePermission('/production/work-order-management');
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
const [selectedTab, setSelectedTab] = useState<string>('all');
|
||||
@@ -152,13 +155,17 @@ export default function ProductionDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
작업자 화면
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleWorkOrderListClick}>
|
||||
작업지시 목록
|
||||
</Button>
|
||||
{screenPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
작업자 화면
|
||||
</Button>
|
||||
)}
|
||||
{workOrderPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkOrderListClick}>
|
||||
작업지시 목록
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
@@ -53,6 +54,7 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const { canCreate: permCanCreate, canUpdate: permCanUpdate, canDelete: permCanDelete } = usePermission();
|
||||
|
||||
// ===== 상태 =====
|
||||
const [mode, setMode] = useState<DetailMode>(initialMode);
|
||||
@@ -104,14 +106,15 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
|
||||
}), [formData, config, initialData]);
|
||||
|
||||
// ===== 권한 계산 =====
|
||||
// config.permissions가 명시적으로 설정되면 우선, 아니면 usePermission() fallback
|
||||
const permissions = useMemo(() => {
|
||||
const p = config.permissions || {};
|
||||
return {
|
||||
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : p.canEdit ?? true,
|
||||
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : p.canDelete ?? true,
|
||||
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : p.canCreate ?? true,
|
||||
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : (p.canEdit !== undefined ? p.canEdit : permCanUpdate),
|
||||
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : (p.canDelete !== undefined ? p.canDelete : permCanDelete),
|
||||
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : (p.canCreate !== undefined ? p.canCreate : permCanCreate),
|
||||
};
|
||||
}, [config.permissions]);
|
||||
}, [config.permissions, permCanUpdate, permCanDelete, permCanCreate]);
|
||||
|
||||
// ===== 모드 헬퍼 =====
|
||||
const isViewMode = mode === 'view';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { toast } from 'sonner';
|
||||
import { Download, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -43,6 +44,7 @@ export function UniversalListPage<T>({
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const { canCreate: permCanCreate, canDelete: permCanDelete } = usePermission();
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
// 원본 데이터 (클라이언트 사이드 필터링용)
|
||||
@@ -825,7 +827,7 @@ export function UniversalListPage<T>({
|
||||
onToggle: () => toggleSelection(id),
|
||||
onRowClick: () => handleRowClick(item),
|
||||
onEdit: () => handleEdit(item),
|
||||
onDelete: () => handleDeleteClick(item),
|
||||
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
|
||||
});
|
||||
},
|
||||
[config, effectiveGetItemId, handleDeleteClick, handleEdit, handleRowClick, effectiveSelectedItems, toggleSelection]
|
||||
@@ -838,7 +840,7 @@ export function UniversalListPage<T>({
|
||||
onToggle,
|
||||
onRowClick: () => handleRowClick(item),
|
||||
onEdit: () => handleEdit(item),
|
||||
onDelete: () => handleDeleteClick(item),
|
||||
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
|
||||
});
|
||||
},
|
||||
[config, handleDeleteClick, handleEdit, handleRowClick]
|
||||
@@ -874,7 +876,7 @@ export function UniversalListPage<T>({
|
||||
}
|
||||
// 공통 헤더 옵션 (달력/등록버튼)
|
||||
dateRangeSelector={config.dateRangeSelector}
|
||||
createButton={config.createButton}
|
||||
createButton={permCanCreate ? config.createButton : undefined}
|
||||
// 탭 콘텐츠
|
||||
tabsContent={config.tabsContent}
|
||||
// 통계 카드
|
||||
|
||||
Reference in New Issue
Block a user