From 708743ca0004b2ad8a0ba15a5a6ef02b5d3982c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 20 Mar 2026 23:13:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[worker]=20=EC=A0=88=EA=B3=A1=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9D=BC=EC=A7=80=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20R2=20presigned=20URL=20=EC=A0=84=ED=99=98=20+=20?= =?UTF-8?q?=ED=92=88=EC=A7=88=EA=B2=80=EC=82=AC=203=EA=B1=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 절곡 작업일지: - API bending_images 맵을 받아서 R2 presigned URL로 이미지 로드 - getBendingImageUrl()에 bendingImages 맵 조회 우선, API fallback 유지 - 4개 섹션(가이드레일, 하단마감재, 셔터박스, 연기차단재) 모두 적용 품질검사: - 요약카드 draft 상태 접수 건수 포함 - 검사완료 버튼 미검사/진행중 시 disabled - 완료 상태 수정 버튼 disabled(흐리게) + 편집 모드 진입 차단 --- .../documents/BendingWorkLogContent.tsx | 7 +++- .../documents/bending/BottomBarSection.tsx | 5 +-- .../documents/bending/GuideRailSection.tsx | 9 ++--- .../documents/bending/ShutterBoxSection.tsx | 9 ++--- .../documents/bending/SmokeBarrierSection.tsx | 5 +-- .../WorkOrders/documents/bending/utils.ts | 33 ++++++++++++++++++- .../production/WorkerScreen/WorkLogModal.tsx | 8 ++++- .../production/WorkerScreen/actions.ts | 2 ++ 8 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx index 99f90d10..ea74e6d5 100644 --- a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx @@ -30,9 +30,10 @@ import { ProductionSummarySection } from './bending/ProductionSummarySection'; interface BendingWorkLogContentProps { data: WorkOrder; lotNoMap?: Record; // BD-{prefix}-{lengthCode} → LOT NO + bendingImages?: Record; // R2 presigned URL 맵 } -export function BendingWorkLogContent({ data: order, lotNoMap }: BendingWorkLogContentProps) { +export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: BendingWorkLogContentProps) { const today = new Date().toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', @@ -166,19 +167,23 @@ export function BendingWorkLogContent({ data: order, lotNoMap }: BendingWorkLogC mapping={mapping} lotNo={order.lotNo} lotNoMap={lotNoMap} + bendingImages={bendingImages} /> ) : ( diff --git a/src/components/production/WorkOrders/documents/bending/BottomBarSection.tsx b/src/components/production/WorkOrders/documents/bending/BottomBarSection.tsx index 75eb6c2f..18863e72 100644 --- a/src/components/production/WorkOrders/documents/bending/BottomBarSection.tsx +++ b/src/components/production/WorkOrders/documents/bending/BottomBarSection.tsx @@ -14,9 +14,10 @@ interface BottomBarSectionProps { bendingInfo: BendingInfoExtended; mapping: MaterialMapping; lotNoMap?: Record; + bendingImages?: Record; } -export function BottomBarSection({ bendingInfo, mapping, lotNoMap }: BottomBarSectionProps) { +export function BottomBarSection({ bendingInfo, mapping, lotNoMap, bendingImages }: BottomBarSectionProps) { const rows = buildBottomBarRows(bendingInfo.bottomBar, mapping, bendingInfo.productCode); if (rows.length === 0) return null; @@ -30,7 +31,7 @@ export function BottomBarSection({ bendingInfo, mapping, lotNoMap }: BottomBarSe {/* 좌측: 이미지 */}
하단마감재 diff --git a/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx b/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx index 5be79ff5..97bfa3db 100644 --- a/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx +++ b/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx @@ -14,7 +14,8 @@ interface GuideRailSectionProps { bendingInfo: BendingInfoExtended; mapping: MaterialMapping; lotNo: string; - lotNoMap?: Record; // BD-{prefix}-{lengthCode} → LOT NO + lotNoMap?: Record; + bendingImages?: Record; } function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }: { @@ -76,7 +77,7 @@ function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }: ); } -export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap }: GuideRailSectionProps) { +export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap, bendingImages }: GuideRailSectionProps) { const { wall, side } = bendingInfo.guideRail; const productCode = bendingInfo.productCode; @@ -100,7 +101,7 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap }: Guid ; + bendingImages?: Record; } -function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; index: number; lotNoMap?: Record }) { +function ShutterBoxSubSection({ box, index, lotNoMap, bendingImages }: { box: ShutterBoxData; index: number; lotNoMap?: Record; bendingImages?: Record }) { const rows = buildShutterBoxRows(box); if (rows.length === 0) return null; @@ -37,7 +38,7 @@ function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; i
{`셔터박스 @@ -92,7 +93,7 @@ function ShutterBoxSubSection({ box, index, lotNoMap }: { box: ShutterBoxData; i ); } -export function ShutterBoxSection({ bendingInfo, lotNoMap }: ShutterBoxSectionProps) { +export function ShutterBoxSection({ bendingInfo, lotNoMap, bendingImages }: ShutterBoxSectionProps) { const boxes = bendingInfo.shutterBox; if (!boxes || boxes.length === 0) return null; @@ -103,7 +104,7 @@ export function ShutterBoxSection({ bendingInfo, lotNoMap }: ShutterBoxSectionPr
{boxes.map((box, idx) => ( - + ))}
); diff --git a/src/components/production/WorkOrders/documents/bending/SmokeBarrierSection.tsx b/src/components/production/WorkOrders/documents/bending/SmokeBarrierSection.tsx index 8e6f1eb9..123ea478 100644 --- a/src/components/production/WorkOrders/documents/bending/SmokeBarrierSection.tsx +++ b/src/components/production/WorkOrders/documents/bending/SmokeBarrierSection.tsx @@ -14,9 +14,10 @@ import { buildSmokeBarrierRows, getBendingImageUrl, fmt, fmtWeight, lookupLotNo interface SmokeBarrierSectionProps { bendingInfo: BendingInfoExtended; lotNoMap?: Record; + bendingImages?: Record; } -export function SmokeBarrierSection({ bendingInfo, lotNoMap }: SmokeBarrierSectionProps) { +export function SmokeBarrierSection({ bendingInfo, lotNoMap, bendingImages }: SmokeBarrierSectionProps) { const rows = buildSmokeBarrierRows(bendingInfo.smokeBarrier); if (rows.length === 0) return null; @@ -30,7 +31,7 @@ export function SmokeBarrierSection({ bendingInfo, lotNoMap }: SmokeBarrierSecti {/* 좌측: 이미지 */}
연기차단재 diff --git a/src/components/production/WorkOrders/documents/bending/utils.ts b/src/components/production/WorkOrders/documents/bending/utils.ts index 3bcc51ef..e948f55e 100644 --- a/src/components/production/WorkOrders/documents/bending/utils.ts +++ b/src/components/production/WorkOrders/documents/bending/utils.ts @@ -127,11 +127,42 @@ export function getMaterialMapping(productCode: string, finishMaterial: string): const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.sam.kr'; -export function getBendingImageUrl( +/** + * 절곡 이미지 키 생성 (bending_images 맵 조회용) + */ +export function getBendingImageKey( category: 'guiderail' | 'bottombar' | 'smokebarrier' | 'box', productCode: string, type?: 'wall' | 'side' | 'both' | 'bottom' | 'rear', ): string { + switch (category) { + case 'guiderail': { + const isLargeProfile = ['KQTS01', 'KTE01'].includes(productCode); + const size = isLargeProfile + ? (type === 'wall' ? '130x75' : '130x125') + : (type === 'wall' ? '120x70' : '120x120'); + return `guiderail_${productCode}_${type}_${size}`; + } + case 'bottombar': + return `bottombar_${productCode}`; + case 'smokebarrier': + return 'smokeban'; + case 'box': + return `box_${type || 'both'}`; + default: + return ''; + } +} + +export function getBendingImageUrl( + category: 'guiderail' | 'bottombar' | 'smokebarrier' | 'box', + productCode: string, + type?: 'wall' | 'side' | 'both' | 'bottom' | 'rear', + bendingImages?: Record, +): string { + const key = getBendingImageKey(category, productCode, type); + if (bendingImages?.[key]) return bendingImages[key]; + // fallback: API 서버 직접 (레거시) switch (category) { case 'guiderail': { const isLargeProfile = ['KQTS01', 'KTE01'].includes(productCode); diff --git a/src/components/production/WorkerScreen/WorkLogModal.tsx b/src/components/production/WorkerScreen/WorkLogModal.tsx index 6723d4a7..34aa98ee 100644 --- a/src/components/production/WorkerScreen/WorkLogModal.tsx +++ b/src/components/production/WorkerScreen/WorkLogModal.tsx @@ -63,6 +63,7 @@ export function WorkLogModal({ const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const [bendingImages, setBendingImages] = useState>({}); const contentWrapperRef = useRef(null); // Lazy Snapshot 대상 문서 ID const [snapshotDocumentId, setSnapshotDocumentId] = useState(null); @@ -129,6 +130,10 @@ export function WorkLogModal({ if (lotsResult.success) { setMaterialLots(lotsResult.data); } + // bending_images 맵 저장 + if (workLogResult.success && workLogResult.data?.bending_images) { + setBendingImages(workLogResult.data.bending_images); + } // Lazy Snapshot: 문서가 있고 rendered_html이 없으면 스냅샷 대상 if (workLogResult.success && workLogResult.data?.document) { const doc = workLogResult.data.document as { id?: number; rendered_html?: string | null }; @@ -147,6 +152,7 @@ export function WorkLogModal({ // 모달 닫힐 때 상태 초기화 setOrder(null); setMaterialLots([]); + setBendingImages({}); setSnapshotDocumentId(null); setError(null); } @@ -250,7 +256,7 @@ export function WorkLogModal({ lotNoMap[lot.item_code] = lot.lot_no; } } - return ; + return ; } default: return ; diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts index df71d8ad..99664948 100644 --- a/src/components/production/WorkerScreen/actions.ts +++ b/src/components/production/WorkerScreen/actions.ts @@ -771,6 +771,7 @@ export async function getWorkLog( document: Record | null; auto_values: Record; work_stats: Record; + bending_images: Record; }; error?: string; }> { @@ -779,6 +780,7 @@ export async function getWorkLog( document: Record | null; auto_values: Record; work_stats: Record; + bending_images: Record; }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`, errorMessage: '작업일지 조회에 실패했습니다.',