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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user