feat: 하드코딩 POP - 체크리스트 토글 + 자재투입 키패드 모달

ChecklistRow:
- detail_type='checklist'/'procedure'/'equip_inspection' → 토글 (확인/미확인)
- detail_type='inspection'/'equip_condition'/'production_result' → 숫자 입력
- judgment_criteria(CAT_JC_01/03) 우선 매핑

MaterialInputSection:
- input → MaterialQtyInputRow (큰 터치 버튼) + MaterialQtyKeypad 모달
- 소수점 지원, 기준값 ±20% 범위 표시
- 범위 외 입력 시 주황색 경고 (차단 아님)
- 기준값 빠른 입력 버튼
This commit is contained in:
SeongHyun Kim
2026-04-07 12:39:55 +09:00
parent 4b62817bf5
commit 2fee120007
@@ -1799,8 +1799,22 @@ function ChecklistRow({
// Inspection type: check limits
const detailType = item.detail_type || "";
const isInspection = detailType === "inspection" || detailType === "number" || detailType.startsWith("inspect");
const isCheckbox = detailType === "checkbox" || detailType === "check";
// 판단기준(judgment_criteria) 우선 → 폴백으로 detail_type 매핑
const jc = (item as ChecklistItem & { judgment_criteria?: string }).judgment_criteria || "";
const isInspection =
jc === "CAT_JC_01" ||
detailType === "inspection" ||
detailType === "number" ||
detailType === "equip_condition" ||
detailType === "production_result" ||
detailType.startsWith("inspect");
const isCheckbox =
jc === "CAT_JC_03" ||
detailType === "checkbox" ||
detailType === "check" ||
detailType === "checklist" ||
detailType === "procedure" ||
detailType === "equip_inspection";
const isPlc = item.input_type === "plc" || detailType === "plc_data";
const hasLimits = !!(item.lower_limit || item.upper_limit);
@@ -2017,6 +2031,125 @@ function buildRangeText(item: ChecklistItem): string {
return parts.join(" | ");
}
/* ================================================================== */
/* Material Quantity Keypad (소수점 지원, 자재 투입용) */
/* ================================================================== */
function MaterialQtyKeypad({
open,
onClose,
onConfirm,
initialValue,
unit,
requiredQty,
itemName,
}: {
open: boolean;
onClose: () => void;
onConfirm: (val: string) => void;
initialValue: string;
unit: string;
requiredQty: number;
itemName: string;
}) {
const [val, setVal] = useState(initialValue || "0");
React.useEffect(() => {
if (open) setVal(initialValue || "0");
}, [open, initialValue]);
const press = (k: string) => {
setVal((prev) => {
if (k === "backspace") return prev.length <= 1 ? "0" : prev.slice(0, -1);
if (k === "clear") return "0";
if (k === "dot") return prev.includes(".") ? prev : prev + ".";
if (k === "ref") return String(requiredQty);
if (prev === "0" && k !== ".") return k;
return prev + k;
});
};
if (!open) return null;
const numVal = parseFloat(val);
const lower = requiredQty * 0.8;
const upper = requiredQty * 1.2;
const outOfRange = !isNaN(numVal) && (numVal < lower || numVal > upper);
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center">
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
<div className="relative bg-white rounded-2xl shadow-2xl p-4 w-[320px] z-10">
<div className="text-center mb-3">
<p className="text-sm text-gray-500 truncate">{itemName}</p>
<p className="text-xs text-blue-500 mt-0.5"> {requiredQty} {unit} (±20%)</p>
<p
className={`text-3xl font-bold mt-2 ${outOfRange ? "text-amber-600" : "text-gray-900"}`}
style={{ fontVariantNumeric: "tabular-nums" }}
>
{val} <span className="text-base text-gray-400">{unit}</span>
</p>
</div>
<div className="grid grid-cols-3 gap-2 mb-3">
{["1","2","3","4","5","6","7","8","9"].map((k) => (
<button key={k} onClick={() => press(k)} className="h-14 rounded-xl bg-gray-100 text-xl font-bold text-gray-800 active:scale-95 active:bg-gray-200 transition-all">{k}</button>
))}
<button onClick={() => press("dot")} className="h-14 rounded-xl bg-gray-100 text-xl font-bold text-gray-800 active:scale-95 transition-all">.</button>
<button onClick={() => press("0")} className="h-14 rounded-xl bg-gray-100 text-xl font-bold text-gray-800 active:scale-95 transition-all">0</button>
<button onClick={() => press("backspace")} className="h-14 rounded-xl bg-gray-200 text-base font-bold text-gray-600 active:scale-95 transition-all"></button>
</div>
<div className="flex gap-2 mb-2">
<button onClick={() => press("clear")} className="flex-1 h-10 rounded-xl bg-gray-100 text-gray-600 text-sm font-bold active:scale-95"></button>
<button onClick={() => press("ref")} className="flex-1 h-10 rounded-xl bg-blue-50 text-blue-600 text-sm font-bold active:scale-95"> ({requiredQty})</button>
</div>
<div className="flex gap-2">
<button onClick={onClose} className="flex-1 h-12 rounded-xl bg-gray-100 text-gray-700 font-semibold active:scale-95"></button>
<button onClick={() => { onConfirm(val); onClose(); }} className="flex-1 h-12 rounded-xl text-white font-bold active:scale-95" style={{ background: outOfRange ? "linear-gradient(135deg, #f59e0b, #d97706)" : "linear-gradient(135deg, #3b82f6, #1d4ed8)" }}>
{outOfRange ? "확인 (범위 외)" : "확인"}
</button>
</div>
</div>
</div>
);
}
/* ================================================================== */
/* Material Qty Input Row (키패드 트리거 버튼) */
/* ================================================================== */
function MaterialQtyInputRow({
material,
value,
onChange,
}: {
material: { id: string; child_item_name: string; required_qty: number; unit: string };
value: string;
onChange: (v: string) => void;
}) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setOpen(true)}
className="flex-1 px-4 py-3 rounded-xl border-2 border-gray-200 text-base font-bold text-left text-gray-900 hover:border-blue-400 active:scale-[0.98] transition-all bg-white"
style={{ minHeight: 56 }}
>
{value || <span className="text-gray-300 font-normal"> </span>}
</button>
<span className="text-sm text-gray-500 shrink-0">{material.unit}</span>
<MaterialQtyKeypad
open={open}
onClose={() => setOpen(false)}
onConfirm={onChange}
initialValue={value}
unit={material.unit}
requiredQty={material.required_qty}
itemName={material.child_item_name}
/>
</div>
);
}
/* ================================================================== */
/* Material Input Section */
/* ================================================================== */
@@ -2116,16 +2249,11 @@ function MaterialInputSection({ processId }: { processId: string }) {
<p className="text-lg font-bold text-blue-600">{m.required_qty} {m.unit}</p>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="number"
placeholder="투입 수량"
value={inputValues[m.id] || ""}
onChange={(e) => setInputValues((prev) => ({ ...prev, [m.id]: e.target.value }))}
className="flex-1 px-4 py-3 rounded-xl border-2 border-gray-200 text-base font-medium focus:outline-none focus:border-blue-400"
/>
<span className="text-sm text-gray-500 shrink-0">{m.unit}</span>
</div>
<MaterialQtyInputRow
material={m}
value={inputValues[m.id] || ""}
onChange={(v) => setInputValues((prev) => ({ ...prev, [m.id]: v }))}
/>
</div>
))}
<button