feat(WEB): 수입검사 관리 대폭 개선, 캘린더 DayTimeView 추가 및 출고 기능 보완

- 수입검사: InspectionCreate/Detail/List 대폭 개선, OrderSelectModal/문서 컴포넌트 신규 추가
- 수입검사: actions/types/mockData/inspectionConfig 전면 리팩토링
- QMS: InspectionModalV2/ImportInspectionDocument 개선
- 캘린더: DayTimeView 신규 추가, CalendarHeader/ScheduleCalendar/utils 확장
- 출고: ShipmentDetail/List/actions 개선, ShipmentOrderDocument/ShippingSlip 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-02 16:46:52 +09:00
parent 1a69324d59
commit ca6247286a
28 changed files with 4195 additions and 1776 deletions

View File

@@ -1,8 +1,10 @@
"use client";
import React, { useState, useEffect } from 'react';
import { AlertCircle, Loader2 } from 'lucide-react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { AlertCircle, Loader2, Save } from 'lucide-react';
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';
@@ -17,7 +19,7 @@ import {
JointbarInspectionDocument,
QualityDocumentUploader,
} from './documents';
import type { ImportInspectionTemplate } from './documents/ImportInspectionDocument';
import type { ImportInspectionTemplate, ImportInspectionRef } from './documents/ImportInspectionDocument';
// 작업일지 + 중간검사 성적서 문서 컴포넌트 import (공정별 신규 버전)
import {
@@ -293,6 +295,10 @@ export const InspectionModalV2 = ({
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
const [templateError, setTemplateError] = useState<string | null>(null);
// 수입검사 저장용 ref/상태
const importDocRef = useRef<ImportInspectionRef>(null);
const [isSaving, setIsSaving] = useState(false);
// 수입검사 템플릿 로드 (모달 열릴 때)
useEffect(() => {
if (isOpen && doc?.type === 'import' && itemName && specification) {
@@ -385,6 +391,23 @@ 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) {
@@ -396,7 +419,7 @@ export const InspectionModalV2 = ({
}
// 템플릿이 로드되면 전달, 아니면 기본 템플릿 사용
return <ImportInspectionDocument template={importTemplate || undefined} />;
return <ImportInspectionDocument ref={importDocRef} template={importTemplate || undefined} />;
};
// 문서 타입에 따른 컨텐츠 렌더링
@@ -433,6 +456,18 @@ export const InspectionModalV2 = ({
console.log('[InspectionModalV2] 다운로드 요청:', doc.type);
};
// 수입검사 저장 버튼 (toolbarExtra)
const importToolbarExtra = doc.type === 'import' ? (
<Button onClick={handleImportSave} 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>
) : undefined;
return (
<DocumentViewer
title={doc.title}
@@ -441,6 +476,7 @@ export const InspectionModalV2 = ({
open={isOpen}
onOpenChange={(open) => !open && onClose()}
onDownload={handleDownload}
toolbarExtra={importToolbarExtra}
>
{renderDocumentContent()}
</DocumentViewer>

View File

@@ -9,8 +9,7 @@
* - API 데이터 기반 동적 렌더링
*/
import React, { useState, useCallback, useMemo } from 'react';
import { DocumentHeader, QualityApprovalTable } from '@/components/document-system';
import React, { useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
// ============================================
// 타입 정의
@@ -92,6 +91,11 @@ export interface ImportInspectionTemplate {
notes?: string[];
}
/** ref를 통한 데이터 접근 인터페이스 */
export interface ImportInspectionRef {
getInspectionData: () => unknown;
}
/** 컴포넌트 Props */
export interface ImportInspectionDocumentProps {
template?: ImportInspectionTemplate;
@@ -325,12 +329,12 @@ const isValueInRange = (
// 컴포넌트
// ============================================
export const ImportInspectionDocument = ({
export const ImportInspectionDocument = forwardRef<ImportInspectionRef, ImportInspectionDocumentProps>(function ImportInspectionDocument({
template = MOCK_EGI_TEMPLATE,
initialValues,
onValuesChange,
readOnly = false,
}: ImportInspectionDocumentProps) => {
}, ref) {
// 검사 항목별 입력값 상태
const [values, setValues] = useState<Record<string, InspectionItemValue>>(() => {
const initial: Record<string, InspectionItemValue> = {};
@@ -416,6 +420,22 @@ export const ImportInspectionDocument = ({
return null;
}, [values, template.inspectionItems]);
// ref를 통한 데이터 접근
useImperativeHandle(ref, () => ({
getInspectionData: () => ({
templateId: template.templateId,
values: Object.values(values),
overallResult,
}),
}), [template.templateId, values, overallResult]);
// 날짜 포맷
const fullDate = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
// OK/NG 선택 핸들러
const handleResultChange = useCallback((itemId: string, result: JudgmentResult) => {
if (readOnly) return;
@@ -599,18 +619,40 @@ export const ImportInspectionDocument = ({
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 문서 헤더 */}
<DocumentHeader
title="수 입 검 사 성 적 서"
logo={{ text: 'KD', subtext: '경동기업' }}
customApproval={
<QualityApprovalTable
type="2col"
approvers={{ writer: headerInfo.approvers.writer }}
reportDate={headerInfo.reportDate}
/>
}
/>
{/* 문서 헤더 (중간검사 성적서 스타일) */}
<div className="flex justify-between items-start mb-6">
<div>
<h1 className="text-2xl font-bold"> </h1>
<p className="text-xs text-gray-500 mt-1 whitespace-nowrap">
: {headerInfo.lotNo || template.templateId} | : {fullDate}
</p>
</div>
{/* 결재란 */}
<table className="border-collapse text-sm flex-shrink-0">
<tbody>
<tr>
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}><br/></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-6 py-3 text-center">{headerInfo.approvers.writer || '-'}</td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
</tr>
<tr>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 - 6컬럼 구조 */}
<table className="w-full border-collapse mb-4 text-xs">
@@ -807,4 +849,4 @@ export const ImportInspectionDocument = ({
</div>
</div>
);
};
});

View File

@@ -107,6 +107,9 @@ export const MOCK_SHIPMENT_DETAIL: ShipmentDetail = {
driverName: '최운전',
driverContact: '010-5555-6666',
remarks: '하차 시 주의 요망',
vehicleDispatches: [],
productGroups: [],
otherParts: [],
};
// 품질관리서 목록