feat(WEB):작업일지 공정관리 양식 연동 및 자재 LOT 동적화
- WorkLogModal: workLogTemplateId/Name prop 추가, 공정관리 매핑 기반 콘텐츠 분기 - WorkerScreen: activeProcessSettings에 workLogTemplateId/Name 추가 - ScreenWorkLogContent: 내화실 LOT 하드코딩 → materialLots item_name별 동적 그룹핑 - SlatWorkLogContent/BendingWorkLogContent: 소규모 수정
This commit is contained in:
@@ -147,7 +147,7 @@ export function BendingWorkLogContent({ data: order }: BendingWorkLogContentProp
|
||||
<td className="border border-gray-400 p-2">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{item.specification || '-'}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{item.quantity}</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -3,24 +3,108 @@
|
||||
/**
|
||||
* 스크린 작업일지 문서 콘텐츠
|
||||
*
|
||||
* 기획서 스크린샷 기준 구성:
|
||||
* - 헤더: "작업일지 (스크린)" + 문서번호/작성일자 + 결재란(작성/승인/승인/승인)
|
||||
* 기존 시스템(5130/output/viewScreenWork.php) 기준 구성:
|
||||
* - 헤더: "작업일지 (스크린)" + 문서번호/작성일자 + 결재란(작성/검토/승인)
|
||||
* - 신청업체(수주일,수주처,담당자,연락처) / 신청내용(현장명,작업일자,제품LOT NO,생산담당자,출고예정일)
|
||||
* - 작업내역 테이블: No, 입고 LOT NO, 제품명, 부호, 제작사이즈(가로/세로), 나머지 높이,
|
||||
* 규격(매수)(1220/900/600/400/300), 제작, 재단 사항, 잔량, 완료
|
||||
* 규격(매수) 6열({기준폭}/900/800/600/400/300)
|
||||
* - 합계
|
||||
* - 내화실 입고 LOT NO
|
||||
* - 비고
|
||||
*
|
||||
* 계산 로직 (5130/output/common/function.php calculateCutSize):
|
||||
* 1. 기준폭: 실리카=1220, 와이어=1180, 화이바=1200
|
||||
* 2. 시접: makeVertical = height + 140 + floor(height/기준폭) * 40
|
||||
* 3. 절단매수(firstCut) = floor(makeVertical / 기준폭)
|
||||
* 4. 나머지높이 = makeVertical - (firstCut * 기준폭)
|
||||
* 5. 나머지 > 임계치 → firstCut++
|
||||
* 6. 나머지높이로 규격 분류 (범위별 0 or 1)
|
||||
*/
|
||||
|
||||
import type { WorkOrder } from '../types';
|
||||
import type { WorkOrder, WorkOrderItem } from '../types';
|
||||
import { SectionHeader, ConstructionApprovalTable } from '@/components/document-system';
|
||||
|
||||
// ===== 절단 계산 로직 (기존 시스템 calculateCutSize 이식) =====
|
||||
|
||||
type FabricType = '실리카' | '와이어' | '화이바';
|
||||
|
||||
interface FabricConfig {
|
||||
width: number; // 기준폭 (mm)
|
||||
threshold: number; // 추가 절단 임계치
|
||||
ranges: Record<string, [number, number]>; // 규격별 분류 범위 [min, max]
|
||||
}
|
||||
|
||||
const FABRIC_CONFIG: Record<FabricType, FabricConfig> = {
|
||||
'실리카': {
|
||||
width: 1220, threshold: 940,
|
||||
ranges: { '900': [841, 940], '800': [641, 840], '600': [441, 640], '400': [341, 440], '300': [1, 340] },
|
||||
},
|
||||
'와이어': {
|
||||
width: 1180, threshold: 900,
|
||||
ranges: { '900': [801, 900], '800': [601, 800], '600': [401, 600], '400': [301, 400], '300': [1, 300] },
|
||||
},
|
||||
'화이바': {
|
||||
width: 1200, threshold: 924,
|
||||
ranges: { '900': [825, 924], '800': [625, 824], '600': [425, 624], '400': [325, 424], '300': [1, 324] },
|
||||
},
|
||||
};
|
||||
|
||||
interface CutResult {
|
||||
firstCut: number; // 기준폭 매수
|
||||
remaining: number; // 나머지 높이
|
||||
sizes: Record<string, number>; // { '900': 0|1, '800': 0|1, ... }
|
||||
}
|
||||
|
||||
function detectFabricType(productName: string): FabricType {
|
||||
if (productName.includes('실리')) return '실리카';
|
||||
if (productName.includes('화이')) return '화이바';
|
||||
return '와이어'; // 기본값
|
||||
}
|
||||
|
||||
function calculateCutSize(fabricType: FabricType, height: number): CutResult {
|
||||
const cfg = FABRIC_CONFIG[fabricType];
|
||||
const { width, threshold, ranges } = cfg;
|
||||
|
||||
// 시접 계산
|
||||
const makeVertical = height + 140 + Math.floor(height / width) * 40;
|
||||
|
||||
// 기본 절단 횟수
|
||||
let firstCut = Math.floor(makeVertical / width);
|
||||
|
||||
// 나머지 높이
|
||||
const remaining = makeVertical - (firstCut * width);
|
||||
|
||||
// 임계치 초과 시 추가 절단
|
||||
if (remaining > threshold) {
|
||||
firstCut++;
|
||||
}
|
||||
|
||||
// 규격별 분류
|
||||
const sizes: Record<string, number> = {};
|
||||
for (const [key, [min, max]] of Object.entries(ranges)) {
|
||||
sizes[key] = (remaining >= min && remaining <= max) ? 1 : 0;
|
||||
}
|
||||
|
||||
return { firstCut, remaining, sizes };
|
||||
}
|
||||
|
||||
// ===== 컴포넌트 =====
|
||||
|
||||
interface MaterialInputLot {
|
||||
lot_no: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
total_qty: number;
|
||||
input_count: number;
|
||||
first_input_at: string;
|
||||
}
|
||||
|
||||
interface ScreenWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
materialLots?: MaterialInputLot[];
|
||||
}
|
||||
|
||||
export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps) {
|
||||
export function ScreenWorkLogContent({ data: order, materialLots = [] }: ScreenWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -37,6 +121,16 @@ export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps)
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
const items = order.items || [];
|
||||
|
||||
// 숫자 천단위 콤마 포맷
|
||||
const fmt = (v?: number) => v != null ? v.toLocaleString() : '-';
|
||||
|
||||
// floorCode에서 부호 추출: "1층/FSS-01" → "FSS-01"
|
||||
const getSymbolCode = (floorCode?: string) => {
|
||||
if (!floorCode || floorCode === '-') return '-';
|
||||
const parts = floorCode.split('/');
|
||||
return parts.length > 1 ? parts.slice(1).join('/') : floorCode;
|
||||
};
|
||||
|
||||
const formattedDueDate = order.dueDate !== '-'
|
||||
? new Date(order.dueDate).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
@@ -45,22 +139,60 @@ export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps)
|
||||
}).replace(/\. /g, '-').replace('.', '')
|
||||
: '-';
|
||||
|
||||
// 규격 사이즈 컬럼
|
||||
const SCREEN_SIZES = ['1220', '900', '600', '400', '300'];
|
||||
// 원단 유형 판별 (첫 번째 아이템 기준)
|
||||
const fabricType = items.length > 0 ? detectFabricType(items[0].productName) : '와이어';
|
||||
const baseWidth = FABRIC_CONFIG[fabricType].width;
|
||||
|
||||
// 규격 사이즈 헤더 (기준폭 + 900/800/600/400/300)
|
||||
const SPEC_SIZES = [String(baseWidth), '900', '800', '600', '400', '300'];
|
||||
|
||||
// 각 아이템별 절단 계산
|
||||
const itemCuts = items.map((item: WorkOrderItem) => {
|
||||
if (!item.height || item.height <= 0) return null;
|
||||
const ft = detectFabricType(item.productName);
|
||||
return calculateCutSize(ft, item.height);
|
||||
});
|
||||
|
||||
// 합계 계산
|
||||
const totals = SPEC_SIZES.reduce<Record<string, number>>((acc, size) => {
|
||||
acc[size] = 0;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
itemCuts.forEach((cut) => {
|
||||
if (!cut) return;
|
||||
totals[String(baseWidth)] += cut.firstCut;
|
||||
for (const key of ['900', '800', '600', '400', '300']) {
|
||||
totals[key] += cut.sizes[key] || 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 투입 LOT 번호 (중복 제거)
|
||||
const lotNoList = materialLots.map(lot => lot.lot_no).filter(Boolean);
|
||||
const lotNoDisplay = lotNoList.length > 0 ? lotNoList.join(', ') : '';
|
||||
|
||||
// 투입 자재 LOT 그룹핑 (item_name별) — 하단 자재별 LOT 섹션용
|
||||
const materialLotGroupMap = new Map<string, string[]>();
|
||||
materialLots.forEach(lot => {
|
||||
const name = lot.item_name || '기타';
|
||||
if (!materialLotGroupMap.has(name)) materialLotGroupMap.set(name, []);
|
||||
materialLotGroupMap.get(name)!.push(lot.lot_no);
|
||||
});
|
||||
const materialLotGroups = Array.from(materialLotGroupMap.entries()).map(([name, lots]) => ({
|
||||
itemName: name,
|
||||
lotNos: lots.filter(Boolean).join(', '),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<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">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
<ConstructionApprovalTable
|
||||
approvers={{ writer: { name: primaryAssignee } }}
|
||||
className="flex-shrink-0"
|
||||
@@ -110,77 +242,90 @@ export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps)
|
||||
|
||||
{/* ===== 작업내역 ===== */}
|
||||
<SectionHeader variant="dark">작업내역</SectionHeader>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<table className="w-full table-fixed border-collapse text-xs mb-6">
|
||||
{/* No. | 입고 LOT NO | 제품명 | 부호 | 가로 | 세로 | 나머지높이 | 규격별 */}
|
||||
<colgroup><col style={{ width: '28px' }} /><col style={{ width: '72px' }} /><col style={{ width: '80px' }} /><col style={{ width: '60px' }} /><col style={{ width: '52px' }} /><col style={{ width: '52px' }} /><col style={{ width: '44px' }} />{SPEC_SIZES.map(s => <col key={s} style={{ width: '40px' }} />)}</colgroup>
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-8" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-2 w-20" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-2" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>부호</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={2}>제작사이즈</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>나머지<br/>높이</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={5}>규격 (매수)</th>
|
||||
<th className="border border-gray-400 p-2 w-14" rowSpan={2}>제작<br/>형태</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>제단<br/>사항</th>
|
||||
<th className="border border-gray-400 p-2 w-14" rowSpan={2}>작업<br/>완료</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>부호</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={2}>제작사이즈(mm)</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>나머지<br/>높이</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={6}>규격 (매수)</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-12">가로</th>
|
||||
<th className="border border-gray-400 p-2 w-12">세로</th>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<th key={size} className="border border-gray-400 p-1 w-10">{size}</th>
|
||||
<th className="border border-gray-400 p-1">가로</th>
|
||||
<th className="border border-gray-400 p-1">세로</th>
|
||||
{SPEC_SIZES.map(size => (
|
||||
<th key={size} className="border border-gray-400 p-1">{size}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-2 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{item.floorCode}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center">-</td>
|
||||
))}
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
</tr>
|
||||
))
|
||||
items.map((item, idx) => {
|
||||
const cut = itemCuts[idx];
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-1 text-center text-[10px]">{lotNoDisplay}</td>
|
||||
<td className="border border-gray-400 p-1 text-[10px]">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap font-bold text-red-600">{fmt(item.width)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap font-bold text-red-600">{fmt(item.height)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center font-bold">{cut && cut.remaining > 0 ? cut.remaining : ''}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">{cut && cut.firstCut > 0 ? cut.firstCut : ''}</td>
|
||||
{['900', '800', '600', '400', '300'].map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center">
|
||||
{cut && cut.sizes[size] > 0 ? cut.sizes[size] : ''}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={15} className="border border-gray-400 p-4 text-center text-gray-400">
|
||||
<td colSpan={13} className="border border-gray-400 p-4 text-center text-gray-400">
|
||||
등록된 품목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* 합계 행 */}
|
||||
<tr className="bg-gray-50 font-medium">
|
||||
<td className="border border-gray-400 p-2 text-center" colSpan={4}>합계</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center">-</td>
|
||||
<tr className="bg-orange-50 font-medium">
|
||||
<td className="border border-gray-400 p-1 text-center" colSpan={7}>합계</td>
|
||||
{SPEC_SIZES.map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center font-bold">
|
||||
{totals[size] > 0 ? totals[size] : ''}
|
||||
</td>
|
||||
))}
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 내화실 입고 LOT NO ===== */}
|
||||
{/* ===== 투입 자재 입고 LOT NO (자재별 동적 행) ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-40">내화실 입고 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2 min-h-[32px]"> </td>
|
||||
</tr>
|
||||
{materialLotGroups.length > 0 ? (
|
||||
materialLotGroups.map((group, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="border border-gray-400 bg-yellow-50 px-3 py-2 font-bold w-40">
|
||||
{group.itemName} 입고 LOT NO
|
||||
</td>
|
||||
<td className="border border-gray-400 px-3 py-2 min-h-[32px]">
|
||||
{group.lotNos || '\u00A0'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-yellow-50 px-3 py-2 font-bold w-40">
|
||||
자재 입고 LOT NO
|
||||
</td>
|
||||
<td className="border border-gray-400 px-3 py-2 min-h-[32px]">{'\u00A0'}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -190,7 +335,7 @@ export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps)
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-40 align-top">비고</td>
|
||||
<td className="border border-gray-400 px-3 py-3 min-h-[60px]">
|
||||
{order.note || ''}
|
||||
{order.note || '사이즈 착오 없이 부탁드립니다.'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -15,11 +15,21 @@
|
||||
import type { WorkOrder } from '../types';
|
||||
import { SectionHeader } from '@/components/document-system';
|
||||
|
||||
interface SlatWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
interface MaterialInputLot {
|
||||
lot_no: string;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
total_qty: number;
|
||||
input_count: number;
|
||||
first_input_at: string;
|
||||
}
|
||||
|
||||
export function SlatWorkLogContent({ data: order }: SlatWorkLogContentProps) {
|
||||
interface SlatWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
materialLots?: MaterialInputLot[];
|
||||
}
|
||||
|
||||
export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -36,6 +46,16 @@ export function SlatWorkLogContent({ data: order }: SlatWorkLogContentProps) {
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
const items = order.items || [];
|
||||
|
||||
// 숫자 천단위 콤마 포맷
|
||||
const fmt = (v?: number) => v != null ? v.toLocaleString() : '-';
|
||||
|
||||
// floorCode에서 부호 추출: "1층/FSS-01" → "FSS-01"
|
||||
const getSymbolCode = (floorCode?: string) => {
|
||||
if (!floorCode || floorCode === '-') return '-';
|
||||
const parts = floorCode.split('/');
|
||||
return parts.length > 1 ? parts.slice(1).join('/') : floorCode;
|
||||
};
|
||||
|
||||
const formattedDueDate = order.dueDate !== '-'
|
||||
? new Date(order.dueDate).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
@@ -44,6 +64,10 @@ export function SlatWorkLogContent({ data: order }: SlatWorkLogContentProps) {
|
||||
}).replace(/\. /g, '-').replace('.', '')
|
||||
: '-';
|
||||
|
||||
// 투입 LOT 번호 (중복 제거)
|
||||
const lotNoList = materialLots.map(lot => lot.lot_no).filter(Boolean);
|
||||
const lotNoDisplay = lotNoList.length > 0 ? lotNoList.join(', ') : '';
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
@@ -128,35 +152,35 @@ export function SlatWorkLogContent({ data: order }: SlatWorkLogContentProps) {
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-8" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-2 w-24" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>방화유리<br/>수량</th>
|
||||
<th className="border border-gray-400 p-2" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={3}>제작사이즈(mm) - 미미제외</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>조인트바<br/>수량</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>코일<br/>사용량</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>설치홈/<br/>부호</th>
|
||||
<th className="border border-gray-400 p-1 w-7" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>방화유리<br/>수량</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={3}>제작사이즈(mm) - 미미제외</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>조인트바<br/>수량</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>코일<br/>사용량</th>
|
||||
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>설치홈/<br/>부호</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-14">가로</th>
|
||||
<th className="border border-gray-400 p-2 w-14">세로</th>
|
||||
<th className="border border-gray-400 p-2 w-14">매수<br/>(세로)</th>
|
||||
<th className="border border-gray-400 p-1 w-14">가로</th>
|
||||
<th className="border border-gray-400 p-1 w-14">세로</th>
|
||||
<th className="border border-gray-400 p-1 w-12">매수<br/>(세로)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-2 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-1 text-center text-[10px]">{lotNoDisplay}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-[10px]">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.width)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.height)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
*
|
||||
* document-system 통합 버전 (2026-01-22)
|
||||
* 공정별 작업일지 지원 (2026-01-29)
|
||||
* 공정관리 양식 매핑 연동 (2026-02-11)
|
||||
* - DocumentViewer 사용
|
||||
* - 공정 타입에 따라 스크린/슬랫/절곡 작업일지 분기
|
||||
* - processType 미지정 시 기존 WorkLogContent (범용) 사용
|
||||
* - 공정관리에서 매핑된 workLogTemplateId/Name 기반으로 콘텐츠 분기
|
||||
* - 양식 미매핑 시 processType 폴백
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { getWorkOrderById } from '../WorkOrders/actions';
|
||||
import { getWorkOrderById, getMaterialInputLots } from '../WorkOrders/actions';
|
||||
import type { MaterialInputLot } from '../WorkOrders/actions';
|
||||
import type { WorkOrder, ProcessType } from '../WorkOrders/types';
|
||||
import { WorkLogContent } from './WorkLogContent';
|
||||
import {
|
||||
@@ -27,10 +29,34 @@ interface WorkLogModalProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workOrderId: string | null;
|
||||
processType?: ProcessType;
|
||||
/** 공정관리에서 매핑된 작업일지 양식 ID */
|
||||
workLogTemplateId?: number;
|
||||
/** 공정관리에서 매핑된 작업일지 양식명 (예: '스크린 작업일지') */
|
||||
workLogTemplateName?: string;
|
||||
}
|
||||
|
||||
export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: WorkLogModalProps) {
|
||||
/**
|
||||
* 양식명 → 공정 타입 매핑
|
||||
* 공정관리에서 매핑된 양식명을 기반으로 콘텐츠 컴포넌트를 결정
|
||||
*/
|
||||
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) {
|
||||
const [order, setOrder] = useState<WorkOrder | null>(null);
|
||||
const [materialLots, setMaterialLots] = useState<MaterialInputLot[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -82,12 +108,18 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
getWorkOrderById(workOrderId)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setOrder(result.data);
|
||||
Promise.all([
|
||||
getWorkOrderById(workOrderId),
|
||||
getMaterialInputLots(workOrderId),
|
||||
])
|
||||
.then(([orderResult, lotsResult]) => {
|
||||
if (orderResult.success && orderResult.data) {
|
||||
setOrder(orderResult.data);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
setError(orderResult.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
if (lotsResult.success) {
|
||||
setMaterialLots(lotsResult.data);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -99,6 +131,7 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W
|
||||
} else if (!open) {
|
||||
// 모달 닫힐 때 상태 초기화
|
||||
setOrder(null);
|
||||
setMaterialLots([]);
|
||||
setError(null);
|
||||
}
|
||||
}, [open, workOrderId, processType]);
|
||||
@@ -108,18 +141,38 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W
|
||||
// 로딩/에러 상태는 DocumentViewer 내부에서 처리
|
||||
const subtitle = order ? `${order.processName} 생산부서` : undefined;
|
||||
|
||||
// 공정 타입에 따라 콘텐츠 분기
|
||||
// 양식 미매핑 안내
|
||||
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>
|
||||
);
|
||||
|
||||
// 공정관리 양식 매핑 기반 콘텐츠 분기
|
||||
const renderContent = () => {
|
||||
if (!order) return null;
|
||||
|
||||
// processType prop 또는 order의 processType 사용
|
||||
const type = processType || order.processType;
|
||||
// 1순위: 공정관리에서 매핑된 양식명으로 결정
|
||||
const templateType = resolveProcessTypeFromTemplate(workLogTemplateName);
|
||||
|
||||
// 2순위: processType 폴백 (양식 미매핑 시)
|
||||
const type = templateType || processType || order.processType;
|
||||
|
||||
// 양식이 매핑되어 있지 않은 경우 안내
|
||||
if (!workLogTemplateId && !processType) {
|
||||
return renderNoTemplate();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'screen':
|
||||
return <ScreenWorkLogContent data={order} />;
|
||||
return <ScreenWorkLogContent data={order} materialLots={materialLots} />;
|
||||
case 'slat':
|
||||
return <SlatWorkLogContent data={order} />;
|
||||
return <SlatWorkLogContent data={order} materialLots={materialLots} />;
|
||||
case 'bending':
|
||||
return <BendingWorkLogContent data={order} />;
|
||||
default:
|
||||
@@ -127,9 +180,12 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W
|
||||
}
|
||||
};
|
||||
|
||||
// 양식명으로 문서 제목 결정
|
||||
const documentTitle = workLogTemplateName || '작업일지';
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="작업일지"
|
||||
title={documentTitle}
|
||||
subtitle={subtitle}
|
||||
preset="inspection"
|
||||
open={open}
|
||||
|
||||
@@ -439,6 +439,8 @@ export default function WorkerScreen() {
|
||||
return {
|
||||
needsWorkLog: process?.needsWorkLog ?? false,
|
||||
hasDocumentTemplate: !!process?.documentTemplateId,
|
||||
workLogTemplateId: process?.workLogTemplateId,
|
||||
workLogTemplateName: process?.workLogTemplateName,
|
||||
};
|
||||
}, [activeTab, processListCache]);
|
||||
|
||||
@@ -1392,6 +1394,8 @@ export default function WorkerScreen() {
|
||||
onOpenChange={setIsWorkLogModalOpen}
|
||||
workOrderId={selectedOrder?.id || null}
|
||||
processType={activeProcessTabKey}
|
||||
workLogTemplateId={activeProcessSettings.workLogTemplateId}
|
||||
workLogTemplateName={activeProcessSettings.workLogTemplateName}
|
||||
/>
|
||||
|
||||
<InspectionReportModal
|
||||
|
||||
Reference in New Issue
Block a user