summaryrefslogtreecommitdiff
path: root/lib/tech-vendors/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors/service.ts')
-rw-r--r--lib/tech-vendors/service.ts478
1 files changed, 478 insertions, 0 deletions
diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts
index 4eba6b2b..f5380889 100644
--- a/lib/tech-vendors/service.ts
+++ b/lib/tech-vendors/service.ts
@@ -2784,4 +2784,482 @@ export async function parseContactImportFile(file: File): Promise<ImportContactD
})
return data
+}
+
+// ================================================
+// Possible Items Excel Import 관련 함수들
+// ================================================
+
+export interface PossibleItemImportData {
+ vendorEmail: string
+ itemCode: string
+ itemType: "조선" | "해양TOP" | "해양HULL"
+}
+
+export interface PossibleItemImportResult {
+ success: boolean
+ totalRows: number
+ successCount: number
+ failedRows: Array<{
+ row: number
+ error: string
+ vendorEmail: string
+ itemCode: string
+ itemType: "조선" | "해양TOP" | "해양HULL"
+ }>
+}
+
+export interface PossibleItemErrorData {
+ vendorEmail: string
+ itemCode: string
+ itemType: string
+ error: string
+}
+
+export interface FoundItem {
+ id: number
+ itemCode: string | null
+ workType: string | null
+ itemList: string | null
+ shipTypes: string | null
+ itemType: "SHIP" | "TOP" | "HULL"
+}
+
+/**
+ * 벤더 이메일로 벤더 찾기 (possible items import용)
+ */
+async function findVendorByEmail(email: string) {
+ const vendor = await db
+ .select({
+ id: techVendors.id,
+ vendorName: techVendors.vendorName,
+ email: techVendors.email,
+ techVendorType: techVendors.techVendorType,
+ })
+ .from(techVendors)
+ .where(eq(techVendors.email, email))
+ .limit(1)
+
+ return vendor[0] || null
+}
+
+/**
+ * 아이템 타입과 코드로 아이템 찾기
+ * 조선의 경우 같은 아이템 코드에 선종이 다른 여러 레코드가 있을 수 있으므로 배열로 반환
+ */
+async function findItemByCodeAndType(itemCode: string, itemType: "조선" | "해양TOP" | "해양HULL"): Promise<FoundItem[]> {
+ try {
+ switch (itemType) {
+ case "조선":
+ const shipItems = await db
+ .select({
+ id: itemShipbuilding.id,
+ itemCode: itemShipbuilding.itemCode,
+ workType: itemShipbuilding.workType,
+ itemList: itemShipbuilding.itemList,
+ shipTypes: itemShipbuilding.shipTypes,
+ })
+ .from(itemShipbuilding)
+ .where(eq(itemShipbuilding.itemCode, itemCode))
+
+ return shipItems.length > 0
+ ? shipItems.map(item => ({ ...item, itemType: "SHIP" as const }))
+ : []
+
+ case "해양TOP":
+ const topItems = await db
+ .select({
+ id: itemOffshoreTop.id,
+ itemCode: itemOffshoreTop.itemCode,
+ workType: itemOffshoreTop.workType,
+ itemList: itemOffshoreTop.itemList,
+ shipTypes: sql<string>`null`.as("shipTypes"),
+ })
+ .from(itemOffshoreTop)
+ .where(eq(itemOffshoreTop.itemCode, itemCode))
+
+ return topItems.length > 0
+ ? topItems.map(item => ({ ...item, itemType: "TOP" as const }))
+ : []
+
+ case "해양HULL":
+ const hullItems = await db
+ .select({
+ id: itemOffshoreHull.id,
+ itemCode: itemOffshoreHull.itemCode,
+ workType: itemOffshoreHull.workType,
+ itemList: itemOffshoreHull.itemList,
+ shipTypes: sql<string>`null`.as("shipTypes"),
+ })
+ .from(itemOffshoreHull)
+ .where(eq(itemOffshoreHull.itemCode, itemCode))
+
+ return hullItems.length > 0
+ ? hullItems.map(item => ({ ...item, itemType: "HULL" as const }))
+ : []
+
+ default:
+ return []
+ }
+ } catch (error) {
+ console.error("Error finding item:", error)
+ return []
+ }
+}
+
+/**
+ * tech-vendor-possible-items에 중복 데이터 확인
+ * 여러 아이템 ID를 한 번에 확인할 수 있도록 수정
+ */
+async function checkPossibleItemDuplicate(vendorId: number, items: FoundItem[]) {
+ try {
+ if (items.length === 0) return []
+
+ const shipIds = items.filter(item => item.itemType === "SHIP").map(item => item.id)
+ const topIds = items.filter(item => item.itemType === "TOP").map(item => item.id)
+ const hullIds = items.filter(item => item.itemType === "HULL").map(item => item.id)
+
+ const whereConditions = [eq(techVendorPossibleItems.vendorId, vendorId)]
+ const orConditions = []
+
+ if (shipIds.length > 0) {
+ orConditions.push(inArray(techVendorPossibleItems.shipbuildingItemId, shipIds))
+ }
+ if (topIds.length > 0) {
+ orConditions.push(inArray(techVendorPossibleItems.offshoreTopItemId, topIds))
+ }
+ if (hullIds.length > 0) {
+ orConditions.push(inArray(techVendorPossibleItems.offshoreHullItemId, hullIds))
+ }
+
+ if (orConditions.length > 0) {
+ whereConditions.push(or(...orConditions))
+ }
+
+ const existing = await db
+ .select({
+ id: techVendorPossibleItems.id,
+ shipbuildingItemId: techVendorPossibleItems.shipbuildingItemId,
+ offshoreTopItemId: techVendorPossibleItems.offshoreTopItemId,
+ offshoreHullItemId: techVendorPossibleItems.offshoreHullItemId,
+ })
+ .from(techVendorPossibleItems)
+ .where(and(...whereConditions))
+
+ return existing
+ } catch (error) {
+ console.error("Error checking duplicate:", error)
+ return []
+ }
+}
+
+/**
+ * possible items Excel 파일에서 데이터 파싱
+ */
+export async function parsePossibleItemsImportFile(file: File): Promise<PossibleItemImportData[]> {
+ const arrayBuffer = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(arrayBuffer)
+
+ const worksheet = workbook.worksheets[0]
+ if (!worksheet) {
+ throw new Error("Excel 파일에 워크시트가 없습니다.")
+ }
+
+ const data: PossibleItemImportData[] = []
+
+ worksheet.eachRow((row, index) => {
+ // 헤더 행 건너뛰기 (1행)
+ if (index === 1) return
+
+ const values = row.values as (string | null)[]
+ if (!values || values.length < 3) return
+
+ const vendorEmail = values[1]?.toString().trim()
+ const itemCode = values[2]?.toString().trim()
+ const itemType = values[3]?.toString().trim()
+
+ // 필수 필드 검증
+ if (!vendorEmail || !itemCode || !itemType) {
+ return
+ }
+
+ // 아이템 타입 검증 및 변환
+ let validatedItemType: "조선" | "해양TOP" | "해양HULL" | null = null
+ if (itemType === "조선") {
+ validatedItemType = "조선"
+ } else if (itemType === "해양TOP") {
+ validatedItemType = "해양TOP"
+ } else if (itemType === "해양HULL") {
+ validatedItemType = "해양HULL"
+ }
+
+ if (!validatedItemType) {
+ return
+ }
+
+ data.push({
+ vendorEmail,
+ itemCode,
+ itemType: validatedItemType,
+ })
+ })
+
+ return data
+}
+
+/**
+ * possible items 일괄 import
+ */
+export async function importPossibleItemsFromExcel(
+ data: PossibleItemImportData[]
+): Promise<PossibleItemImportResult> {
+ const result: PossibleItemImportResult = {
+ success: true,
+ totalRows: data.length,
+ successCount: 0,
+ failedRows: [],
+ }
+
+ for (let i = 0; i < data.length; i++) {
+ const row = data[i]
+ const rowNumber = i + 1
+
+ try {
+ // 1. 벤더 이메일로 벤더 찾기
+ if (!row.vendorEmail || !row.vendorEmail.trim()) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "벤더 이메일은 필수입니다.",
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ continue
+ }
+
+ const vendor = await findVendorByEmail(row.vendorEmail.trim())
+ if (!vendor) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `벤더 이메일 '${row.vendorEmail}'을(를) 찾을 수 없습니다.`,
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ continue
+ }
+
+ // 2. 아이템 코드로 아이템 찾기
+ if (!row.itemCode || !row.itemCode.trim()) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "아이템 코드는 필수입니다.",
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ continue
+ }
+
+ const items = await findItemByCodeAndType(row.itemCode.trim(), row.itemType)
+ if (!items || items.length === 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `아이템 코드 '${row.itemCode}'을(를) '${row.itemType}' 타입에서 찾을 수 없습니다.`,
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ continue
+ }
+
+ // 3. 중복 데이터 확인 (모든 아이템에 대해)
+ const existingItems = await checkPossibleItemDuplicate(vendor.id, items)
+
+ // 중복되지 않은 아이템들만 필터링
+ const nonDuplicateItems = items.filter(item => {
+ const existingItem = existingItems.find(existing => {
+ if (item.itemType === "SHIP") {
+ return existing.shipbuildingItemId === item.id
+ } else if (item.itemType === "TOP") {
+ return existing.offshoreTopItemId === item.id
+ } else if (item.itemType === "HULL") {
+ return existing.offshoreHullItemId === item.id
+ }
+ return false
+ })
+ return !existingItem
+ })
+
+ if (nonDuplicateItems.length === 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "모든 아이템이 이미 등록되어 있습니다.",
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ continue
+ }
+
+ // 4. tech-vendor-possible-items에 데이터 삽입 (중복되지 않은 아이템들만)
+ for (const item of nonDuplicateItems) {
+ const insertData: {
+ vendorId: number
+ shipbuildingItemId?: number
+ offshoreTopItemId?: number
+ offshoreHullItemId?: number
+ } = {
+ vendorId: vendor.id,
+ }
+
+ if (item.itemType === "SHIP") {
+ insertData.shipbuildingItemId = item.id
+ } else if (item.itemType === "TOP") {
+ insertData.offshoreTopItemId = item.id
+ } else if (item.itemType === "HULL") {
+ insertData.offshoreHullItemId = item.id
+ }
+
+ await db.insert(techVendorPossibleItems).values(insertData)
+ result.successCount++
+ }
+
+ // 부분 성공/실패 처리: 일부 아이템만 등록된 경우
+ const duplicateCount = items.length - nonDuplicateItems.length
+ if (duplicateCount > 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `${duplicateCount}개 아이템이 중복되어 제외되었습니다.`,
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType as "조선" | "해양TOP" | "해양HULL",
+ })
+ }
+
+ } catch (error) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: error instanceof Error ? error.message : "알 수 없는 오류",
+ vendorEmail: row.vendorEmail,
+ itemCode: row.itemCode,
+ itemType: row.itemType,
+ })
+ }
+ }
+
+ // 캐시 무효화
+ revalidateTag("tech-vendor-possible-items")
+
+ return result
+}
+
+/**
+ * possible items 템플릿 Excel 파일 생성
+ */
+export async function generatePossibleItemsImportTemplate(): Promise<Blob> {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("벤더_Possible_Items_템플릿")
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: "벤더이메일*", key: "vendorEmail", width: 25 },
+ { header: "아이템코드*", key: "itemCode", width: 20 },
+ { header: "아이템타입*", key: "itemType", width: 15 },
+ ]
+
+ // 헤더 스타일 설정
+ const headerRow = worksheet.getRow(1)
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFE0E0E0" },
+ }
+
+ // 예시 데이터 추가
+ worksheet.addRow({
+ vendorEmail: "vendor@example.com",
+ itemCode: "ITEM001",
+ itemType: "조선",
+ })
+
+ worksheet.addRow({
+ vendorEmail: "vendor@example.com",
+ itemCode: "TOP001",
+ itemType: "해양TOP",
+ })
+
+ worksheet.addRow({
+ vendorEmail: "vendor@example.com",
+ itemCode: "HULL001",
+ itemType: "해양HULL",
+ })
+
+ // 설명 시트 추가
+ const infoSheet = workbook.addWorksheet("설명")
+ infoSheet.getColumn(1).width = 50
+ infoSheet.getColumn(2).width = 100
+
+ infoSheet.addRow(["템플릿 사용 방법"])
+ infoSheet.addRow(["1. 벤더이메일", "벤더의 이메일 주소 (필수)"])
+ infoSheet.addRow(["2. 아이템코드", "아이템 코드 (필수)"])
+ infoSheet.addRow(["3. 아이템타입", "조선, 해양TOP, 해양HULL 중 하나 (필수)"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["중요 안내"])
+ infoSheet.addRow(["• 조선 아이템의 경우", "같은 아이템 코드라도 선종이 다른 여러 레코드가 있을 수 있습니다."])
+ infoSheet.addRow(["• 조선 아이템 등록 시", "아이템 코드 하나로 선종이 다른 모든 레코드가 자동으로 등록됩니다."])
+ infoSheet.addRow(["• 해양TOP/HULL의 경우", "아이템 코드 하나에 하나의 레코드만 존재합니다."])
+ infoSheet.addRow([])
+ infoSheet.addRow(["주의사항"])
+ infoSheet.addRow(["• 벤더이메일은 시스템에 등록된 이메일이어야 합니다."])
+ infoSheet.addRow(["• 아이템코드는 해당 타입의 아이템 테이블에 존재해야 합니다."])
+ infoSheet.addRow(["• 이미 등록된 아이템-벤더 조합은 중복 등록되지 않습니다."])
+ infoSheet.addRow(["• 조선 아이템의 경우 일부 선종만 중복인 경우 나머지 선종은 등록됩니다."])
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ return new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
+}
+
+/**
+ * possible items 에러 Excel 파일 생성
+ */
+export async function generatePossibleItemsErrorExcel(errors: PossibleItemErrorData[]): Promise<Blob> {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("Import_에러_내역")
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: "벤더이메일", key: "vendorEmail", width: 25 },
+ { header: "아이템코드", key: "itemCode", width: 20 },
+ { header: "아이템타입", key: "itemType", width: 15 },
+ { header: "에러내용", key: "error", width: 50 },
+ ]
+
+ // 헤더 스타일 설정
+ const headerRow = worksheet.getRow(1)
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFFFCCCC" },
+ }
+
+ // 에러 데이터 추가
+ errors.forEach(error => {
+ worksheet.addRow({
+ vendorEmail: error.vendorEmail,
+ itemCode: error.itemCode,
+ itemType: error.itemType,
+ error: error.error,
+ })
+ })
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ return new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
} \ No newline at end of file