구매관리 발주서 폼 저장/삭제 + general 양식 다이얼로그

- 백엔드: POST /api/purchase/order-form/save (마스터 55 + 파트 40 컬럼 UPSERT + 삭제파트 cascade, 트랜잭션, wace mergePurchaseOrderMaster/PartInfo 1:1)
- 백엔드: DELETE /api/purchase/order-form/:objid (마스터+파트 cascade)
- 프론트 lib/api: initOrderForm/getOrderForm/saveOrderForm/deleteOrderForm
- 프론트 컴포넌트: PurchaseOrderGeneralFormDialog — wace purchaseOrderFormPopup_general.jsp 1:1 (좌 5필드/우 담당자 + 회사정보 2줄/그리드 10컬럼/총공급가액/보안문구)
- /purchase/proposal "발주서생성" 버튼 활성화 → 품의서 자동 채움 다이얼로그
- /purchase/order 행 클릭/체크 → 수정/삭제 액션 + 다이얼로그
- Radix UI 접근성: DialogTitle/Description sr-only 처리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-19 13:06:18 +09:00
parent 6a0705c5b7
commit e48bd83667
7 changed files with 1088 additions and 4 deletions
@@ -18,6 +18,10 @@ import { PageHeader } from "@/components/common/PageHeader";
import { purchaseApi, PurchaseListFilter, OptionItem, getYearOptions } from "@/lib/api/purchase";
import { apiClient } from "@/lib/api/client";
import { exportToExcel } from "@/lib/utils/excelExport";
import { Button } from "@/components/ui/button";
import { Pencil, Trash2 } from "lucide-react";
import { PurchaseOrderGeneralFormDialog } from "@/components/purchase/PurchaseOrderGeneralFormDialog";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
const MAIL_SEND_OPTS: SmartSelectOption[] = [
{ code: "Y", label: "발송완료" },
@@ -65,6 +69,11 @@ export default function PurchaseOrderWacePage() {
const [productOpts, setProductOpts] = useState<OptionItem[]>([]);
const [purchaseOpts, setPurchaseOpts] = useState<OptionItem[]>([]);
// 수정 다이얼로그
const [editOpen, setEditOpen] = useState(false);
const [editObjid, setEditObjid] = useState<string>("");
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const yearOpts = useMemo(() => getYearOptions(), []);
const fetchList = useCallback(async (override?: Partial<PurchaseListFilter>) => {
@@ -138,7 +147,40 @@ export default function PurchaseOrderWacePage() {
return (
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
<PageHeader loading={loading} onSearch={handleSearch} onReset={handleReset} />
<PageHeader loading={loading} onSearch={handleSearch} onReset={handleReset}
actions={<>
<Button size="sm" variant="outline" className="h-8 gap-1 px-2 text-xs"
disabled={checkedIds.length !== 1}
onClick={() => {
const id = checkedIds[0]; if (!id) return;
setEditObjid(id);
setEditOpen(true);
}}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" className="h-8 gap-1 px-2 text-xs"
disabled={checkedIds.length === 0}
onClick={async () => {
const ok = await confirm(`선택한 ${checkedIds.length}건을 삭제하시겠어요?`, {
description: "발주서와 품목이 함께 삭제돼요.",
variant: "destructive",
confirmText: "삭제",
});
if (!ok) return;
try {
for (const id of checkedIds) {
await purchaseApi.deleteOrderForm(id);
}
toast.success("삭제 완료");
setCheckedIds([]);
fetchList();
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "삭제 실패");
}
}}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</>} />
<CompactFilterBar totalText={<> {total.toLocaleString()}</>}>
<CompactFilterField label="년도" width={100}>
@@ -233,7 +275,21 @@ export default function PurchaseOrderWacePage() {
exportToExcel(exportRows, "발주서관리.xlsx", "발주서");
}}
showChart
onRowClick={(row: any) => {
if (!row?.objid) return;
setEditObjid(String(row.objid));
setEditOpen(true);
}}
/>
<PurchaseOrderGeneralFormDialog
open={editOpen}
pomObjid={editObjid}
onClose={() => setEditOpen(false)}
onSaved={() => { setEditOpen(false); fetchList(); }}
/>
{ConfirmDialogComponent}
</div>
);
}
@@ -17,6 +17,7 @@ import { PageHeader } from "@/components/common/PageHeader";
import { apiClient } from "@/lib/api/client";
import { purchaseApi, PurchaseListFilter, OptionItem } from "@/lib/api/purchase";
import { exportToExcel } from "@/lib/utils/excelExport";
import { PurchaseOrderGeneralFormDialog } from "@/components/purchase/PurchaseOrderGeneralFormDialog";
const PARENT_PURCHASE_TYPE = "0001814"; // 구매유형 comm_code
const PARENT_PART_TYPE = "0000001"; // 제품구분 comm_code
@@ -46,6 +47,10 @@ export default function ProposalPage() {
const [partTypeOpts, setPartTypeOpts] = useState<SmartSelectOption[]>([]);
const [userOpts, setUserOpts] = useState<OptionItem[]>([]);
// 발주서생성 다이얼로그
const [orderFormOpen, setOrderFormOpen] = useState(false);
const [orderFormProposalId, setOrderFormProposalId] = useState<string>("");
const fetchList = useCallback(async (override?: Partial<PurchaseListFilter>) => {
setLoading(true);
try {
@@ -124,7 +129,12 @@ export default function ProposalPage() {
</Button>
<Button size="sm" variant="default" className="h-8 gap-1 px-2 text-xs"
disabled={checkedIds.length !== 1}
onClick={() => toast.info("발주서생성 — purchase_order_part 신설 후 활성")}>
onClick={() => {
const proposalId = checkedIds[0];
if (!proposalId) return;
setOrderFormProposalId(proposalId);
setOrderFormOpen(true);
}}>
<ClipboardCheck className="h-3.5 w-3.5" />
</Button>
</>}
@@ -194,6 +204,13 @@ export default function ProposalPage() {
}}
showChart
/>
<PurchaseOrderGeneralFormDialog
open={orderFormOpen}
proposalObjid={orderFormProposalId}
onClose={() => setOrderFormOpen(false)}
onSaved={() => { setOrderFormOpen(false); fetchList(); }}
/>
</div>
);
}