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 c5d5f169..3ae24070 100644
--- a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx
+++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx
@@ -263,6 +263,185 @@ const COLORS = {
kpiDefect: 'text-red-600',
} as const;
+// ========================================
+// 숫자 키패드 모달 (체크리스트/자재투입 공용)
+// ========================================
+
+const NUMPAD_KEYS = [
+ { label: "7", action: "7" },
+ { label: "8", action: "8" },
+ { label: "9", action: "9" },
+ { label: "\u2190", action: "backspace" },
+ { label: "4", action: "4" },
+ { label: "5", action: "5" },
+ { label: "6", action: "6" },
+ { label: "C", action: "clear" },
+ { label: "1", action: "1" },
+ { label: "2", action: "2" },
+ { label: "3", action: "3" },
+ { label: ".", action: "dot" },
+];
+
+interface NumericKeypadModalProps {
+ open: boolean;
+ onClose: () => void;
+ onConfirm: (value: string) => void;
+ title?: string;
+ unit?: string;
+ initialValue?: string;
+ lowerLimit?: number;
+ upperLimit?: number;
+}
+
+function NumericKeypadModal({
+ open,
+ onClose,
+ onConfirm,
+ title,
+ unit,
+ initialValue,
+ lowerLimit,
+ upperLimit,
+}: NumericKeypadModalProps) {
+ const [value, setValue] = useState(initialValue || "0");
+ const hasRange = lowerLimit !== undefined && upperLimit !== undefined && !isNaN(lowerLimit) && !isNaN(upperLimit);
+
+ useEffect(() => {
+ if (open) {
+ setValue(initialValue || "0");
+ }
+ }, [open, initialValue]);
+
+ const handleKey = useCallback((action: string) => {
+ setValue((prev) => {
+ switch (action) {
+ case "backspace":
+ return prev.length <= 1 ? "0" : prev.slice(0, -1);
+ case "clear":
+ return "0";
+ case "dot":
+ return prev.includes(".") ? prev : prev + ".";
+ default: {
+ if (prev === "0" && action !== ".") return action;
+ return prev + action;
+ }
+ }
+ });
+ }, []);
+
+ const handleConfirm = () => {
+ const num = parseFloat(value);
+ if (isNaN(num)) return;
+ onConfirm(value);
+ onClose();
+ };
+
+ if (!open) return null;
+
+ const numValue = parseFloat(value);
+ const isOutOfRange = hasRange && !isNaN(numValue) && (numValue < lowerLimit! || numValue > upperLimit!);
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ {title || "숫자 입력"}
+
+
+
+
+
+ {/* Range indicator */}
+ {hasRange && (
+
+ 기준: {lowerLimit} ~ {upperLimit}{unit ? ` ${unit}` : ""}
+
+ )}
+
+ {/* Display */}
+
+
+ {unit && (
+ {unit}
+ )}
+
+
+ {/* Numpad grid */}
+
+ {NUMPAD_KEYS.map((key) => (
+
+ ))}
+ {/* Bottom row: 0 (span 2) + Confirm (span 2) */}
+
+
+
+
+
+
+ );
+}
+
// ========================================
// Props
// ========================================
@@ -2200,6 +2379,65 @@ interface MaterialInputRecord {
recorded_at: string | null;
}
+// === 자재 수량 입력 (터치 키패드) ===
+interface MaterialQtyInputProps {
+ materialId: string;
+ materialName?: string;
+ unit: string;
+ requiredQty: number;
+ value: string;
+ isConfirmed: boolean;
+ onChange: (val: string) => void;
+}
+
+function MaterialQtyInput({
+ materialId: _materialId,
+ materialName,
+ unit,
+ requiredQty,
+ value,
+ isConfirmed,
+ onChange,
+}: MaterialQtyInputProps) {
+ const [keypadOpen, setKeypadOpen] = useState(false);
+
+ if (isConfirmed) {
+ return (
+
+ {value || "-"}
+ {unit}
+
+ );
+ }
+
+ // ±20% 범위
+ const lower = requiredQty * 0.8;
+ const upper = requiredQty * 1.2;
+
+ return (
+
+
+ {unit}
+ setKeypadOpen(false)}
+ onConfirm={(val) => onChange(val)}
+ title={materialName || "자재 투입"}
+ unit={unit}
+ initialValue={value || ""}
+ lowerLimit={lower}
+ upperLimit={upper}
+ />
+
+ );
+}
+
interface MaterialInputSectionProps {
workOrderProcessId: string;
isConfirmed: boolean;
@@ -2369,21 +2607,15 @@ function MaterialInputSection({ workOrderProcessId, isConfirmed }: MaterialInput
{mat.unit || "EA"}
-
- {isConfirmed ? (
- {inputValues[mat.id] || "-"}
- ) : (
- updateInputValue(mat.id, e.target.value)}
- placeholder="0"
- />
- )}
- {mat.unit || "EA"}
-
+ updateInputValue(mat.id, val)}
+ />
|
{status.label && (
@@ -2934,43 +3166,53 @@ function InspectInputRouter(props: { item: WorkResultRow; disabled: boolean; sav
}
}
-// === 수치 입력 (우측 전용) ===
+// === 수치 입력 (우측 전용) — 터치 키패드 모달 ===
function InspectNumericInput({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) {
- const [inputVal, setInputVal] = useState(item.result_value ?? "");
+ const [keypadOpen, setKeypadOpen] = useState(false);
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);
+ const handleConfirm = (val: string) => {
+ const numVal = parseFloat(val);
let passed: string | null = null;
if (hasRange) passed = numVal >= lower && numVal <= upper ? "Y" : "N";
- onSave(item.id, inputVal, passed, "completed");
+ onSave(item.id, val, passed, "completed");
};
+ const displayValue = item.result_value ?? "";
+
return (
<>
- setInputVal(e.target.value)}
- onBlur={handleSave}
+
{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 && }
+ setKeypadOpen(false)}
+ onConfirm={handleConfirm}
+ title={item.detail_label || item.detail_content}
+ unit={item.unit ?? undefined}
+ initialValue={displayValue}
+ lowerLimit={hasRange ? lower : undefined}
+ upperLimit={hasRange ? upper : undefined}
+ />
>
);
}
@@ -3039,7 +3281,10 @@ function InspectSelectInput({ item, disabled, saving, onSave }: { item: WorkResu
variant={currentValue === opt ? "blue" : "gray"}
onClick={() => handleSelect(opt)}
disabled={disabled}
- style={{ minHeight: 52, minWidth: 100, fontSize: 16 }}
+ style={{
+ minHeight: 56, minWidth: 100, fontSize: 16,
+ ...(currentValue === opt ? { boxShadow: '0 0 0 3px #3b82f6, 0 4px 12px rgba(59,130,246,0.3), inset 0 1px 0 rgba(255,255,255,0.4)' } : {}),
+ }}
>
{opt}
@@ -3063,7 +3308,7 @@ function InspectTextInput({ item, disabled, saving, onSave }: { item: WorkResult
<>
setInputVal(e.target.value)}
disabled={disabled}
@@ -3073,7 +3318,7 @@ function InspectTextInput({ item, disabled, saving, onSave }: { item: WorkResult
variant={judged === "Y" ? "green" : "gray"}
onClick={() => handleJudge("Y")}
disabled={disabled}
- style={{ minHeight: 48, minWidth: 80, fontSize: 15 }}
+ style={{ minHeight: 56, minWidth: 90, fontSize: 16 }}
>
합격
@@ -3081,7 +3326,7 @@ function InspectTextInput({ item, disabled, saving, onSave }: { item: WorkResult
variant={judged === "N" ? "red" : "gray"}
onClick={() => handleJudge("N")}
disabled={disabled}
- style={{ minHeight: 48, minWidth: 80, fontSize: 15 }}
+ style={{ minHeight: 56, minWidth: 90, fontSize: 16 }}
>
불합격
@@ -3090,40 +3335,92 @@ function InspectTextInput({ item, disabled, saving, onSave }: { item: WorkResult
);
}
-// === 체크박스 (우측 전용) ===
+// === 체크박스 (우측 전용) — 큰 터치 토글 ===
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");
+ {
+ if (disabled) return;
+ onSave(item.id, "Y", "Y", "completed");
}}
- />
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 110, fontSize: 18,
+ ...(checked ? { 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)' } : {}),
+ }}
+ >
+ 확인
+
+ {
+ if (disabled) return;
+ onSave(item.id, "N", "N", "pending");
+ }}
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 110, fontSize: 18,
+ ...(!checked && item.result_value === "N" ? { 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)' } : {}),
+ }}
+ >
+ 미확인
+
{saving && }
>
);
}
-// === 자유입력 (우측 전용) ===
+// === 자유입력 (우측 전용) — number는 키패드, text는 큰 입력칸 ===
function InputOnlyItem({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) {
+ const isNumber = item.input_type === "number";
const [inputVal, setInputVal] = useState(item.result_value ?? "");
- const inputType = item.input_type === "number" ? "number" : "text";
+ const [keypadOpen, setKeypadOpen] = useState(false);
+
const handleBlur = () => {
if (!inputVal || disabled) return;
onSave(item.id, inputVal, null, "completed");
};
+
+ if (isNumber) {
+ return (
+ <>
+
+ {item.status === "completed" && !saving && 완료}
+ {saving && }
+ setKeypadOpen(false)}
+ onConfirm={(val) => {
+ setInputVal(val);
+ onSave(item.id, val, null, "completed");
+ }}
+ title={item.detail_label || item.detail_content}
+ unit={item.unit ?? undefined}
+ initialValue={inputVal}
+ />
+ >
+ );
+ }
+
return (
<>
setInputVal(e.target.value)}
onBlur={handleBlur}
@@ -3135,22 +3432,26 @@ function InputOnlyItem({ item, disabled, saving, onSave }: { item: WorkResultRow
);
}
-// === 절차 확인 (우측 전용) ===
+// === 절차 확인 (우측 전용) — 큰 터치 토글 ===
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");
+ {
+ if (disabled) return;
+ onSave(item.id, checked ? "N" : "Y", null, checked ? "pending" : "completed");
}}
- />
- 확인
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 130, fontSize: 18,
+ ...(checked ? { 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)' } : {}),
+ }}
+ >
+ {checked ? "확인됨" : "확인"}
+
{saving && }
- {item.status === "completed" && !saving && 완료}
>
);
}
@@ -3218,20 +3519,42 @@ function ResultInputOnly({ item, disabled, saving, onSave }: { item: WorkResultR
function CheckItem({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) {
const checked = item.result_value === "Y";
return (
-
- {
- const val = v ? "Y" : "N";
- onSave(item.id, val, v ? "Y" : "N", v ? "completed" : "pending");
- }}
- />
- {item.detail_label || item.detail_content}
- {item.is_required === "Y" && 필수}
- {saving && }
- {item.status === "completed" && !saving && 완료}
+
+
+ {item.detail_label || item.detail_content}
+ {item.is_required === "Y" && 필수}
+ {saving && }
+
+
+ {
+ if (disabled) return;
+ onSave(item.id, "Y", "Y", "completed");
+ }}
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 130, fontSize: 18,
+ ...(checked ? { 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)' } : {}),
+ }}
+ >
+ 확인
+
+ {
+ if (disabled) return;
+ onSave(item.id, "N", "N", "pending");
+ }}
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 130, fontSize: 18,
+ ...(!checked && item.result_value === "N" ? { 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)' } : {}),
+ }}
+ >
+ 미확인
+
+
);
}
@@ -3257,21 +3580,22 @@ function InspectRouter(props: { item: WorkResultRow; disabled: boolean; saving:
// ===== inspect: 수치(범위) =====
function InspectNumeric({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) {
- const [inputVal, setInputVal] = useState(item.result_value ?? "");
+ const [keypadOpen, setKeypadOpen] = useState(false);
const lower = parseFloat(item.lower_limit ?? "");
const upper = parseFloat(item.upper_limit ?? "");
const hasRange = !isNaN(lower) && !isNaN(upper);
- const handleBlur = () => {
- if (!inputVal || disabled) return;
- const numVal = parseFloat(inputVal);
+ const handleConfirm = (val: string) => {
+ const numVal = parseFloat(val);
let passed: string | null = null;
if (hasRange) {
passed = numVal >= lower && numVal <= upper ? "Y" : "N";
}
- onSave(item.id, inputVal, passed, "completed");
+ onSave(item.id, val, passed, "completed");
};
+ const displayValue = item.result_value ?? "";
+
return (
@@ -3290,12 +3614,32 @@ function InspectNumeric({ item, disabled, saving, onSave }: { item: WorkResultRo
{item.inspection_method}
)}
- setInputVal(e.target.value)} onBlur={handleBlur} disabled={disabled} placeholder="측정값 입력" />
+
{item.unit && {item.unit}}
{saving && }
{item.is_passed === "Y" && !saving && 합격}
{item.is_passed === "N" && !saving && 불합격}
+ setKeypadOpen(false)}
+ onConfirm={handleConfirm}
+ title={item.detail_label || item.detail_content}
+ unit={item.unit ?? undefined}
+ initialValue={displayValue}
+ lowerLimit={hasRange ? lower : undefined}
+ upperLimit={hasRange ? upper : undefined}
+ />
);
}
@@ -3454,8 +3798,9 @@ function InspectText({ item, disabled, saving, onSave }: { item: WorkResultRow;
// ========================================
function InputItem({ item, disabled, saving, onSave }: { item: WorkResultRow; disabled: boolean; saving: boolean; onSave: ChecklistItemProps["onSave"] }) {
+ const isNumber = item.input_type === "number";
const [inputVal, setInputVal] = useState(item.result_value ?? "");
- const inputType = item.input_type === "number" ? "number" : "text";
+ const [keypadOpen, setKeypadOpen] = useState(false);
const handleBlur = () => {
if (!inputVal || disabled) return;
@@ -3466,7 +3811,34 @@ function InputItem({ item, disabled, saving, onSave }: { item: WorkResultRow; di
@@ -3489,15 +3861,20 @@ function ProcedureItem({ item, disabled, saving, onSave }: { item: WorkResultRow
{item.is_required === "Y" && 필수}
- {
- onSave(item.id, v ? "Y" : "N", null, v ? "completed" : "pending");
+ {
+ if (disabled) return;
+ onSave(item.id, checked ? "N" : "Y", null, checked ? "pending" : "completed");
}}
- />
- 확인
+ disabled={disabled}
+ style={{
+ minHeight: 56, minWidth: 130, fontSize: 18,
+ ...(checked ? { 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)' } : {}),
+ }}
+ >
+ {checked ? "확인됨" : "확인"}
+
{saving && }
|