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:
@@ -0,0 +1,435 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 제품검사성적서 문서
|
||||
*
|
||||
* 기획서 기반:
|
||||
* - 결재라인 (작성/승인)
|
||||
* - 기본정보 (제품명, LOT NO, 제품코드, 로트크기 등)
|
||||
* - 제품 사진
|
||||
* - 검사항목 테이블 (No, 검사항목, 검사기준, 검사방법, 검사주기, 측정값, 판정)
|
||||
* - No/검사항목: 그룹별 rowSpan 병합
|
||||
* - 검사방법/검사주기: 크로스그룹 병합 (methodSpan/freqSpan) + 그룹 내 병합 하이브리드
|
||||
* - 판정: □ 적합 / □ 부적합 클릭 가능, judgmentSpan으로 크로스그룹 병합
|
||||
* - 특이사항 + 종합판정(자동계산)을 테이블 마지막 행으로 포함
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { ConstructionApprovalTable } from '@/components/document-system';
|
||||
import type { InspectionReportDocument as InspectionReportDocumentType, ReportInspectionItem } from '../types';
|
||||
|
||||
interface InspectionReportDocumentProps {
|
||||
data: InspectionReportDocumentType;
|
||||
}
|
||||
|
||||
/** 검사항목을 No 기준으로 그룹화 */
|
||||
function groupItemsByNo(items: ReportInspectionItem[]) {
|
||||
const groups: { no: number; category: string; rows: ReportInspectionItem[] }[] = [];
|
||||
let currentGroup: (typeof groups)[number] | null = null;
|
||||
|
||||
for (const item of items) {
|
||||
if (!currentGroup || currentGroup.no !== item.no) {
|
||||
currentGroup = { no: item.no, category: item.category, rows: [] };
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup.rows.push(item);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 크로스그룹 병합 커버리지 계산: { map: flatIndex→rowSpan, covered: Set<coveredIndices> } */
|
||||
function buildCoverageMap(items: ReportInspectionItem[], field: 'methodSpan' | 'freqSpan' | 'judgmentSpan' | 'subCategorySpan' | 'measuredValueSpan') {
|
||||
const map: Record<number, number> = {};
|
||||
const covered = new Set<number>();
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
const span = item[field];
|
||||
if (span && span > 0) {
|
||||
map[idx] = span;
|
||||
for (let i = idx + 1; i < idx + span && i < items.length; i++) {
|
||||
covered.add(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { map, covered };
|
||||
}
|
||||
|
||||
export function InspectionReportDocument({ data }: InspectionReportDocumentProps) {
|
||||
// 판정 인터랙션을 위한 stateful items
|
||||
const [items, setItems] = useState<ReportInspectionItem[]>(() => data.inspectionItems);
|
||||
|
||||
const groups = useMemo(() => groupItemsByNo(items), [items]);
|
||||
|
||||
// 크로스그룹 병합 렌더맵
|
||||
const methodCoverage = useMemo(() => buildCoverageMap(items, 'methodSpan'), [items]);
|
||||
const freqCoverage = useMemo(() => buildCoverageMap(items, 'freqSpan'), [items]);
|
||||
const judgmentCoverage = useMemo(() => buildCoverageMap(items, 'judgmentSpan'), [items]);
|
||||
const subCatCoverage = useMemo(() => buildCoverageMap(items, 'subCategorySpan'), [items]);
|
||||
const measuredValueCoverage = useMemo(() => buildCoverageMap(items, 'measuredValueSpan'), [items]);
|
||||
|
||||
// 그룹별 flat index 오프셋
|
||||
const groupOffsets = useMemo(() => {
|
||||
const offsets: number[] = [];
|
||||
let offset = 0;
|
||||
for (const group of groups) {
|
||||
offsets.push(offset);
|
||||
offset += group.rows.length;
|
||||
}
|
||||
return offsets;
|
||||
}, [groups]);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const calculatedJudgment = useMemo(() => {
|
||||
const processedBySpan = new Set<number>();
|
||||
const judgments: ('적합' | '부적합' | undefined)[] = [];
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
if (item.hideJudgment) return;
|
||||
if (processedBySpan.has(idx)) return;
|
||||
|
||||
if (item.judgmentSpan) {
|
||||
judgments.push(item.judgment);
|
||||
for (let i = idx + 1; i < idx + item.judgmentSpan && i < items.length; i++) {
|
||||
processedBySpan.add(i);
|
||||
}
|
||||
} else if (!judgmentCoverage.covered.has(idx)) {
|
||||
judgments.push(item.judgment);
|
||||
}
|
||||
});
|
||||
|
||||
if (judgments.some(j => j === '부적합')) return '불합격';
|
||||
if (judgments.every(j => j === '적합')) return '합격';
|
||||
return '합격';
|
||||
}, [items, judgmentCoverage.covered]);
|
||||
|
||||
// 판정 클릭 핸들러
|
||||
const handleJudgmentClick = useCallback((flatIdx: number, value: '적합' | '부적합') => {
|
||||
setItems(prev => {
|
||||
const next = [...prev];
|
||||
next[flatIdx] = {
|
||||
...next[flatIdx],
|
||||
judgment: next[flatIdx].judgment === value ? undefined : value,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 min-h-full text-[11px]">
|
||||
{/* 헤더: 제목 (좌측) + 결재란 (우측) */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-widest mb-2">제 품 검 사 성 적 서</h1>
|
||||
<div className="text-[10px] space-y-1">
|
||||
<div className="flex gap-4">
|
||||
<span>문서번호: <strong>{data.documentNumber}</strong></span>
|
||||
<span>작성일자: <strong>{data.createdDate}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConstructionApprovalTable
|
||||
approvers={{
|
||||
writer: data.approvalLine[0]
|
||||
? { name: data.approvalLine[0].name, department: data.approvalLine[0].department }
|
||||
: undefined,
|
||||
approver1: data.approvalLine[1]
|
||||
? { name: data.approvalLine[1].name, department: data.approvalLine[1].department }
|
||||
: undefined,
|
||||
approver2: data.approvalLine[2]
|
||||
? { name: data.approvalLine[2].name, department: data.approvalLine[2].department }
|
||||
: undefined,
|
||||
approver3: data.approvalLine[3]
|
||||
? { name: data.approvalLine[3].name, department: data.approvalLine[3].department }
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기본 정보 */}
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">기본 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 w-28 font-medium border-r border-gray-300">제품명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.productName || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 w-28 font-medium border-r border-gray-300">제품 LOT NO</td>
|
||||
<td className="px-2 py-1">{data.productLotNo || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">제품코드</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.productCode || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">로트크기</td>
|
||||
<td className="px-2 py-1">{data.lotSize || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">수주처</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.client || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">검사일자</td>
|
||||
<td className="px-2 py-1">{data.inspectionDate || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">현장명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.siteName || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">검사자</td>
|
||||
<td className="px-2 py-1">{data.inspector || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 제품 사진 */}
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">제품 사진</div>
|
||||
<div className="p-4 flex items-center justify-center min-h-[200px]">
|
||||
{data.productImage ? (
|
||||
<img
|
||||
src={data.productImage}
|
||||
alt="제품 사진"
|
||||
className="max-h-[300px] object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-400 text-center">
|
||||
<div className="border-2 border-dashed border-gray-300 p-8 rounded">
|
||||
제품 사진 없음
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검사항목 테이블 + 특이사항 + 종합판정 */}
|
||||
<table className="w-full border-collapse border border-gray-400 mb-4">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 px-2 py-1 w-10 text-center">No.</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-24">검사항목</th>
|
||||
<th className="border border-gray-400 px-2 py-1" colSpan={2}>검사기준</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-16 text-center whitespace-pre-line">검사{'\n'}방법</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-16 text-center whitespace-pre-line">검사{'\n'}주기</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-16 text-center">측정값</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-28 text-center">판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((group, groupIdx) =>
|
||||
group.rows.map((row, rowIdx) => {
|
||||
const flatIdx = groupOffsets[groupIdx] + rowIdx;
|
||||
|
||||
// === 검사방법 셀 렌더 결정 ===
|
||||
let renderMethod = false;
|
||||
let methodRowSpan = 1;
|
||||
if (methodCoverage.map[flatIdx] !== undefined) {
|
||||
// 크로스그룹 병합 시작점
|
||||
renderMethod = true;
|
||||
methodRowSpan = methodCoverage.map[flatIdx];
|
||||
} else if (!methodCoverage.covered.has(flatIdx) && rowIdx === 0) {
|
||||
// 크로스그룹에 속하지 않음 → 그룹 내 병합 (첫 행)
|
||||
renderMethod = true;
|
||||
methodRowSpan = group.rows.length;
|
||||
}
|
||||
|
||||
// === 검사주기 셀 렌더 결정 ===
|
||||
let renderFreq = false;
|
||||
let freqRowSpan = 1;
|
||||
if (freqCoverage.map[flatIdx] !== undefined) {
|
||||
renderFreq = true;
|
||||
freqRowSpan = freqCoverage.map[flatIdx];
|
||||
} else if (!freqCoverage.covered.has(flatIdx) && rowIdx === 0) {
|
||||
renderFreq = true;
|
||||
freqRowSpan = group.rows.length;
|
||||
}
|
||||
|
||||
// === 판정 셀 렌더 결정 ===
|
||||
let renderJudgment = false;
|
||||
let judgmentRowSpan = 1;
|
||||
if (judgmentCoverage.map[flatIdx] !== undefined) {
|
||||
// 크로스그룹 판정 병합 시작점
|
||||
renderJudgment = true;
|
||||
judgmentRowSpan = judgmentCoverage.map[flatIdx];
|
||||
} else if (!judgmentCoverage.covered.has(flatIdx)) {
|
||||
// 일반 per-row 판정
|
||||
renderJudgment = true;
|
||||
judgmentRowSpan = 1;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={flatIdx} className="border-b border-gray-300">
|
||||
{/* No. - 그룹 첫 행만 rowSpan */}
|
||||
{rowIdx === 0 && (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 text-center align-middle font-medium"
|
||||
rowSpan={group.rows.length}
|
||||
>
|
||||
{group.no}
|
||||
</td>
|
||||
)}
|
||||
{/* 검사항목 - 그룹 첫 행만 rowSpan */}
|
||||
{rowIdx === 0 && (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 align-middle font-medium whitespace-pre-line"
|
||||
rowSpan={group.rows.length}
|
||||
>
|
||||
{group.category}
|
||||
</td>
|
||||
)}
|
||||
{/* 검사기준: subCategory 셀 */}
|
||||
{(() => {
|
||||
// subCategorySpan 시작점 → 병합 셀 렌더
|
||||
if (subCatCoverage.map[flatIdx] !== undefined) {
|
||||
return (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 align-middle font-medium whitespace-pre-line text-center"
|
||||
rowSpan={subCatCoverage.map[flatIdx]}
|
||||
>
|
||||
{row.subCategory}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
// 다른 셀의 subCategorySpan에 포함 → 렌더 안함
|
||||
if (subCatCoverage.covered.has(flatIdx)) {
|
||||
return null;
|
||||
}
|
||||
// 개별 subCategory 있음 → 단일 셀
|
||||
if (row.subCategory) {
|
||||
return (
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium whitespace-pre-line">
|
||||
{row.subCategory}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
// subCategory 없음 → 렌더 안함 (criteria가 colSpan=2)
|
||||
return null;
|
||||
})()}
|
||||
{/* 검사기준: criteria 셀 */}
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 whitespace-pre-line"
|
||||
colSpan={
|
||||
// subCategory 셀이 없는 경우 (no subCategory AND not covered by span) → colSpan=2
|
||||
!row.subCategory && !subCatCoverage.covered.has(flatIdx) && subCatCoverage.map[flatIdx] === undefined
|
||||
? 2
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{row.criteria}
|
||||
</td>
|
||||
{/* 검사방법 */}
|
||||
{renderMethod && (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 text-center align-middle whitespace-pre-line"
|
||||
rowSpan={methodRowSpan}
|
||||
>
|
||||
{row.method || ''}
|
||||
</td>
|
||||
)}
|
||||
{/* 검사주기 */}
|
||||
{renderFreq && (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 text-center align-middle whitespace-pre-line"
|
||||
rowSpan={freqRowSpan}
|
||||
>
|
||||
{row.frequency || ''}
|
||||
</td>
|
||||
)}
|
||||
{/* 측정값 */}
|
||||
{(() => {
|
||||
if (measuredValueCoverage.map[flatIdx] !== undefined) {
|
||||
return (
|
||||
<td
|
||||
className="border border-gray-400 px-2 py-1 text-center align-middle"
|
||||
rowSpan={measuredValueCoverage.map[flatIdx]}
|
||||
>
|
||||
{row.measuredValue || ''}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (measuredValueCoverage.covered.has(flatIdx)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">
|
||||
{row.measuredValue || ''}
|
||||
</td>
|
||||
);
|
||||
})()}
|
||||
{/* 판정 */}
|
||||
{renderJudgment && (
|
||||
row.hideJudgment ? (
|
||||
<td
|
||||
className="border border-gray-400 px-1 py-1"
|
||||
rowSpan={judgmentRowSpan}
|
||||
/>
|
||||
) : (
|
||||
<td
|
||||
className="border border-gray-400 px-1 py-1 text-center align-middle"
|
||||
rowSpan={judgmentRowSpan}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1 text-[10px]">
|
||||
<button
|
||||
type="button"
|
||||
className={`cursor-pointer hover:opacity-80 ${
|
||||
row.judgment === '적합' ? 'font-bold text-blue-600' : 'text-gray-400'
|
||||
}`}
|
||||
onClick={() => handleJudgmentClick(flatIdx, '적합')}
|
||||
>
|
||||
{row.judgment === '적합' ? '■' : '□'} 적합
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`cursor-pointer hover:opacity-80 ${
|
||||
row.judgment === '부적합' ? 'font-bold text-red-600' : 'text-gray-400'
|
||||
}`}
|
||||
onClick={() => handleJudgmentClick(flatIdx, '부적합')}
|
||||
>
|
||||
{row.judgment === '부적합' ? '■' : '□'} 부적합
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="border border-gray-400 px-2 py-4 text-center text-gray-400">
|
||||
검사항목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* 특이사항 + 종합판정 (테이블 마지막 행) */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-2 py-2 font-bold text-center" colSpan={2}>
|
||||
특이사항
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-2" colSpan={4}>
|
||||
{data.specialNotes || ''}
|
||||
</td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-2 py-2 font-bold text-center">
|
||||
종합판정
|
||||
</td>
|
||||
<td className={`border border-gray-400 px-2 py-2 text-center font-bold text-sm ${
|
||||
calculatedJudgment === '합격' ? 'text-blue-600' : 'text-red-600'
|
||||
}`}>
|
||||
{calculatedJudgment}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 서명 영역 */}
|
||||
<div className="mt-8 text-center text-[10px]">
|
||||
<p>위 내용과 같이 제품검사 결과를 보고합니다.</p>
|
||||
<div className="mt-6">
|
||||
<p>{data.createdDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 제품검사성적서 모달
|
||||
* DocumentViewer를 사용하여 문서 표시 + 인쇄/PDF 기능 제공
|
||||
*/
|
||||
|
||||
import { Save } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { InspectionReportDocument } from './InspectionReportDocument';
|
||||
import type { InspectionReportDocument as InspectionReportDocumentType } from '../types';
|
||||
|
||||
interface InspectionReportModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
data: InspectionReportDocumentType | null;
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
export function InspectionReportModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
onSave,
|
||||
}: InspectionReportModalProps) {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="제품검사성적서"
|
||||
preset="readonly"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
pdfMeta={{
|
||||
documentNumber: data.documentNumber,
|
||||
createdDate: data.createdDate,
|
||||
}}
|
||||
toolbarExtra={
|
||||
<Button onClick={onSave} size="sm">
|
||||
<Save className="w-4 h-4 mr-1.5" />
|
||||
저장
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<InspectionReportDocument data={data} />
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 제품검사요청서 문서
|
||||
*
|
||||
* 기획서 기반:
|
||||
* - 결재라인 (작성/승인)
|
||||
* - 기본정보 (수주처, 업체명, 담당자 등)
|
||||
* - 입력사항 (건축공사장, 자재유통업자, 공사시공자, 공사감리자)
|
||||
* - 검사대상 사전 고지 정보 테이블
|
||||
*/
|
||||
|
||||
import { ConstructionApprovalTable } from '@/components/document-system';
|
||||
import { isOrderSpecSame } from '../mockData';
|
||||
import type { InspectionRequestDocument as InspectionRequestDocumentType } from '../types';
|
||||
|
||||
interface InspectionRequestDocumentProps {
|
||||
data: InspectionRequestDocumentType;
|
||||
}
|
||||
|
||||
export function InspectionRequestDocument({ data }: InspectionRequestDocumentProps) {
|
||||
return (
|
||||
<div className="bg-white p-8 min-h-full text-[11px]">
|
||||
{/* 헤더: 제목 (좌측) + 결재란 (우측) */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-widest mb-2">제 품 검 사 요 청 서</h1>
|
||||
<div className="text-[10px] space-y-1">
|
||||
<div className="flex gap-4">
|
||||
<span>문서번호: <strong>{data.documentNumber}</strong></span>
|
||||
<span>작성일자: <strong>{data.createdDate}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConstructionApprovalTable
|
||||
approvers={{
|
||||
writer: data.approvalLine[0]
|
||||
? { name: data.approvalLine[0].name, department: data.approvalLine[0].department }
|
||||
: undefined,
|
||||
approver1: data.approvalLine[1]
|
||||
? { name: data.approvalLine[1].name, department: data.approvalLine[1].department }
|
||||
: undefined,
|
||||
approver2: data.approvalLine[2]
|
||||
? { name: data.approvalLine[2].name, department: data.approvalLine[2].department }
|
||||
: undefined,
|
||||
approver3: data.approvalLine[3]
|
||||
? { name: data.approvalLine[3].name, department: data.approvalLine[3].department }
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기본 정보 */}
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">기본 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 w-28 font-medium border-r border-gray-300">수주처</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.client || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 w-28 font-medium border-r border-gray-300">업체명</td>
|
||||
<td className="px-2 py-1">{data.companyName || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">담당자</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.manager || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">수주번호</td>
|
||||
<td className="px-2 py-1">{data.orderNumber || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">담당자 연락처</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.managerContact || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">현장명</td>
|
||||
<td className="px-2 py-1">{data.siteName || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">납품일</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.deliveryDate || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">현장 주소</td>
|
||||
<td className="px-2 py-1">{data.siteAddress || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">총 개소</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.totalLocations || '-'}</td>
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">접수일</td>
|
||||
<td className="px-2 py-1">{data.receptionDate || '-'}</td>
|
||||
</tr>
|
||||
<tr className="border-t border-gray-300">
|
||||
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-300">검사방문요청일</td>
|
||||
<td className="px-2 py-1" colSpan={3}>{data.visitRequestDate || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 입력사항: 4개 섹션 */}
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">입력사항</div>
|
||||
|
||||
{/* 건축공사장 정보 */}
|
||||
<div className="border-b border-gray-300">
|
||||
<div className="bg-gray-100 px-2 py-1 font-medium border-b border-gray-300">건축공사장 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">현장명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.constructionSite.siteName || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">대지위치</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.constructionSite.landLocation || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 w-20 font-medium border-r border-gray-300">지번</td>
|
||||
<td className="px-2 py-1">{data.constructionSite.lotNumber || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 자재유통업자 정보 */}
|
||||
<div className="border-b border-gray-300">
|
||||
<div className="bg-gray-100 px-2 py-1 font-medium border-b border-gray-300">자재유통업자 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">회사명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.materialDistributor.companyName || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">회사주소</td>
|
||||
<td className="px-2 py-1">{data.materialDistributor.companyAddress || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">대표자명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.materialDistributor.representativeName || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">전화번호</td>
|
||||
<td className="px-2 py-1">{data.materialDistributor.phone || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 공사시공자 정보 */}
|
||||
<div className="border-b border-gray-300">
|
||||
<div className="bg-gray-100 px-2 py-1 font-medium border-b border-gray-300">공사시공자 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">회사명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.constructorInfo.companyName || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">회사주소</td>
|
||||
<td className="px-2 py-1">{data.constructorInfo.companyAddress || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">성명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.constructorInfo.name || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">전화번호</td>
|
||||
<td className="px-2 py-1">{data.constructorInfo.phone || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 공사감리자 정보 */}
|
||||
<div>
|
||||
<div className="bg-gray-100 px-2 py-1 font-medium border-b border-gray-300">공사감리자 정보</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-300">
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">사무소명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.supervisor.officeName || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 w-28 font-medium border-r border-gray-300">사무소주소</td>
|
||||
<td className="px-2 py-1">{data.supervisor.officeAddress || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">성명</td>
|
||||
<td className="px-2 py-1 border-r border-gray-300">{data.supervisor.name || '-'}</td>
|
||||
<td className="bg-gray-50 px-2 py-1 font-medium border-r border-gray-300">전화번호</td>
|
||||
<td className="px-2 py-1">{data.supervisor.phone || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검사대상 사전 고지 정보 */}
|
||||
<div className="border border-gray-400 mb-4">
|
||||
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">검사대상 사전 고지 정보</div>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 border-b border-gray-400">
|
||||
<th className="border-r border-gray-400 px-2 py-1 w-10 text-center">No.</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1">수주번호</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1">층수</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1">부호</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1 text-center">수주 가로</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1 text-center">수주 세로</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1 text-center">시공 가로</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1 text-center">시공 세로</th>
|
||||
<th className="border-r border-gray-400 px-2 py-1 text-center">일치</th>
|
||||
<th className="px-2 py-1">변경사유</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.priorNoticeItems.map((item, index) => {
|
||||
const isSame = isOrderSpecSame(item);
|
||||
return (
|
||||
<tr key={item.id} className="border-b border-gray-300">
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center">{index + 1}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1">{item.orderNumber}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1">{item.floor}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1">{item.symbol}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center">{item.orderWidth}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center">{item.orderHeight}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center">{item.constructionWidth}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center">{item.constructionHeight}</td>
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center font-medium">
|
||||
<span className={isSame ? 'text-green-700' : 'text-red-700'}>
|
||||
{isSame ? '일치' : '불일치'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-1">{item.changeReason || '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{data.priorNoticeItems.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10} className="px-2 py-4 text-center text-gray-400">
|
||||
검사대상 사전 고지 정보가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 서명 영역 */}
|
||||
<div className="mt-8 text-center text-[10px]">
|
||||
<p>위 내용과 같이 제품검사를 요청합니다.</p>
|
||||
<div className="mt-6">
|
||||
<p>{data.createdDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 제품검사요청서 모달
|
||||
* DocumentViewer를 사용하여 문서 표시 + 인쇄/PDF 기능 제공
|
||||
*/
|
||||
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { InspectionRequestDocument } from './InspectionRequestDocument';
|
||||
import type { InspectionRequestDocument as InspectionRequestDocumentType } from '../types';
|
||||
|
||||
interface InspectionRequestModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
data: InspectionRequestDocumentType | null;
|
||||
}
|
||||
|
||||
export function InspectionRequestModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
}: InspectionRequestModalProps) {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="제품검사요청서"
|
||||
preset="readonly"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
pdfMeta={{
|
||||
documentNumber: data.documentNumber,
|
||||
createdDate: data.createdDate,
|
||||
}}
|
||||
>
|
||||
<InspectionRequestDocument data={data} />
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { InspectionRequestDocument } from './InspectionRequestDocument';
|
||||
export { InspectionRequestModal } from './InspectionRequestModal';
|
||||
export { InspectionReportDocument } from './InspectionReportDocument';
|
||||
export { InspectionReportModal } from './InspectionReportModal';
|
||||
Reference in New Issue
Block a user