diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-26 12:09:39 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-26 12:09:39 +0000 |
| commit | 1110427907bbe9c11a378da4c1a233b83b5ca3b1 (patch) | |
| tree | 8bd7ed2ce7ec47a7f05693f5d3afcc22b1bb7e19 | |
| parent | 5f479f7252a7aa3328bfe186893de8b011e21b15 (diff) | |
(김준회) 구매정의서 구현 - PO (shi & vendor)
| -rw-r--r-- | app/[lng]/evcp/(evcp)/po/page.tsx | 32 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/po/page.tsx | 76 | ||||
| -rw-r--r-- | db/schema/contract.ts | 70 | ||||
| -rw-r--r-- | db/seeds/sap-ecc-po-seed.ts | 682 | ||||
| -rw-r--r-- | lib/po/vendor-table/mock-data.ts | 1940 | ||||
| -rw-r--r-- | lib/po/vendor-table/service.ts | 417 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-columns.tsx | 462 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-table.tsx | 267 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx | 66 | ||||
| -rw-r--r-- | lib/po/vendor-table/types.ts | 119 | ||||
| -rw-r--r-- | lib/po/vendor-table/validations.ts | 58 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-columns.tsx | 511 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-items-dialog.tsx | 199 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-table.tsx | 247 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-toolbar-actions.tsx | 214 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/po-mapper.ts | 64 | ||||
| -rw-r--r-- | package.json | 3 |
17 files changed, 5378 insertions, 49 deletions
diff --git a/app/[lng]/evcp/(evcp)/po/page.tsx b/app/[lng]/evcp/(evcp)/po/page.tsx index d8e32963..7479df8c 100644 --- a/app/[lng]/evcp/(evcp)/po/page.tsx +++ b/app/[lng]/evcp/(evcp)/po/page.tsx @@ -5,23 +5,23 @@ import { getValidFilters } from "@/lib/data-table" import { Skeleton } from "@/components/ui/skeleton" import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" import { Shell } from "@/components/shell" -import { getPOs } from "@/lib/po/service" -import { searchParamsCache } from "@/lib/po/validations" -import { PoListsTable } from "@/lib/po/table/po-table" +import { getVendorPOs } from "@/lib/po/vendor-table/service" +import { vendorPoSearchParamsCache } from "@/lib/po/vendor-table/validations" +import { ShiVendorPoTable } from "@/lib/po/vendor-table/shi-vendor-po-table" import { InformationButton } from "@/components/information/information-button" -interface IndexPageProps { +interface VendorPOPageProps { searchParams: Promise<SearchParams> } -export default async function IndexPage(props: IndexPageProps) { +export default async function VendorPONew(props: VendorPOPageProps) { const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) + const search = vendorPoSearchParamsCache.parse(searchParams) const validFilters = getValidFilters(search.filters) const promises = Promise.all([ - getPOs({ + getVendorPOs({ ...search, filters: validFilters, }), @@ -29,39 +29,33 @@ export default async function IndexPage(props: IndexPageProps) { return ( <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> <div className="flex items-center justify-between space-y-2"> <div> <div className="flex items-center gap-2"> <h2 className="text-2xl font-bold tracking-tight"> - PO 확인 및 전자서명 + PO 관리 </h2> - <InformationButton pagePath="evcp/po" /> + <InformationButton pagePath="evcp/po-new" /> </div> - {/* <p className="text-muted-foreground"> - 기간계 시스템으로부터 PO를 확인하고 협력업체에게 전자서명을 요청할 수 있습니다. 요쳥된 전자서명의 이력 또한 확인할 수 있습니다. - - </p> */} </div> </div> </div> - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> </React.Suspense> <React.Suspense fallback={ <DataTableSkeleton - columnCount={6} + columnCount={8} searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + filterableColumnCount={3} + cellWidths={["10rem", "15rem", "12rem", "10rem", "12rem", "10rem", "10rem", "8rem"]} shrinkZero /> } > - <PoListsTable promises={promises} /> + <ShiVendorPoTable promises={promises} /> </React.Suspense> </Shell> ) diff --git a/app/[lng]/partners/(partners)/po/page.tsx b/app/[lng]/partners/(partners)/po/page.tsx new file mode 100644 index 00000000..ebe7601e --- /dev/null +++ b/app/[lng]/partners/(partners)/po/page.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { redirect } from "next/navigation" + +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getVendorPOs } from "@/lib/po/vendor-table/service" +import { vendorPoSearchParamsCache } from "@/lib/po/vendor-table/validations" +import { VendorPoTable } from "@/lib/po/vendor-table/vendor-po-table" +import { InformationButton } from "@/components/information/information-button" + +interface VendorPOPageProps { + searchParams: Promise<SearchParams> +} + +export default async function VendorPO(props: VendorPOPageProps) { + const searchParams = await props.searchParams + const search = vendorPoSearchParamsCache.parse(searchParams) + + // 세션에서 벤더 정보 가져오기 + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + return ( + <div className="flex h-full items-center justify-center p-6"> + 정상적인 벤더에 소속된 계정이 아닙니다. + </div> + ) + } + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getVendorPOs({ + ...search, + filters: validFilters, + vendorId: session.user.companyId, // 벤더 필터링 적용 + }), + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 벤더 PO 관리 + </h2> + <InformationButton pagePath="partners/po" /> + </div> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={1} + filterableColumnCount={3} + cellWidths={["10rem", "15rem", "12rem", "10rem", "12rem", "10rem", "10rem", "8rem"]} + shrinkZero + /> + } + > + <VendorPoTable promises={promises} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/db/schema/contract.ts b/db/schema/contract.ts index f56fc36e..4790d717 100644 --- a/db/schema/contract.ts +++ b/db/schema/contract.ts @@ -8,7 +8,6 @@ import { date, boolean, unique, - jsonb, uniqueIndex, pgView } from "drizzle-orm/pg-core" import { projects } from "./projects" @@ -31,37 +30,74 @@ export const contracts = pgTable("contracts", { .references(() => vendors.id, { onDelete: "cascade" }), // 계약/PO 번호(유니크) - contractNo: varchar("contract_no", { length: 100 }).notNull().unique(), - contractName: varchar("contract_name", { length: 255 }).notNull(), + contractNo: varchar("contract_no", { length: 100 }).notNull().unique(), // EBELN + contractName: varchar("contract_name", { length: 255 }).notNull(), // ZTITLE // 계약/PO 상태나 기간 - status: varchar("status", { length: 50 }).notNull().default("ACTIVE"), + status: varchar("status", { length: 50 }).notNull().default("ACTIVE"), // ? startDate: date("start_date"), // 발주일(혹은 유효 시작일) endDate: date("end_date"), // 계약 종료일/유효 기간 등 - // --- PO에 자주 쓰이는 필드 추가 --- - paymentTerms: text("payment_terms"), // 지급 조건(예: 30일 후 현금, 선금/잔금 등) - deliveryTerms: text("delivery_terms"), // 납품 조건(Incoterms 등) - deliveryDate: date("delivery_date"), // 납품 기한(납기 예정일) - deliveryLocation: varchar("delivery_location", { length: 255 }), // 납품 장소 + // --- SAP ECC 인터페이스 매핑 필드들 --- + // 기본 PO 정보 + paymentTerms: text("payment_terms"), // 지급 조건 (ZTERM - 지급조건코드) + deliveryTerms: text("delivery_terms"), // 납품 조건 (INCO1 - 인도조건코드) + deliveryDate: date("delivery_date"), // 납품 기한 (ZPO_DLV_DT - PO납기일자, 개별 품목별) + shippmentPlace: varchar("shippment_place", { length: 255 }), // 선적지 (ZSHIPMT_PLC_CD - 선적지코드) + deliveryLocation: varchar("delivery_location", { length: 255 }), // 하역지 (ZUNLD_PLC_CD - 하역지코드) + + // SAP ECC 추가 필드들 + poVersion: integer("po_version"), // PO 버전 (ZPO_VER - 발주버전) + purchaseDocType: varchar("purchase_doc_type", { length: 10 }), // 구매문서유형 (BSART) + purchaseOrg: varchar("purchase_org", { length: 10 }), // 구매조직 (EKORG - 구매조직코드) + purchaseGroup: varchar("purchase_group", { length: 10 }), // 구매그룹 (EKGRP - 구매그룹코드) + exchangeRate: numeric("exchange_rate", { precision: 9, scale: 5 }), // 환율 (WKURS) + poConfirmStatus: varchar("po_confirm_status", { length: 10 }), // PO확인상태 (ZPO_CNFM_STAT) + + // 계약/보증 관련 + contractGuaranteeCode: varchar("contract_guarantee_code", { length: 2 }), // 계약보증코드 (ZCNRT_GRNT_CD) + defectGuaranteeCode: varchar("defect_guarantee_code", { length: 2 }), // 하자보증코드 (ZDFCT_GRNT_CD) + guaranteePeriodCode: varchar("guarantee_period_code", { length: 2 }), // 보증기간코드 (ZGRNT_PRD_CD) + advancePaymentYn: varchar("advance_payment_yn", { length: 1 }), // 선급금여부 (ZPAMT_YN) + + // 금액 관련 (KRW 변환) + budgetAmount: numeric("budget_amount", { precision: 17, scale: 2 }), // 예산금액 (ZBGT_AMT) + budgetCurrency: varchar("budget_currency", { length: 5 }), // 예산통화 (ZBGT_CURR) + totalAmountKrw: numeric("total_amount_krw", { precision: 17, scale: 2 }), // 발주금액KRW (ZPO_AMT_KRW) + + // 전자계약/승인 관련 + electronicContractYn: varchar("electronic_contract_yn", { length: 1 }), // 전자계약필요여부 (ZELC_CNRT_ND_YN) + electronicApprovalDate: date("electronic_approval_date"), // 전자승인일자 (ZELC_AGR_DT) + electronicApprovalTime: varchar("electronic_approval_time", { length: 6 }), // 전자승인시간 (ZELC_AGR_TM) + ownerApprovalYn: varchar("owner_approval_yn", { length: 1 }), // 선주승인필요여부 (ZOWN_AGR_IND_YN) + + // 기타 + plannedInOutFlag: varchar("planned_in_out_flag", { length: 1 }), // 계획내외구분 (ZPLN_INO_GB) + settlementStandard: varchar("settlement_standard", { length: 1 }), // 정산기준 (ZECAL_BSE) + weightSettlementFlag: varchar("weight_settlement_flag", { length: 1 }), // 중량정산구분 (ZWGT_ECAL_GB) + + // 연동제 관련 + priceIndexYn: varchar("price_index_yn", { length: 1 }), // 납품대금연동제대상여부 (ZDLV_PRICE_T) + writtenContractNo: varchar("written_contract_no", { length: 20 }), // 서면계약번호 (ZWEBELN) + contractVersion: integer("contract_version"), // 서면계약차수 (ZVER_NO) // 가격/금액 관련 - currency: varchar("currency", { length: 10 }).default("KRW"), // 통화 (KRW, USD 등) - totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 총 계약 금액(아이템 합산 등) - discount: numeric("discount", { precision: 12, scale: 2 }), // 전체 할인 - tax: numeric("tax", { precision: 12, scale: 2 }), // 전체 세금 - shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 배송비 - netTotal: numeric("net_total", { precision: 12, scale: 2 }), // (합계) - (할인) + (세금) + (배송비) + currency: varchar("currency", { length: 10 }).default("KRW"), // 통화 (KRW, USD 등) // ZPO_CURR + totalAmount: numeric("total_amount", { precision: 12, scale: 2 }), // 총 계약 금액(아이템 합산 등) // ZPO_AMT + discount: numeric("discount", { precision: 12, scale: 2 }), // 전체 할인 // 인터페이스에 없음 (개별 품목별로는 있음) + tax: numeric("tax", { precision: 12, scale: 2 }), // 전체 세금 // 인터페이스에 없음 (개별 품목별로는 있음) + shippingFee: numeric("shipping_fee", { precision: 12, scale: 2 }), // 배송비 // 인터페이스에 없음 (개별 품목별로는 있음) + netTotal: numeric("net_total", { precision: 12, scale: 2 }), // (합계) - (할인) + (세금) + (배송비) // 인터페이스에 없음 (개별 품목별로는 있음) // 부분 납품/부분 결제 가능 여부 partialShippingAllowed: boolean("partial_shipping_allowed").default(false), partialPaymentAllowed: boolean("partial_payment_allowed").default(false), // 추가 메모/비고 - remarks: text("remarks"), + remarks: text("remarks"), // 발주노트 1, 2가 있는데 메모용인것으로 추정 // 버전 관리 (PO 재발행 등) - version: integer("version").default(1), + version: integer("version").default(1), // 생성/수정 시각 createdAt: timestamp("created_at").defaultNow().notNull(), diff --git a/db/seeds/sap-ecc-po-seed.ts b/db/seeds/sap-ecc-po-seed.ts new file mode 100644 index 00000000..ce5130a2 --- /dev/null +++ b/db/seeds/sap-ecc-po-seed.ts @@ -0,0 +1,682 @@ +/** + * SAP ECC PO 데이터 종합 시딩 스크립트 + * + * 이 스크립트는 다음 데이터를 생성합니다: + * 1. 프로젝트 (projects) + * 2. 벤더 (vendors + vendor_contacts) + * 3. 아이템 (items) + * 4. 계약 (contracts + contract_items) + * + * 실행: npm run seed:sap-po + */ + +import { faker } from "@faker-js/faker"; +import db from "@/db/db"; +import { projects } from "@/db/schema/projects"; +import { vendors, vendorContacts, vendorPossibleItems } from "@/db/schema/vendors"; +import { items } from "@/db/schema/items"; +import { contracts, contractItems } from "@/db/schema/contract"; + +// SAP ECC 코드값 매핑 테이블 +const SAP_CODES = { + // 구매문서유형 (BSART) + PURCHASE_DOC_TYPES: [ + { code: "NB", name: "표준 구매오더" }, + { code: "UB", name: "재고 전송 오더" }, + { code: "KB", name: "위탁 구매오더" }, + { code: "LP", name: "서비스 구매오더" }, + ], + + // 구매조직 (EKORG) + PURCHASE_ORGS: [ + { code: "1000", name: "SHI 구매조직" }, + { code: "2000", name: "해외 구매조직" }, + { code: "3000", name: "프로젝트 구매조직" }, + ], + + // 구매그룹 (EKGRP) + PURCHASE_GROUPS: [ + { code: "001", name: "철강재 구매그룹" }, + { code: "002", name: "기계장비 구매그룹" }, + { code: "003", name: "전기전자 구매그룹" }, + { code: "004", name: "서비스 구매그룹" }, + ], + + // 지급조건 (ZTERM) + PAYMENT_TERMS: [ + { code: "Z001", name: "월말결제" }, + { code: "Z002", name: "선급금 30%, 잔금 70%" }, + { code: "Z003", name: "현금결제" }, + { code: "Z004", name: "60일 후 결제" }, + ], + + // 인도조건 (INCO1) + DELIVERY_TERMS: [ + { code: "FOB", name: "FOB" }, + { code: "CIF", name: "CIF" }, + { code: "EXW", name: "EXW" }, + { code: "DDP", name: "DDP" }, + ], + + // 통화 (ZPO_CURR) + CURRENCIES: ["KRW", "USD", "EUR", "JPY"], + + // 계약상태 (ZPO_CNFM_STAT) + CONTRACT_STATUS: [ + { code: "01", name: "승인대기" }, + { code: "02", name: "승인완료" }, + { code: "03", name: "계약완료" }, + { code: "04", name: "진행중" }, + { code: "99", name: "취소됨" }, + ], +}; + +// 프로젝트 데이터 생성 +async function seedProjects() { + console.log("🚀 Seeding projects..."); + + await db.delete(projects); + + const projectsData = [ + { + code: "SHI2024001", + name: "해상풍력 프로젝트 A", + type: "ship", + pspid: "WBS-2024-001-OFFSHORE-A", + }, + { + code: "SHI2024002", + name: "해상풍력 프로젝트 B", + type: "ship", + pspid: "WBS-2024-002-OFFSHORE-B", + }, + { + code: "SHI2024003", + name: "조선 프로젝트 C", + type: "ship", + pspid: "WBS-2024-003-SHIPYARD-C", + }, + { + code: "SHI2024004", + name: "해양플랜트 프로젝트 D", + type: "ship", + pspid: "WBS-2024-004-OFFSHORE-D", + }, + { + code: "SHI2024005", + name: "LNG 선박 프로젝트 E", + type: "ship", + pspid: "WBS-2024-005-LNG-SHIP-E", + }, + ]; + + const insertedProjects = []; + for (const projectData of projectsData) { + const [inserted] = await db.insert(projects).values(projectData).returning(); + insertedProjects.push(inserted); + console.log(` ✅ Project: ${projectData.code} - ${projectData.name}`); + } + + return insertedProjects; +} + +// 벤더 데이터 생성 +async function seedVendors() { + console.log("🚀 Seeding vendors..."); + + await db.delete(vendorPossibleItems); + await db.delete(vendorContacts); + await db.delete(vendors); + + const vendorsData = [ + { + vendorName: "현대제철 주식회사", + vendorCode: "V100001", + taxId: "134-81-00364", + address: "서울특별시 강남구 테헤란로 440", + country: "대한민국", + phone: "02-3457-0114", + email: "contact@hyundai-steel.com", + status: "ACTIVE" as const, + }, + { + vendorName: "포스코 주식회사", + vendorCode: "V100002", + taxId: "220-81-00307", + address: "경상북도 포항시 남구 동해안로 6267", + country: "대한민국", + phone: "054-220-0114", + email: "info@posco.com", + status: "ACTIVE" as const, + }, + { + vendorName: "두산중공업 주식회사", + vendorCode: "V100003", + taxId: "135-81-00473", + address: "경상남도 창원시 성산구 두산볼바르 22", + country: "대한민국", + phone: "055-278-9114", + email: "contact@doosan.com", + status: "ACTIVE" as const, + }, + { + vendorName: "Samsung Heavy Industries Co., Ltd.", + vendorCode: "V100004", + taxId: "135-81-00432", + address: "경상남도 거제시 장평3로 1", + country: "대한민국", + phone: "055-630-0114", + email: "info@shi.samsung.com", + status: "ACTIVE" as const, + }, + { + vendorName: "HD한국조선해양 주식회사", + vendorCode: "V100005", + taxId: "135-81-00364", + address: "울산광역시 동구 방어진순환도로 400", + country: "대한민국", + phone: "052-202-2114", + email: "contact@hd-ksoe.com", + status: "ACTIVE" as const, + }, + ]; + + const insertedVendors = []; + for (const vendorData of vendorsData) { + const [insertedVendor] = await db.insert(vendors).values(vendorData).returning(); + insertedVendors.push(insertedVendor); + + // 각 벤더마다 연락처 2-3개 생성 + const contactCount = faker.number.int({ min: 2, max: 3 }); + for (let i = 0; i < contactCount; i++) { + const contactData = { + vendorId: insertedVendor.id, + contactName: faker.person.fullName({ sex: "male" }), + contactPosition: faker.helpers.arrayElement([ + "구매담당자", "영업담당자", "기술담당자", "품질담당자", "대표이사" + ]), + contactEmail: faker.internet.email(), + contactPhone: faker.phone.number(), + isPrimary: i === 0, + }; + await db.insert(vendorContacts).values(contactData); + } + + console.log(` ✅ Vendor: ${vendorData.vendorCode} - ${vendorData.vendorName}`); + } + + return insertedVendors; +} + +// 아이템 데이터 생성 (더 많은 아이템 추가) +async function seedItems() { + console.log("🚀 Seeding items..."); + + await db.delete(items); + + const itemsData = [ + // 철강재 관련 + { + ProjectNo: "SHI2024001", + itemCode: "STL-H-BEAM-300", + itemName: "H형강 300x300", + packageCode: "STEEL-STRUCT", + smCode: "SM001", + description: "구조용 H형강 300x300x10x15", + parentItemCode: "STL-STRUCT", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "SS", + gradeMaterial: "SS400", + }, + { + ProjectNo: "SHI2024001", + itemCode: "STL-PLATE-20T", + itemName: "강판 20T", + packageCode: "STEEL-PLATE", + smCode: "SM002", + description: "일반구조용 강판 20mm 두께", + parentItemCode: "STL-PLATE", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "KG", + steelType: "SS", + gradeMaterial: "SS400", + }, + { + ProjectNo: "SHI2024001", + itemCode: "STL-ANGLE-100X100", + itemName: "앵글강 100x100", + packageCode: "STEEL-ANGLE", + smCode: "SM003", + description: "등변앵글강 100x100x10", + parentItemCode: "STL-STRUCT", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: "SS", + gradeMaterial: "SS400", + }, + { + ProjectNo: "SHI2024001", + itemCode: "STL-CHANNEL-200", + itemName: "채널강 200", + packageCode: "STEEL-CHANNEL", + smCode: "SM004", + description: "용접구조용 채널강 200x80x7.5", + parentItemCode: "STL-STRUCT", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: "SS", + gradeMaterial: "SS400", + }, + + // 배관 관련 + { + ProjectNo: "SHI2024002", + itemCode: "PIPE-CARBON-100A", + itemName: "탄소강관 100A", + packageCode: "PIPE-CARBON", + smCode: "SM005", + description: "배관용 탄소강관 100A SCH40", + parentItemCode: "PIPE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: "CS", + gradeMaterial: "STPG370", + }, + { + ProjectNo: "SHI2024002", + itemCode: "PIPE-STAINLESS-80A", + itemName: "스테인리스강관 80A", + packageCode: "PIPE-STAINLESS", + smCode: "SM006", + description: "스테인리스강관 80A SCH40", + parentItemCode: "PIPE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: "SS", // SUS는 2글자 제한 초과, SS로 변경 + gradeMaterial: "SUS304", + }, + { + ProjectNo: "SHI2024002", + itemCode: "PIPE-ELBOW-100A", + itemName: "엘보 100A", + packageCode: "PIPE-FITTING", + smCode: "SM007", + description: "탄소강 엘보 100A 90도", + parentItemCode: "PIPE-FITTING", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "CS", + gradeMaterial: "STPG370", + }, + + // 밸브 관련 + { + ProjectNo: "SHI2024002", + itemCode: "VALVE-GATE-100A", + itemName: "게이트밸브 100A", + packageCode: "VALVE-GATE", + smCode: "SM008", + description: "주철제 게이트밸브 100A 16K", + parentItemCode: "VALVE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "CI", + gradeMaterial: "FC200", + }, + { + ProjectNo: "SHI2024002", + itemCode: "VALVE-BALL-80A", + itemName: "볼밸브 80A", + packageCode: "VALVE-BALL", + smCode: "SM009", + description: "스테인리스 볼밸브 80A 16K", + parentItemCode: "VALVE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "SS", // SUS는 2글자 제한 초과, SS로 변경 + gradeMaterial: "SUS316", + }, + { + ProjectNo: "SHI2024002", + itemCode: "VALVE-CHECK-100A", + itemName: "체크밸브 100A", + packageCode: "VALVE-CHECK", + smCode: "SM010", + description: "주철제 체크밸브 100A 16K", + parentItemCode: "VALVE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "CI", + gradeMaterial: "FC200", + }, + + // 전기/전자 관련 + { + ProjectNo: "SHI2024003", + itemCode: "MOTOR-3PH-15KW", + itemName: "3상 유도전동기 15kW", + packageCode: "MOTOR-3PH", + smCode: "SM011", + description: "3상 유도전동기 15kW 380V 60Hz", + parentItemCode: "MOTOR-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: null, + gradeMaterial: null, + }, + { + ProjectNo: "SHI2024003", + itemCode: "MOTOR-3PH-30KW", + itemName: "3상 유도전동기 30kW", + packageCode: "MOTOR-3PH", + smCode: "SM012", + description: "3상 유도전동기 30kW 380V 60Hz", + parentItemCode: "MOTOR-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: null, + gradeMaterial: null, + }, + { + ProjectNo: "SHI2024003", + itemCode: "CABLE-POWER-4SQ", + itemName: "전력케이블 4SQ", + packageCode: "CABLE-POWER", + smCode: "SM013", + description: "CV 전력케이블 4SQ 4C 600V", + parentItemCode: "CABLE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: null, + gradeMaterial: null, + }, + { + ProjectNo: "SHI2024003", + itemCode: "PANEL-MCC-1000A", + itemName: "MCC 판넬 1000A", + packageCode: "PANEL-MCC", + smCode: "SM014", + description: "모터제어반 1000A 380V", + parentItemCode: "PANEL-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: null, + gradeMaterial: null, + }, + + // 기계장비 관련 + { + ProjectNo: "SHI2024004", + itemCode: "PUMP-CENTRIFUGAL-100", + itemName: "원심펌프 100m3/h", + packageCode: "PUMP-CENTRIFUGAL", + smCode: "SM015", + description: "원심펌프 100m3/h 50m 15kW", + parentItemCode: "PUMP-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "CS", + gradeMaterial: "WCC", + }, + { + ProjectNo: "SHI2024004", + itemCode: "COMPRESSOR-SCREW-100", + itemName: "스크류압축기 100kW", + packageCode: "COMPRESSOR-SCREW", + smCode: "SM016", + description: "스크류 공기압축기 100kW 8kg/cm2", + parentItemCode: "COMPRESSOR-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: null, + gradeMaterial: null, + }, + { + ProjectNo: "SHI2024005", + itemCode: "CRANE-OVERHEAD-10T", + itemName: "천장크레인 10톤", + packageCode: "CRANE-OVERHEAD", + smCode: "SM017", + description: "천장크레인 10톤 20m span", + parentItemCode: "CRANE-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "EA", + steelType: "SS", + gradeMaterial: "SS400", + }, + { + ProjectNo: "SHI2024005", + itemCode: "CONVEYOR-BELT-500", + itemName: "벨트컨베이어 500mm", + packageCode: "CONVEYOR-BELT", + smCode: "SM018", + description: "벨트컨베이어 500mm폭 50m", + parentItemCode: "CONVEYOR-STD", + itemLevel: 2, + deleteFlag: "N", + unitOfMeasure: "M", + steelType: "SS", + gradeMaterial: "SS400", + }, + ]; + + const insertedItems = []; + for (const itemData of itemsData) { + const [inserted] = await db.insert(items).values(itemData).returning(); + insertedItems.push(inserted); + console.log(` ✅ Item: ${itemData.itemCode} - ${itemData.itemName}`); + } + + return insertedItems; +} + +// 계약 데이터 생성 (SAP ECC 필드 포함) +async function seedContracts(insertedProjects: Array<{ id: number; code: string; name: string }>, insertedVendors: Array<{ id: number; vendorName: string; vendorCode: string }>, insertedItems: Array<{ id: number; itemCode: string; itemName: string; description: string; packageCode: string; gradeMaterial: string | null; unitOfMeasure: string }>) { + console.log("🚀 Seeding contracts with SAP ECC fields..."); + + await db.delete(contractItems); + await db.delete(contracts); + + // 계약 데이터 생성 (더 많은 계약 생성) + const contractsData = []; + + for (let i = 0; i < 15; i++) { + const project = faker.helpers.arrayElement(insertedProjects); + const vendor = faker.helpers.arrayElement(insertedVendors); + const purchaseDocType = faker.helpers.arrayElement(SAP_CODES.PURCHASE_DOC_TYPES); + const purchaseOrg = faker.helpers.arrayElement(SAP_CODES.PURCHASE_ORGS); + const purchaseGroup = faker.helpers.arrayElement(SAP_CODES.PURCHASE_GROUPS); + const paymentTerms = faker.helpers.arrayElement(SAP_CODES.PAYMENT_TERMS); + const deliveryTerms = faker.helpers.arrayElement(SAP_CODES.DELIVERY_TERMS); + const currency = faker.helpers.arrayElement(SAP_CODES.CURRENCIES); + const contractStatus = faker.helpers.arrayElement(SAP_CODES.CONTRACT_STATUS); + + const contractNo = `PO${new Date().getFullYear()}${String(i + 1).padStart(6, '0')}`; + const totalAmount = faker.number.float({ min: 100000, max: 10000000, multipleOf: 0.01 }); + const exchangeRate = currency === 'KRW' ? 1 : faker.number.float({ min: 1100, max: 1400, multipleOf: 0.01 }); + const totalAmountKrw = currency === 'KRW' ? totalAmount : totalAmount * exchangeRate; + + const contractData = { + // 기본 필드 + projectId: project.id, + vendorId: vendor.id, + contractNo, + contractName: `${purchaseDocType.name} - ${faker.commerce.productName()}`, + status: contractStatus.name, + startDate: faker.date.recent({ days: 30 }).toISOString().split('T')[0], + endDate: faker.date.future({ years: 1 }).toISOString().split('T')[0], + + // SAP ECC 기본 필드들 + paymentTerms: paymentTerms.name, + deliveryTerms: deliveryTerms.code, + shippmentPlace: faker.helpers.arrayElement(["부산항", "인천항", "울산항", "포항항"]), + deliveryLocation: faker.helpers.arrayElement(["거제조선소", "울산조선소", "부산신항", "인천항"]), + + // SAP ECC 추가 필드들 + poVersion: faker.number.int({ min: 1, max: 5 }), + purchaseDocType: purchaseDocType.code, + purchaseOrg: purchaseOrg.code, + purchaseGroup: purchaseGroup.code, + exchangeRate: exchangeRate.toString(), + poConfirmStatus: contractStatus.code, + + // 계약/보증 관련 + contractGuaranteeCode: faker.helpers.arrayElement(["1", "2", "3"]), + defectGuaranteeCode: faker.helpers.arrayElement(["1", "2"]), + guaranteePeriodCode: faker.helpers.arrayElement(["1", "2", "3"]), + advancePaymentYn: faker.helpers.arrayElement(["Y", "N"]), + + // 금액 관련 + budgetAmount: (totalAmount * 1.1).toString(), + budgetCurrency: currency, + currency, + totalAmount: totalAmount.toString(), + totalAmountKrw: totalAmountKrw.toString(), + + // 전자계약/승인 관련 + electronicContractYn: faker.helpers.arrayElement(["Y", "N"]), + electronicApprovalDate: faker.helpers.maybe(() => + faker.date.recent({ days: 10 }).toISOString().split('T')[0], { probability: 0.7 } + ), + electronicApprovalTime: faker.helpers.maybe(() => + faker.date.recent().toTimeString().slice(0, 8).replace(/:/g, ''), { probability: 0.7 } + ), + ownerApprovalYn: faker.helpers.arrayElement(["Y", "N"]), + + // 기타 + plannedInOutFlag: faker.helpers.arrayElement(["I", "O"]), + settlementStandard: faker.helpers.arrayElement(["1", "2", "3"]), + weightSettlementFlag: faker.helpers.arrayElement(["Y", "N"]), + + // 연동제 관련 + priceIndexYn: faker.helpers.arrayElement(["Y", "N"]), + writtenContractNo: faker.helpers.maybe(() => `WC${contractNo.slice(2)}`, { probability: 0.6 }), + contractVersion: faker.number.int({ min: 1, max: 3 }), + + netTotal: totalAmount.toString(), + remarks: faker.lorem.sentence(), + version: 1, + }; + + contractsData.push(contractData); + } + + const insertedContracts = []; + for (const contractData of contractsData) { + const [insertedContract] = await db.insert(contracts).values(contractData).returning(); + insertedContracts.push(insertedContract); + + // 각 계약마다 계약 아이템 3-8개 생성 (중복 방지) + const itemCount = faker.number.int({ min: 3, max: 8 }); + const shuffledItems = faker.helpers.shuffle([...insertedItems]); + const selectedItems = shuffledItems.slice(0, Math.min(itemCount, shuffledItems.length)); + + for (const item of selectedItems) { + const quantity = faker.number.int({ min: 1, max: 100 }); + const unitPrice = faker.number.float({ min: 1000, max: 100000, multipleOf: 0.01 }); + const taxRate = faker.helpers.arrayElement([0.00, 10.00]); // 면세 또는 10% 세율 + const taxAmount = (unitPrice * quantity * taxRate) / 100; + const totalLineAmount = (unitPrice * quantity) + taxAmount; + + // SAP ECC 관련 상세 설명 생성 + const detailedDescription = `${item.itemName} - ${item.description} +품목코드: ${item.itemCode} +패키지: ${item.packageCode} +규격: ${faker.helpers.arrayElement(['KS규격', 'JIS규격', 'ASTM규격', 'DIN규격'])} +재질: ${item.gradeMaterial || 'N/A'} +단위: ${item.unitOfMeasure}`; + + // 비고에 SAP 관련 정보 추가 + const sapRemarks = [ + `납기: ${faker.date.future({ years: 1 }).toISOString().split('T')[0]}`, + `검사여부: ${faker.helpers.arrayElement(['필요', '불필요'])}`, + `저장위치: ${faker.helpers.arrayElement(['본사창고', '현장창고', '협력업체'])}`, + `운송조건: ${faker.helpers.arrayElement(['FOB', 'CIF', 'EXW', 'DDP'])}`, + `품질등급: ${faker.helpers.arrayElement(['A급', 'B급', '특급'])}` + ]; + + const itemData = { + contractId: insertedContract.id, + itemId: item.id, + description: detailedDescription, + quantity, + unitPrice: unitPrice.toString(), + taxRate: taxRate.toString(), + taxAmount: taxAmount.toString(), + totalLineAmount: totalLineAmount.toString(), + remark: faker.helpers.maybe(() => + `${faker.helpers.arrayElement(sapRemarks)} | ${faker.lorem.sentence()}`, + { probability: 0.7 } + ), + }; + + await db.insert(contractItems).values(itemData); + } + + console.log(` ✅ Contract: ${contractData.contractNo} - ${contractData.contractName}`); + } + + return insertedContracts; +} + +// 메인 시딩 함수 +async function main() { + try { + console.log("🌱 Starting SAP ECC PO data seeding...\n"); + + const insertedProjects = await seedProjects(); + console.log(`✅ Created ${insertedProjects.length} projects\n`); + + const insertedVendors = await seedVendors(); + console.log(`✅ Created ${insertedVendors.length} vendors\n`); + + const insertedItems = await seedItems(); + console.log(`✅ Created ${insertedItems.length} items\n`); + + const insertedContracts = await seedContracts(insertedProjects, insertedVendors, insertedItems); + console.log(`✅ Created ${insertedContracts.length} contracts\n`); + + console.log("🎉 SAP ECC PO data seeding completed successfully!"); + + // 요약 출력 + console.log("\n📊 Seeding Summary:"); + console.log(` - Projects: ${insertedProjects.length}`); + console.log(` - Vendors: ${insertedVendors.length} (각각 2-3명의 연락처)`); + console.log(` - Items: ${insertedItems.length} (철강재, 배관, 밸브, 전기, 기계장비)`); + console.log(` - Contracts: ${insertedContracts.length} (각각 3-8개의 계약품목)`); + + // 예상 contract_items 수 계산 + const estimatedContractItems = insertedContracts.length * 5.5; // 평균 5.5개 + console.log(` - Contract Items: 약 ${Math.round(estimatedContractItems)}개 (상세 품목정보 포함)`); + + process.exit(0); + } catch (error) { + console.error("❌ Seeding failed:", error); + process.exit(1); + } +} + +// 스크립트 실행 +if (require.main === module) { + main(); +} + +export { main as seedSapEccPoData }; diff --git a/lib/po/vendor-table/mock-data.ts b/lib/po/vendor-table/mock-data.ts new file mode 100644 index 00000000..a932ccbb --- /dev/null +++ b/lib/po/vendor-table/mock-data.ts @@ -0,0 +1,1940 @@ +import { VendorPO, VendorPOItem } from "./types" + +// 상세품목 목업 데이터 +export const mockVendorPOItems: VendorPOItem[] = [ + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, +] + +// 벤더 PO 목업 데이터 +export const mockVendorPOs: VendorPO[] = [ + { + id: 1, + contractNo: "PO-2024-001", + revision: "Rev.01", + itemNo: "ITM-001", + contractStatus: "승인대기", + contractType: "구매계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 A", + contractName: "철강재 공급계약", + contractPeriod: "2024-01-01 ~ 2024-12-31", + contractQuantity: "300 EA", + currency: "KRW", + paymentTerms: "월말결제", + tax: "10%", + exchangeRate: "1,330.50", + deliveryTerms: "FOB", + purchaseManager: "이구매", + poReceiveDate: "2024-01-15", + contractDate: "2024-01-20", + lcNo: "LC-2024-001", + priceIndexTarget: true, + linkedContractNo: "PO-2024-000", + lastModifiedDate: "2024-01-25", + lastModifiedBy: "김수정", + items: mockVendorPOItems.filter(item => item.contractNo === "PO-2024-001") + }, + { + id: 2, + contractNo: "PO-2024-002", + revision: "Rev.00", + itemNo: "ITM-003", + contractStatus: "계약완료", + contractType: "서비스계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 B", + contractName: "설치 서비스 계약", + contractPeriod: "2024-02-01 ~ 2024-06-30", + contractQuantity: "1 LOT", + currency: "USD", + paymentTerms: "선급금 30%, 잔금 70%", + tax: "면세", + exchangeRate: "1,330.50", + deliveryTerms: "CIF", + purchaseManager: "박구매", + poReceiveDate: "2024-02-01", + contractDate: "2024-02-05", + priceIndexTarget: false, + lastModifiedDate: "2024-02-10", + lastModifiedBy: "이수정", + items: [] + }, + { + id: 3, + contractNo: "PO-2024-003", + revision: "Rev.02", + itemNo: "ITM-004", + contractStatus: "수정요청", + contractType: "구매계약", + details: "상세보기", + projectName: "조선 프로젝트 C", + contractName: "선박용 부품 공급계약", + contractPeriod: "2024-03-01 ~ 2024-09-30", + contractQuantity: "150 SET", + currency: "EUR", + paymentTerms: "인도 후 30일", + tax: "10%", + exchangeRate: "1,450.20", + deliveryTerms: "EXW", + purchaseManager: "최구매", + poReceiveDate: "2024-03-01", + contractDate: "2024-03-10", + lcNo: "LC-2024-003", + priceIndexTarget: true, + linkedContractNo: "PO-2024-002", + lastModifiedDate: "2024-03-15", + lastModifiedBy: "정수정", + items: [] + }, + { + id: 4, + contractNo: "PO-2024-004", + revision: "Rev.00", + itemNo: "ITM-005", + contractStatus: "거절됨", + contractType: "임가공계약", + details: "상세보기", + projectName: "플랜트 프로젝트 D", + contractName: "가공 서비스 계약", + contractPeriod: "2024-04-01 ~ 2024-08-31", + contractQuantity: "500 HR", + currency: "KRW", + paymentTerms: "월 단위 정산", + tax: "10%", + exchangeRate: "1,330.50", + deliveryTerms: "DDP", + purchaseManager: "김구매", + poReceiveDate: "2024-04-01", + contractDate: "", + priceIndexTarget: false, + lastModifiedDate: "2024-04-05", + lastModifiedBy: "박수정", + items: [] + }, + { + id: 5, + contractNo: "PO-2024-005", + revision: "Rev.01", + itemNo: "ITM-006", + contractStatus: "진행중", + contractType: "구매계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 E", + contractName: "전기설비 공급계약", + contractPeriod: "2024-05-01 ~ 2024-11-30", + contractQuantity: "25 SET", + currency: "USD", + paymentTerms: "LC 90일", + tax: "면세", + exchangeRate: "1,330.50", + deliveryTerms: "FOB", + purchaseManager: "이구매", + poReceiveDate: "2024-05-01", + contractDate: "2024-05-10", + lcNo: "LC-2024-005", + priceIndexTarget: true, + lastModifiedDate: "2024-05-15", + lastModifiedBy: "최수정", + items: [] + } +] + +// 페이지네이션을 위한 헬퍼 함수 +export function getVendorPOsPage( + page: number = 1, + perPage: number = 10, + search?: string, + filters?: any[] +): { data: VendorPO[]; pageCount: number; total: number } { + let filteredData = [...mockVendorPOs] + + // 검색 필터링 + if (search && search.trim()) { + const searchTerm = search.toLowerCase() + filteredData = filteredData.filter(po => + po.contractNo.toLowerCase().includes(searchTerm) || + po.contractName.toLowerCase().includes(searchTerm) || + po.projectName.toLowerCase().includes(searchTerm) || + po.contractStatus.toLowerCase().includes(searchTerm) + ) + } + + // 추가 필터 적용 (필요시 구현) + // if (filters && filters.length > 0) { + // // 필터 로직 구현 + // } + + const total = filteredData.length + const pageCount = Math.ceil(total / perPage) + const offset = (page - 1) * perPage + const data = filteredData.slice(offset, offset + perPage) + + return { data, pageCount, total } +} diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts new file mode 100644 index 00000000..88f6ddd5 --- /dev/null +++ b/lib/po/vendor-table/service.ts @@ -0,0 +1,417 @@ +"use server"; + +import { GetVendorPOSchema } from "./validations"; +import { getVendorPOsPage } from "./mock-data"; +import { VendorPO, VendorPOItem } from "./types"; +import db from "@/db/db"; +import { contracts, contractItems } from "@/db/schema/contract"; +import { projects } from "@/db/schema/projects"; +import { vendors } from "@/db/schema/vendors"; +import { items } from "@/db/schema/items"; +import { eq, and, or, ilike, count, desc, asc } from "drizzle-orm"; + +/** + * 벤더 PO 목록 조회 + * contracts 테이블에서 실제 데이터를 조회합니다. + */ +export async function getVendorPOs(input: GetVendorPOSchema) { + try { + // 실제 데이터베이스 조회 + const offset = (input.page - 1) * input.perPage; + + // 검색 조건 구성 + let whereConditions = []; + if (input.search) { + const searchTerm = `%${input.search}%`; + whereConditions.push( + or( + ilike(contracts.contractNo, searchTerm), + ilike(contracts.contractName, searchTerm), + ilike(projects.name, searchTerm), + ilike(vendors.vendorName, searchTerm) + ) + ); + } + + // 벤더 필터링 (partners 페이지에서 사용) + if (input.vendorId && input.vendorId > 0) { + whereConditions.push(eq(contracts.vendorId, input.vendorId)); + } + + // 필터 조건 추가 + if (input.filters && input.filters.length > 0) { + for (const filter of input.filters) { + if (filter.id && filter.value) { + switch (filter.id) { + case "contractStatus": + whereConditions.push(ilike(contracts.status, `%${filter.value}%`)); + break; + case "contractType": + whereConditions.push(ilike(contracts.purchaseDocType, `%${filter.value}%`)); + break; + case "currency": + whereConditions.push(eq(contracts.currency, filter.value)); + break; + // 추가 필터 조건들... + } + } + } + } + + const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; + + // 정렬 조건 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => { + switch (item.id) { + case "contractNo": + return item.desc ? desc(contracts.contractNo) : asc(contracts.contractNo); + case "contractName": + return item.desc ? desc(contracts.contractName) : asc(contracts.contractName); + case "lastModifiedDate": + return item.desc ? desc(contracts.updatedAt) : asc(contracts.updatedAt); + default: + return desc(contracts.updatedAt); + } + }) + : [desc(contracts.updatedAt)]; + + // 데이터 조회 (조인 포함) + const rawData = await db + .select({ + // contracts 테이블 필드들 + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + currency: contracts.currency, + totalAmount: contracts.totalAmount, + totalAmountKrw: contracts.totalAmountKrw, + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + deliveryLocation: contracts.deliveryLocation, + shippmentPlace: contracts.shippmentPlace, + exchangeRate: contracts.exchangeRate, + + // SAP ECC 추가 필드들 + poVersion: contracts.poVersion, + purchaseDocType: contracts.purchaseDocType, + purchaseOrg: contracts.purchaseOrg, + purchaseGroup: contracts.purchaseGroup, + poConfirmStatus: contracts.poConfirmStatus, + contractGuaranteeCode: contracts.contractGuaranteeCode, + defectGuaranteeCode: contracts.defectGuaranteeCode, + guaranteePeriodCode: contracts.guaranteePeriodCode, + advancePaymentYn: contracts.advancePaymentYn, + budgetAmount: contracts.budgetAmount, + budgetCurrency: contracts.budgetCurrency, + electronicContractYn: contracts.electronicContractYn, + electronicApprovalDate: contracts.electronicApprovalDate, + electronicApprovalTime: contracts.electronicApprovalTime, + ownerApprovalYn: contracts.ownerApprovalYn, + plannedInOutFlag: contracts.plannedInOutFlag, + settlementStandard: contracts.settlementStandard, + weightSettlementFlag: contracts.weightSettlementFlag, + priceIndexYn: contracts.priceIndexYn, + writtenContractNo: contracts.writtenContractNo, + contractVersion: contracts.contractVersion, + + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + + // 조인된 테이블 필드들 + projectName: projects.name, + vendorName: vendors.vendorName, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(finalWhere) + .orderBy(...orderBy) + .offset(offset) + .limit(input.perPage); + + // 총 개수 조회 + const [{ totalCount }] = await db + .select({ totalCount: count() }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(finalWhere); + + const pageCount = Math.ceil(totalCount / input.perPage); + + // VendorPO 타입으로 변환 + const data: VendorPO[] = rawData.map(row => ({ + id: row.id, + contractNo: row.contractNo || '', + revision: 'Rev.01', // mock 데이터용 기본값 + itemNo: 'ITM-AUTO', // mock 데이터용 기본값 + contractStatus: row.status || '', + contractType: row.purchaseDocType || '', + details: '상세보기', // mock 데이터용 기본값 + projectName: row.projectName || '', + contractName: row.contractName || '', + contractPeriod: row.startDate && row.endDate + ? `${row.startDate} ~ ${row.endDate}` + : '', + contractQuantity: '1 LOT', // 기본값 (실제로는 contract_items에서 계산 필요) + currency: row.currency || 'KRW', + paymentTerms: row.paymentTerms || '', + tax: '10%', // 기본값 (실제로는 contract_items에서 계산 필요) + exchangeRate: row.exchangeRate?.toString() || '', + deliveryTerms: row.deliveryTerms || '', + purchaseManager: '', // 사용자 테이블 조인 필요 + poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', + contractDate: row.startDate || '', + lcNo: undefined, + priceIndexTarget: row.priceIndexYn === 'Y', + linkedContractNo: undefined, + lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', + lastModifiedBy: '', // 사용자 테이블 조인 필요 + + // SAP ECC 추가 필드들 + poVersion: row.poVersion || undefined, + purchaseDocType: row.purchaseDocType || undefined, + purchaseOrg: row.purchaseOrg || undefined, + purchaseGroup: row.purchaseGroup || undefined, + poConfirmStatus: row.poConfirmStatus || undefined, + contractGuaranteeCode: row.contractGuaranteeCode || undefined, + defectGuaranteeCode: row.defectGuaranteeCode || undefined, + guaranteePeriodCode: row.guaranteePeriodCode || undefined, + advancePaymentYn: row.advancePaymentYn || undefined, + budgetAmount: row.budgetAmount ? Number(row.budgetAmount) : undefined, + budgetCurrency: row.budgetCurrency || undefined, + totalAmount: row.totalAmount ? Number(row.totalAmount) : undefined, + totalAmountKrw: row.totalAmountKrw ? Number(row.totalAmountKrw) : undefined, + electronicContractYn: row.electronicContractYn || undefined, + electronicApprovalDate: row.electronicApprovalDate || undefined, + electronicApprovalTime: row.electronicApprovalTime || undefined, + ownerApprovalYn: row.ownerApprovalYn || undefined, + plannedInOutFlag: row.plannedInOutFlag || undefined, + settlementStandard: row.settlementStandard || undefined, + weightSettlementFlag: row.weightSettlementFlag || undefined, + priceIndexYn: row.priceIndexYn || undefined, + writtenContractNo: row.writtenContractNo || undefined, + contractVersion: row.contractVersion || undefined, + })); + + return { + data, + pageCount + }; + + // 목업 데이터 사용 (개발/테스트용) + // const result = getVendorPOsPage( + // input.page, + // input.perPage, + // input.search, + // input.filters + // ); + + // 실제 데이터베이스 연동시에는 아래와 같은 구조로 구현 + // const offset = (input.page - 1) * input.perPage; + // + // // 검색 조건 구성 + // let whereConditions = []; + // if (input.search) { + // const searchTerm = `%${input.search}%`; + // whereConditions.push( + // or( + // ilike(vendorPOTable.contractNo, searchTerm), + // ilike(vendorPOTable.contractName, searchTerm), + // ilike(vendorPOTable.projectName, searchTerm) + // ) + // ); + // } + // + // // 필터 조건 추가 + // if (input.contractStatus) { + // whereConditions.push(eq(vendorPOTable.contractStatus, input.contractStatus)); + // } + // + // const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; + // + // // 정렬 조건 + // const orderBy = input.sort.length > 0 + // ? input.sort.map((item) => + // item.desc + // ? desc(vendorPOTable[item.id]) + // : asc(vendorPOTable[item.id]) + // ) + // : [desc(vendorPOTable.lastModifiedDate)]; + // + // // 데이터 조회 + // const data = await db + // .select() + // .from(vendorPOTable) + // .where(finalWhere) + // .orderBy(...orderBy) + // .offset(offset) + // .limit(input.perPage); + // + // // 총 개수 조회 + // const [{ count }] = await db + // .select({ count: count() }) + // .from(vendorPOTable) + // .where(finalWhere); + // + // const pageCount = Math.ceil(count / input.perPage); + + return { + data: result.data, + pageCount: result.pageCount + }; + } catch (err) { + console.error("Error in getVendorPOs:", err); + return { data: [], pageCount: 0 }; + } +} + +/** + * 벤더 PO 상세 정보 조회 + */ +export async function getVendorPOById(id: number): Promise<VendorPO | null> { + try { + // 목업 데이터에서 조회 + const result = getVendorPOsPage(1, 100); // 모든 데이터 가져오기 + const po = result.data.find(item => item.id === id); + + return po || null; + } catch (err) { + console.error("Error in getVendorPOById:", err); + return null; + } +} + +/** + * 벤더 PO 액션 처리 + * PCR생성, 승인, 거절 등의 액션을 처리 + */ +export async function handleVendorPOAction( + poId: number, + action: string, + data?: any +): Promise<{ success: boolean; message: string }> { + try { + // 목업에서는 성공 응답만 반환 + // 실제 구현시에는 각 액션별로 비즈니스 로직 구현 + + switch (action) { + case "pcr-create": + return { success: true, message: "PCR이 성공적으로 생성되었습니다." }; + case "approve": + return { success: true, message: "계약이 승인되었습니다." }; + case "cancel-approve": + return { success: true, message: "승인이 취소되었습니다." }; + case "reject-contract": + return { success: true, message: "계약이 거절되었습니다." }; + case "print-contract": + return { success: true, message: "계약서 출력이 요청되었습니다." }; + default: + return { success: false, message: "알 수 없는 액션입니다." }; + } + } catch (err) { + console.error("Error in handleVendorPOAction:", err); + return { success: false, message: "액션 처리 중 오류가 발생했습니다." }; + } +} + +/** + * 특정 계약의 상세품목 조회 + * contract_items 테이블에서 실제 데이터를 조회합니다. + */ +export async function getVendorPOItems(contractId: number): Promise<VendorPOItem[]> { + try { + const rawItems = await db + .select({ + // contract_items 테이블 필드들 + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + description: contractItems.description, + quantity: contractItems.quantity, + unitPrice: contractItems.unitPrice, + taxRate: contractItems.taxRate, + taxAmount: contractItems.taxAmount, + totalLineAmount: contractItems.totalLineAmount, + remark: contractItems.remark, + + // contracts 테이블 필드들 + contractNo: contracts.contractNo, + + // items 테이블 필드들 + itemCode: items.itemCode, + itemName: items.itemName, + packageCode: items.packageCode, + unitOfMeasure: items.unitOfMeasure, + gradeMaterial: items.gradeMaterial, + steelType: items.steelType, + smCode: items.smCode, + }) + .from(contractItems) + .leftJoin(contracts, eq(contractItems.contractId, contracts.id)) + .leftJoin(items, eq(contractItems.itemId, items.id)) + .where(eq(contractItems.contractId, contractId)) + .orderBy(contractItems.id); + + // VendorPOItem 타입으로 변환 + const vendorPOItems: VendorPOItem[] = rawItems.map(row => ({ + contractNo: row.contractNo || '', + itemNo: row.itemCode || 'AUTO-ITEM', // mock 데이터용 + prNo: `PR-${new Date().getFullYear()}-${String(row.id).padStart(4, '0')}`, // mock 데이터용 + materialGroup: row.packageCode || 'Unknown Group', + priceStandard: 'FOB', // mock 데이터용 기본값 + materialNo: row.itemCode || '', + itemDescription: row.itemName || '', + materialSpec: row.description || '', + fittingNo: undefined, // contract_items에 없는 필드 + cert: undefined, // contract_items에 없는 필드 + material: row.gradeMaterial || undefined, + specification: row.description || '', + quantity: row.quantity || 1, + quantityUnit: row.unitOfMeasure || 'EA', + weight: undefined, // contract_items에 없는 필드 + weightUnit: undefined, // contract_items에 없는 필드 + totalWeight: undefined, // contract_items에 없는 필드 + unitPrice: row.unitPrice ? Number(row.unitPrice) : 0, + priceUnit: 'KRW', // 기본값 + priceUnitValue: '원/EA', // 기본값 + contractAmount: row.totalLineAmount ? Number(row.totalLineAmount) : 0, + adjustmentAmount: undefined, // contract_items에 없는 필드 + deliveryDate: new Date().toISOString().split('T')[0], // 기본값 (오늘 날짜) + vatType: row.taxRate && Number(row.taxRate) > 0 ? '과세' : '면세', + steelSpec: row.steelType || undefined, + prManager: 'AUTO-MANAGER', // mock 데이터용 기본값 + })); + + return vendorPOItems; + } catch (err) { + console.error("Error in getVendorPOItems:", err); + throw err; + } +} + +/** + * 계약번호로 상세품목 조회 (외부에서 contractNo로 호출할 때 사용) + */ +export async function getVendorPOItemsByContractNo(contractNo: string): Promise<VendorPOItem[]> { + try { + // 먼저 계약 ID 조회 + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.contractNo, contractNo), + }); + + if (!contract) { + console.warn(`Contract not found: ${contractNo}`); + return []; + } + + return await getVendorPOItems(contract.id); + } catch (err) { + console.error("Error in getVendorPOItemsByContractNo:", err); + throw err; + } +} diff --git a/lib/po/vendor-table/shi-vendor-po-columns.tsx b/lib/po/vendor-table/shi-vendor-po-columns.tsx new file mode 100644 index 00000000..041e0c05 --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-columns.tsx @@ -0,0 +1,462 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { + SendIcon, + FileTextIcon, + MoreHorizontalIcon, +} from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +import { VendorPO } from "./types" + +// 서명 요청 전용 액션 타입 +type ShiVendorPORowAction = { + row: { original: VendorPO } + type: "view-items" | "signature-request" +} + +interface GetShiVendorColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<ShiVendorPORowAction | null>> + selectedRows: number[] + onRowSelect: (id: number, selected: boolean) => void +} + +export function getShiVendorColumns({ + setRowAction, + selectedRows, + onRowSelect, +}: GetShiVendorColumnsProps): ColumnDef<VendorPO>[] { + return [ + // 선택 체크박스 (1개만 선택 가능) + { + id: "select", + header: () => <div className="text-center">선택</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Checkbox + checked={selectedRows.includes(row.original.id)} + onCheckedChange={(checked) => { + onRowSelect(row.original.id, !!checked) + }} + aria-label="행 선택" + /> + </div> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + // No. (ID) + { + accessorKey: "id", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="No." /> + ), + cell: ({ row }) => { + const id = row.getValue("id") as number + return <div className="text-sm font-mono">{id}</div> + }, + size: 60, + }, + + // PO/계약번호 + { + accessorKey: "contractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약번호" /> + ), + cell: ({ row }) => { + const contractNo = row.getValue("contractNo") as string + return ( + <div className="font-medium"> + {contractNo} + </div> + ) + }, + size: 120, + }, + + // Rev. / 품번 (PO 버전) + { + accessorKey: "poVersion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev. / 품번" /> + ), + cell: ({ row }) => { + const version = row.getValue("poVersion") as number + return <div className="text-sm font-medium">{version || '-'}</div> + }, + size: 80, + }, + + // 계약상태 + { + accessorKey: "contractStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("contractStatus") as string + return ( + <Badge variant="outline"> + {status || '-'} + </Badge> + ) + }, + size: 100, + }, + + // 계약종류 + { + accessorKey: "contractType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약종류" /> + ), + cell: ({ row }) => { + const type = row.getValue("contractType") as string + return <Badge variant="outline">{type || '-'}</Badge> + }, + size: 100, + }, + + // 상세품목 (버튼) + { + id: "itemsAction", + header: () => <div className="text-center">상세품목</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5 mr-1" /> + 보기 + </Button> + </div> + ), + enableSorting: false, + size: 80, + }, + + // 프로젝트 + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => { + const projectName = row.getValue("projectName") as string + return ( + <div className="max-w-[150px] truncate" title={projectName}> + {projectName || '-'} + </div> + ) + }, + size: 150, + }, + + // 계약명/자재내역 + { + accessorKey: "contractName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약명/자재내역" /> + ), + cell: ({ row }) => { + const contractName = row.getValue("contractName") as string + return ( + <div className="max-w-[200px] truncate" title={contractName}> + {contractName || '-'} + </div> + ) + }, + size: 200, + }, + + // PO/계약기간 + { + accessorKey: "contractPeriod", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약기간" /> + ), + cell: ({ row }) => { + const period = row.getValue("contractPeriod") as string + return ( + <div className="text-sm whitespace-nowrap"> + {period || '-'} + </div> + ) + }, + size: 150, + }, + + // PO/계약금액 + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약금액" /> + ), + cell: ({ row }) => { + const amount = row.getValue("totalAmount") as string | number + return <div className="text-sm text-right font-mono">{amount || '-'}</div> + }, + size: 120, + }, + + // 계약통화 + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약통화" /> + ), + cell: ({ row }) => { + const currency = row.getValue("currency") as string + return <div className="text-sm font-mono">{currency || '-'}</div> + }, + size: 80, + }, + + // 지불조건 + { + accessorKey: "paymentTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="지불조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("paymentTerms") as string + return ( + <div className="max-w-[120px] truncate text-sm" title={terms}> + {terms || '-'} + </div> + ) + }, + size: 120, + }, + + // Tax + { + accessorKey: "tax", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tax" /> + ), + cell: ({ row }) => { + const tax = row.getValue("tax") as string + return <div className="text-sm">{tax || '-'}</div> + }, + size: 80, + }, + + // 환율 + { + accessorKey: "exchangeRate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="환율" /> + ), + cell: ({ row }) => { + const rate = row.getValue("exchangeRate") as string + return <div className="text-sm font-mono">{rate || '-'}</div> + }, + size: 100, + }, + + // 인도조건 + { + accessorKey: "deliveryTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="인도조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("deliveryTerms") as string + return <div className="text-sm">{terms || '-'}</div> + }, + size: 100, + }, + + // 구매/계약담당 + { + accessorKey: "purchaseManager", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="구매/계약담당" /> + ), + cell: ({ row }) => { + const manager = row.getValue("purchaseManager") as string + return <div className="text-sm">{manager || '-'}</div> + }, + size: 120, + }, + + // PO/계약수신일 + { + accessorKey: "poReceiveDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약수신일" /> + ), + cell: ({ row }) => { + const date = row.getValue("poReceiveDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 계약체결일 + { + accessorKey: "contractDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약체결일" /> + ), + cell: ({ row }) => { + const date = row.getValue("contractDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // L/C No. + { + accessorKey: "lcNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="L/C No." /> + ), + cell: ({ row }) => { + const lcNo = row.getValue("lcNo") as string + return <div className="text-sm">{lcNo || '-'}</div> + }, + size: 120, + }, + + // 납품대금 연동제 대상 + { + accessorKey: "priceIndexTarget", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="납품대금 연동제 대상" /> + ), + cell: ({ row }) => { + const target = row.getValue("priceIndexTarget") as string | boolean + return <div className="text-sm">{target?.toString() || '-'}</div> + }, + size: 140, + }, + + // 연계 PO/계약번호 + { + accessorKey: "linkedContractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="연계 PO/계약번호" /> + ), + cell: ({ row }) => { + const linkedNo = row.getValue("linkedContractNo") as string + return <div className="text-sm">{linkedNo || '-'}</div> + }, + size: 140, + }, + + // 최종수정일 + { + accessorKey: "lastModifiedDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("lastModifiedDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "lastModifiedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정자" /> + ), + cell: ({ row }) => { + const user = row.getValue("lastModifiedBy") as string + return <div className="text-sm">{user || '-'}</div> + }, + size: 120, + }, + + // 액션 컬럼 (서명 요청 중심) + { + id: "actions", + enableHiding: false, + header: () => <div className="text-center">Actions</div>, + cell: ({ row }) => { + return ( + <div className="flex items-center justify-center gap-2"> + {/* 서명 요청 버튼 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2 text-blue-600 border-blue-200 hover:bg-blue-50" + onClick={() => setRowAction({ row, type: "signature-request" })} + > + <SendIcon className="h-3.5 w-3.5 mr-1" aria-hidden="true" /> + 서명 요청 + </Button> + </TooltipTrigger> + <TooltipContent> + 벤더에게 전자서명 요청 + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 드롭다운 메뉴 (추가 액션) + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">Open menu</span> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>액션</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 상세품목 보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "signature-request" })} + className="text-blue-600" + > + <SendIcon className="mr-2 h-4 w-4" /> + 서명 요청 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> */} + </div> + ) + }, + size: 160, + minSize: 160, + }, + ] +}
\ No newline at end of file diff --git a/lib/po/vendor-table/shi-vendor-po-table.tsx b/lib/po/vendor-table/shi-vendor-po-table.tsx new file mode 100644 index 00000000..851f831e --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-table.tsx @@ -0,0 +1,267 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { toast } from "sonner" + +import { getVendorPOs, handleVendorPOAction } from "./service" +import { getShiVendorColumns } from "./shi-vendor-po-columns" +import { VendorPO, VendorPOActionType } from "./types" +import { VendorPOItemsDialog } from "./vendor-po-items-dialog" +import { ShiVendorPOToolbarActions } from "./shi-vendor-po-toolbar-actions" +import { SignatureRequestModal } from "@/lib/po/table/sign-request-dialog" + +interface ShiVendorPoTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getVendorPOs>>, + ] + > +} + +// Interface for signing party (서명 요청에 사용) +interface SigningParty { + signerEmail: string; + signerName: string; + signerPosition: string; + signerType: "REQUESTER" | "VENDOR"; + vendorContactId?: number; +} + +export function ShiVendorPoTable({ promises }: ShiVendorPoTableProps) { + const [data, setData] = React.useState<{ + data: VendorPO[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + const [selectedRows, setSelectedRows] = React.useState<number[]>([]) + + // 데이터 로딩 + React.useEffect(() => { + promises.then(([result]) => { + console.log("Vendor PO data:", result.data) + setData(result); + }); + }, [promises]); + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<VendorPO> | null>(null) + + // 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [signatureModalOpen, setSignatureModalOpen] = React.useState(false) + const [selectedPO, setSelectedPO] = React.useState<VendorPO | null>(null) + + // 행 선택 처리 (1개만 선택 가능) + const handleRowSelect = (id: number, selected: boolean) => { + if (selected) { + setSelectedRows([id]) // 1개만 선택 + } else { + setSelectedRows([]) + } + } + + // 행 액션 처리 + React.useEffect(() => { + if (!rowAction) return + + const po = rowAction.row.original + setSelectedPO(po) + + switch (rowAction.type as VendorPOActionType) { + case "view-items": + setItemsDialogOpen(true) + break + case "signature-request": + setSignatureModalOpen(true) + break + case "item-status": + setItemsDialogOpen(true) + break + default: + toast.info("해당 기능은 개발 중입니다.") + } + + setRowAction(null) + }, [rowAction]) + + // 액션 처리 함수 + const handleAction = async (poId: number, action: string) => { + try { + const result = await handleVendorPOAction(poId, action) + if (result.success) { + toast.success(result.message) + // 필요시 데이터 새로고침 + } else { + toast.error(result.message) + } + } catch (error) { + console.error("Action error:", error) + toast.error("액션 처리 중 오류가 발생했습니다.") + } + } + + // 서명 요청 처리 함수 + const handleSignatureRequest = async ( + values: { signers: SigningParty[] }, + contractId: number + ): Promise<void> => { + try { + // TODO: 실제 서명 요청 API 호출 + console.log("Signature request for contract:", contractId, values); + toast.success("서명 요청이 성공적으로 전송되었습니다."); + } catch (error) { + console.error("Error sending signature requests:", error); + toast.error("서명 요청 전송 중 오류가 발생했습니다."); + } + } + + const columns = React.useMemo( + () => getShiVendorColumns({ + setRowAction, + selectedRows, + onRowSelect: handleRowSelect + }), + [selectedRows] + ) + + const filterFields: DataTableFilterField<VendorPO>[] = [ + { + id: "contractStatus", + label: "계약상태", + options: [ + { label: "승인대기", value: "승인대기" }, + { label: "계약완료", value: "계약완료" }, + { label: "진행중", value: "진행중" }, + { label: "수정요청", value: "수정요청" }, + { label: "거절됨", value: "거절됨" }, + ] + }, + { + id: "contractType", + label: "계약종류", + options: [ + { label: "구매계약", value: "구매계약" }, + { label: "서비스계약", value: "서비스계약" }, + { label: "임가공계약", value: "임가공계약" }, + ] + }, + { + id: "currency", + label: "계약통화", + options: [ + { label: "KRW", value: "KRW" }, + { label: "USD", value: "USD" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" }, + ] + } + ] + + const advancedFilterFields: DataTableAdvancedFilterField<VendorPO>[] = [ + { + id: "contractNo", + label: "PO/계약번호", + type: "text", + }, + { + id: "contractName", + label: "계약명/자재내역", + type: "text", + }, + { + id: "projectName", + label: "프로젝트", + type: "text", + }, + { + id: "purchaseManager", + label: "구매/계약담당", + type: "text", + }, + { + id: "poReceiveDate", + label: "PO/계약수신일", + type: "date", + }, + { + id: "contractDate", + label: "계약체결일", + type: "date", + }, + { + id: "lastModifiedDate", + label: "최종수정일", + type: "date", + }, + ] + + const { table } = useDataTable({ + data: data.data, + columns, + pageCount: data.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "lastModifiedDate", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable + table={table} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <ShiVendorPOToolbarActions + table={table} + selectedRows={selectedRows} + onAction={handleAction} + onViewItems={(po) => { + setSelectedPO(po) + setItemsDialogOpen(true) + }} + /> + </DataTableAdvancedToolbar> + </DataTable> + + <VendorPOItemsDialog + open={itemsDialogOpen} + onOpenChange={setItemsDialogOpen} + po={selectedPO} + /> + + {/* 서명 요청 모달 */} + {selectedPO && ( + <SignatureRequestModal + contract={{ + id: selectedPO.id, + contractNo: selectedPO.contractNo, + contractName: selectedPO.contractName, + // 필요한 다른 필드들 매핑 + } as any} + open={signatureModalOpen} + onOpenChange={setSignatureModalOpen} + onSubmit={handleSignatureRequest} + /> + )} + </> + ) +} diff --git a/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx b/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx new file mode 100644 index 00000000..a18c02da --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, RefreshCcw } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { VendorPO } from "./types" + +interface ShiVendorPOToolbarActionsProps { + table: Table<VendorPO> + selectedRows: number[] + onAction: (poId: number, action: string) => Promise<void> + onViewItems: (po: VendorPO) => void +} + +export function ShiVendorPOToolbarActions({ + table, + selectedRows, + onAction, + onViewItems +}: ShiVendorPOToolbarActionsProps) { + + const handleRefresh = async () => { + try { + toast.success("데이터를 새로고침했습니다.") + // TODO: 실제 데이터 새로고침 로직 추가 + window.location.reload() + } catch (error) { + toast.error("데이터 새로고침 중 오류가 발생했습니다.") + } + } + + return ( + <div className="flex items-center gap-2"> + {/* Refresh 버튼 */} + <Button + variant="samsung" + size="sm" + className="gap-2" + onClick={handleRefresh} + > + <RefreshCcw className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Refresh</span> + </Button> + + {/* Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "vendor-po-list", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + </div> + ) +} diff --git a/lib/po/vendor-table/types.ts b/lib/po/vendor-table/types.ts new file mode 100644 index 00000000..97572ffc --- /dev/null +++ b/lib/po/vendor-table/types.ts @@ -0,0 +1,119 @@ +/** + * 벤더용 PO 데이터 타입 정의 + */ + +export interface VendorPO { + id: number + // 선택 체크박스 (1개만 선택 가능) + + // 메인 테이블 정보 + contractNo: string // PO/계약번호 (EBELN) + revision: string // Rev. (mock 데이터용) + itemNo: string // 품번 (mock 데이터용) + contractStatus: string // 계약상태 (ZPO_CNFM_STAT) + contractType: string // 계약종류 (BSART) + details: string // 상세 (mock 데이터용) + projectName: string // 프로젝트 + contractName: string // 계약명/자재내역 (ZTITLE) + contractPeriod: string // PO/계약기간 + contractQuantity: string // PO/계약수량 + currency: string // 계약통화 (ZPO_CURR) + paymentTerms: string // 지불조건 (ZTERM) + tax: string // Tax + exchangeRate: string // 환율 (WKURS) + deliveryTerms: string // 인도조건 (INCO1) + purchaseManager: string // 구매/계약담당 + poReceiveDate: string // PO/계약수신일 (AEDAT) + contractDate: string // 계약체결일 + lcNo?: string // L/C No. + priceIndexTarget: boolean // 납품대금 연동제 대상 (ZDLV_PRICE_T) + linkedContractNo?: string // 연계 PO/계약번호 + lastModifiedDate: string // 최종수정일 + lastModifiedBy: string // 최종수정자 + + // SAP ECC 추가 필드들 + poVersion?: number // PO 버전 (ZPO_VER) + purchaseDocType?: string // 구매문서유형 (BSART) + purchaseOrg?: string // 구매조직 (EKORG) + purchaseGroup?: string // 구매그룹 (EKGRP) + poConfirmStatus?: string // PO확인상태 (ZPO_CNFM_STAT) + + // 계약/보증 관련 + contractGuaranteeCode?: string // 계약보증코드 (ZCNRT_GRNT_CD) + defectGuaranteeCode?: string // 하자보증코드 (ZDFCT_GRNT_CD) + guaranteePeriodCode?: string // 보증기간코드 (ZGRNT_PRD_CD) + advancePaymentYn?: string // 선급금여부 (ZPAMT_YN) + + // 금액 관련 + budgetAmount?: number // 예산금액 (ZBGT_AMT) + budgetCurrency?: string // 예산통화 (ZBGT_CURR) + totalAmount?: number // 총 계약 금액 (ZPO_AMT) + totalAmountKrw?: number // 발주금액KRW (ZPO_AMT_KRW) + + // 전자계약/승인 관련 + electronicContractYn?: string // 전자계약필요여부 (ZELC_CNRT_ND_YN) + electronicApprovalDate?: string // 전자승인일자 (ZELC_AGR_DT) + electronicApprovalTime?: string // 전자승인시간 (ZELC_AGR_TM) + ownerApprovalYn?: string // 선주승인필요여부 (ZOWN_AGR_IND_YN) + + // 기타 + plannedInOutFlag?: string // 계획내외구분 (ZPLN_INO_GB) + settlementStandard?: string // 정산기준 (ZECAL_BSE) + weightSettlementFlag?: string // 중량정산구분 (ZWGT_ECAL_GB) + + // 연동제 관련 + priceIndexYn?: string // 납품대금연동제대상여부 (ZDLV_PRICE_T) + writtenContractNo?: string // 서면계약번호 (ZWEBELN) + contractVersion?: number // 서면계약차수 (ZVER_NO) + + // 상세품목 정보 (다이얼로그에서 표시) + items?: VendorPOItem[] +} + +export interface VendorPOItem { + contractNo: string // PO/계약번호 + itemNo: string // 품번 + prNo: string // P/R번호 + materialGroup: string // 자재그룹(명) + priceStandard: string // 단가기준 + materialNo: string // 자재번호 + itemDescription: string // 품목/자재내역 + materialSpec: string // 자재내역사양 + designMaterialNo?: string // 설계자재번호 + fittingNo?: string // Fitting No. + cert?: string // Cert. + material?: string // 재질 + specification: string // 규격 + quantity: number // 수량 + quantityUnit: string // 수량단위 + weight?: number // 중량 + weightUnit?: string // 중량단위 + totalWeight?: number // 총중량 + unitPrice: number // 단가기준 (단가) + priceUnit: string // 단가단위 + priceUnitValue: string // 가격단위값 + contractAmount: number // PO계약금액 + adjustmentAmount?: number // 조정금액 + deliveryDate: string // 납기일자 + vatType: string // VAT구분 + steelSpec?: string // 철의장 SPEC + prManager: string // P/R 담당자 +} + +// 파싱된 벤더 PO 타입 (JSON 필드들이 파싱된 상태) +export interface VendorPOParsed extends Omit<VendorPO, 'items'> { + items: VendorPOItem[] +} + +// 벤더 PO 액션 타입들 +export type VendorPOActionType = + | "pcr-create" // PCR생성 + | "item-status" // 상세품목현황 + | "contract-detail" // 계약상세 + | "po-note" // PO Note + | "price-index" // 연동표입력 + | "approve" // 승인 + | "cancel-approve" // 승인취소 + | "reject-contract" // 계약거절 + | "print-contract" // 계약서출력 + | "view-items" // 상세품목 보기 (다이얼로그) diff --git a/lib/po/vendor-table/validations.ts b/lib/po/vendor-table/validations.ts new file mode 100644 index 00000000..70b3d87a --- /dev/null +++ b/lib/po/vendor-table/validations.ts @@ -0,0 +1,58 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { VendorPO } from "./types" + +export const vendorPoSearchParamsCache = createSearchParamsCache({ + // UI 모드나 플래그 관련 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 (최종수정일 기준 내림차순) + sort: getSortingStateParser<VendorPO>().withDefault([ + { id: "lastModifiedDate", desc: true }, + ]), + + // 벤더 PO 관련 필드 + contractNo: parseAsString.withDefault(""), // PO/계약번호 + contractName: parseAsString.withDefault(""), // 계약명/자재내역 + projectName: parseAsString.withDefault(""), // 프로젝트 + contractStatus: parseAsString.withDefault(""), // 계약상태 + contractType: parseAsString.withDefault(""), // 계약종류 + currency: parseAsString.withDefault(""), // 계약통화 + paymentTerms: parseAsString.withDefault(""), // 지불조건 + deliveryTerms: parseAsString.withDefault(""), // 인도조건 + purchaseManager: parseAsString.withDefault(""), // 구매/계약담당 + + // 날짜 관련 + poReceiveDate: parseAsString.withDefault(""), // PO/계약수신일 + contractDate: parseAsString.withDefault(""), // 계약체결일 + lastModifiedDate: parseAsString.withDefault(""), // 최종수정일 + + // 기타 + lcNo: parseAsString.withDefault(""), // L/C No. + priceIndexTarget: parseAsStringEnum(["true", "false"]).withDefault(""), // 납품대금 연동제 대상 + linkedContractNo: parseAsString.withDefault(""), // 연계 PO/계약번호 + lastModifiedBy: parseAsString.withDefault(""), // 최종수정자 + + // 벤더 필터링 (내부적으로 사용) + vendorId: parseAsInteger.withDefault(0), // 특정 벤더의 PO만 조회 + + // 고급 필터(Advanced) & 검색 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) + +// 최종 타입 +export type GetVendorPOSchema = Awaited<ReturnType<typeof vendorPoSearchParamsCache.parse>> diff --git a/lib/po/vendor-table/vendor-po-columns.tsx b/lib/po/vendor-table/vendor-po-columns.tsx new file mode 100644 index 00000000..1a655b0c --- /dev/null +++ b/lib/po/vendor-table/vendor-po-columns.tsx @@ -0,0 +1,511 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { + FileTextIcon, + MoreHorizontalIcon, + EyeIcon, + PrinterIcon, + FileXIcon, + PlusIcon, + EditIcon +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { VendorPO, VendorPOActionType } from "./types" + +// 벤더 PO용 행 액션 타입 +type VendorPORowAction = { + row: { original: VendorPO } + type: VendorPOActionType +} + +interface GetVendorColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<VendorPORowAction | null>> + selectedRows?: number[] + onRowSelect?: (id: number, selected: boolean) => void +} + +export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect }: GetVendorColumnsProps): ColumnDef<VendorPO>[] { + return [ + // 선택 체크박스 (1개만 선택 가능) + { + id: "select", + header: () => <div className="text-center">선택</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Checkbox + checked={selectedRows.includes(row.original.id)} + onCheckedChange={(checked) => { + if (onRowSelect) { + onRowSelect(row.original.id, !!checked) + } + }} + aria-label="행 선택" + /> + </div> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + // No. (ID) + { + accessorKey: "id", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="No." /> + ), + cell: ({ row }) => { + const id = row.getValue("id") as number + return <div className="text-sm font-mono">{id}</div> + }, + size: 60, + }, + + // PO/계약번호 + { + accessorKey: "contractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약번호" /> + ), + cell: ({ row }) => { + const contractNo = row.getValue("contractNo") as string + return ( + <div className="font-medium"> + {contractNo} + </div> + ) + }, + size: 120, + }, + + // Rev. / 품번 (PO 버전) + { + accessorKey: "poVersion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev. / 품번" /> + ), + cell: ({ row }) => { + const version = row.getValue("poVersion") as number + return <div className="text-sm font-medium">{version || '-'}</div> + }, + size: 80, + }, + + // 계약상태 + { + accessorKey: "contractStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("contractStatus") as string + return ( + <Badge variant="outline"> + {status || '-'} + </Badge> + ) + }, + size: 100, + }, + + // 계약종류 + { + accessorKey: "contractType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약종류" /> + ), + cell: ({ row }) => { + const type = row.getValue("contractType") as string + return <Badge variant="outline">{type || '-'}</Badge> + }, + size: 100, + }, + + // 상세품목 (버튼) + { + id: "itemsAction", + header: () => <div className="text-center">상세품목</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5 mr-1" /> + 보기 + </Button> + </div> + ), + enableSorting: false, + size: 80, + }, + + // 프로젝트 + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => { + const projectName = row.getValue("projectName") as string + return ( + <div className="max-w-[150px] truncate" title={projectName}> + {projectName || '-'} + </div> + ) + }, + size: 150, + }, + + // 계약명/자재내역 + { + accessorKey: "contractName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약명/자재내역" /> + ), + cell: ({ row }) => { + const contractName = row.getValue("contractName") as string + return ( + <div className="max-w-[200px] truncate" title={contractName}> + {contractName || '-'} + </div> + ) + }, + size: 200, + }, + + // PO/계약기간 + { + accessorKey: "contractPeriod", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약기간" /> + ), + cell: ({ row }) => { + const period = row.getValue("contractPeriod") as string + return ( + <div className="text-sm whitespace-nowrap"> + {period || '-'} + </div> + ) + }, + size: 150, + }, + + // PO/계약금액 + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약금액" /> + ), + cell: ({ row }) => { + const amount = row.getValue("totalAmount") as string | number + return <div className="text-sm text-right font-mono">{amount || '-'}</div> + }, + size: 120, + }, + + // 계약통화 + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약통화" /> + ), + cell: ({ row }) => { + const currency = row.getValue("currency") as string + return <div className="text-sm font-mono">{currency || '-'}</div> + }, + size: 80, + }, + + // 지불조건 + { + accessorKey: "paymentTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="지불조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("paymentTerms") as string + return ( + <div className="max-w-[120px] truncate text-sm" title={terms}> + {terms || '-'} + </div> + ) + }, + size: 120, + }, + + // Tax + { + accessorKey: "tax", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tax" /> + ), + cell: ({ row }) => { + const tax = row.getValue("tax") as string + return <div className="text-sm">{tax || '-'}</div> + }, + size: 80, + }, + + // 환율 + { + accessorKey: "exchangeRate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="환율" /> + ), + cell: ({ row }) => { + const rate = row.getValue("exchangeRate") as string + return <div className="text-sm font-mono">{rate || '-'}</div> + }, + size: 100, + }, + + // 인도조건 + { + accessorKey: "deliveryTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="인도조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("deliveryTerms") as string + return <div className="text-sm">{terms || '-'}</div> + }, + size: 100, + }, + + // 구매/계약담당 + { + accessorKey: "purchaseManager", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="구매/계약담당" /> + ), + cell: ({ row }) => { + const manager = row.getValue("purchaseManager") as string + return <div className="text-sm">{manager || '-'}</div> + }, + size: 120, + }, + + // PO/계약수신일 + { + accessorKey: "poReceiveDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약수신일" /> + ), + cell: ({ row }) => { + const date = row.getValue("poReceiveDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 계약체결일 + { + accessorKey: "contractDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약체결일" /> + ), + cell: ({ row }) => { + const date = row.getValue("contractDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // L/C No. + { + accessorKey: "lcNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="L/C No." /> + ), + cell: ({ row }) => { + const lcNo = row.getValue("lcNo") as string + return <div className="text-sm">{lcNo || '-'}</div> + }, + size: 120, + }, + + // 납품대금 연동제 대상 + { + accessorKey: "priceIndexTarget", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="납품대금 연동제 대상" /> + ), + cell: ({ row }) => { + const target = row.getValue("priceIndexTarget") as string | boolean + return <div className="text-sm">{target?.toString() || '-'}</div> + }, + size: 140, + }, + + // 연계 PO/계약번호 + { + accessorKey: "linkedContractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="연계 PO/계약번호" /> + ), + cell: ({ row }) => { + const linkedNo = row.getValue("linkedContractNo") as string + return <div className="text-sm">{linkedNo || '-'}</div> + }, + size: 140, + }, + + // 최종수정일 + { + accessorKey: "lastModifiedDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("lastModifiedDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "lastModifiedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정자" /> + ), + cell: ({ row }) => { + const user = row.getValue("lastModifiedBy") as string + return <div className="text-sm">{user || '-'}</div> + }, + size: 120, + }, + + // 액션 버튼들 + { + id: "actions", + enableHiding: false, + header: () => <div className="text-center">액션</div>, + cell: function Cell({ row }) { + return ( + <div className="flex gap-1"> + {/* 상세품목 버튼 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5" aria-hidden="true" /> + </Button> + </TooltipTrigger> + <TooltipContent> + 상세품목 보기 + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 드롭다운 메뉴 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">Open menu</span> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>액션</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 상세품목 보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "pcr-create" })} + > + <PlusIcon className="mr-2 h-4 w-4" /> + PCR생성 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "approve" })} + > + 승인 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "cancel-approve" })} + > + 승인취소 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "reject-contract" })} + className="text-red-600" + > + <FileXIcon className="mr-2 h-4 w-4" /> + 계약거절 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "print-contract" })} + > + <PrinterIcon className="mr-2 h-4 w-4" /> + 계약서출력 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "contract-detail" })} + > + <EyeIcon className="mr-2 h-4 w-4" /> + 계약상세 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "po-note" })} + > + <EditIcon className="mr-2 h-4 w-4" /> + PO Note + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "price-index" })} + > + 연동표입력 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + ); + }, + size: 120, + minSize: 100, + }, + ] +}
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-items-dialog.tsx b/lib/po/vendor-table/vendor-po-items-dialog.tsx new file mode 100644 index 00000000..d3b33371 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-items-dialog.tsx @@ -0,0 +1,199 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Skeleton } from "@/components/ui/skeleton" +import { VendorPO, VendorPOItem } from "./types" +import { getVendorPOItemsByContractNo } from "./service" + +interface VendorPOItemsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: VendorPO | null +} + +export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDialogProps) { + const [items, setItems] = React.useState<VendorPOItem[]>([]) + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState<string | null>(null) + + // 상세품목 데이터 로드 + React.useEffect(() => { + if (!open || !po) { + setItems([]) + setError(null) + return + } + + const loadItems = async () => { + setLoading(true) + setError(null) + try { + const vendorPOItems = await getVendorPOItemsByContractNo(po.contractNo) + setItems(vendorPOItems) + } catch (err) { + console.error("Failed to load vendor PO items:", err) + setError("상세품목을 불러오는 중 오류가 발생했습니다.") + } finally { + setLoading(false) + } + } + + loadItems() + }, [open, po]) + + if (!po) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-[95vw] max-h-[90vh] w-full flex flex-col"> + <DialogHeader className="flex-shrink-0"> + <DialogTitle className="text-lg font-semibold"> + 상세품목 현황 - {po.contractNo} + </DialogTitle> + <DialogDescription> + {po.contractName} ({items.length}개 품목) + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden"> + {loading ? ( + <div className="space-y-4"> + <div className="text-center py-4 text-muted-foreground"> + 상세품목을 불러오는 중... + </div> + <div className="space-y-2"> + {Array.from({ length: 3 }).map((_, i) => ( + <Skeleton key={i} className="h-12 w-full" /> + ))} + </div> + </div> + ) : error ? ( + <div className="text-center py-8 text-destructive"> + {error} + </div> + ) : items.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + 등록된 상세품목이 없습니다. + </div> + ) : ( + <div className="overflow-auto max-h-[60vh]"> + <Table> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead className="min-w-[120px] whitespace-nowrap">PO/계약번호</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">품번</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">P/R번호</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">자재그룹(명)</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">단가기준</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">자재번호</TableHead> + <TableHead className="min-w-[200px] whitespace-nowrap">품목/자재내역</TableHead> + <TableHead className="min-w-[200px] whitespace-nowrap">자재내역사양</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">설계자재번호</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">Fitting No.</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">Cert.</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">재질</TableHead> + <TableHead className="min-w-[150px] whitespace-nowrap">규격</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">수량</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">수량단위</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">중량</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">중량단위</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">총중량</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">단가기준</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">단가단위</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">가격단위값</TableHead> + <TableHead className="min-w-[120px] text-right whitespace-nowrap">PO계약금액</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">조정금액</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">납기일자</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">VAT구분</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">철의장 SPEC</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">P/R 담당자</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {items.map((item, index) => ( + <TableRow key={`${item.contractNo}-${item.itemNo}-${index}`}> + <TableCell className="font-medium">{item.contractNo || '-'}</TableCell> + <TableCell>{item.itemNo || '-'}</TableCell> + <TableCell>{item.prNo || '-'}</TableCell> + <TableCell>{item.materialGroup || '-'}</TableCell> + <TableCell>{item.priceStandard || '-'}</TableCell> + <TableCell className="font-mono text-sm">{item.materialNo || '-'}</TableCell> + <TableCell className="max-w-[200px]"> + <div className="truncate" title={item.itemDescription || ''}> + {item.itemDescription || '-'} + </div> + </TableCell> + <TableCell className="max-w-[200px]"> + <div className="truncate" title={item.materialSpec || ''}> + {item.materialSpec || '-'} + </div> + </TableCell> + <TableCell>{item.designMaterialNo || '-'}</TableCell> + <TableCell>{item.fittingNo || '-'}</TableCell> + <TableCell>{item.cert || '-'}</TableCell> + <TableCell>{item.material || '-'}</TableCell> + <TableCell>{item.specification || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.quantity?.toLocaleString() || '-'} + </TableCell> + <TableCell>{item.quantityUnit || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.weight ? item.weight.toLocaleString() : '-'} + </TableCell> + <TableCell>{item.weightUnit || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.totalWeight ? item.totalWeight.toLocaleString() : '-'} + </TableCell> + <TableCell className="text-right font-mono"> + {item.unitPrice?.toLocaleString() || '-'} + </TableCell> + <TableCell>{item.priceUnit || '-'}</TableCell> + <TableCell>{item.priceUnitValue || '-'}</TableCell> + <TableCell className="text-right font-mono font-semibold"> + {item.contractAmount?.toLocaleString() || '-'} + </TableCell> + <TableCell className="text-right font-mono"> + {item.adjustmentAmount ? item.adjustmentAmount.toLocaleString() : '-'} + </TableCell> + <TableCell>{item.deliveryDate || '-'}</TableCell> + <TableCell>{item.vatType || '-'}</TableCell> + <TableCell>{item.steelSpec || '-'}</TableCell> + <TableCell>{item.prManager || '-'}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + </div> + + {items.length > 0 && ( + <div className="flex justify-between items-center pt-4 border-t flex-shrink-0"> + <div className="text-sm text-muted-foreground"> + 총 {items.length}개 품목 + </div> + <div className="text-sm font-medium"> + 총 계약금액: {items.reduce((sum, item) => sum + item.contractAmount, 0).toLocaleString()} 원 + </div> + </div> + )} + </DialogContent> + </Dialog> + ) +} diff --git a/lib/po/vendor-table/vendor-po-table.tsx b/lib/po/vendor-table/vendor-po-table.tsx new file mode 100644 index 00000000..a3ad4949 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-table.tsx @@ -0,0 +1,247 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { toast } from "sonner" + +import { getVendorPOs, handleVendorPOAction } from "./service" +import { getVendorColumns } from "./vendor-po-columns" +import { VendorPO, VendorPOActionType } from "./types" +import { VendorPOItemsDialog } from "./vendor-po-items-dialog" +import { VendorPOToolbarActions } from "./vendor-po-toolbar-actions" + +interface VendorPoTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getVendorPOs>>, + ] + > +} + +export function VendorPoTable({ promises }: VendorPoTableProps) { + const [data, setData] = React.useState<{ + data: VendorPO[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + const [selectedRows, setSelectedRows] = React.useState<number[]>([]) + + // 데이터 로딩 + React.useEffect(() => { + promises.then(([result]) => { + console.log("Vendor PO data:", result.data) + setData(result); + }); + }, [promises]); + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<VendorPO> | null>(null) + + // 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [selectedPO, setSelectedPO] = React.useState<VendorPO | null>(null) + + // 행 선택 처리 (1개만 선택 가능) + const handleRowSelect = (id: number, selected: boolean) => { + if (selected) { + setSelectedRows([id]) // 1개만 선택 + } else { + setSelectedRows([]) + } + } + + // 행 액션 처리 + React.useEffect(() => { + if (!rowAction) return + + const po = rowAction.row.original + setSelectedPO(po) + + switch (rowAction.type as VendorPOActionType) { + case "view-items": + setItemsDialogOpen(true) + break + case "pcr-create": + handleAction(po.id, "pcr-create") + break + case "approve": + handleAction(po.id, "approve") + break + case "cancel-approve": + handleAction(po.id, "cancel-approve") + break + case "reject-contract": + handleAction(po.id, "reject-contract") + break + case "print-contract": + handleAction(po.id, "print-contract") + break + case "item-status": + setItemsDialogOpen(true) + break + case "contract-detail": + toast.info("계약상세 기능은 개발 중입니다.") + break + case "po-note": + toast.info("PO Note 기능은 개발 중입니다.") + break + case "price-index": + toast.info("연동표입력 기능은 개발 중입니다.") + break + default: + toast.info("해당 기능은 개발 중입니다.") + } + + setRowAction(null) + }, [rowAction]) + + // 액션 처리 함수 + const handleAction = async (poId: number, action: string) => { + try { + const result = await handleVendorPOAction(poId, action) + if (result.success) { + toast.success(result.message) + // 필요시 데이터 새로고침 + } else { + toast.error(result.message) + } + } catch (error) { + console.error("Action error:", error) + toast.error("액션 처리 중 오류가 발생했습니다.") + } + } + + const columns = React.useMemo( + () => getVendorColumns({ + setRowAction, + selectedRows, + onRowSelect: handleRowSelect + }), + [selectedRows] + ) + + const filterFields: DataTableFilterField<VendorPO>[] = [ + { + id: "contractStatus", + label: "계약상태", + options: [ + { label: "승인대기", value: "승인대기" }, + { label: "계약완료", value: "계약완료" }, + { label: "진행중", value: "진행중" }, + { label: "수정요청", value: "수정요청" }, + { label: "거절됨", value: "거절됨" }, + ] + }, + { + id: "contractType", + label: "계약종류", + options: [ + { label: "구매계약", value: "구매계약" }, + { label: "서비스계약", value: "서비스계약" }, + { label: "임가공계약", value: "임가공계약" }, + ] + }, + { + id: "currency", + label: "계약통화", + options: [ + { label: "KRW", value: "KRW" }, + { label: "USD", value: "USD" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" }, + ] + } + ] + + const advancedFilterFields: DataTableAdvancedFilterField<VendorPO>[] = [ + { + id: "contractNo", + label: "PO/계약번호", + type: "text", + }, + { + id: "contractName", + label: "계약명/자재내역", + type: "text", + }, + { + id: "projectName", + label: "프로젝트", + type: "text", + }, + { + id: "purchaseManager", + label: "구매/계약담당", + type: "text", + }, + { + id: "poReceiveDate", + label: "PO/계약수신일", + type: "date", + }, + { + id: "contractDate", + label: "계약체결일", + type: "date", + }, + { + id: "lastModifiedDate", + label: "최종수정일", + type: "date", + }, + ] + + const { table } = useDataTable({ + data: data.data, + columns, + pageCount: data.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "lastModifiedDate", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable + table={table} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <VendorPOToolbarActions + table={table} + selectedRows={selectedRows} + onAction={handleAction} + onViewItems={(po) => { + setSelectedPO(po) + setItemsDialogOpen(true) + }} + /> + </DataTableAdvancedToolbar> + </DataTable> + + <VendorPOItemsDialog + open={itemsDialogOpen} + onOpenChange={setItemsDialogOpen} + po={selectedPO} + /> + </> + ) +}
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-toolbar-actions.tsx b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx new file mode 100644 index 00000000..800a9e40 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx @@ -0,0 +1,214 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { + EyeIcon, + EditIcon, + FileXIcon, + PrinterIcon, +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { toast } from "sonner" +import { VendorPO } from "./types" + +interface VendorPOToolbarActionsProps { + table: Table<VendorPO> + selectedRows: number[] + onAction: (poId: number, action: string) => Promise<void> + onViewItems?: (po: VendorPO) => void +} + +export function VendorPOToolbarActions({ + table, + selectedRows, + onAction, + onViewItems +}: VendorPOToolbarActionsProps) { + const hasSelectedRow = selectedRows.length === 1 + const selectedPO = hasSelectedRow ? + table.getRowModel().rows.find(row => selectedRows.includes(row.original.id))?.original + : null + + const handleToolbarAction = async (action: string) => { + if (!hasSelectedRow || !selectedPO) { + toast.error("먼저 PO를 선택해주세요.") + return + } + + // view-items 액션은 특별히 처리 + if (action === "view-items") { + if (onViewItems) { + onViewItems(selectedPO) + } + return + } + + await onAction(selectedPO.id, action) + } + + return ( + <div className="flex items-center gap-2"> + {/* 주요 액션 버튼들 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="default" + size="sm" + onClick={() => handleToolbarAction("pcr-create")} + disabled={!hasSelectedRow} + className="h-8" + > + PCR생성 + </Button> + </TooltipTrigger> + <TooltipContent>선택된 PO에 대한 PCR을 생성합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("view-items")} + disabled={!hasSelectedRow} + className="h-8" + > + 상세품목현황 + </Button> + </TooltipTrigger> + <TooltipContent>상세품목 현황을 확인합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 승인 관련 액션 */} + {selectedPO?.contractStatus !== "승인완료" && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("approve")} + disabled={!hasSelectedRow} + className="h-8 text-green-600 border-green-600 hover:bg-green-50" + > + 승인 + </Button> + </TooltipTrigger> + <TooltipContent>선택된 계약을 승인합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + )} + + {selectedPO?.contractStatus === "승인완료" && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("cancel-approve")} + disabled={!hasSelectedRow} + className="h-8 text-orange-600 border-orange-600 hover:bg-orange-50" + > + 승인취소 + </Button> + </TooltipTrigger> + <TooltipContent>승인을 취소합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + )} + + {/* 더 많은 액션 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + size="sm" + disabled={!hasSelectedRow} + className="h-8" + > + 더 많은 액션 + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[200px]"> + <DropdownMenuLabel>계약 관련</DropdownMenuLabel> + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("contract-detail")} + disabled={!hasSelectedRow} + > + <EyeIcon className="mr-2 h-4 w-4" /> + 계약상세 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => handleToolbarAction("po-note")} + disabled={!hasSelectedRow} + > + <EditIcon className="mr-2 h-4 w-4" /> + PO Note + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => handleToolbarAction("price-index")} + disabled={!hasSelectedRow} + > + <EditIcon className="mr-2 h-4 w-4" /> + 연동표입력 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("reject-contract")} + disabled={!hasSelectedRow} + className="text-red-600 focus:text-red-600" + > + <FileXIcon className="mr-2 h-4 w-4" /> + 계약거절 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("print-contract")} + disabled={!hasSelectedRow} + > + <PrinterIcon className="mr-2 h-4 w-4" /> + 계약서출력 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* 선택된 행 정보 표시 + {hasSelectedRow && selectedPO && ( + <div className="flex items-center gap-2 ml-4 text-sm text-muted-foreground"> + <span>선택됨:</span> + <span className="font-medium">{selectedPO.contractNo}</span> + <span>({selectedPO.contractName})</span> + </div> + )} */} + </div> + ) +} diff --git a/lib/soap/ecc/mapper/po-mapper.ts b/lib/soap/ecc/mapper/po-mapper.ts index 6e282b98..9303cbcd 100644 --- a/lib/soap/ecc/mapper/po-mapper.ts +++ b/lib/soap/ecc/mapper/po-mapper.ts @@ -108,23 +108,63 @@ export async function mapECCPOHeaderToBusiness( throw new Error(`벤더를 찾을 수 없습니다: LIFNR=${eccHeader.LIFNR}`); } - // 매핑 + // 매핑 - SAP ECC 필드명과 함께 주석 추가 const mappedData: ContractData = { projectId, vendorId, - contractNo: eccHeader.EBELN || '', - contractName: eccHeader.ZTITLE || eccHeader.EBELN || '', - status: eccHeader.ZPO_CNFM_STAT || 'ACTIVE', - startDate: parseDate(eccHeader.ZPO_DT || null), + contractNo: eccHeader.EBELN || '', // EBELN - 구매오더번호 + contractName: eccHeader.ZTITLE || eccHeader.EBELN || '', // ZTITLE - 발주제목 + status: eccHeader.ZPO_CNFM_STAT || 'ACTIVE', // ZPO_CNFM_STAT - 구매오더확인상태 + startDate: parseDate(eccHeader.ZPO_DT || null), // ZPO_DT - 발주일자 endDate: null, // ZMM_DT에서 가져와야 함 deliveryDate: null, // ZMM_DT에서 가져와야 함 - paymentTerms: eccHeader.ZTERM || null, - deliveryTerms: eccHeader.INCO1 || null, - deliveryLocation: eccHeader.ZUNLD_PLC_CD || null, - currency: eccHeader.ZPO_CURR || 'KRW', - totalAmount: parseAmount(eccHeader.ZPO_AMT || null), - netTotal: parseAmount(eccHeader.ZPO_AMT || null), - remarks: eccHeader.ETC_2 || null, + + // SAP ECC 기본 필드들 + paymentTerms: eccHeader.ZTERM || null, // ZTERM - 지급조건코드 + deliveryTerms: eccHeader.INCO1 || null, // INCO1 - 인도조건코드 + shippmentPlace: eccHeader.ZSHIPMT_PLC_CD || null, // ZSHIPMT_PLC_CD - 선적지코드 + deliveryLocation: eccHeader.ZUNLD_PLC_CD || null, // ZUNLD_PLC_CD - 하역지코드 + + // SAP ECC 추가 필드들 + poVersion: eccHeader.ZPO_VER ? parseInt(eccHeader.ZPO_VER) : null, // ZPO_VER - 발주버전 + purchaseDocType: eccHeader.BSART || null, // BSART - 구매문서유형 + purchaseOrg: eccHeader.EKORG || null, // EKORG - 구매조직코드 + purchaseGroup: eccHeader.EKGRP || null, // EKGRP - 구매그룹코드 + exchangeRate: eccHeader.WKURS ? parseAmount(eccHeader.WKURS) : null, // WKURS - 환율 + poConfirmStatus: eccHeader.ZPO_CNFM_STAT || null, // ZPO_CNFM_STAT - 구매오더확인상태 + + // 계약/보증 관련 + contractGuaranteeCode: eccHeader.ZCNRT_GRNT_CD || null, // ZCNRT_GRNT_CD - 계약보증코드 + defectGuaranteeCode: eccHeader.ZDFCT_GRNT_CD || null, // ZDFCT_GRNT_CD - 하자보증코드 + guaranteePeriodCode: eccHeader.ZGRNT_PRD_CD || null, // ZGRNT_PRD_CD - 보증기간코드 + advancePaymentYn: eccHeader.ZPAMT_YN || null, // ZPAMT_YN - 선급금여부 + + // 금액 관련 + budgetAmount: parseAmount(eccHeader.ZBGT_AMT || null), // ZBGT_AMT - 예산금액 + budgetCurrency: eccHeader.ZBGT_CURR || null, // ZBGT_CURR - 예산금액 통화키 + currency: eccHeader.ZPO_CURR || 'KRW', // ZPO_CURR - 통화키 + totalAmount: parseAmount(eccHeader.ZPO_AMT || null), // ZPO_AMT - 발주금액 + totalAmountKrw: parseAmount(eccHeader.ZPO_AMT_KRW || null), // ZPO_AMT_KRW - 발주금액 KRW + + // 전자계약/승인 관련 + electronicContractYn: eccHeader.ZELC_CNRT_ND_YN || null, // ZELC_CNRT_ND_YN - 전자계약필요여부 + electronicApprovalDate: parseDate(eccHeader.ZELC_AGR_DT || null), // ZELC_AGR_DT - 전자승인일자 + electronicApprovalTime: eccHeader.ZELC_AGR_TM || null, // ZELC_AGR_TM - 전자승인시간 + ownerApprovalYn: eccHeader.ZOWN_AGR_IND_YN || null, // ZOWN_AGR_IND_YN - 선주승인필요여부 + + // 기타 + plannedInOutFlag: eccHeader.ZPLN_INO_GB || null, // ZPLN_INO_GB - 계획내외구분 + settlementStandard: eccHeader.ZECAL_BSE || null, // ZECAL_BSE - 정산기준 + weightSettlementFlag: eccHeader.ZWGT_ECAL_GB || null, // ZWGT_ECAL_GB - 중량정산구분 + + // 연동제 관련 + priceIndexYn: eccHeader.ZDLV_PRICE_T || null, // ZDLV_PRICE_T - 납품대금연동제대상여부 + writtenContractNo: eccHeader.ZWEBELN || null, // ZWEBELN - 서면계약번호 + contractVersion: eccHeader.ZVER_NO ? parseInt(eccHeader.ZVER_NO) : null, // ZVER_NO - 서면계약차수 + + netTotal: parseAmount(eccHeader.ZPO_AMT || null), // ZPO_AMT와 동일 + remarks: eccHeader.ETC_2 || null, // ETC_2 - 확장2 + // 기본값들 discount: null, tax: null, diff --git a/package.json b/package.json index 924eb83d..49fd3a4f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "export NODE_OPTIONS=--openssl-legacy-provider && ./node_modules/next/dist/bin/next build", "start": "export NODE_OPTIONS=--openssl-legacy-provider && ./node_modules/next/dist/bin/next start", "lint": "./node_modules/next/dist/bin/next lint", - "db:seed_2": "tsx db/seeds_2/seed.ts" + "db:seed_2": "tsx db/seeds_2/seed.ts", + "seed:sap-po": "tsx db/seeds/sap-ecc-po-seed.ts" }, "dependencies": { "@codemirror/commands": "^6.8.1", |
