Files
wace_rps/frontend/components/purchase/PurchaseOrderGeneralFormDialog.tsx
T
hjjeong 17b08c7a09 구매관리 발주서 메일 발송 + PDF 다운로드 + 행추가/삭제 제거
- backend purchaseOrderMailService 신설 — getOrderMailInfo / getPartnerManagerList /
  sendOrderMail (SMTP PURCHASE, 발송 성공 시 mail_send_yn='Y'/mail_send_date 갱신)
- backend routes — GET /order-form/mail-info/:objid, POST /order-form/mail,
  GET /options/partner-managers/:partnerObjid
- frontend lib/utils/purchaseOrderPdf — html2canvas-pro + jsPDF (A4, scale=2,
  input/textarea → 텍스트 변환). download:true 면 파일 저장, 아니면 base64 반환
- PurchaseOrderMailDialog 신설 — EstimateMailDialog 패턴 단순화 (한글/영문 본문 분기,
  공급업체 단일 email 자동 채움)
- 3개 양식 다이얼로그 — 읽기전용 + 저장된 발주서일 때 "메일 발송" + "PDF 다운로드"
  버튼 노출. window.print 간이판 제거
- 3개 양식 다이얼로그 — "행 추가"/"선택 행 삭제" 버튼 + 그리드 체크박스 컬럼 제거
  (wace 운영판은 모두 주석 처리/부재. 발주서는 품의서에서 자동 채움된 품목 그대로 사용)
2026-05-19 14:57:47 +09:00

616 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// 구매관리 > 발주서관리 — general 양식 등록/수정 다이얼로그 (PDF 양식 1:1)
// wace 원본: purchaseOrder/purchaseOrderFormPopup_general.jsp (운영판 폼)
// - 좌 박스 5필드: 발주번호 / 발주일자 / 수신업체 / 합계금액(VAT별도) / 결제방식
// (납기일·납품장소는 운영판 주석 처리됨 — 그대로 미노출)
// - 우 박스: "담당자" 라벨 + 담당자1 + 담당자1 연락처/이메일 + 담당자2 + 담당자2 연락처/이메일
// + "㈜알피에스 대표이사 이 동 헌" + "대전광역시 유성구 국제과학10로8(둔곡동 402-4번지)"
// - 저장/닫기 버튼: 폼 박스 우측 상단
// - 그리드 컬럼: ☑ / No / 품명 / 규격 / 수량 / 단위 / 배송지 / 단가 / 공급가액 / 비고 / 입고요청일
// - 푸터: 총 공급 가액 (VAT별도) + 보안 문구 (빨간색)
//
// 저장: purchaseApi.saveOrderForm({ master, parts, deletedPartObjids })
// - SUPPLY_UNIT_PRICE = ORDER_QTY × PARTNER_PRICE (행 자동 계산)
// - TOTAL_SUPPLY_PRICE = Σ SUPPLY_UNIT_PRICE (마스터 자동 합산)
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Download, Mail } from "lucide-react";
import { toast } from "sonner";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { DateInput } from "@/components/common/DateInput";
import { NumberInput } from "@/components/common/NumberInput";
import { purchaseApi, OptionItem } from "@/lib/api/purchase";
import { generatePurchaseOrderPdf } from "@/lib/utils/purchaseOrderPdf";
import { PurchaseOrderMailDialog } from "./PurchaseOrderMailDialog";
interface Props {
open: boolean;
onClose: () => void;
onSaved?: (result: { objid: string; purchase_order_no: string }) => void;
/** 수정 모드 시 POM OBJID — 미지정이고 proposalObjid 있으면 신규 (품의서 자동채움) */
pomObjid?: string;
/** 신규 등록 시 발주 원천 품의서 OBJID */
proposalObjid?: string;
}
interface PartRow {
rowKey: string;
objid: string;
part_objid: string;
part_no: string;
part_name: string;
spec: string;
order_qty: number | "";
qty: number | "";
unit: string;
part_delivery_place: string;
partner_price: number | "";
supply_unit_price: number;
remark: string;
delivery_request_date: string;
currency?: string;
_src?: string;
}
interface MasterState {
objid: string;
purchase_order_no: string;
purchase_date: string;
partner_objid: string;
payment_terms: string;
sales_mng_user_id: string;
manager_name: string;
manager_position: string;
manager_phone: string;
manager_email: string;
sales_mng_user_id2: string;
manager_name2: string;
manager_position2: string;
manager_phone2: string;
manager_email2: string;
title: string;
request_content: string;
sales_request_objid: string;
contract_mgmt_objid: string;
form_type: string;
status: string;
/** 결재 상태 — wace isModify 분기에 사용 ('결재중'/'결재완료' 시 읽기전용) */
appr_status: string;
}
const EMPTY_MASTER: MasterState = {
objid: "", purchase_order_no: "", purchase_date: "",
partner_objid: "", payment_terms: "",
sales_mng_user_id: "", manager_name: "", manager_position: "",
manager_phone: "", manager_email: "",
sales_mng_user_id2: "", manager_name2: "", manager_position2: "",
manager_phone2: "", manager_email2: "",
title: "", request_content: "",
sales_request_objid: "", contract_mgmt_objid: "",
form_type: "general", status: "create",
appr_status: "",
};
const UNIT_GROUP_ID = "0001399"; // wace unit_cd (0001400=EA)
const DELIVERY_PLACE_GROUP = "0001146"; // wace delivery_place_cd
const PAYMENT_TERMS_GROUP = "0001074"; // wace payment_terms_cd
let _rk = 0;
const nextKey = () => `r${++_rk}_${Date.now()}`;
const toNum = (v: any): number => {
if (v == null || v === "") return 0;
const n = Number(String(v).replace(/,/g, ""));
return Number.isFinite(n) ? n : 0;
};
const fmt = (n: number) => n.toLocaleString("ko-KR");
interface UserOptionExt extends OptionItem {
name?: string;
position?: string;
phone?: string;
email?: string;
}
export function PurchaseOrderGeneralFormDialog({
open, onClose, onSaved, pomObjid, proposalObjid,
}: Props) {
const isEdit = !!pomObjid;
const [master, setMaster] = useState<MasterState>(EMPTY_MASTER);
const [parts, setParts] = useState<PartRow[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [vendorOpts, setVendorOpts] = useState<SmartSelectOption[]>([]);
const [userOpts, setUserOpts] = useState<UserOptionExt[]>([]);
const userOptsForSelect: SmartSelectOption[] = useMemo(
() => userOpts.map((u) => ({ code: u.code, label: u.label })),
[userOpts],
);
useEffect(() => {
if (!open) return;
setMaster(EMPTY_MASTER);
setParts([]);
(async () => {
try {
const [vs, us] = await Promise.all([
purchaseApi.listVendors(),
purchaseApi.listUsers(),
]);
setVendorOpts(vs.map((v) => ({ code: v.code, label: v.label })));
setUserOpts(us as UserOptionExt[]);
} catch {/* skip */}
})();
setLoading(true);
(async () => {
try {
if (isEdit && pomObjid) {
const r = await purchaseApi.getOrderForm(pomObjid);
applyServerData(r.master ?? {}, r.parts ?? []);
} else if (proposalObjid) {
const r = await purchaseApi.initOrderForm(proposalObjid);
applyServerData(r.master ?? {}, r.parts ?? []);
}
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "초기 로드 실패");
} finally {
setLoading(false);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, isEdit, pomObjid, proposalObjid]);
const applyServerData = (m: Record<string, any>, ps: Record<string, any>[]) => {
setMaster({
objid: String(m.objid ?? ""),
purchase_order_no: String(m.purchase_order_no ?? ""),
purchase_date: String(m.purchase_date ?? ""),
partner_objid: String(m.partner_objid ?? ""),
payment_terms: String(m.payment_terms ?? ""),
sales_mng_user_id: String(m.sales_mng_user_id ?? ""),
manager_name: String(m.manager_name ?? ""),
manager_position: String(m.manager_position ?? ""),
manager_phone: String(m.manager_phone ?? ""),
manager_email: String(m.manager_email ?? ""),
sales_mng_user_id2: String(m.sales_mng_user_id2 ?? ""),
manager_name2: String(m.manager_name2 ?? ""),
manager_position2: String(m.manager_position2 ?? ""),
manager_phone2: String(m.manager_phone2 ?? ""),
manager_email2: String(m.manager_email2 ?? ""),
title: String(m.title ?? ""),
request_content: String(m.request_content ?? ""),
sales_request_objid: String(m.sales_request_objid ?? m.proposal_objid ?? ""),
contract_mgmt_objid: String(m.contract_mgmt_objid ?? ""),
form_type: String(m.form_type ?? "general"),
status: String(m.status ?? "create"),
appr_status: String(m.appr_status ?? ""),
});
setParts(ps.map((p) => ({
rowKey: nextKey(),
objid: String(p.objid ?? ""),
part_objid: String(p.part_objid ?? ""),
part_no: String(p.part_no ?? ""),
part_name: String(p.part_name ?? ""),
spec: String(p.spec ?? ""),
order_qty: p.order_qty === "" || p.order_qty == null ? "" : Number(p.order_qty),
qty: p.qty === "" || p.qty == null
? (p.order_qty === "" || p.order_qty == null ? "" : Number(p.order_qty))
: Number(p.qty),
unit: String(p.unit || "0001400"),
part_delivery_place: String(p.part_delivery_place || "RPS"),
partner_price: p.partner_price === "" || p.partner_price == null ? "" : Number(p.partner_price),
supply_unit_price: toNum(p.supply_unit_price ?? toNum(p.order_qty) * toNum(p.partner_price)),
remark: String(p.remark ?? ""),
delivery_request_date: String(p.delivery_request_date ?? ""),
currency: String(p.currency ?? ""),
_src: p._src,
})));
};
const totalSupplyPrice = useMemo(
() => parts.reduce((sum, p) => sum + toNum(p.supply_unit_price), 0),
[parts],
);
// 읽기전용 모드 — wace _general.jsp isModify 분기 1:1
// APPR_STATUS='결재중' / '결재완료' / STATUS='cancel' 일 때 입력 잠금
const isReadOnly = useMemo(() => {
const a = master.appr_status;
return a === "결재중" || a === "결재완료" || master.status === "cancel";
}, [master.appr_status, master.status]);
const pdfContainerRef = useRef<HTMLDivElement>(null);
const [mailOpen, setMailOpen] = useState(false);
const [generatingPdf, setGeneratingPdf] = useState(false);
/** PDF 다운로드 — html2canvas + jsPDF 로 발주서 양식 컨테이너 캡처 */
const handleDownload = async () => {
if (!pdfContainerRef.current) return;
setGeneratingPdf(true);
try {
const filename = `${master.purchase_order_no || "purchase_order"}.pdf`;
await generatePurchaseOrderPdf(pdfContainerRef.current, { download: true, filename });
} catch (e: any) {
toast.error("PDF 생성 실패: " + (e?.message ?? ""));
} finally {
setGeneratingPdf(false);
}
};
/** 메일 발송 — 다이얼로그가 onRequestPdf 콜백으로 PDF 생성 요청 */
const handleRequestPdf = async (): Promise<string> => {
if (!pdfContainerRef.current) throw new Error("발주서 컨테이너를 찾을 수 없습니다");
return generatePurchaseOrderPdf(pdfContainerRef.current);
};
// 담당자 select 변경 시 hidden 필드(name/position/phone/email) 자동 채움
const onManagerChange = (slot: 1 | 2, userId: string) => {
const u = userOpts.find((o) => o.code === userId);
setMaster((prev) => ({
...prev,
[`sales_mng_user_id${slot === 2 ? "2" : ""}`]: userId,
[`manager_name${slot === 2 ? "2" : ""}`]: u?.name ?? u?.label ?? "",
[`manager_position${slot === 2 ? "2" : ""}`]: u?.position ?? "",
[`manager_phone${slot === 2 ? "2" : ""}`]: u?.phone ?? "",
[`manager_email${slot === 2 ? "2" : ""}`]: u?.email ?? "",
}));
};
const updateRow = (rowKey: string, patch: Partial<PartRow>) => {
setParts((prev) => prev.map((r) => {
if (r.rowKey !== rowKey) return r;
const merged: PartRow = { ...r, ...patch };
const q = toNum(merged.order_qty);
const u = toNum(merged.partner_price);
merged.qty = merged.order_qty;
merged.supply_unit_price = q * u;
return merged;
}));
};
const handleSave = async () => {
if (!master.partner_objid) { toast.warning("수신업체를 선택하세요"); return; }
if (parts.length === 0) { toast.warning("발주 품목이 없습니다"); return; }
setSaving(true);
try {
const payload = {
master: {
...master,
total_supply_price: String(totalSupplyPrice),
total_supply_unit_price: String(totalSupplyPrice),
total_price: String(totalSupplyPrice),
},
parts: parts.map((p) => ({
objid: p.objid,
part_objid: p.part_objid,
part_no: p.part_no,
part_name: p.part_name,
spec: p.spec,
order_qty: toNum(p.order_qty),
qty: toNum(p.qty || p.order_qty),
unit: p.unit,
part_delivery_place: p.part_delivery_place,
partner_price: toNum(p.partner_price),
supply_unit_price: toNum(p.supply_unit_price),
remark: p.remark,
delivery_request_date: p.delivery_request_date,
currency: p.currency,
})),
deletedPartObjids: [],
};
const res = await purchaseApi.saveOrderForm(payload);
toast.success(`저장 완료 (${res.purchase_order_no})`);
onSaved?.(res);
onClose();
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패");
} finally {
setSaving(false);
}
};
// ───────── 운영판 PDF 양식 1:1 (테두리 박스 + 좌/우 분할) ─────────
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent className="max-w-[1280px] w-[96vw] max-h-[94vh] overflow-hidden flex flex-col p-0 gap-0 bg-white">
{/* Radix UI 접근성 — 시각 노출 없이 타이틀/설명 제공 */}
<DialogTitle className="sr-only"> ()</DialogTitle>
<DialogDescription className="sr-only">wace PDF </DialogDescription>
<div ref={pdfContainerRef} className="flex-1 overflow-y-auto p-4 text-[12px]" style={{ fontFamily: "'Malgun Gothic', '맑은 고딕', sans-serif" }}>
{/* ── 상단 메인 박스 (PDF 양식) ── */}
<div className="border-2 border-black">
{/* 1행: 로고 + 타이틀 */}
<div className="flex items-center border-b-2 border-black">
<div className="w-[140px] text-center py-2">
{/* wace `<%=request.getContextPath()%>/images/logo.png` 1:1 */}
<img src="/images/rps-logo-on.png" alt="RPS Logo"
style={{ maxWidth: "100px", height: "auto" }} />
</div>
<div className="flex-1 text-center py-3 text-[28px] font-bold" style={{ letterSpacing: "20px" }}>
</div>
<div className="w-[140px]"></div>
</div>
{/* 2행: 좌(기본정보) + 우(담당자) */}
<div className="flex">
{/* 좌: 기본정보 5필드 */}
<div className="w-[42%] p-3 border-r border-black">
<table className="w-full border-collapse">
<tbody>
<tr>
<td className="py-1.5 font-semibold whitespace-nowrap pr-3 w-[160px]">1. :</td>
<td className="py-1.5 font-bold">
<span>{master.purchase_order_no || <span className="text-gray-400"></span>}</span>
</td>
</tr>
<tr>
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">2. :</td>
<td className="py-1.5">
<div className="w-[210px]">
<DateInput value={master.purchase_date}
onChange={(v) => setMaster({ ...master, purchase_date: v })}
disabled={isReadOnly} />
</div>
</td>
</tr>
<tr>
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">3. :</td>
<td className="py-1.5">
<div className="w-[260px]">
<SmartSelect options={vendorOpts}
value={master.partner_objid}
onValueChange={(v) => setMaster({ ...master, partner_objid: v })}
disabled={isReadOnly} />
</div>
</td>
</tr>
<tr>
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">4. (VAT별도) :</td>
<td className="py-1.5 font-bold">
{fmt(totalSupplyPrice)} <span className="font-normal"></span>
</td>
</tr>
<tr>
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">5. :</td>
<td className="py-1.5">
<div className="w-[260px]">
<CommCodeSelect groupId={PAYMENT_TERMS_GROUP}
value={master.payment_terms}
onValueChange={(v) => setMaster({ ...master, payment_terms: v })}
withAll={false}
disabled={isReadOnly} />
</div>
</td>
</tr>
</tbody>
</table>
</div>
{/* 우: 담당자 박스 + 회사정보 */}
<div className="flex-1 p-3">
<table className="w-full border-collapse border border-black text-[11px]">
<tbody>
<tr>
<td rowSpan={4} className="w-[60px] text-center font-bold border border-black align-middle bg-gray-100"
style={{ letterSpacing: "3px" }}>
<br/><br/>
</td>
<td className="border border-black px-2 py-1 text-center">
<div className="w-full max-w-[220px] mx-auto">
<SmartSelect options={userOptsForSelect}
value={master.sales_mng_user_id}
onValueChange={(v) => onManagerChange(1, v)}
disabled={isReadOnly} />
</div>
</td>
</tr>
<tr>
<td className="border border-black px-2 py-1 text-center relative">
<span>(<span>{master.manager_phone || "-"}</span> / <span>{master.manager_email || "-"}</span>)</span>
{/* 직인 — wace `images/stamp_rps.png` (운영 파일은 stamp_seal.png) onerror hide */}
<img src="/images/rps-stamp-seal.png" alt="직인"
className="absolute"
style={{ right: 5, top: -10, width: 45, height: 45, opacity: 0.9 }}
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none"; }} />
</td>
</tr>
<tr>
<td className="border border-black px-2 py-1 text-center">
<div className="w-full max-w-[220px] mx-auto">
<SmartSelect options={userOptsForSelect}
value={master.sales_mng_user_id2}
onValueChange={(v) => onManagerChange(2, v)}
disabled={isReadOnly} />
</div>
</td>
</tr>
<tr>
<td className="border border-black px-2 py-1 text-center">
<span>(<span>{master.manager_phone2 || "-"}</span> / <span>{master.manager_email2 || "-"}</span>)</span>
</td>
</tr>
<tr>
<td colSpan={2} className="border border-black border-b-0 px-2 py-1.5 text-center text-[14px] font-bold">
</td>
</tr>
<tr>
<td colSpan={2} className="border border-black border-t-0 px-2 py-1.5 text-center text-[11px] font-bold">
108( 402-4)
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/* 읽기전용 안내 — wace isModify=false 분기 */}
{isReadOnly && (
<div className="mt-2 text-[11px] text-amber-700 bg-amber-50 border border-amber-300 px-3 py-1.5 rounded">
/ .
{master.appr_status ? ` (결재상태: ${master.appr_status})` : ""}
{master.status === "cancel" ? " (취소됨)" : ""}
</div>
)}
{/* 버튼 영역 (그리드 위, 우측 정렬) — wace _general.jsp 941-948 1:1 */}
<div className="flex justify-end gap-2 my-3">
{!isReadOnly && (
<Button size="sm" className="h-8 px-5 text-[13px]"
style={{ background: "#dfeffc", color: "#000" }}
onClick={handleSave} disabled={saving || loading}>
{saving ? "저장 중..." : "저장"}
</Button>
)}
{isReadOnly && master.objid && (
<>
<Button size="sm" variant="outline" className="h-8 gap-1 px-3 text-xs"
onClick={() => setMailOpen(true)}>
<Mail className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="h-8 gap-1 px-3 text-[13px]"
style={{ background: "#dfeffc", color: "#000" }}
onClick={handleDownload} disabled={generatingPdf}>
<Download className="h-3.5 w-3.5" />
{generatingPdf ? "생성 중..." : "PDF 다운로드"}
</Button>
</>
)}
<Button size="sm" variant="outline" className="h-8 px-5 text-[13px]"
style={{ background: "#dfeffc" }}
onClick={onClose} disabled={saving}>
</Button>
</div>
{/* 그리드 */}
<div className="border border-black overflow-x-auto">
<table className="w-full text-[11px] border-collapse">
<thead>
<tr className="bg-[#f0f4fa]">
<th className="w-10 border border-black px-1 py-1.5">No</th>
<th className="min-w-[140px] border border-black px-1 py-1.5"></th>
<th className="min-w-[120px] border border-black px-1 py-1.5"></th>
<th className="w-[70px] border border-black px-1 py-1.5"></th>
<th className="w-[80px] border border-black px-1 py-1.5"></th>
<th className="w-[110px] border border-black px-1 py-1.5"></th>
<th className="w-[100px] border border-black px-1 py-1.5"></th>
<th className="w-[110px] border border-black px-1 py-1.5"></th>
<th className="min-w-[160px] border border-black px-1 py-1.5"></th>
<th className="w-[110px] border border-black px-1 py-1.5"></th>
</tr>
</thead>
<tbody>
{parts.length === 0 ? (
<tr>
<td colSpan={10} className="text-center py-8 border border-black text-gray-500">
</td>
</tr>
) : parts.map((row, i) => (
<tr key={row.rowKey} className="hover:bg-blue-50/30">
<td className="border border-black px-1 py-0.5 text-center text-gray-600">{i + 1}</td>
<td className="border border-black px-1 py-0.5">
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
value={row.part_name}
onChange={(e) => updateRow(row.rowKey, { part_name: e.target.value })}
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
value={row.spec}
onChange={(e) => updateRow(row.rowKey, { spec: e.target.value })}
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<NumberInput value={row.order_qty} decimals={0}
onChange={(v) => updateRow(row.rowKey, { order_qty: v })}
className="h-7 text-[11px]"
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<CommCodeSelect groupId={UNIT_GROUP_ID}
value={row.unit}
onValueChange={(v) => updateRow(row.rowKey, { unit: v })}
withAll={false} className="h-7"
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<CommCodeSelect groupId={DELIVERY_PLACE_GROUP}
value={row.part_delivery_place}
onValueChange={(v) => updateRow(row.rowKey, { part_delivery_place: v })}
withAll={false} className="h-7"
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<NumberInput value={row.partner_price} decimals={0}
onChange={(v) => updateRow(row.rowKey, { partner_price: v })}
className="h-7 text-[11px]"
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5 text-right tabular-nums pr-2">
{fmt(row.supply_unit_price)}
</td>
<td className="border border-black px-1 py-0.5">
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
value={row.remark}
onChange={(e) => updateRow(row.rowKey, { remark: e.target.value })}
disabled={isReadOnly} />
</td>
<td className="border border-black px-1 py-0.5">
<DateInput value={row.delivery_request_date}
onChange={(v) => updateRow(row.rowKey, { delivery_request_date: v })}
size="sm"
disabled={isReadOnly} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 합계 영역 */}
<div className="flex justify-end mt-3">
<table className="border-collapse">
<tbody>
<tr>
<td className="bg-[#e8f0e1] border border-black px-6 py-2 text-[13px] font-bold whitespace-nowrap" style={{ letterSpacing: "2px" }}>
(VAT별도)
</td>
<td className="border border-black px-4 py-2 text-right font-bold text-[16px] min-w-[200px] tabular-nums">
{fmt(totalSupplyPrice)}
</td>
</tr>
</tbody>
</table>
</div>
{/* 보안 문구 */}
<div className="text-right text-[11px] text-red-600 mt-3">
(CONFIDENTIAL) : (APPROVAL) .
</div>
</div>
</DialogContent>
<PurchaseOrderMailDialog
open={mailOpen}
onOpenChange={setMailOpen}
pomObjid={master.objid || null}
formType="general"
onRequestPdf={handleRequestPdf}
onSent={() => setMailOpen(false)}
/>
</Dialog>
);
}