feat(WEB):작업일지 공정관리 양식 연동 및 자재 LOT 동적화

- WorkLogModal: workLogTemplateId/Name prop 추가, 공정관리 매핑 기반 콘텐츠 분기
- WorkerScreen: activeProcessSettings에 workLogTemplateId/Name 추가
- ScreenWorkLogContent: 내화실 LOT 하드코딩 → materialLots item_name별 동적 그룹핑
- SlatWorkLogContent/BendingWorkLogContent: 소규모 수정
This commit is contained in:
2026-02-12 00:01:03 +09:00
parent 91100ca635
commit fcd5408052
5 changed files with 328 additions and 99 deletions

View File

@@ -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>
))

View File

@@ -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]">&nbsp;</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>

View File

@@ -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>
))
) : (

View File

@@ -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}

View File

@@ -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