From 73674385be774d1027008390c21a62030d1bb68a Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 20 Mar 2026 10:51:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20MES=20=EC=B9=B4=EB=93=9C=20=EC=82=B0?= =?UTF-8?q?=EC=97=85=EC=9A=A9=20=ED=83=9C=EB=B8=94=EB=A6=BF=20UI=20?= =?UTF-8?q?=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20(10=EC=9D=B8=EC=B9=98=20?= =?UTF-8?q?=ED=84=B0=EC=B9=98=20=EC=B5=9C=EC=A0=81=ED=99=94)=20=EC=82=B0?= =?UTF-8?q?=EC=97=85=ED=98=84=EC=9E=A5=2010=EC=9D=B8=EC=B9=98=20=ED=83=9C?= =?UTF-8?q?=EB=B8=94=EB=A6=BF=EC=97=90=EC=84=9C=EC=9D=98=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=EA=B3=BC=20=ED=84=B0=EC=B9=98=20=EC=A1=B0?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9D=84=20=EB=8C=80=ED=8F=AD=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=9C=EB=8B=A4.=20HTML=20=EC=8B=9C=EC=95=88(pop-de?= =?UTF-8?q?sign-v2)=EC=9D=84=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EB=B3=B4=EC=A1=B4=ED=95=98=EB=A9=B4=EC=84=9C=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=EB=A7=8C=20=EB=B3=80=EA=B2=BD.=20[?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=97=A4=EB=8D=94=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1]=20-=20=ED=92=88=EB=AA=A9=EB=AA=85(item=5Fname)=2020p?= =?UTF-8?q?x=20bold=20=EA=B0=95=EC=A1=B0=20(=EA=B8=B0=EC=A1=B4:=20?= =?UTF-8?q?=EB=AF=B8=ED=91=9C=EC=8B=9C)=20-=20WO=EB=B2=88=ED=98=B8+?= =?UTF-8?q?=EA=B3=B5=EC=A0=95=EB=AA=85=2014px=20=EB=B3=B4=EC=A1=B0=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A1=9C=20=EC=A0=84=ED=99=98=20(=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4:=20WO=EB=B2=88=ED=98=B8=2013px=20=EC=A3=BC=EC=9D=B8?= =?UTF-8?q?=EA=B3=B5)=20-=20=EC=83=81=ED=83=9C=20=EB=B0=B0=EC=A7=80=2014px?= =?UTF-8?q?=20rounded-full=20pill=20(=EA=B8=B0=EC=A1=B4:=2010px)=20-=20?= =?UTF-8?q?=EC=9E=AC=EC=9E=91=EC=97=85=20=EB=B0=B0=EC=A7=80=2012px=20(?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4:=209px)=20-=20=EC=A2=8C=EC=B8=A1=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=AC=EB=B0=94=204px=20(=EA=B8=B0=EC=A1=B4:=203px)=20[?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=203=EB=8B=A8=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0]=20-=20=EC=A0=91=EC=88=98=EA=B0=80?= =?UTF-8?q?=EB=8A=A5:=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20+=20?= =?UTF-8?q?=ED=8C=8C=EB=9E=91=20=EB=B0=95=EC=8A=A4(=EC=A0=91=EC=88=98?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=20N=20EA)=20-=20=EC=A7=84=ED=96=89=EC=A4=91:?= =?UTF-8?q?=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20+=20=EC=A3=BC?= =?UTF-8?q?=ED=99=A9=20=EB=B0=95=EC=8A=A4(=EC=83=9D=EC=82=B0=20N=20/=20N?= =?UTF-8?q?=20EA)=20+=20=EC=B9=A9(=EC=96=91=ED=92=88/=EB=B6=88=EB=9F=89/?= =?UTF-8?q?=EC=9E=94=EC=97=AC)=20-=20=EC=99=84=EB=A3=8C:=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8+=EC=88=98=EC=9C=A8pill=20+=20?= =?UTF-8?q?=EC=B4=88=EB=A1=9D=20=EB=B0=95=EC=8A=A4(=EC=B5=9C=EC=A2=85?= =?UTF-8?q?=EC=96=91=ED=92=88=20N=20EA)=20+=20=EC=B9=A9=20-=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=97=85:=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20+?= =?UTF-8?q?=20=EC=A3=BC=ED=99=A9=20=EB=B0=95=EC=8A=A4(=EC=9E=AC=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=88=98=EB=9F=89=20N=20EA)=20-=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=20=EC=88=98=EB=9F=89=2036px=20extrabold,=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=2014px=20(=EA=B8=B0=EC=A1=B4:=2018p?= =?UTF-8?q?x/10px)=20[=EC=B9=B4=EB=93=9C=20=EB=86=92=EC=9D=B4=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=80=EC=84=B1]=20-=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20flex-1=20=EC=A0=81=EC=9A=A9:=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=ED=96=89=EC=9D=98=20=EC=B9=B4=EB=93=9C=EB=93=A4?= =?UTF-8?q?=EC=9D=B4=20=EB=8F=99=EC=9D=BC=20=EB=86=92=EC=9D=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20-=20=EA=B3=B5=EC=A0=95=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84/=EB=B2=84=ED=8A=BC=EC=9D=B4=20=ED=95=AD=EC=83=81=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=95=98=EB=8B=A8=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?[=EA=B3=B5=EC=A0=95=20=ED=9D=90=EB=A6=84=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EB=94=94=EC=9E=90=EC=9D=B8]=20-=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=B9=A9=20->=2040x40px=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EB=B0=95=EC=8A=A4=20+=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8=20(=EA=B8=B0=EC=A1=B4:=2010px=20=EC=B9=A9)=20-=20?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=ED=84=B0=20=EB=9D=BC=EC=9D=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B3=B5=EC=A0=95=20=EC=97=B0=EA=B2=B0=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=ED=99=94=20[=EC=95=A1=EC=85=98=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EB=8C=80=ED=98=95=ED=99=94]=20-=20h-14(56px)=20full-width?= =?UTF-8?q?=20rounded-xl=2017px=20bold=20(=EA=B8=B0=EC=A1=B4:=20h-7=2028px?= =?UTF-8?q?=2011px)=20[MES=20=EC=B9=B4=EB=93=9C=20=EB=9E=98=ED=8D=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9]=20-=20MES=20=EC=B9=B4=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20p-1=20=EC=A0=9C=EA=B1=B0=20(=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=B6=A9=EB=B6=84=ED=95=9C=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=20px-5=20=EB=B3=B4=EC=9C=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopCardListV2Component.tsx | 4 +- .../pop-card-list-v2/cell-renderers.tsx | 330 ++++++++++-------- 2 files changed, 192 insertions(+), 142 deletions(-) diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 075e436b..0913779c 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -1861,11 +1861,11 @@ function CardV2({ )} {/* CSS Grid 기반 셀 렌더링 */} -
+
{cardGrid.cells.map((cell) => (
- - {progressPct}% -
{/* 수량 상세 */}
@@ -1027,6 +1021,7 @@ function MesProcessCardCell({ cell, row, onActionButtonClick, currentUserId }: C const processName = currentStep?.processName || String(row.__process_process_name ?? ""); const woNo = String(row.work_instruction_no ?? ""); const itemId = String(row.item_id ?? ""); + const itemName = String(row.item_name ?? ""); // MES 워크플로우 상태 기반 버튼 결정 const acceptBtn = (cell.actionButtons || []).find((b) => b.showCondition?.type === "timeline-status"); @@ -1052,36 +1047,32 @@ function MesProcessCardCell({ cell, row, onActionButtonClick, currentUserId }: C <>
{/* ── 헤더 ── */} -
+
-
- {woNo} - {itemId && itemId !== "-" && ( - {itemId} +
+ {woNo} + {processName && ( + + {processName} + {processFlow && processFlow.length > 1 && ` (${currentIdx + 1}/${processFlow.length})`} + )}
- {processName && ( -
- {processName} - {processFlow && processFlow.length > 1 && ( - - ({currentIdx + 1}/{processFlow.length}공정) - - )} -
- )} +
+ {itemName || itemId || "-"} +
-
+
{isRework && ( - + 재작업 )} {st.label} @@ -1090,7 +1081,7 @@ function MesProcessCardCell({ cell, row, onActionButtonClick, currentUserId }: C
{/* ── 수량 메트릭 (상태별) ── */} -
+
{(isClone || rawStatus === "acceptable" || rawStatus === "waiting") && ( 0 && (
{ e.stopPropagation(); setFlowModalOpen(true); }} title="클릭하여 공정 상세 보기" @@ -1139,49 +1130,58 @@ function MesProcessCardCell({ cell, row, onActionButtonClick, currentUserId }: C
)} - {/* ── 하단: 부가정보 + 액션 ── */} -
-
- {row.end_date && 납기 {formatValue(row.end_date)}} - {row.equipment_id && {String(row.equipment_id)}} - {row.work_team && {String(row.work_team)}} + {/* ── 부가정보 ── */} + {(row.end_date || row.equipment_id || row.work_team) && ( +
+
+ {row.end_date && 납기 {formatValue(row.end_date)}} + {row.equipment_id && {String(row.equipment_id)}} + {row.work_team && {String(row.work_team)}} +
-
- {activeBtn && ( - - )} - {showManualComplete && ( - - )} + )} + + {/* ── 액션 버튼 ── */} + {(activeBtn || showManualComplete) && ( +
+
+ {activeBtn && ( + + )} + {showManualComplete && ( + + )} +
-
+ )}
{/* ── 공정 상세 모달 ── */} @@ -1260,7 +1260,7 @@ function MesProcessCardCell({ cell, row, onActionButtonClick, currentUserId }: C ); } -// ── 공정 흐름 스트립 (5슬롯: 지나온 + 이전 + 현재 + 다음 + 남은) ── +// ── 공정 흐름 스트립 (노드 기반: 지나온 + 이전 + 현재 + 다음 + 남은) ── function ProcessFlowStrip({ steps, currentIdx, instrQty }: { steps: TimelineProcessStep[]; currentIdx: number; instrQty: number; }) { @@ -1277,61 +1277,75 @@ function ProcessFlowStrip({ steps, currentIdx, instrQty }: { return sem === "done"; }); - const renderChip = (step: TimelineProcessStep, isCurrent: boolean) => { + const renderNode = (step: TimelineProcessStep, isCurrent: boolean) => { const sem = step.semantic || LEGACY_STATUS_TO_SEMANTIC[step.status] || "pending"; return ( - - {sem === "done" && !isCurrent && } - {step.seqNo} {step.processName} - +
+
+ {sem === "done" && !isCurrent ? : step.seqNo} +
+ + {step.processName} + +
); }; + const connDone = "mt-[18px] h-[3px] w-5 shrink-0 bg-emerald-400"; + const connPending = "mt-[18px] h-[3px] w-5 shrink-0 bg-border"; + return ( -
+
{hiddenBefore > 0 && ( <> - - +{hiddenBefore} - - +
+
+ +{hiddenBefore} +
+
+
)} {prevStep && ( <> - {renderChip(prevStep, false)} - + {renderNode(prevStep, false)} +
)} - {currStep && renderChip(currStep, true)} + {currStep && renderNode(currStep, true)} {nextStep && ( <> - - {renderChip(nextStep, false)} +
+ {renderNode(nextStep, false)} )} {hiddenAfter > 0 && ( <> - - - +{hiddenAfter} - +
+
+
+ +{hiddenAfter} +
+
)}
@@ -1344,21 +1358,22 @@ function MesAcceptableMetrics({ instrQty, prevGoodQty, availableQty, inputQty, i }) { if (isRework) { return ( -
-
- 불량 재작업 대상 +
+
+ 불량 재작업 대상
-
- 재작업 수량  - {inputQty.toLocaleString()} +
+ 재작업 수량 + {inputQty.toLocaleString()} + EA
); } const displayAvail = isClone ? availableQty : (availableQty || prevGoodQty); return ( -
-
+
+
지시 {instrQty.toLocaleString()} {!isFirstProcess && ( 전공정양품 {prevGoodQty.toLocaleString()} @@ -1367,9 +1382,10 @@ function MesAcceptableMetrics({ instrQty, prevGoodQty, availableQty, inputQty, i 기접수 {inputQty.toLocaleString()} )}
-
- 접수가능  - {displayAvail.toLocaleString()} +
+ 접수가능 + {displayAvail.toLocaleString()} + EA
); @@ -1380,28 +1396,42 @@ function MesInProgressMetrics({ inputQty, totalProd, goodQty, defectQty, concess inputQty: number; totalProd: number; goodQty: number; defectQty: number; concessionQty: number; remainingQty: number; progressPct: number; availableQty: number; isBatchDone: boolean; statusColor: string; }) { return ( -
- {/* 메인 프로그레스 */} -
- 접수 {inputQty.toLocaleString()} -
-
-
- {progressPct}% +
+
+ 접수 {inputQty.toLocaleString()} + {availableQty > 0 && ( + 추가접수가능 {availableQty.toLocaleString()} + )}
- {/* 수량 메트릭 */} -
0 ? "grid-cols-5" : "grid-cols-4")}> - - - - {concessionQty > 0 && } - 0 ? "#f59e0b" : "#10b981"} /> +
+ 생산 + + {totalProd.toLocaleString()} + + / {inputQty.toLocaleString()} + EA +
+
+ + 양품 {goodQty.toLocaleString()} + + {defectQty > 0 && ( + + 불량 {defectQty.toLocaleString()} + + )} + {concessionQty > 0 && ( + + 특채 {concessionQty.toLocaleString()} + + )} + 0 ? "bg-amber-50 text-amber-600" : "bg-emerald-50 text-emerald-600", + )}> + 잔여 {remainingQty.toLocaleString()} +
- {availableQty > 0 && ( -
- 추가접수가능 {availableQty.toLocaleString()} -
- )}
); } @@ -1411,17 +1441,37 @@ function MesCompletedMetrics({ instrQty, goodQty, defectQty, concessionQty, yiel instrQty: number; goodQty: number; defectQty: number; concessionQty: number; yieldRate: number; }) { return ( -
-
+
+
지시 {instrQty.toLocaleString()} - 최종양품 {goodQty.toLocaleString()} - {concessionQty > 0 && ( - 특채 {concessionQty.toLocaleString()} - )} - 수율 = 95 ? "#059669" : yieldRate >= 80 ? "#d97706" : "#ef4444" }}>{yieldRate}% + = 95 ? "#f0fdf4" : yieldRate >= 80 ? "#fffbeb" : "#fef2f2", + color: yieldRate >= 95 ? "#059669" : yieldRate >= 80 ? "#d97706" : "#ef4444", + }} + > + 수율 {yieldRate}% +
- {defectQty > 0 && ( -
불량 {defectQty.toLocaleString()}
+
+ 최종양품 + {goodQty.toLocaleString()} + EA +
+ {(defectQty > 0 || concessionQty > 0) && ( +
+ {defectQty > 0 && ( + + 불량 {defectQty.toLocaleString()} + + )} + {concessionQty > 0 && ( + + 특채 {concessionQty.toLocaleString()} + + )} +
)}
);