diff --git a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx index 2d199537..30fe9a7f 100644 --- a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx @@ -134,22 +134,23 @@ const DEFAULT_CFG: PopWorkDetailConfig = { }; // ======================================== -// ISA-101 디자인 토큰 (산업 터치 기준) +// ISA-101 디자인 토큰 (combined-final 기준) // ======================================== const DESIGN = { button: { height: 56, minWidth: 100 }, input: { height: 56 }, stat: { valueSize: 40, labelSize: 14, weight: 700 }, - section: { titleSize: 16, gap: 20 }, + section: { titleSize: 13, gap: 20 }, tab: { height: 48 }, footer: { height: 64 }, - header: { height: 56 }, - kpi: { valueSize: 40, labelSize: 14, weight: 700 }, + header: { height: 48 }, + kpi: { valueSize: 44, labelSize: 13, weight: 800 }, nav: { height: 56 }, - infoBar: { labelSize: 14, valueSize: 16 }, - defectRow: { height: 56 }, - bg: { page: '#F5F5F5', card: '#FFFFFF', header: '#263238' }, + infoBar: { labelSize: 12, valueSize: 14 }, + defectRow: { height: 44 }, + sidebar: { width: 208 }, + bg: { page: '#F5F5F5', card: '#FFFFFF', header: '#1a1a2e', infoBar: '#1a1a2e' }, } as const; const COLORS = { @@ -707,6 +708,30 @@ export function PopWorkDetailComponent({ await handleTimerAction("complete"); }, [handleTimerAction]); + // 현재 탭의 그룹 목록 + const currentTabGroups = useMemo( + () => (activePhaseTab && !resultTabActive ? groupsByPhase[activePhaseTab] ?? [] : []), + [activePhaseTab, resultTabActive, groupsByPhase] + ); + + // 탭 전환 시 해당 phase의 첫 번째 그룹 자동 선택 + const handlePhaseTabChange = useCallback((phase: string) => { + setActivePhaseTab(phase); + setResultTabActive(false); + const phaseGrps = groupsByPhase[phase]; + if (phaseGrps && phaseGrps.length > 0) { + setSelectedGroupId(phaseGrps[0].itemId); + } + contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + }, [groupsByPhase]); + + const handleResultTabClick = useCallback(() => { + setResultTabActive(true); + setActivePhaseTab(null); + setSelectedGroupId(null); + contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + }, []); + // ======================================== // 안전 장치 // ======================================== @@ -749,37 +774,32 @@ export function PopWorkDetailComponent({ } const selectedGroup = groups.find((g) => g.itemId === selectedGroupId); - // 현재 탭의 그룹 목록 - const currentTabGroups = useMemo( - () => (activePhaseTab && !resultTabActive ? groupsByPhase[activePhaseTab] ?? [] : []), - [activePhaseTab, resultTabActive, groupsByPhase] - ); - - // 탭 전환 시 해당 phase의 첫 번째 그룹 자동 선택 - const handlePhaseTabChange = useCallback((phase: string) => { - setActivePhaseTab(phase); - setResultTabActive(false); - const phaseGrps = groupsByPhase[phase]; - if (phaseGrps && phaseGrps.length > 0) { - setSelectedGroupId(phaseGrps[0].itemId); - } - contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); - }, [groupsByPhase]); - - const handleResultTabClick = useCallback(() => { - setResultTabActive(true); - setActivePhaseTab(null); - setSelectedGroupId(null); - contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); - }, []); - // ======================================== - // 렌더링 + // 렌더링 (combined-final 레이아웃) // ======================================== + const woNo = parentRow?.wo_no ? String(parentRow.wo_no) : ""; + return ( -
- {/* ── 고정 헤더: 작업 정보 ── */} +
+ {/* ── 모달 헤더: 미니멀 ── */} +
+
+

작업 상세

+ {woNo && {woNo}} +
+ +
+ + {/* ── 정보바: 미니멀 다크 (고정) ── */} {cfg.infoBar.enabled && ( 0 ? cfg.infoBar.fields : DEFAULT_INFO_FIELDS} @@ -788,299 +808,360 @@ export function PopWorkDetailComponent({ /> )} - {/* ── KPI 카드 (항상 표시) ── */} - + {/* ── 본문: 사이드바 + 콘텐츠 ── */} +
- {/* ── 탭 바 ── */} -
- {availablePhases.map((phase) => { - const progress = phaseProgress[phase]; - const isActive = !resultTabActive && activePhaseTab === phase; - return ( - - ); - })} - {hasResultSections && ( - - )} -
+ {/* ===== 사이드바 ===== */} +
+
+ {/* 페이즈별 그룹 */} + {availablePhases.map((phase) => { + const phaseGrps = groupsByPhase[phase] || []; + const progress = phaseProgress[phase]; + const allDone = progress && progress.done >= progress.total && progress.total > 0; + const anyActive = phaseGrps.some((g) => g.stepStatus === "active"); - {/* ── 콘텐츠 영역 (스크롤) ── */} -
- {/* 실적 입력 패널 (hidden으로 상태 유지, unmount 방지) */} - {hasResultSections && ( -
- { - setProcessData((prev) => prev ? { ...prev, ...updated } : prev); - publish("process_completed", { workOrderProcessId, status: updated?.status }); - }} - /> -
- )} - - {/* 체크리스트 영역 */} -
- {cfg.displayMode === "step" ? ( - /* ======== 스텝 모드 ======== */ - <> - {showQuantityPanel || allItemsCompleted ? ( - /* 수량 등록 + 공정 완료 화면 */ -
- -

모든 작업 항목이 완료되었습니다

- - {cfg.showQuantityInput && !isProcessCompleted && !hasResultSections && ( -
-

실적 수량 등록

-
-
- 양품 - setGoodQty(e.target.value)} placeholder="0" /> -
-
- 불량 - setDefectQty(e.target.value)} placeholder="0" /> -
- {(parseInt(goodQty, 10) || 0) + (parseInt(defectQty, 10) || 0) > 0 && ( -

- 합계: {(parseInt(goodQty, 10) || 0) + (parseInt(defectQty, 10) || 0)} -

- )} -
+ return ( +
+ {/* 페이즈 헤더 */} +
+
+ {allDone ? ( + + ) : anyActive ? ( + + ) : ( + + )}
- )} + + {cfg.phaseLabels[phase] ?? phase} + + + {progress?.done ?? 0}/{progress?.total ?? 0} + +
- {isProcessCompleted && ( - - 공정이 완료되었습니다 - - )} + {/* 그룹 항목 */} +
+ {phaseGrps.map((g) => { + const isSelected = selectedGroupId === g.itemId && !resultTabActive; + return ( + + ); + })} +
- ) : ( - /* 단계별 항목 표시 */ - <> - {/* 그룹 헤더 + 타이머 + 진행률 */} - {selectedGroup && ( - <> - -
-
- {currentItemIdx + 1} / {currentItems.length} + ); + })} + + {/* 실적 그룹 */} + {hasResultSections && ( +
+
+
+ +
+ 실적 + + {isProcessCompleted ? "확정" : "미확정"} + +
+
+ +
+
+ )} +
+
+ + {/* ===== 메인 콘텐츠 ===== */} +
+ {/* 실적 입력 패널 (hidden으로 상태 유지) */} + {hasResultSections && ( +
+ { + setProcessData((prev) => prev ? { ...prev, ...updated } : prev); + publish("process_completed", { workOrderProcessId, status: updated?.status }); + }} + /> +
+ )} + + {/* 체크리스트 영역 */} +
+ {cfg.displayMode === "step" ? ( + /* ======== 스텝 모드 ======== */ + <> + {showQuantityPanel || allItemsCompleted ? ( +
+ +

모든 작업 항목이 완료되었습니다

+ {cfg.showQuantityInput && !isProcessCompleted && !hasResultSections && ( +
+

실적 수량 등록

+
+
+ 양품 + setGoodQty(e.target.value)} placeholder="0" /> +
+
+ 불량 + setDefectQty(e.target.value)} placeholder="0" /> +
+ {(parseInt(goodQty, 10) || 0) + (parseInt(defectQty, 10) || 0) > 0 && ( +

+ 합계: {(parseInt(goodQty, 10) || 0) + (parseInt(defectQty, 10) || 0)} +

+ )}
-
-
0 && selectedGroup.total > 0 ? ((selectedGroup.completed / selectedGroup.total) * 100) : 0}%`, - }} +
+ )} + {isProcessCompleted && ( + + 공정이 완료되었습니다 + + )} +
+ ) : ( + <> + {/* 그룹 헤더 + 타이머 + 진행률 */} + {selectedGroup && ( + <> + +
+
+ {currentItemIdx + 1} / {currentItems.length} +
+
+
0 && selectedGroup.total > 0 ? ((selectedGroup.completed / selectedGroup.total) * 100) : 0}%`, + }} + /> +
+
+ + )} + + {/* 현재 항목 1개 표시 */} +
+ {currentItems[currentItemIdx] && ( +
+ {currentItems[currentItemIdx].started_at && ( +
+ + {currentItems[currentItemIdx].recorded_at + ? formatDuration(currentItems[currentItemIdx].started_at!, currentItems[currentItemIdx].recorded_at!) + : "진행 중..."} +
+ )} +
-
- - )} + )} +
- {/* 현재 항목 1개 표시 */} -
- {currentItems[currentItemIdx] && ( -
- {currentItems[currentItemIdx].started_at && ( -
- - {currentItems[currentItemIdx].recorded_at - ? formatDuration(currentItems[currentItemIdx].started_at!, currentItems[currentItemIdx].recorded_at!) - : "진행 중..."} -
+ {/* 스텝 네비게이션 */} +
+ + + + {selectedGroup?.title} ({currentItemIdx + 1}/{currentItems.length}) + + +
+ + )} + + ) : ( + /* ======== 리스트 모드 ======== */ +
+ {/* KPI 카드 (콘텐츠와 함께 스크롤) */} + + + {/* 그룹 헤더 + 타이머 */} + {selectedGroup && ( + + )} + + {/* 체크리스트 콘텐츠 */} +
+ {selectedGroupId && ( +
+ {currentItems.map((item) => ( + -
- )} -
- - {/* 스텝 네비게이션 */} -
- - - - {selectedGroup?.title} ({currentItemIdx + 1}/{currentItems.length}) - - - -
- - )} - - ) : ( - /* ======== 리스트 모드 ======== */ - <> - {/* 탭 내 그룹 목록 표시 (그룹이 여러 개인 경우) */} - {currentTabGroups.length > 1 && ( -
- {currentTabGroups.map((g) => ( - - ))} + ))} +
+ )}
- )} - - {/* 그룹 헤더 + 타이머 */} - {selectedGroup && ( - - )} - - {/* 체크리스트 콘텐츠 */} -
- {selectedGroupId && ( -
- {currentItems.map((item) => ( - - ))} -
- )}
- - )} + )} +
{/* ── 고정 풋터 액션바 ── */} {!isProcessCompleted && (
{/* 일시정지 */} - + {/* 불량등록 */} - + {/* 작업완료 (2단계 확인) */} {!confirmCompleteOpen ? ( - + ) : (
- - +
)}
@@ -1151,10 +1227,10 @@ export function PopWorkDetailComponent({ {isProcessCompleted && (
- + 공정이 완료되었습니다 @@ -1390,274 +1466,277 @@ function ResultPanel({ }; return ( -
-
+
+ {/* KPI 카드 (실적 탭에서도 표시) */} + + +
{/* 확정 상태 배너 */} {isConfirmed && ( -
- - 실적이 확정되었습니다 +
+ + 실적이 확정되었습니다
)} - {/* 공정 현황: 접수량 / 작업완료 / 잔여 + 앞공정 완료량 */} -
-
공정 현황
-
-
-
{inputQty}
-
접수량
-
-
-
{accumulatedTotal}
-
작업완료
-
-
-
0 ? COLORS.warning : COLORS.good} style={{ fontSize: `${DESIGN.stat.valueSize}px`, fontWeight: DESIGN.stat.weight }}> - {remainingQty} -
-
잔여
-
- {availableInfo && availableInfo.availableQty > 0 && ( -
-
{availableInfo.availableQty}
-
추가접수가능
-
- )} -
- {inputQty > 0 && ( -
-
-
- )} - {availableInfo && ( -
- 앞공정 완료: {availableInfo.prevGoodQty} - 지시수량: {availableInfo.instructionQty} -
- )} -
- - {/* 누적 실적 현황 */} -
-
누적 실적
-
-
-
{accumulatedTotal}
-
총생산
-
-
-
{accumulatedGood}
-
양품
-
-
-
{accumulatedDefect}
-
불량
-
-
-
{history.length}
-
차수
-
-
- {accumulatedTotal > 0 && ( -
-
0 ? (accumulatedGood / accumulatedTotal) * 100 : 0}%` }} - /> -
- )} -
- {/* 이번 차수 실적 입력 */} {!isConfirmed && ( -
-
이번 차수 실적
+
+
이번 차수 실적 입력
{/* 생산수량 */} {enabledSections.some((s) => s.type === "total-qty") && ( -
- -
- setBatchQty(e.target.value)} - placeholder="0" - /> - EA -
-
- )} - - {/* 양품/불량 */} - {enabledSections.some((s) => s.type === "good-defect") && ( -
- -
+
+
+
- 양품 - 0 ? String(batchGood) : ""} - readOnly - placeholder="자동" - /> -
-
- 불량 - setBatchDefect(e.target.value)} + className="h-14 w-36 rounded-xl border-2 border-gray-200 px-4 text-center text-2xl font-bold text-gray-900 transition-colors focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" + value={batchQty} + onChange={(e) => setBatchQty(e.target.value)} placeholder="0" /> + EA
- {(parseInt(batchQty, 10) || 0) > 0 && ( -

- 양품 {batchGood} = 생산 {batchQty} - 불량 {batchDefect || 0} -

- )} -
- )} - - {/* 불량 유형 상세 */} - {enabledSections.some((s) => s.type === "defect-types") && ( -
-
- - -
- {defectEntries.length === 0 ? ( -

등록된 불량 유형이 없습니다.

- ) : ( -
- {defectEntries.map((entry, idx) => ( -
- - updateDefectEntry(idx, "qty", e.target.value)} - placeholder="수량" - /> - - + {enabledSections.some((s) => s.type === "good-defect") && ( + <> +
+ + 0 ? String(batchGood) : ""} + readOnly + /> +
+
+ + setBatchDefect(e.target.value)} + placeholder="0" + /> +
+ {(parseInt(batchQty, 10) || 0) > 0 && ( +
+

양품 {batchGood} = 생산 {batchQty} - 불량 {batchDefect || 0}

- ))} -
+ )} + )}
)} - - {/* 비고 */} - {enabledSections.some((s) => s.type === "note") && ( -
- -