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 && (
-
-
-
실적이 확정되었습니다
+
+
+ 실적이 확정되었습니다
)}
- {/* 공정 현황: 접수량 / 작업완료 / 잔여 + 앞공정 완료량 */}
-
-
공정 현황
-
-
-
-
{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") && (
-
-
-
+
+
- {(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="수량"
- />
-
-
removeDefectEntry(idx)}>
-
-
+ {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") && (
-
-
-
- )}
-
- {/* 미구현 섹션 플레이스홀더 (순서 보존) */}
- {enabledSections
- .filter((s) => !IMPLEMENTED_SECTIONS.has(s.type))
- .map((s) => (
-
-
-
-
{SECTION_LABELS[s.type] ?? s.type}
-
준비 중인 기능입니다
-
-
- ))}
-
- {/* 등록 버튼 */}
-
-
- {saving ? : }
- 실적 등록
-
-
)}
- {/* 이전 실적 이력 */}
-
-
등록 이력
+ {/* 불량 유형 상세 */}
+ {!isConfirmed && enabledSections.some((s) => s.type === "defect-types") && (
+
+
+
불량 유형 상세
+
+ {totalDefectFromEntries > 0 && (
+
합계: {totalDefectFromEntries}개
+ )}
+
+
+ 유형 추가
+
+
+
+ {defectEntries.length === 0 ? (
+
등록된 불량 유형이 없습니다.
+ ) : (
+
+ {defectEntries.map((entry, idx) => {
+ const dt = defectTypes.find((d) => d.defect_code === entry.defect_code);
+ return (
+
+
+
+
+
+
+ updateDefectEntry(idx, "qty", e.target.value)}
+ placeholder="수량"
+ />
+
+
+
+
+
removeDefectEntry(idx)}
+ >
+
+
+
+ {dt && (
+
+
+ {dt.defect_type}
+
+ 심각도: {dt.severity}
+
+ )}
+
+ );
+ })}
+
+ {/* 불량 합계 */}
+ {defectEntries.length > 0 && (
+
+
불량 유형 합계
+
+ {DISPOSITION_OPTIONS.map((opt) => {
+ const cnt = defectEntries.reduce(
+ (s, e) => s + (e.disposition === opt.value ? (parseInt(e.qty, 10) || 0) : 0), 0
+ );
+ return {opt.label}: {cnt};
+ })}
+ 총 {totalDefectFromEntries}개
+
+
+ )}
+
+ )}
+
+ )}
+
+ {/* 비고 */}
+ {!isConfirmed && enabledSections.some((s) => s.type === "note") && (
+
+ )}
+
+ {/* 미구현 섹션 플레이스홀더 (순서 보존) */}
+ {!isConfirmed && enabledSections
+ .filter((s) => !IMPLEMENTED_SECTIONS.has(s.type))
+ .map((s) => (
+
+
+
+
{SECTION_LABELS[s.type] ?? s.type}
+
준비 중인 기능입니다
+
+
+ ))}
+
+ {/* 등록 버튼 */}
+ {!isConfirmed && (
+
+
+ {saving ? : }
+ 실적 등록 {batchQty ? `(${batchQty}개)` : ""}
+
+
+ )}
+
+ {/* 등록 이력 (테이블 형태 - combined-final) */}
+
+
등록 이력
{historyLoading ? (
-
+
불러오는 중...
) : history.length === 0 ? (
-
등록된 실적이 없습니다.
+
등록된 실적이 없습니다.
) : (
-
- {[...history].reverse().map((h) => (
-
-
- #{h.seq}
- +{h.batch_qty}
- 양품 +{h.batch_good}
- {h.batch_defect > 0 && (
- 불량 +{h.batch_defect}
- )}
-
-
- 누적 {h.accumulated_total}
- {new Date(h.changed_at).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" })}
-
-
- ))}
-
+
+
+
+ | 차수 |
+ 생산수량 |
+ 양품 |
+ 불량 |
+ 누적 |
+ 시각 |
+
+
+
+ {[...history].reverse().map((h, i) => (
+
+ |
+
+ #{h.seq}
+
+ |
+ +{h.batch_qty} |
+ +{h.batch_good} |
+ +{h.batch_defect} |
+ {h.accumulated_total} |
+
+ {new Date(h.changed_at).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" })}
+ |
+
+ ))}
+
+
)}
+
+ {/* 여백 */}
+
{/* 실적 확정 버튼 제거됨 - 자동 완료로 대체 (2026-03-17 결정) */}
@@ -1703,13 +1782,11 @@ interface InfoBarProps {
function InfoBar({ fields, parentRow, processName }: InfoBarProps) {
return (
{fields.map((f) => {
@@ -1717,9 +1794,9 @@ function InfoBar({ fields, parentRow, processName }: InfoBarProps) {
? processName
: parentRow[f.column];
return (
-
-
{f.label}
-
{val != null ? String(val) : "-"}
+
+ {f.label}
+ {val != null ? String(val) : "-"}
);
})}
@@ -1740,30 +1817,38 @@ interface KpiCardsProps {
function KpiCards({ inputQty, completedQty, defectQty }: KpiCardsProps) {
const remaining = Math.max(0, inputQty - completedQty);
const cards = [
- { label: "접수량", value: inputQty, color: COLORS.kpiInput },
- { label: "작업완료", value: completedQty, color: COLORS.kpiComplete },
- { label: "잔여", value: remaining, color: COLORS.kpiRemaining },
- { label: "불량", value: defectQty, color: COLORS.kpiDefect },
+ { label: "접수량", value: inputQty, color: "text-gray-900", labelColor: "text-gray-400" },
+ { label: "완료", value: completedQty, color: "text-blue-600", labelColor: "text-blue-400" },
+ { label: "잔여", value: remaining, color: "text-amber-600", labelColor: "text-amber-400" },
+ { label: "불량", value: defectQty, color: "text-red-600", labelColor: "text-red-400" },
];
return (
-
- {cards.map((c) => (
-
-
- {c.value}
-
-
- {c.label}
-
-
+
+ {cards.map((c, i) => (
+
+ {i > 0 && }
+
+
+ {c.value}
+
+
+ {c.label}
+
+
+
))}
);
@@ -1797,58 +1882,58 @@ function GroupTimerHeader({
onTimerAction,
}: GroupTimerHeaderProps) {
return (
-
+
{/* 그룹 제목 + 진행 카운트 */}
-
+
- {group.title}
-
+ {group.title}
+
{group.completed}/{group.total}
-
+
{isGroupCompleted && (
-
완료
+
완료
)}
{/* 그룹 타이머 */}
{cfg.showTimer && (
-
+
-
- {groupTimerFormatted}
- 작업
+
+ {groupTimerFormatted}
+ 작업
-
-
{groupElapsedFormatted}
-
경과
+
+ {groupElapsedFormatted}
+ 경과
{!isProcessCompleted && !isGroupCompleted && (
{!isGroupStarted && (
-
onTimerAction("start")}>
- 시작
+ onTimerAction("start")}>
+ 시작
)}
{isGroupStarted && !isGroupPaused && (
<>
- onTimerAction("pause")}>
- 정지
+ onTimerAction("pause")}>
+ 정지
- onTimerAction("complete")}>
- 완료
+ onTimerAction("complete")}>
+ 완료
>
)}
{isGroupStarted && isGroupPaused && (
<>
- onTimerAction("resume")}>
- 재개
+ onTimerAction("resume")}>
+ 재개
- onTimerAction("complete")}>
- 완료
+ onTimerAction("complete")}>
+ 완료
>
)}
@@ -1875,6 +1960,29 @@ function StepStatusIcon({ status }: { status: "pending" | "active" | "completed"
}
}
+// ========================================
+// 사이드바 아이템 상태 아이콘 (combined-final)
+// ========================================
+
+function SidebarStepIcon({ status, isSelected }: { status: "pending" | "active" | "completed"; isSelected: boolean }) {
+ if (status === "completed") {
+ return (
+
+ );
+ }
+ if (isSelected || status === "active") {
+ return (
+
+ );
+ }
+ return ;
+}
+
// ========================================
// 체크리스트 행 래퍼 (행 전체 터치 영역 + 상태 표시)
// ========================================