Files
vexplor/frontend/components/pop/hardcoded/inbound/NumberPadModal.tsx
SeongHyun Kim ecf79c9e50 feat: POP v2 하드코딩 화면 — 홈 + 입고 프로세스
- 홈 화면: KPI 캐러셀, 메뉴 아이콘, 최근 활동, 공지 배너
- 입고유형선택: 외부 7개 + 내부 3개 아이콘, 금일 입고 KPI
- 구매입고: 거래처 선택, 발주 품목 카드, 담기/취소
- 장바구니: 체크박스, 포장 정보, 검사 상태, 확정
- 숫자 키패드: 터치 입력, MAX, 포장등록
- 포장 선택: 6종 단위 (박스/포대/팩/묶음/롤/통)
- 검사 모달: 마스터 기반 체크리스트, 측정값, 양품/불량
- 공통 PopShell: 헤더(시계+프로필), 배너, 푸터
- 반응형 4모드 (태블릿/핸드폰 가로세로)
2026-04-01 17:19:12 +09:00

315 lines
11 KiB
TypeScript

"use client";
import React, { useState, useCallback, useEffect } from "react";
import { PackagingModal, type PackageUnit } from "./PackagingModal";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export interface PackageEntry {
unit: PackageUnit;
count: number;
qtyPerUnit: number;
}
interface NumberPadModalProps {
open: boolean;
onClose: () => void;
onConfirm: (qty: number, packages: PackageEntry[]) => void;
maxQty: number;
itemName: string;
initialQty?: number;
initialPackages?: PackageEntry[];
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function NumberPadModal({
open,
onClose,
onConfirm,
maxQty,
itemName,
initialQty,
initialPackages,
}: NumberPadModalProps) {
const [display, setDisplay] = useState("0");
const [packages, setPackages] = useState<PackageEntry[]>([]);
const [packagingModalOpen, setPackagingModalOpen] = useState(false);
const [pendingUnit, setPendingUnit] = useState<PackageUnit | null>(null);
const [packQtyInput, setPackQtyInput] = useState("");
const [packCountInput, setPackCountInput] = useState("");
/* Reset on open */
useEffect(() => {
if (open) {
setDisplay(initialQty !== undefined ? String(initialQty) : "0");
setPackages(initialPackages ?? []);
setPendingUnit(null);
setPackQtyInput("");
setPackCountInput("");
}
}, [open, initialQty, initialPackages]);
/* Calculate package total */
const packageTotal = packages.reduce((sum, p) => sum + p.count * p.qtyPerUnit, 0);
const remainingQty = maxQty - packageTotal;
/* Numpad input handler */
const handleInput = useCallback(
(key: string) => {
setDisplay((prev) => {
switch (key) {
case "backspace":
return prev.length <= 1 ? "0" : prev.slice(0, -1);
case "clear":
return "0";
case "max":
return String(maxQty);
default: {
// digit
const next = prev === "0" ? key : prev + key;
const num = parseInt(next, 10);
if (isNaN(num)) return prev;
return String(Math.min(num, maxQty));
}
}
});
},
[maxQty]
);
/* Confirm */
const handleConfirm = () => {
const qty = parseInt(display, 10) || 0;
if (qty <= 0) return;
onConfirm(qty, packages);
onClose();
};
/* Package unit selected from modal */
const handlePackageUnitSelect = (unit: PackageUnit) => {
setPendingUnit(unit);
setPackQtyInput("");
setPackCountInput("1");
};
/* Add package entry */
const handleAddPackage = () => {
if (!pendingUnit) return;
const qtyPerUnit = parseInt(packQtyInput, 10);
const count = parseInt(packCountInput, 10) || 1;
if (isNaN(qtyPerUnit) || qtyPerUnit <= 0) return;
const newEntry: PackageEntry = {
unit: pendingUnit,
count,
qtyPerUnit,
};
const newPackages = [...packages, newEntry];
setPackages(newPackages);
// Update display to package total
const total = newPackages.reduce((s, p) => s + p.count * p.qtyPerUnit, 0);
setDisplay(String(Math.min(total, maxQty)));
setPendingUnit(null);
setPackQtyInput("");
setPackCountInput("");
};
/* Remove package */
const handleRemovePackage = (idx: number) => {
const newPackages = packages.filter((_, i) => i !== idx);
setPackages(newPackages);
const total = newPackages.reduce((s, p) => s + p.count * p.qtyPerUnit, 0);
if (total > 0) {
setDisplay(String(Math.min(total, maxQty)));
}
};
if (!open) return null;
const 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: "MAX", action: "max" },
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Overlay */}
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
{/* Panel */}
<div className="relative bg-white w-[90%] max-w-[340px] rounded-2xl shadow-2xl z-10 overflow-hidden">
{/* Header - blue gradient */}
<div
className="flex items-center justify-between px-4 py-3"
style={{ background: "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)" }}
>
<span className="text-[13px] text-white/90 bg-white/20 px-3 py-1 rounded-full">
{maxQty.toLocaleString()} EA
</span>
<button
onClick={() => setPackagingModalOpen(true)}
className="px-3 py-1.5 bg-green-50/90 border border-green-300 rounded-lg text-[13px] font-medium text-green-800 hover:bg-green-100 active:scale-95 transition-all"
>
{"\uD83D\uDCE6"}
</button>
</div>
{/* Body */}
<div className="p-4">
{/* Display */}
<input
type="text"
readOnly
value={parseInt(display, 10).toLocaleString()}
className="w-full px-4 py-3 text-right text-3xl font-bold border-2 border-gray-200 rounded-xl bg-gray-50 text-gray-900 mb-3"
style={{ fontVariantNumeric: "tabular-nums" }}
/>
{/* Registered packages */}
{packages.length > 0 && (
<div className="mb-3 p-2.5 bg-green-50 border border-green-200 rounded-lg">
<div className="flex flex-col gap-1">
{packages.map((pkg, idx) => (
<div key={idx} className="flex items-center justify-between px-2 py-1.5 bg-white rounded text-xs">
<span className="text-gray-700">
{pkg.unit.icon} {pkg.count}{pkg.unit.label} x {pkg.qtyPerUnit.toLocaleString()}EA
</span>
<div className="flex items-center gap-2">
<span className="font-semibold text-green-600">
= {(pkg.count * pkg.qtyPerUnit).toLocaleString()}EA
</span>
<button
onClick={() => handleRemovePackage(idx)}
className="w-5 h-5 bg-red-100 text-red-500 rounded text-xs flex items-center justify-center hover:bg-red-200"
>
x
</button>
</div>
</div>
))}
</div>
<div className="flex justify-between mt-2 pt-2 border-t border-green-200 text-xs font-semibold">
<span className="text-green-700"> : {packageTotal.toLocaleString()}EA</span>
<span className="text-red-500">: {remainingQty.toLocaleString()}EA</span>
</div>
</div>
)}
{/* Pending unit input */}
{pendingUnit && (
<div className="mb-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-xs font-semibold text-blue-700 mb-2">
{pendingUnit.icon} {pendingUnit.label}
</p>
<div className="flex gap-2 items-end">
<div className="flex-1">
<label className="text-[10px] text-gray-500"></label>
<input
type="number"
value={packCountInput}
onChange={(e) => setPackCountInput(e.target.value)}
className="w-full h-9 px-2 text-center text-sm font-semibold border border-gray-200 rounded-lg"
placeholder="1"
min={1}
/>
</div>
<span className="text-xs text-gray-400 pb-2">x</span>
<div className="flex-1">
<label className="text-[10px] text-gray-500">(EA)</label>
<input
type="number"
value={packQtyInput}
onChange={(e) => setPackQtyInput(e.target.value)}
className="w-full h-9 px-2 text-center text-sm font-semibold border border-gray-200 rounded-lg"
placeholder="100"
min={1}
autoFocus
/>
</div>
<button
onClick={handleAddPackage}
className="h-9 px-3 bg-blue-500 text-white text-xs font-semibold rounded-lg hover:bg-blue-600 active:scale-95 transition-all shrink-0"
>
</button>
</div>
</div>
)}
{/* Guide text */}
<div className={`text-center text-[13px] py-2 mb-3 rounded-md ${
packages.length > 0
? "bg-blue-50 text-blue-700 font-medium"
: "bg-gray-50 text-gray-500"
}`}>
{packages.length > 0
? `${packages.map(p => `${p.count}${p.unit.label} x ${p.qtyPerUnit}EA`).join(" + ")} = ${packageTotal}EA`
: "수량을 입력하세요"
}
</div>
{/* Numpad grid: 4x3 + bottom row */}
<div className="grid grid-cols-4 gap-2.5">
{KEYS.map((key) => (
<button
key={key.action}
onClick={() => handleInput(key.action)}
className={`h-14 rounded-xl text-lg font-semibold active:scale-95 transition-all ${
key.action === "backspace" || key.action === "clear"
? "bg-amber-100 text-amber-700 hover:bg-amber-200"
: key.action === "max"
? "bg-blue-100 text-blue-700 hover:bg-blue-200 text-sm"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
}`}
>
{key.label}
</button>
))}
{/* Bottom row: 0 (span 2) + Confirm (span 2) */}
<button
onClick={() => handleInput("0")}
className="col-span-2 h-14 rounded-xl text-lg font-semibold bg-gray-100 text-gray-900 hover:bg-gray-200 active:scale-95 transition-all"
>
0
</button>
<button
onClick={handleConfirm}
className="col-span-2 h-14 rounded-xl text-lg font-bold text-white active:scale-95 transition-all"
style={{
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
}}
>
</button>
</div>
</div>
</div>
{/* Packaging modal (nested) */}
<PackagingModal
open={packagingModalOpen}
onClose={() => setPackagingModalOpen(false)}
onSelect={handlePackageUnitSelect}
/>
</div>
);
}