- eslint.config.mjs 규칙 강화 및 정리 - 전역 unused import/변수 제거 (312개 파일) - next.config.ts, middleware, proxy route 개선 - CopyableCell molecule 추가 - 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리 - IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선 - execute-server-action 에러 핸들링 보강
232 lines
7.2 KiB
TypeScript
232 lines
7.2 KiB
TypeScript
/**
|
|
* 할인하기 모달
|
|
*
|
|
* - 공급가액 표시 (읽기 전용)
|
|
* - 할인율 입력 (% 단위, 소수점 첫째자리까지)
|
|
* - 할인금액 입력 (원 단위)
|
|
* - 할인 후 공급가액 자동 계산
|
|
* - 할인율 ↔ 할인금액 상호 계산
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { formatNumber } from "@/lib/utils/amount";
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "../ui/dialog";
|
|
import { Button } from "../ui/button";
|
|
import { Input } from "../ui/input";
|
|
import { Label } from "../ui/label";
|
|
|
|
// =============================================================================
|
|
// Props
|
|
// =============================================================================
|
|
|
|
interface DiscountModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
/** 공급가액 (할인 전 금액) */
|
|
supplyAmount: number;
|
|
/** 기존 할인율 (%) */
|
|
initialDiscountRate?: number;
|
|
/** 기존 할인금액 (원) */
|
|
initialDiscountAmount?: number;
|
|
/** 적용 콜백 */
|
|
onApply: (discountRate: number, discountAmount: number) => void;
|
|
}
|
|
|
|
// =============================================================================
|
|
// 컴포넌트
|
|
// =============================================================================
|
|
|
|
export function DiscountModal({
|
|
open,
|
|
onOpenChange,
|
|
supplyAmount,
|
|
initialDiscountRate = 0,
|
|
initialDiscountAmount = 0,
|
|
onApply,
|
|
}: DiscountModalProps) {
|
|
// ---------------------------------------------------------------------------
|
|
// 상태
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const [discountRate, setDiscountRate] = useState<string>("");
|
|
const [discountAmount, setDiscountAmount] = useState<string>("");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 초기화
|
|
// ---------------------------------------------------------------------------
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
// 모달이 열릴 때 초기값 설정
|
|
if (initialDiscountRate > 0) {
|
|
setDiscountRate(initialDiscountRate.toString());
|
|
setDiscountAmount(initialDiscountAmount.toString());
|
|
} else if (initialDiscountAmount > 0) {
|
|
setDiscountAmount(initialDiscountAmount.toString());
|
|
const rate = supplyAmount > 0 ? (initialDiscountAmount / supplyAmount) * 100 : 0;
|
|
setDiscountRate(rate.toFixed(1));
|
|
} else {
|
|
setDiscountRate("");
|
|
setDiscountAmount("");
|
|
}
|
|
}
|
|
}, [open, initialDiscountRate, initialDiscountAmount, supplyAmount]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 핸들러
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// 할인율 변경 → 할인금액 자동 계산
|
|
const handleDiscountRateChange = useCallback(
|
|
(value: string) => {
|
|
// 숫자와 소수점만 허용
|
|
const sanitized = value.replace(/[^0-9.]/g, "");
|
|
|
|
// 소수점이 여러 개인 경우 첫 번째만 유지
|
|
const parts = sanitized.split(".");
|
|
const formatted = parts.length > 2
|
|
? parts[0] + "." + parts.slice(1).join("")
|
|
: sanitized;
|
|
|
|
setDiscountRate(formatted);
|
|
|
|
const rate = parseFloat(formatted) || 0;
|
|
if (rate >= 0 && rate <= 100) {
|
|
const amount = Math.round(supplyAmount * (rate / 100));
|
|
setDiscountAmount(amount.toString());
|
|
}
|
|
},
|
|
[supplyAmount]
|
|
);
|
|
|
|
// 할인금액 변경 → 할인율 자동 계산
|
|
const handleDiscountAmountChange = useCallback(
|
|
(value: string) => {
|
|
// 숫자만 허용
|
|
const sanitized = value.replace(/[^0-9]/g, "");
|
|
setDiscountAmount(sanitized);
|
|
|
|
const amount = parseInt(sanitized) || 0;
|
|
if (supplyAmount > 0 && amount >= 0 && amount <= supplyAmount) {
|
|
const rate = (amount / supplyAmount) * 100;
|
|
setDiscountRate(rate.toFixed(1));
|
|
}
|
|
},
|
|
[supplyAmount]
|
|
);
|
|
|
|
// 적용
|
|
const handleApply = useCallback(() => {
|
|
const rate = parseFloat(discountRate) || 0;
|
|
const amount = parseInt(discountAmount) || 0;
|
|
onApply(rate, amount);
|
|
onOpenChange(false);
|
|
}, [discountRate, discountAmount, onApply, onOpenChange]);
|
|
|
|
// 취소
|
|
const handleCancel = useCallback(() => {
|
|
onOpenChange(false);
|
|
}, [onOpenChange]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 계산
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const discountedAmount = supplyAmount - (parseInt(discountAmount) || 0);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 렌더링
|
|
// ---------------------------------------------------------------------------
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[400px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-center">할인하기</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
{/* 공급가액 (읽기 전용) */}
|
|
<div className="space-y-2">
|
|
<Label>공급가액</Label>
|
|
<Input
|
|
value={formatNumber(supplyAmount)}
|
|
disabled
|
|
className="bg-gray-50 text-right font-medium"
|
|
/>
|
|
</div>
|
|
|
|
{/* 할인율 */}
|
|
<div className="space-y-2">
|
|
<Label>할인율</Label>
|
|
<div className="relative">
|
|
<Input
|
|
type="text"
|
|
placeholder="0"
|
|
value={discountRate}
|
|
onChange={(e) => handleDiscountRateChange(e.target.value)}
|
|
className="pr-8 text-right"
|
|
/>
|
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
|
|
%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 할인금액 */}
|
|
<div className="space-y-2">
|
|
<Label>할인금액</Label>
|
|
<div className="relative">
|
|
<Input
|
|
type="text"
|
|
placeholder="0"
|
|
value={discountAmount ? formatNumber(parseInt(discountAmount)) : ""}
|
|
onChange={(e) => handleDiscountAmountChange(e.target.value.replace(/,/g, ""))}
|
|
className="pr-8 text-right"
|
|
/>
|
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
|
|
원
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 할인 후 공급가액 (읽기 전용) */}
|
|
<div className="space-y-2">
|
|
<Label>할인 후 공급가액</Label>
|
|
<Input
|
|
value={formatNumber(discountedAmount)}
|
|
disabled
|
|
className="bg-gray-50 text-right font-bold text-blue-600"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCancel}
|
|
className="flex-1"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleApply}
|
|
className="flex-1 bg-gray-800 hover:bg-gray-900 text-white"
|
|
>
|
|
적용
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|