From 935fd22e17afc034a472bc2d159de7b9f5e5dcae Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 20 Nov 2025 19:36:01 +0900 Subject: (김준회) PO, POS, swp - PO: 발주서출력기능 초안 - 벤더측 POS 다운로드 기능 추가 - Contract 생성시 Status 설정 (mapper) - swp document registration table 로직 리팩터링 - swp: 입력가능 문서번호 validation 추가 (리스트 메뉴에서 Completed 된 건) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/po/vendor-table/service.ts | 38 +- lib/po/vendor-table/shi-vendor-po-table.tsx | 1 + lib/po/vendor-table/types.ts | 14 +- lib/po/vendor-table/vendor-po-actions.tsx | 2 +- lib/po/vendor-table/vendor-po-items-dialog.tsx | 131 ++++++- lib/po/vendor-table/vendor-po-print-dialog.tsx | 514 +++++++++++++++++++++++++ lib/po/vendor-table/vendor-po-table.tsx | 53 +-- lib/soap/ecc/mapper/po-mapper.ts | 9 +- lib/swp/table/swp-help-dialog.tsx | 4 +- lib/swp/table/swp-inbox-table.tsx | 459 +++++++++++++--------- lib/swp/table/swp-table-columns.tsx | 36 +- lib/swp/table/swp-table-toolbar.tsx | 18 +- lib/swp/table/swp-table.tsx | 24 +- 13 files changed, 1081 insertions(+), 222 deletions(-) create mode 100644 lib/po/vendor-table/vendor-po-print-dialog.tsx (limited to 'lib') diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts index bc4a693b..e73a6083 100644 --- a/lib/po/vendor-table/service.ts +++ b/lib/po/vendor-table/service.ts @@ -7,6 +7,7 @@ import { contracts, contractItems, ContractStatus } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; import { vendors } from "@/db/schema/vendors"; import { items } from "@/db/schema/items"; +import { users } from "@/db/schema/users"; import { revalidatePath } from "next/cache"; import { eq, and, or, ilike, count, desc, asc, SQL } from "drizzle-orm"; @@ -223,14 +224,19 @@ export async function getVendorPOById(id: number): Promise { id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, + contractContent: contracts.contractContent, + remarks: contracts.remarks, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, + deliveryDate: contracts.deliveryDate, currency: contracts.currency, totalAmount: contracts.totalAmount, totalAmountKrw: contracts.totalAmountKrw, paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, + deliveryLocation: contracts.deliveryLocation, + shippmentPlace: contracts.shippmentPlace, exchangeRate: contracts.exchangeRate, poVersion: contracts.poVersion, purchaseDocType: contracts.purchaseDocType, @@ -255,10 +261,25 @@ export async function getVendorPOById(id: number): Promise { contractVersion: contracts.contractVersion, createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, + + // 프로젝트 정보 projectName: projects.name, + projectCode: projects.code, + + // 벤더 정보 + vendorName: vendors.vendorName, + vendorAddress: vendors.address, + vendorAddressDetail: vendors.addressDetail, + vendorPhone: vendors.phone, + vendorEmail: vendors.email, + + // 구매담당자 정보 (purchaseGroup으로 조회) + purchaseManagerName: users.name, }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .leftJoin(users, eq(contracts.purchaseGroup, users.userCode)) .where(eq(contracts.id, id)) .limit(1); @@ -274,7 +295,10 @@ export async function getVendorPOById(id: number): Promise { contractType: row.purchaseDocType || '', details: '상세보기', projectName: row.projectName || '', + projectCode: row.projectCode || undefined, contractName: row.contractName || '', + contractContent: row.contractContent || undefined, + remarks: row.remarks || undefined, contractPeriod: row.startDate && row.endDate ? `${row.startDate} ~ ${row.endDate}` : '', contractQuantity: '1 LOT', currency: row.currency || 'KRW', @@ -282,7 +306,7 @@ export async function getVendorPOById(id: number): Promise { tax: '10%', exchangeRate: row.exchangeRate?.toString() || '', deliveryTerms: row.deliveryTerms || '', - purchaseManager: '', + purchaseManager: row.purchaseManagerName || '', poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', contractDate: row.startDate || '', lcNo: undefined, @@ -290,6 +314,18 @@ export async function getVendorPOById(id: number): Promise { linkedContractNo: undefined, lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', lastModifiedBy: '', + + // 벤더 정보 + vendorName: row.vendorName || undefined, + vendorAddress: row.vendorAddress || undefined, + vendorAddressDetail: row.vendorAddressDetail || undefined, + vendorPhone: row.vendorPhone || undefined, + vendorEmail: row.vendorEmail || undefined, + + // 구매담당자 정보 + purchaseManagerName: row.purchaseManagerName || undefined, + + // SAP 필드 poVersion: row.poVersion || undefined, purchaseDocType: row.purchaseDocType || undefined, purchaseOrg: row.purchaseOrg || undefined, diff --git a/lib/po/vendor-table/shi-vendor-po-table.tsx b/lib/po/vendor-table/shi-vendor-po-table.tsx index 6c5f62ef..6567bf5a 100644 --- a/lib/po/vendor-table/shi-vendor-po-table.tsx +++ b/lib/po/vendor-table/shi-vendor-po-table.tsx @@ -218,6 +218,7 @@ export function ShiVendorPoTable({ promises }: ShiVendorPoTableProps) { open={itemsDialogOpen} onOpenChange={setItemsDialogOpen} po={selectedPO} + viewerType="evcp" /> diff --git a/lib/po/vendor-table/types.ts b/lib/po/vendor-table/types.ts index e318ff39..fbed2387 100644 --- a/lib/po/vendor-table/types.ts +++ b/lib/po/vendor-table/types.ts @@ -13,7 +13,8 @@ export interface VendorPO { contractStatus: string // 계약상태 (ZPO_CNFM_STAT) contractType: string // 계약종류 (BSART) details: string // 상세 (mock 데이터용) - projectName: string // 프로젝트 + projectName: string // 프로젝트 이름 + projectCode?: string // 프로젝트 코드 (PSPID) contractName: string // 계약명/자재내역 (ZTITLE) contractPeriod: string // PO/계약기간 contractQuantity: string // PO/계약수량 @@ -70,6 +71,17 @@ export interface VendorPO { contractContent?: string // 계약서 내용 (ZMM_NOTE에서 추출) remarks?: string // 비고 (ECC에서 추가 정보) + // 벤더 정보 (발주서 출력용) + vendorName?: string // 공급자명 + vendorAddress?: string // 공급자 주소 + vendorAddressDetail?: string // 공급자 상세주소 + vendorPhone?: string // 공급자 전화번호 + vendorFax?: string // 공급자 팩스번호 + vendorEmail?: string // 공급자 이메일 + + // 구매담당자 정보 (발주서 출력용) + purchaseManagerName?: string // 구매담당자 이름 + // 상세품목 정보 (다이얼로그에서 표시) items?: VendorPOItem[] } diff --git a/lib/po/vendor-table/vendor-po-actions.tsx b/lib/po/vendor-table/vendor-po-actions.tsx index 08fe3f88..e36b745b 100644 --- a/lib/po/vendor-table/vendor-po-actions.tsx +++ b/lib/po/vendor-table/vendor-po-actions.tsx @@ -199,7 +199,7 @@ export function VendorPOActions({ row, setRowAction }: VendorPOActionsProps) { disabled={isLoading} > - 계약서출력 + 발주서 출력 void po: VendorPO | null + /** + * 뷰어 타입 + * - 'evcp': EVCP 사용자 (암호화된 파일 직접 다운로드) + * - 'partners': 협력사 사용자 (복호화된 파일 다운로드) + */ + viewerType?: 'evcp' | 'partners' } -export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDialogProps) { +export function VendorPOItemsDialog({ open, onOpenChange, po, viewerType = 'partners' }: VendorPOItemsDialogProps) { const [items, setItems] = React.useState([]) const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) + // POS 파일 선택 다이얼로그 상태 + const [posDialogOpen, setPosDialogOpen] = React.useState(false) + const [selectedMaterialCode, setSelectedMaterialCode] = React.useState("") + const [posFiles, setPosFiles] = React.useState>([]) + const [loadingPosFiles, setLoadingPosFiles] = React.useState(false) + const [downloadingFileIndex, setDownloadingFileIndex] = React.useState(null) + + // POS 파일 목록 조회 및 다이얼로그 열기 + const handleOpenPosDialog = async (materialCode: string) => { + if (!materialCode) { + toast.error("자재코드가 없습니다") + return + } + + setLoadingPosFiles(true) + setSelectedMaterialCode(materialCode) + + try { + toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` }) + + const result = await checkPosFileExists(materialCode) + + if (result.exists && result.files && result.files.length > 0) { + // 파일 정보를 상세하게 가져오기 위해 getDownloadUrlByMaterialCode 사용 + const detailResult = await getDownloadUrlByMaterialCode(materialCode) + + if (detailResult.success && detailResult.availableFiles) { + setPosFiles(detailResult.availableFiles) + setPosDialogOpen(true) + toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` }) + } else { + toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } else { + toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } catch (error) { + console.error("POS 파일 조회 오류:", error) + toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` }) + } finally { + setLoadingPosFiles(false) + } + } + + // POS 파일 다운로드 실행 + const handleDownloadPosFile = async (fileIndex: number, fileName: string) => { + if (!selectedMaterialCode) return + + setDownloadingFileIndex(fileIndex) + + try { + toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` }) + + // viewerType에 따라 다른 엔드포인트 사용 + const endpoint = viewerType === 'partners' + ? `/api/pos/download-on-demand-partners` // 복호화 포함 + : `/api/pos/download-on-demand` // 암호화 파일 그대로 + + const downloadUrl = `${endpoint}?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}` + + toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` }) + window.open(downloadUrl, '_blank', 'noopener,noreferrer') + + // 다운로드 시작 후 잠시 대기 후 상태 초기화 + setTimeout(() => { + setDownloadingFileIndex(null) + }, 1000) + } catch (error) { + console.error("POS 파일 다운로드 오류:", error) + toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` }) + setDownloadingFileIndex(null) + } + } + + // POS 다이얼로그 닫기 + const handleClosePosDialog = () => { + setPosDialogOpen(false) + setSelectedMaterialCode("") + setPosFiles([]) + setDownloadingFileIndex(null) + } + // 상세품목 데이터 로드 React.useEffect(() => { if (!open || !po) { @@ -123,6 +223,7 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia 기본총액(BRTWR) 조정금액(ZPDT_EXDS_AMT) 최종정가(NETWR) + POS 예정시작일자 납기일자 예정종료일자 @@ -223,6 +324,24 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia return item.NETWR ? formatNumber(item.NETWR, decimals) : '-' })()} + + {item.materialNo ? ( + + ) : ( + - + )} + {item.ZPLN_ST_DT || '-'} {item.ZPO_DLV_DT || item.deliveryDate || '-'} {item.ZPLN_ED_DT || '-'} @@ -277,6 +396,16 @@ export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDia )} + + {/* POS 파일 선택 다이얼로그 */} + ) diff --git a/lib/po/vendor-table/vendor-po-print-dialog.tsx b/lib/po/vendor-table/vendor-po-print-dialog.tsx new file mode 100644 index 00000000..f4e30798 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-print-dialog.tsx @@ -0,0 +1,514 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { PrinterIcon } from "lucide-react" +import { VendorPO, VendorPOItem } from "./types" +import { formatNumber } from "@/lib/utils" +import { getVendorPOById, getVendorPOItems } from "./service" + +interface VendorPOPrintDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: VendorPO | null +} + +// 발주서 헤더 컴포넌트 (페이지마다 반복) +function POHeader({ po }: { po: VendorPO }) { + return ( +
+ {/* 로고 및 타이틀 */} + + + + + + + + +
+
+ {/* 구매처 로고 위치 */} + PURCHASER LOGO +
+
+
+ 발주서 / PURCHASE ORDER +
+
+
+ {/* 공급자 로고 위치 */} + SUPPLIER LOGO +
+
+ + {/* 회사 정보 및 PO 정보 */} + + + + + + + +
+ + + + + + + +
거제조선소 : 경상남도 거제시 장평3로 80 (장평동)
SHIP YARD : 80, Jangpyeong 3-ro, Geoje-si, Gyeongsangnam-do, 53261, Rep. of KOREA
FAX NO : +82-55-630-5768
TEL NO : +82-55-631-4434
+
+ + + + + + + + + + + + + + + + + + + +
+ PO번호 + + {po.contractNo} +
+ REV + + {po.poVersion ? String(po.poVersion).padStart(2, '0') : '00'} +
+ PO발행일 + + {po.contractDate || '-'} +
+ 구매담당 + + {po.purchaseManagerName || po.purchaseGroup || '-'} +
+
+ + {/* 출력 정보 */} +
+ OUTPUT : {new Date().toLocaleString('ko-KR')} +
+
+ ) +} + +export function VendorPOPrintDialog({ + open, + onOpenChange, + po, +}: VendorPOPrintDialogProps) { + const [loading, setLoading] = React.useState(false) + const [poDetails, setPoDetails] = React.useState(null) + const [poItems, setPoItems] = React.useState([]) + + // PO 상세 정보 로드 + React.useEffect(() => { + if (open && po) { + setLoading(true) + Promise.all([ + getVendorPOById(po.id), + getVendorPOItems(po.id) + ]) + .then(([details, items]) => { + setPoDetails(details) + setPoItems(items) + }) + .catch((error) => { + console.error("Failed to load PO details:", error) + }) + .finally(() => { + setLoading(false) + }) + } + }, [open, po]) + + const handlePrint = () => { + window.print() + } + + if (!po) return null + + const displayPO = poDetails || po + + // 총 수량 계산 + const totalQuantity = poItems.reduce((sum, item) => sum + (item.quantity || 0), 0) + + return ( + <> + {/* 인쇄 전용 스타일 - Tailwind와 독립적으로 작동 */} +