2025-12-23 21:13:07 +09:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 작업일지 모달
|
|
|
|
|
*
|
2026-01-22 15:07:17 +09:00
|
|
|
* document-system 통합 버전 (2026-01-22)
|
2026-01-29 22:56:01 +09:00
|
|
|
* 공정별 작업일지 지원 (2026-01-29)
|
2026-02-12 00:01:03 +09:00
|
|
|
* 공정관리 양식 매핑 연동 (2026-02-11)
|
2026-01-22 15:07:17 +09:00
|
|
|
* - DocumentViewer 사용
|
2026-02-12 00:01:03 +09:00
|
|
|
* - 공정관리에서 매핑된 workLogTemplateId/Name 기반으로 콘텐츠 분기
|
|
|
|
|
* - 양식 미매핑 시 processType 폴백
|
2025-12-23 21:13:07 +09:00
|
|
|
*/
|
|
|
|
|
|
2026-03-06 17:46:06 +09:00
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
2026-02-12 20:07:31 +09:00
|
|
|
import { Loader2, Save } from 'lucide-react';
|
2026-01-22 15:07:17 +09:00
|
|
|
import { DocumentViewer } from '@/components/document-system';
|
2026-02-12 20:07:31 +09:00
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { toast } from 'sonner';
|
2026-03-06 20:59:25 +09:00
|
|
|
import { getWorkOrderById, getMaterialInputLots, patchDocumentSnapshot } from '../WorkOrders/actions';
|
|
|
|
|
import { saveWorkLog, getWorkLog } from './actions';
|
2026-02-12 00:01:03 +09:00
|
|
|
import type { MaterialInputLot } from '../WorkOrders/actions';
|
2026-01-29 22:56:01 +09:00
|
|
|
import type { WorkOrder, ProcessType } from '../WorkOrders/types';
|
2026-01-22 15:07:17 +09:00
|
|
|
import { WorkLogContent } from './WorkLogContent';
|
2026-01-29 22:56:01 +09:00
|
|
|
import {
|
|
|
|
|
ScreenWorkLogContent,
|
|
|
|
|
SlatWorkLogContent,
|
|
|
|
|
BendingWorkLogContent,
|
|
|
|
|
} from '../WorkOrders/documents';
|
2025-12-23 21:13:07 +09:00
|
|
|
|
|
|
|
|
interface WorkLogModalProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
2026-01-14 15:39:07 +09:00
|
|
|
workOrderId: string | null;
|
2026-01-29 22:56:01 +09:00
|
|
|
processType?: ProcessType;
|
2026-02-12 00:01:03 +09:00
|
|
|
/** 공정관리에서 매핑된 작업일지 양식 ID */
|
|
|
|
|
workLogTemplateId?: number;
|
|
|
|
|
/** 공정관리에서 매핑된 작업일지 양식명 (예: '스크린 작업일지') */
|
|
|
|
|
workLogTemplateName?: string;
|
2025-12-23 21:13:07 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 00:01:03 +09:00
|
|
|
/**
|
|
|
|
|
* 양식명 → 공정 타입 매핑
|
|
|
|
|
* 공정관리에서 매핑된 양식명을 기반으로 콘텐츠 컴포넌트를 결정
|
|
|
|
|
*/
|
|
|
|
|
function resolveProcessTypeFromTemplate(templateName?: string): ProcessType | undefined {
|
|
|
|
|
if (!templateName) return undefined;
|
|
|
|
|
if (templateName.includes('스크린')) return 'screen';
|
|
|
|
|
if (templateName.includes('슬랫')) return 'slat';
|
|
|
|
|
if (templateName.includes('절곡')) return 'bending';
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function WorkLogModal({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
workOrderId,
|
|
|
|
|
processType,
|
|
|
|
|
workLogTemplateId,
|
|
|
|
|
workLogTemplateName,
|
|
|
|
|
}: WorkLogModalProps) {
|
2026-01-14 15:39:07 +09:00
|
|
|
const [order, setOrder] = useState<WorkOrder | null>(null);
|
2026-02-12 00:01:03 +09:00
|
|
|
const [materialLots, setMaterialLots] = useState<MaterialInputLot[]>([]);
|
2026-01-14 15:39:07 +09:00
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2026-02-12 20:07:31 +09:00
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2026-01-14 15:39:07 +09:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-03-06 17:46:06 +09:00
|
|
|
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
2026-03-06 20:59:25 +09:00
|
|
|
// Lazy Snapshot 대상 문서 ID
|
|
|
|
|
const [snapshotDocumentId, setSnapshotDocumentId] = useState<number | null>(null);
|
2026-01-14 15:39:07 +09:00
|
|
|
|
2026-01-29 22:56:01 +09:00
|
|
|
// 목업 WorkOrder 생성
|
|
|
|
|
const createMockOrder = (id: string, pType?: ProcessType): WorkOrder => ({
|
|
|
|
|
id,
|
|
|
|
|
workOrderNo: 'KD-WO-260129-01',
|
|
|
|
|
lotNo: 'KD-SA-260129-01',
|
|
|
|
|
processId: 1,
|
|
|
|
|
processName: pType === 'slat' ? '슬랫' : pType === 'bending' ? '절곡' : '스크린',
|
|
|
|
|
processCode: pType || 'screen',
|
|
|
|
|
processType: pType || 'screen',
|
|
|
|
|
status: 'in_progress',
|
|
|
|
|
client: '(주)경동',
|
|
|
|
|
projectName: '서울 강남 현장',
|
|
|
|
|
dueDate: '2026-02-05',
|
|
|
|
|
assignee: '홍길동',
|
|
|
|
|
assignees: [{ id: '1', name: '홍길동', isPrimary: true }],
|
|
|
|
|
orderDate: '2026-01-20',
|
|
|
|
|
scheduledDate: '2026-01-29',
|
|
|
|
|
shipmentDate: '2026-02-05',
|
|
|
|
|
salesOrderDate: '2026-01-15',
|
|
|
|
|
isAssigned: true,
|
|
|
|
|
isStarted: true,
|
|
|
|
|
priority: 3,
|
|
|
|
|
priorityLabel: '긴급',
|
|
|
|
|
shutterCount: 12,
|
|
|
|
|
department: '생산부',
|
|
|
|
|
items: [
|
2026-02-09 10:45:57 +09:00
|
|
|
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
|
|
|
|
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
|
|
|
|
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
2026-01-29 22:56:01 +09:00
|
|
|
],
|
fix: 프로젝트 전체 TypeScript 타입에러 408개 수정 (tsc --noEmit 0 errors)
- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage)
- 페이지(app/[locale]) 타입 호환성 수정 (80개)
- 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement)
- 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults)
- 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders)
- 견적/단가 모듈 타입 수정 (Quotes, Pricing)
- 건설 모듈 타입 수정 (49개, 17개 하위 모듈)
- HR 모듈 타입 수정 (CardManagement, VacationManagement 등)
- 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등)
- 게시판 모듈 타입 수정 (BoardManagement, BoardList 등)
- 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등)
- 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등)
- 유틸/훅/API 타입 수정 (hooks, contexts, lib)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:07:58 +09:00
|
|
|
currentStep: 2,
|
2026-01-29 22:56:01 +09:00
|
|
|
issues: [],
|
fix: 프로젝트 전체 TypeScript 타입에러 408개 수정 (tsc --noEmit 0 errors)
- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage)
- 페이지(app/[locale]) 타입 호환성 수정 (80개)
- 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement)
- 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults)
- 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders)
- 견적/단가 모듈 타입 수정 (Quotes, Pricing)
- 건설 모듈 타입 수정 (49개, 17개 하위 모듈)
- HR 모듈 타입 수정 (CardManagement, VacationManagement 등)
- 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등)
- 게시판 모듈 타입 수정 (BoardManagement, BoardList 등)
- 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등)
- 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등)
- 유틸/훅/API 타입 수정 (hooks, contexts, lib)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:07:58 +09:00
|
|
|
note: '',
|
2026-01-29 22:56:01 +09:00
|
|
|
});
|
|
|
|
|
|
2026-01-14 15:39:07 +09:00
|
|
|
// 모달 열릴 때 데이터 fetch
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open && workOrderId) {
|
2026-01-29 22:56:01 +09:00
|
|
|
// 목업 ID인 경우 API 호출 생략
|
|
|
|
|
if (workOrderId.startsWith('mock-')) {
|
|
|
|
|
setOrder(createMockOrder(workOrderId, processType));
|
|
|
|
|
setError(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 15:39:07 +09:00
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
2026-02-12 00:01:03 +09:00
|
|
|
Promise.all([
|
|
|
|
|
getWorkOrderById(workOrderId),
|
|
|
|
|
getMaterialInputLots(workOrderId),
|
2026-03-06 20:59:25 +09:00
|
|
|
getWorkLog(workOrderId),
|
2026-02-12 00:01:03 +09:00
|
|
|
])
|
2026-03-06 20:59:25 +09:00
|
|
|
.then(([orderResult, lotsResult, workLogResult]) => {
|
2026-02-12 00:01:03 +09:00
|
|
|
if (orderResult.success && orderResult.data) {
|
|
|
|
|
setOrder(orderResult.data);
|
2026-01-14 15:39:07 +09:00
|
|
|
} else {
|
2026-02-12 00:01:03 +09:00
|
|
|
setError(orderResult.error || '데이터를 불러올 수 없습니다.');
|
|
|
|
|
}
|
|
|
|
|
if (lotsResult.success) {
|
|
|
|
|
setMaterialLots(lotsResult.data);
|
2026-01-14 15:39:07 +09:00
|
|
|
}
|
2026-03-06 20:59:25 +09:00
|
|
|
// Lazy Snapshot: 문서가 있고 rendered_html이 없으면 스냅샷 대상
|
|
|
|
|
if (workLogResult.success && workLogResult.data?.document) {
|
|
|
|
|
const doc = workLogResult.data.document as { id?: number; rendered_html?: string | null };
|
|
|
|
|
if (doc.id && !doc.rendered_html) {
|
|
|
|
|
setSnapshotDocumentId(doc.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-14 15:39:07 +09:00
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
setError('서버 오류가 발생했습니다.');
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
});
|
|
|
|
|
} else if (!open) {
|
|
|
|
|
// 모달 닫힐 때 상태 초기화
|
|
|
|
|
setOrder(null);
|
2026-02-12 00:01:03 +09:00
|
|
|
setMaterialLots([]);
|
2026-03-06 20:59:25 +09:00
|
|
|
setSnapshotDocumentId(null);
|
2026-01-14 15:39:07 +09:00
|
|
|
setError(null);
|
|
|
|
|
}
|
2026-01-29 22:56:01 +09:00
|
|
|
}, [open, workOrderId, processType]);
|
2026-01-14 15:39:07 +09:00
|
|
|
|
2026-03-06 20:59:25 +09:00
|
|
|
// Lazy Snapshot: 콘텐츠 렌더링 완료 후 rendered_html이 없는 문서에 스냅샷 저장
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!snapshotDocumentId || isLoading || !order) return;
|
|
|
|
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
const html = contentWrapperRef.current?.innerHTML;
|
|
|
|
|
if (html && html.length > 50) {
|
|
|
|
|
patchDocumentSnapshot(snapshotDocumentId, html).then((result) => {
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setSnapshotDocumentId(null);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}, [snapshotDocumentId, isLoading, order]);
|
|
|
|
|
|
2026-02-12 20:07:31 +09:00
|
|
|
// 저장 핸들러
|
|
|
|
|
const handleSave = useCallback(async () => {
|
|
|
|
|
if (!workOrderId || !order) return;
|
|
|
|
|
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
// 현재 아이템 데이터를 table_data로 변환
|
|
|
|
|
const tableData = (order.items || []).map((item) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
item_name: item.productName,
|
|
|
|
|
specification: item.specification || item.floorCode,
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
unit: item.unit || 'EA',
|
|
|
|
|
}));
|
|
|
|
|
|
2026-03-06 17:46:06 +09:00
|
|
|
// HTML 스냅샷 캡처 (MNG 출력용)
|
|
|
|
|
const renderedHtml = contentWrapperRef.current?.innerHTML || undefined;
|
|
|
|
|
|
2026-02-12 20:07:31 +09:00
|
|
|
const result = await saveWorkLog(workOrderId, {
|
|
|
|
|
table_data: tableData,
|
|
|
|
|
title: workLogTemplateName || '작업일지',
|
2026-03-06 17:46:06 +09:00
|
|
|
rendered_html: renderedHtml,
|
2026-02-12 20:07:31 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
toast.success('작업일지가 저장되었습니다.');
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
toast.error('저장 중 오류가 발생했습니다.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
}, [workOrderId, order, workLogTemplateName]);
|
|
|
|
|
|
2026-01-14 15:39:07 +09:00
|
|
|
if (!workOrderId) return null;
|
|
|
|
|
|
2026-01-22 15:07:17 +09:00
|
|
|
// 로딩/에러 상태는 DocumentViewer 내부에서 처리
|
|
|
|
|
const subtitle = order ? `${order.processName} 생산부서` : undefined;
|
2025-12-23 21:13:07 +09:00
|
|
|
|
2026-02-12 00:01:03 +09:00
|
|
|
// 양식 미매핑 안내
|
|
|
|
|
const renderNoTemplate = () => (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-64 gap-4 bg-white">
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
이 공정에 작업일지 양식이 매핑되지 않았습니다.
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
공정관리에서 작업일지 양식을 설정해주세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 공정관리 양식 매핑 기반 콘텐츠 분기
|
2026-01-29 22:56:01 +09:00
|
|
|
const renderContent = () => {
|
|
|
|
|
if (!order) return null;
|
|
|
|
|
|
2026-02-12 00:01:03 +09:00
|
|
|
// 1순위: 공정관리에서 매핑된 양식명으로 결정
|
|
|
|
|
const templateType = resolveProcessTypeFromTemplate(workLogTemplateName);
|
|
|
|
|
|
|
|
|
|
// 2순위: processType 폴백 (양식 미매핑 시)
|
|
|
|
|
const type = templateType || processType || order.processType;
|
|
|
|
|
|
|
|
|
|
// 양식이 매핑되어 있지 않은 경우 안내
|
|
|
|
|
if (!workLogTemplateId && !processType) {
|
|
|
|
|
return renderNoTemplate();
|
|
|
|
|
}
|
2026-01-29 22:56:01 +09:00
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'screen':
|
2026-02-12 00:01:03 +09:00
|
|
|
return <ScreenWorkLogContent data={order} materialLots={materialLots} />;
|
2026-01-29 22:56:01 +09:00
|
|
|
case 'slat':
|
2026-02-12 00:01:03 +09:00
|
|
|
return <SlatWorkLogContent data={order} materialLots={materialLots} />;
|
2026-02-22 02:13:13 +09:00
|
|
|
case 'bending': {
|
|
|
|
|
const lotNoMap: Record<string, string> = {};
|
|
|
|
|
for (const lot of materialLots) {
|
|
|
|
|
if (lot.item_code.startsWith('BD-')) {
|
|
|
|
|
lotNoMap[lot.item_code] = lot.lot_no;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return <BendingWorkLogContent data={order} lotNoMap={lotNoMap} />;
|
|
|
|
|
}
|
2026-01-29 22:56:01 +09:00
|
|
|
default:
|
|
|
|
|
return <WorkLogContent data={order} />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 00:01:03 +09:00
|
|
|
// 양식명으로 문서 제목 결정
|
|
|
|
|
const documentTitle = workLogTemplateName || '작업일지';
|
|
|
|
|
|
2026-02-12 20:07:31 +09:00
|
|
|
const toolbarExtra = (
|
|
|
|
|
<Button onClick={handleSave} disabled={isSaving} size="sm">
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Save className="w-4 h-4 mr-1.5" />
|
|
|
|
|
)}
|
|
|
|
|
{isSaving ? '저장 중...' : '저장'}
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-23 21:13:07 +09:00
|
|
|
return (
|
2026-01-22 15:07:17 +09:00
|
|
|
<DocumentViewer
|
2026-02-12 00:01:03 +09:00
|
|
|
title={documentTitle}
|
2026-01-22 15:07:17 +09:00
|
|
|
subtitle={subtitle}
|
|
|
|
|
preset="inspection"
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={onOpenChange}
|
2026-02-12 20:07:31 +09:00
|
|
|
toolbarExtra={toolbarExtra}
|
2026-01-22 15:07:17 +09:00
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex items-center justify-center h-64 bg-white">
|
|
|
|
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
2025-12-23 21:13:07 +09:00
|
|
|
</div>
|
2026-01-22 15:07:17 +09:00
|
|
|
) : error || !order ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-64 gap-4 bg-white">
|
|
|
|
|
<p className="text-muted-foreground">{error || '데이터를 불러올 수 없습니다.'}</p>
|
2025-12-23 21:13:07 +09:00
|
|
|
</div>
|
2026-01-22 15:07:17 +09:00
|
|
|
) : (
|
2026-03-06 17:46:06 +09:00
|
|
|
<div ref={contentWrapperRef}>
|
|
|
|
|
{renderContent()}
|
|
|
|
|
</div>
|
2026-01-22 15:07:17 +09:00
|
|
|
)}
|
|
|
|
|
</DocumentViewer>
|
2025-12-23 21:13:07 +09:00
|
|
|
);
|
2026-01-22 15:07:17 +09:00
|
|
|
}
|