210 lines
7.4 KiB
TypeScript
210 lines
7.4 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { Delete } from "lucide-react";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogPortal,
|
||
|
|
DialogOverlay,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||
|
|
import {
|
||
|
|
PackageUnitModal,
|
||
|
|
PACKAGE_UNITS,
|
||
|
|
type PackageUnit,
|
||
|
|
} from "./PackageUnitModal";
|
||
|
|
|
||
|
|
interface NumberInputModalProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
unit?: string;
|
||
|
|
initialValue?: number;
|
||
|
|
initialPackageUnit?: string;
|
||
|
|
min?: number;
|
||
|
|
maxValue?: number;
|
||
|
|
onConfirm: (value: number, packageUnit?: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function NumberInputModal({
|
||
|
|
open,
|
||
|
|
onOpenChange,
|
||
|
|
unit = "EA",
|
||
|
|
initialValue = 0,
|
||
|
|
initialPackageUnit,
|
||
|
|
min = 0,
|
||
|
|
maxValue = 999999,
|
||
|
|
onConfirm,
|
||
|
|
}: NumberInputModalProps) {
|
||
|
|
const [displayValue, setDisplayValue] = useState("");
|
||
|
|
const [packageUnit, setPackageUnit] = useState<string | undefined>(undefined);
|
||
|
|
const [isPackageModalOpen, setIsPackageModalOpen] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
setDisplayValue(initialValue > 0 ? String(initialValue) : "");
|
||
|
|
setPackageUnit(initialPackageUnit);
|
||
|
|
}
|
||
|
|
}, [open, initialValue, initialPackageUnit]);
|
||
|
|
|
||
|
|
const handleNumberClick = (num: string) => {
|
||
|
|
const newStr = displayValue + num;
|
||
|
|
const numericValue = parseInt(newStr, 10);
|
||
|
|
setDisplayValue(numericValue > maxValue ? String(maxValue) : newStr);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBackspace = () =>
|
||
|
|
setDisplayValue((prev) => prev.slice(0, -1));
|
||
|
|
const handleClear = () => setDisplayValue("");
|
||
|
|
const handleMax = () => setDisplayValue(String(maxValue));
|
||
|
|
|
||
|
|
const handleConfirm = () => {
|
||
|
|
const numericValue = parseInt(displayValue, 10) || 0;
|
||
|
|
const finalValue = Math.max(min, Math.min(maxValue, numericValue));
|
||
|
|
onConfirm(finalValue, packageUnit);
|
||
|
|
onOpenChange(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePackageUnitSelect = (selected: PackageUnit) => {
|
||
|
|
setPackageUnit(selected);
|
||
|
|
};
|
||
|
|
|
||
|
|
const matchedUnit = packageUnit
|
||
|
|
? PACKAGE_UNITS.find((u) => u.value === packageUnit)
|
||
|
|
: null;
|
||
|
|
const packageUnitLabel = matchedUnit?.label ?? null;
|
||
|
|
const packageUnitEmoji = matchedUnit?.emoji ?? "📦";
|
||
|
|
|
||
|
|
const displayText = displayValue
|
||
|
|
? parseInt(displayValue, 10).toLocaleString()
|
||
|
|
: "";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogPortal>
|
||
|
|
<DialogOverlay />
|
||
|
|
<DialogPrimitive.Content
|
||
|
|
className="bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[1000] w-full max-w-[95vw] translate-x-[-50%] translate-y-[-50%] overflow-hidden border shadow-lg duration-200 sm:max-w-[360px] sm:rounded-lg"
|
||
|
|
>
|
||
|
|
{/* 파란 헤더 */}
|
||
|
|
<div className="flex items-center justify-between bg-blue-500 px-4 py-3">
|
||
|
|
<span className="rounded-full bg-blue-400/50 px-3 py-1 text-sm font-medium text-white">
|
||
|
|
최대 {maxValue.toLocaleString()} {unit}
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setIsPackageModalOpen(true)}
|
||
|
|
className="flex items-center gap-1 rounded-full bg-white/20 px-3 py-1.5 text-sm font-medium text-white hover:bg-white/30 active:bg-white/40"
|
||
|
|
>
|
||
|
|
{packageUnitEmoji} {packageUnitLabel ? `${packageUnitLabel} ✓` : "포장등록"}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3 p-4">
|
||
|
|
{/* 숫자 표시 영역 */}
|
||
|
|
<div className="flex min-h-[72px] items-center justify-end rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-4">
|
||
|
|
{displayText ? (
|
||
|
|
<span className="text-4xl font-bold tracking-tight text-gray-900">
|
||
|
|
{displayText}
|
||
|
|
</span>
|
||
|
|
) : (
|
||
|
|
<span className="text-2xl text-gray-300">0</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 안내 텍스트 */}
|
||
|
|
<p className="text-muted-foreground text-center text-sm">
|
||
|
|
수량을 입력하세요
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{/* 키패드 4x4 */}
|
||
|
|
<div className="grid grid-cols-4 gap-2">
|
||
|
|
{/* 1행: 7 8 9 ← (주황) */}
|
||
|
|
{["7", "8", "9"].map((n) => (
|
||
|
|
<button
|
||
|
|
key={n}
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl border border-gray-200 bg-gray-50 text-xl font-semibold text-gray-800 active:bg-gray-200"
|
||
|
|
onClick={() => handleNumberClick(n)}
|
||
|
|
>
|
||
|
|
{n}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="flex h-14 items-center justify-center rounded-2xl bg-amber-100 text-amber-600 active:bg-amber-200"
|
||
|
|
onClick={handleBackspace}
|
||
|
|
>
|
||
|
|
<Delete className="h-5 w-5" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* 2행: 4 5 6 C (주황) */}
|
||
|
|
{["4", "5", "6"].map((n) => (
|
||
|
|
<button
|
||
|
|
key={n}
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl border border-gray-200 bg-gray-50 text-xl font-semibold text-gray-800 active:bg-gray-200"
|
||
|
|
onClick={() => handleNumberClick(n)}
|
||
|
|
>
|
||
|
|
{n}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl bg-amber-100 text-base font-bold text-amber-600 active:bg-amber-200"
|
||
|
|
onClick={handleClear}
|
||
|
|
>
|
||
|
|
C
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* 3행: 1 2 3 MAX (파란) */}
|
||
|
|
{["1", "2", "3"].map((n) => (
|
||
|
|
<button
|
||
|
|
key={n}
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl border border-gray-200 bg-gray-50 text-xl font-semibold text-gray-800 active:bg-gray-200"
|
||
|
|
onClick={() => handleNumberClick(n)}
|
||
|
|
>
|
||
|
|
{n}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl bg-blue-100 text-sm font-bold text-blue-600 active:bg-blue-200"
|
||
|
|
onClick={handleMax}
|
||
|
|
>
|
||
|
|
MAX
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* 4행: 0 / 확인 (초록, 3칸) */}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="h-14 rounded-2xl border border-gray-200 bg-gray-50 text-xl font-semibold text-gray-800 active:bg-gray-200"
|
||
|
|
onClick={() => handleNumberClick("0")}
|
||
|
|
>
|
||
|
|
0
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="col-span-3 h-14 rounded-2xl bg-green-500 text-base font-bold text-white active:bg-green-600"
|
||
|
|
onClick={handleConfirm}
|
||
|
|
>
|
||
|
|
확인
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</DialogPrimitive.Content>
|
||
|
|
</DialogPortal>
|
||
|
|
</Dialog>
|
||
|
|
|
||
|
|
{/* 포장 단위 선택 모달 */}
|
||
|
|
<PackageUnitModal
|
||
|
|
open={isPackageModalOpen}
|
||
|
|
onOpenChange={setIsPackageModalOpen}
|
||
|
|
onSelect={handlePackageUnitSelect}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|