From 3249611cfceab3ff1c83275afc9af9bd78b177b3 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 26 Mar 2026 17:25:57 +0900 Subject: [PATCH] =?UTF-8?q?pop-work-detail:=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=20v2=20=EC=A0=84=EB=A9=B4=20=EA=B0=9C=ED=8E=B8=20-=20=EA=B8=80?= =?UTF-8?q?=EB=A1=9C=EC=8B=9C/=EC=9E=85=EC=B2=B4=EA=B0=90=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=8A=A4=ED=83=80=EC=9D=BC=20(GlossyButton=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80)=20-?= =?UTF-8?q?=20=EC=B2=B4=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A2=8C?= =?UTF-8?q?=EC=A0=95=EB=B3=B4/=EC=9A=B0=EC=9E=85=EB=A0=A5=20=EB=B6=84?= =?UTF-8?q?=ED=95=A0=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20(=EC=97=AC?= =?UTF-8?q?=EB=B0=B1=20=EC=B5=9C=EC=86=8C=ED=99=94)=20-=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20sticky=20=EA=B3=A0=EC=A0=95=20+=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91/=EC=9D=BC=EC=8B=9C=EC=A0=95=EC=A7=80/?= =?UTF-8?q?=EC=9E=AC=EA=B0=9C=20=EC=A0=84=ED=99=98=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?-=20=ED=92=8B=ED=84=B0=203=EB=B2=84=ED=8A=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=E2=86=92=20=EA=B0=81=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=ED=95=98=EB=8B=A8=EC=97=90=20=EC=9E=91=EC=97=85=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=B2=84=ED=8A=BC=20=EB=B0=B0=EC=B9=98=20-=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=ED=95=AD=EB=AA=A9=20=EB=AF=B8=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=8B=9C=20=EB=8B=A4=EC=9D=8C=20=EA=B3=B5=EC=A0=95?= =?UTF-8?q?=20=ED=83=AD=20=EC=A0=84=ED=99=98=20=EC=B0=A8=EB=8B=A8=20-=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EA=B8=80=EC=9E=90=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=ED=99=95=EB=8C=80=20(=EB=B2=84=ED=8A=BC=2018px+,=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=EB=AA=85=2015px,=20=ED=83=80=EC=9D=B4=EB=A8=B8=2026px?= =?UTF-8?q?)=20-=20=EB=B0=B0=EA=B2=BD=20=ED=9D=B0=EC=83=89=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopWorkDetailComponent.tsx | 815 ++++++++++++++---- 1 file changed, 658 insertions(+), 157 deletions(-) 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 9cb08dc7..d03b552e 100644 --- a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from "react" import { Loader2, Play, Pause, CheckCircle2, AlertCircle, Timer, Package, ChevronLeft, ChevronRight, Check, X, CircleDot, ClipboardList, - Plus, Trash2, Save, FileCheck, Construction, AlertTriangle, Zap, RefreshCw, + Plus, Trash2, Save, FileCheck, Construction, Zap, RefreshCw, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -139,21 +139,118 @@ const DEFAULT_CFG: PopWorkDetailConfig = { // ======================================== const DESIGN = { - button: { height: 56, minWidth: 100 }, + button: { height: 60, minWidth: 130 }, input: { height: 56 }, - stat: { valueSize: 40, labelSize: 14, weight: 700 }, - section: { titleSize: 13, gap: 20 }, - tab: { height: 48 }, + stat: { valueSize: 40, labelSize: 16, weight: 700 }, + section: { titleSize: 16, gap: 16 }, + tab: { height: 52 }, footer: { height: 64 }, - header: { height: 48 }, - kpi: { valueSize: 44, labelSize: 13, weight: 800 }, + header: { height: 52 }, + kpi: { valueSize: 44, labelSize: 15, weight: 800 }, nav: { height: 56 }, - infoBar: { labelSize: 12, valueSize: 14 }, - defectRow: { height: 44 }, - sidebar: { width: 208 }, + infoBar: { labelSize: 13, valueSize: 16 }, + defectRow: { height: 48 }, + sidebar: { width: 220 }, bg: { page: '#F5F5F5', card: '#FFFFFF', header: '#1a1a2e', infoBar: '#1a1a2e' }, } as const; +// ======================================== +// Glossy 버튼 스타일 (v3-rev2 참고) +// ======================================== + +const glossyBase: React.CSSProperties = { + position: 'relative', + borderRadius: 16, + fontWeight: 800, + cursor: 'pointer', + textShadow: '0 1px 3px rgba(0,0,0,0.3)', + transition: 'all 0.15s ease', + border: 'none', + color: 'white', +}; + +const glossyOverlay: React.CSSProperties = { + content: "''", + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: '50%', + borderRadius: '16px 16px 0 0', + background: 'linear-gradient(to bottom, rgba(255,255,255,0.4), rgba(255,255,255,0.05))', + pointerEvents: 'none', +}; + +const GLOSSY_VARIANTS = { + green: { + background: 'linear-gradient(to bottom, #4ade80, #16a34a)', + boxShadow: '0 4px 12px rgba(34,197,94,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + blue: { + background: 'linear-gradient(to bottom, #60a5fa, #2563eb)', + boxShadow: '0 4px 12px rgba(59,130,246,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + red: { + background: 'linear-gradient(to bottom, #f87171, #dc2626)', + boxShadow: '0 4px 12px rgba(239,68,68,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + teal: { + background: 'linear-gradient(to bottom, #2dd4bf, #0d9488)', + boxShadow: '0 4px 12px rgba(20,184,166,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + purple: { + background: 'linear-gradient(to bottom, #a78bfa, #7c3aed)', + boxShadow: '0 4px 12px rgba(139,92,246,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + yellow: { + background: 'linear-gradient(to bottom, #fbbf24, #d97706)', + boxShadow: '0 4px 12px rgba(245,158,11,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, + gray: { + background: 'linear-gradient(to bottom, #9ca3af, #6b7280)', + boxShadow: '0 4px 12px rgba(107,114,128,0.3), inset 0 1px 0 rgba(255,255,255,0.4)', + }, +} as const; + +type GlossyVariant = keyof typeof GLOSSY_VARIANTS; + +function GlossyButton({ + variant, + children, + onClick, + disabled, + className, + style, +}: { + variant: GlossyVariant; + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; +}) { + const variantStyle = GLOSSY_VARIANTS[variant]; + return ( + + ); +} + const COLORS = { good: 'text-green-600', defect: 'text-red-600', @@ -235,9 +332,6 @@ export function PopWorkDetailComponent({ const [resultTabActive, setResultTabActive] = useState(false); const hasResultSections = !!(cfg.resultSections && cfg.resultSections.some((s) => s.enabled)); - // 2단계 확인 (작업완료 버튼) - const [confirmCompleteOpen, setConfirmCompleteOpen] = useState(false); - // 불량 유형 목록 (부모에서 1회 로드, ResultPanel에 전달) const [cachedDefectTypes, setCachedDefectTypes] = useState([]); @@ -544,14 +638,18 @@ export function PopWorkDetailComponent({ (r) => r.source_work_item_id === selectedGroupId ); const requiredItems = groupItems.filter((r) => r.is_required === "Y"); - const allRequiredDone = requiredItems.length > 0 && requiredItems.every((r) => r.status === "completed"); + const allRequiredDone = requiredItems.length === 0 || requiredItems.every((r) => r.status === "completed"); const allDone = groupItems.every((r) => r.status === "completed"); - if (allRequiredDone || allDone) { + // 필수 항목 미체크 시 다음 공정으로 안 넘어감 + if (!allRequiredDone) return; + + if (allDone) { const idx = groups.findIndex((g) => g.itemId === selectedGroupId); if (idx >= 0 && idx < groups.length - 1) { const nextGroup = groups[idx + 1]; setSelectedGroupId(nextGroup.itemId); + setActivePhaseTab(nextGroup.phase); contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); toast.success(`${groups[idx].title} 완료 → ${nextGroup.title}`); } @@ -783,17 +881,17 @@ export function PopWorkDetailComponent({ return (
- {/* ── 모달 헤더: 미니멀 ── */} + {/* ── 모달 헤더: 글자 크게 ── */}
-

작업 상세

- {woNo && {woNo}} +

작업 상세

+ {woNo && {woNo}}
{cfg.phaseLabels[phase] ?? phase} {progress?.done ?? 0}/{progress?.total ?? 0} @@ -888,11 +988,12 @@ export function PopWorkDetailComponent({ {g.title} @@ -1109,7 +1210,7 @@ export function PopWorkDetailComponent({ defectQty={parseInt(processData?.defect_qty ?? "0", 10) || 0} /> - {/* 그룹 헤더 + 타이머 */} + {/* 그룹 헤더 + 타이머 (sticky) */} {selectedGroup && ( +
{selectedGroupId && ( -
+ <> {currentItems.map((item) => ( ))} -
+ + {/* 각 그룹 하단: 작업완료 버튼 */} + {!isProcessCompleted && selectedGroup && !isGroupCompleted && ( +
+ handleGroupTimerAction("complete")} + onNavigateNext={() => { + // 필수 미완료 시 넘어가지 않음 + const requiredItems = currentItems.filter((r) => r.is_required === "Y"); + const allRequiredDone = requiredItems.length === 0 || requiredItems.every((r) => r.status === "completed"); + if (!allRequiredDone) { + toast.error("필수 항목을 모두 완료해주세요."); + return; + } + // 다음 그룹으로 이동 + const idx = groups.findIndex((g) => g.itemId === selectedGroupId); + if (idx >= 0 && idx < groups.length - 1) { + const nextGroup = groups[idx + 1]; + setSelectedGroupId(nextGroup.itemId); + setActivePhaseTab(nextGroup.phase); + contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + } + }} + /> +
+ )} + )}
@@ -1146,86 +1276,7 @@ export function PopWorkDetailComponent({ - {/* ── 고정 풋터 액션바 ── */} - {!isProcessCompleted && ( -
- {/* 일시정지 */} - - - {/* 불량등록 */} - - - {/* 작업완료 (2단계 확인) */} - {!confirmCompleteOpen ? ( - - ) : ( -
- - -
- )} -
- )} - + {/* 풋터: 공정 완료 상태 표시만 (3버튼 제거 → 각 그룹 하단에 작업완료 배치) */} {isProcessCompleted && (
- {f.label} - {val != null ? String(val) : "-"} + {f.label} + {val != null ? String(val) : "-"}
); })} @@ -2155,60 +2206,49 @@ 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")} style={{ minHeight: 48, minWidth: 120, fontSize: 16 }}> + 시작 + )} {isGroupStarted && !isGroupPaused && ( - <> - - - + onTimerAction("pause")} style={{ minHeight: 48, minWidth: 120, fontSize: 16 }}> + 일시정지 + )} {isGroupStarted && isGroupPaused && ( - <> - - - + onTimerAction("resume")} style={{ minHeight: 48, minWidth: 120, fontSize: 16 }}> + 재개 + )}
)} @@ -2269,34 +2309,167 @@ function ChecklistRowItem({ item, saving, disabled, onSave }: { const isCompleted = item.status === "completed"; const isRequired = item.is_required === "Y"; + // 좌정보/우입력 분할 레이아웃 (여백 최소화) + const leftBg = isCompleted + ? '#d4edda' + : isRequired + ? '#fde8e8' + : '#f0f2f5'; + const leftBorderColor = isCompleted + ? '#b8d8be' + : isRequired + ? '#f5c6c6' + : '#dde0e5'; + + // 범위/기준값 표시 텍스트 + const rangeText = buildRangeText(item); + return (
- {/* 좌측 상태 바 */} -
- {/* 필수 표시 */} - {isRequired && !isCompleted && ( -
- * +
+ {/* 왼쪽: 정보 (항목명 + 기준값/범위) */} +
+
+ + {item.detail_label || item.detail_content} + + {isRequired && !isCompleted && *} +
+ {rangeText && ( +
+ {rangeText} +
+ )} +
+ + {/* 오른쪽: 입력 (측정값, 체크박스, OK/NG) */} +
+
- )} -
-
); } +/** 기준값/범위 텍스트 생성 */ +function buildRangeText(item: WorkResultRow): string { + const parts: string[] = []; + const lower = item.lower_limit; + const upper = item.upper_limit; + const unit = item.unit || ''; + + if (lower && upper) { + parts.push(`${lower}~${upper}${unit ? ' ' + unit : ''}`); + } else if (item.spec_value) { + parts.push(`기준: ${item.spec_value}`); + } + + if (item.is_required === "Y") parts.push("필수"); + + const dt = item.detail_type ?? ""; + if (dt === "check") parts.push("체크"); + else if (dt.startsWith("inspect")) { + const inputType = item.input_type ?? ""; + if (inputType === "ox") parts.push("O/X 판정"); + else if (inputType === "select") parts.push("선택형"); + else if (inputType === "text") parts.push("텍스트"); + } + + return parts.join(" | "); +} + +// ======================================== +// 그룹 하단 작업완료 버튼 +// ======================================== + +function GroupCompleteButton({ + group, + currentItems, + isGroupStarted, + onComplete, + onNavigateNext, +}: { + group: WorkGroup; + currentItems: WorkResultRow[]; + isGroupStarted: boolean; + onComplete: () => void; + onNavigateNext: () => void; +}) { + const [confirmOpen, setConfirmOpen] = useState(false); + const requiredItems = currentItems.filter((r) => r.is_required === "Y"); + const allRequiredDone = requiredItems.length === 0 || requiredItems.every((r) => r.status === "completed"); + const allDone = currentItems.every((r) => r.status === "completed"); + const isDisabled = !isGroupStarted || (!allRequiredDone); + + if (confirmOpen) { + return ( +
+ setConfirmOpen(false)} + style={{ flex: 1, minHeight: 64, fontSize: 18, borderRadius: 16 }} + > + 취소 + + { + setConfirmOpen(false); + onComplete(); + }} + style={{ flex: 2, minHeight: 64, fontSize: 20, borderRadius: 16 }} + > + + 정말 작업완료 + +
+ ); + } + + return ( + { + if (allDone) { + setConfirmOpen(true); + } else { + // 필수만 완료된 경우 → 다음 공정으로 + onNavigateNext(); + } + }} + style={{ width: '100%', minHeight: 64, fontSize: 20, borderRadius: 16 }} + > + + + + {isDisabled + ? `작업완료 (필수 항목 미완료)` + : allDone + ? `작업완료` + : `다음 단계로` + } + + ); +} + // ======================================== // 체크리스트 개별 항목 (라우터) // ======================================== @@ -2349,6 +2522,334 @@ function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) { } } +// ======================================== +// 체크리스트 입력부 (우측 영역 전용 - 분할 레이아웃) +// ======================================== + +function ChecklistItemInput({ item, saving, disabled, onSave }: ChecklistItemProps) { + const isDisabled = disabled || saving; + const dt = item.detail_type ?? ""; + + if (dt.startsWith("inspect")) { + const normalized = { ...item, detail_type: "inspect" } as WorkResultRow; + if (!normalized.input_type && dt.includes("_")) { + const suffix = dt.split("_").slice(1).join("_"); + const typeMap: Record = { numeric: "numeric_range", ox: "ox", text: "text", select: "select" }; + normalized.input_type = typeMap[suffix] ?? suffix; + } + return ; + } + + switch (dt) { + case "check": + return ; + case "input": + return ; + case "procedure": + return ; + case "material": + return ; + case "result": + return ; + case "info": + return {item.detail_label || item.detail_content}; + default: + return 알 수 없는 유형; + } +} + +// === 입력 전용 inspect 라우터 === +function InspectInputRouter(props: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const inputType = props.item.input_type ?? "numeric_range"; + switch (inputType) { + case "ox": + return ; + case "select": + return ; + case "text": + return ; + default: + return ; + } +} + +// === 수치 입력 (우측 전용) === +function InspectNumericInput({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const lower = parseFloat(item.lower_limit ?? ""); + const upper = parseFloat(item.upper_limit ?? ""); + const hasRange = !isNaN(lower) && !isNaN(upper); + + const handleSave = () => { + if (!inputVal || disabled) return; + const numVal = parseFloat(inputVal); + let passed: string | null = null; + if (hasRange) passed = numVal >= lower && numVal <= upper ? "Y" : "N"; + onSave(item.id, inputVal, passed, "completed"); + }; + + return ( + <> + setInputVal(e.target.value)} + onBlur={handleSave} + disabled={disabled} + placeholder="입력" + /> + {item.unit && {item.unit}} + {item.is_passed === "Y" && !saving && PASS} + {item.is_passed === "N" && !saving && FAIL} + {item.status === "completed" && item.is_passed === null && !saving && 완료} + {!item.status || (item.status !== "completed" && !saving) ? ( + + 저장 + + ) : null} + {saving && } + + ); +} + +// === O/X (우측 전용) === +function InspectOXInput({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const criteriaOK = item.spec_value ?? "OK"; + const handleSelect = (value: string) => { + if (disabled) return; + const passed = value === criteriaOK ? "Y" : "N"; + onSave(item.id, value, passed, "completed"); + }; + + return ( + <> + handleSelect("OK")} + disabled={disabled} + style={{ + minHeight: 56, minWidth: 130, fontSize: 20, + ...(item.result_value === "OK" ? { boxShadow: '0 0 0 3px #22c55e, 0 4px 12px rgba(34,197,94,0.3), inset 0 1px 0 rgba(255,255,255,0.4)' } : {}), + }} + > + O + + handleSelect("NG")} + disabled={disabled} + style={{ + minHeight: 56, minWidth: 130, fontSize: 20, + ...(item.result_value === "NG" ? { boxShadow: '0 0 0 3px #ef4444, 0 4px 12px rgba(239,68,68,0.3), inset 0 1px 0 rgba(255,255,255,0.4)' } : {}), + }} + > + X + + {saving && } + + ); +} + +// === 선택형 (우측 전용) === +function InspectSelectInput({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const currentValue = item.result_value ?? ""; + let options: string[] = []; + let passValues: string[] = []; + try { + const parsed = JSON.parse(item.spec_value ?? "{}"); + options = Array.isArray(parsed.options) ? parsed.options : []; + passValues = Array.isArray(parsed.passValues) ? parsed.passValues : []; + } catch { + options = (item.spec_value ?? "").split(",").filter(Boolean); + } + const handleSelect = (value: string) => { + if (disabled) return; + const passed = passValues.includes(value) ? "Y" : "N"; + onSave(item.id, value, passed, "completed"); + }; + + return ( + <> + {options.map((opt) => ( + handleSelect(opt)} + disabled={disabled} + style={{ minHeight: 52, minWidth: 100, fontSize: 16 }} + > + {opt} + + ))} + {saving && } + + ); +} + +// === 텍스트 + 수동판정 (우측 전용) === +function InspectTextInput({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const [judged, setJudged] = useState(item.is_passed); + const handleJudge = (passed: string) => { + if (disabled) return; + setJudged(passed); + onSave(item.id, inputVal || "-", passed, "completed"); + }; + + return ( + <> + setInputVal(e.target.value)} + disabled={disabled} + placeholder="내용 입력" + /> + handleJudge("Y")} + disabled={disabled} + style={{ minHeight: 48, minWidth: 80, fontSize: 15 }} + > + 합격 + + handleJudge("N")} + disabled={disabled} + style={{ minHeight: 48, minWidth: 80, fontSize: 15 }} + > + 불합격 + + {saving && } + + ); +} + +// === 체크박스 (우측 전용) === +function CheckInputOnly({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const checked = item.result_value === "Y"; + return ( + <> + {item.status === "completed" && 완료} + { + const val = v ? "Y" : "N"; + onSave(item.id, val, v ? "Y" : "N", v ? "completed" : "pending"); + }} + /> + {saving && } + + ); +} + +// === 자유입력 (우측 전용) === +function InputOnlyItem({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const inputType = item.input_type === "number" ? "number" : "text"; + const handleBlur = () => { + if (!inputVal || disabled) return; + onSave(item.id, inputVal, null, "completed"); + }; + return ( + <> + setInputVal(e.target.value)} + onBlur={handleBlur} + disabled={disabled} + placeholder="값 입력" + /> + {saving && } + + ); +} + +// === 절차 확인 (우측 전용) === +function ProcedureInputOnly({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const checked = item.result_value === "Y"; + return ( + <> + { + onSave(item.id, v ? "Y" : "N", null, v ? "completed" : "pending"); + }} + /> + 확인 + {saving && } + {item.status === "completed" && !saving && 완료} + + ); +} + +// === 자재/LOT (우측 전용) === +function MaterialInputOnly({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const handleSubmit = () => { + if (!inputVal || disabled) return; + onSave(item.id, inputVal, null, "completed"); + }; + return ( + <> + setInputVal(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + disabled={disabled} + placeholder="LOT/바코드" + /> + + 확인 + + {saving && } + {item.status === "completed" && !saving && 투입} + + ); +} + +// === 실적 입력 (우측 전용) === +function ResultInputOnly({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) { + let savedData: { good?: string; defect?: string; defectType?: string; note?: string } = {}; + try { savedData = JSON.parse(item.result_value ?? "{}"); } catch { savedData = {}; } + const [good, setGood] = useState(savedData.good ?? ""); + const [defect, setDefect] = useState(savedData.defect ?? ""); + const handleSave = () => { + if (disabled) return; + const val = JSON.stringify({ good, defect }); + onSave(item.id, val, null, "completed"); + }; + return ( + <> +
+ 양품 + setGood(e.target.value)} disabled={disabled} placeholder="0" /> +
+
+ 불량 + setDefect(e.target.value)} disabled={disabled} placeholder="0" /> +
+ + 등록 + + {saving && } + + ); +} + // ======================================== // check: 체크박스 // ========================================