// app/api/vendors/route.ts import { NextRequest, NextResponse } from 'next/server' import { unstable_noStore } from 'next/cache' import { revalidateTag } from 'next/cache' import { randomUUID } from 'crypto' import * as fs from 'fs/promises' import * as path from 'path' import { eq } from 'drizzle-orm' import { PgTransaction } from 'drizzle-orm/pg-core' import db from '@/db/db' import { users, vendors, vendorContacts, vendorAttachments } from '@/db/schema' import { insertVendor } from '@/lib/vendors/repository' import { getErrorMessage } from '@/lib/handle-error' // Types interface CreateVendorData { vendorName: string vendorCode?: string address?: string country?: string phone?: string email: string website?: string status?: string taxId: string vendorTypeId: number items?: string representativeName?: string representativeBirth?: string representativeEmail?: string representativePhone?: string corporateRegistrationNumber?: string representativeWorkExpirence?: boolean } interface ContactData { contactName: string contactPosition?: string contactDepartment?: string contactTask?: string contactEmail: string contactPhone?: string isPrimary?: boolean } // File attachment types const FILE_TYPES = { BUSINESS_REGISTRATION: 'BUSINESS_REGISTRATION', ISO_CERTIFICATION: 'ISO_CERTIFICATION', CREDIT_REPORT: 'CREDIT_REPORT', BANK_ACCOUNT_COPY: 'BANK_ACCOUNT_COPY' } as const type FileType = typeof FILE_TYPES[keyof typeof FILE_TYPES] async function storeVendorFiles( tx: PgTransaction, vendorId: number, files: File[], attachmentType: FileType ) { const vendorDir = path.join( process.cwd(), "public", "vendors", String(vendorId) ) await fs.mkdir(vendorDir, { recursive: true }) for (const file of files) { // Convert file to buffer const ab = await file.arrayBuffer() const buffer = Buffer.from(ab) // Generate a unique filename const uniqueName = `${randomUUID()}-${file.name}` const relativePath = path.join("vendors", String(vendorId), uniqueName) const absolutePath = path.join(process.cwd(), "public", relativePath) // Write to disk await fs.writeFile(absolutePath, buffer) // Insert attachment record await tx.insert(vendorAttachments).values({ vendorId, fileName: file.name, filePath: "/" + relativePath.replace(/\\/g, "/"), attachmentType, }) } } export async function POST(request: NextRequest) { unstable_noStore() try { const formData = await request.formData() // Parse vendor data and contacts from JSON strings const vendorDataString = formData.get('vendorData') as string const contactsString = formData.get('contacts') as string if (!vendorDataString || !contactsString) { return NextResponse.json( { error: 'Missing vendor data or contacts' }, { status: 400 } ) } const vendorData: CreateVendorData = JSON.parse(vendorDataString) const contacts: ContactData[] = JSON.parse(contactsString) // Extract files by type const businessRegistrationFiles = formData.getAll('businessRegistration') as File[] const isoCertificationFiles = formData.getAll('isoCertification') as File[] const creditReportFiles = formData.getAll('creditReport') as File[] const bankAccountFiles = formData.getAll('bankAccount') as File[] // Validate required files if (businessRegistrationFiles.length === 0) { return NextResponse.json( { error: '사업자등록증을 업로드해주세요.' }, { status: 400 } ) } if (isoCertificationFiles.length === 0) { return NextResponse.json( { error: 'ISO 인증서를 업로드해주세요.' }, { status: 400 } ) } if (creditReportFiles.length === 0) { return NextResponse.json( { error: '신용평가보고서를 업로드해주세요.' }, { status: 400 } ) } if (vendorData.country !== "KR" && bankAccountFiles.length === 0) { return NextResponse.json( { error: '대금지급 통장사본을 업로드해주세요.' }, { status: 400 } ) } // Check for existing email const existingUser = await db .select({ id: users.id }) .from(users) .where(eq(users.email, vendorData.email)) .limit(1) if (existingUser.length > 0) { return NextResponse.json( { error: `이미 등록된 이메일입니다. 다른 이메일을 사용해주세요. (Email ${vendorData.email} already exists in the system)` }, { status: 400 } ) } // Check for existing taxId (only if taxId is provided) if (vendorData.taxId && vendorData.taxId.trim()) { const existingVendor = await db .select({ id: vendors.id }) .from(vendors) .where(eq(vendors.taxId, vendorData.taxId)) .limit(1) if (existingVendor.length > 0) { return NextResponse.json( { error: `이미 등록된 사업자등록번호입니다. (Tax ID ${vendorData.taxId} already exists in the system)` }, { status: 400 } ) } } // Create vendor and handle files in transaction await db.transaction(async (tx) => { // Insert the vendor const [newVendor] = await insertVendor(tx, { vendorName: vendorData.vendorName, vendorCode: vendorData.vendorCode || null, address: vendorData.address || null, country: vendorData.country || null, phone: vendorData.phone || null, email: vendorData.email, website: vendorData.website || null, status: vendorData.status ?? "PENDING_REVIEW", taxId: vendorData.taxId, vendorTypeId: vendorData.vendorTypeId, items: vendorData.items || null, // Representative info representativeName: vendorData.representativeName || null, representativeBirth: vendorData.representativeBirth || null, representativeEmail: vendorData.representativeEmail || null, representativePhone: vendorData.representativePhone || null, corporateRegistrationNumber: vendorData.corporateRegistrationNumber || null, representativeWorkExpirence: vendorData.representativeWorkExpirence || false, }) // Store files by type if (businessRegistrationFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, businessRegistrationFiles, FILE_TYPES.BUSINESS_REGISTRATION) } if (isoCertificationFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, isoCertificationFiles, FILE_TYPES.ISO_CERTIFICATION) } if (creditReportFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, creditReportFiles, FILE_TYPES.CREDIT_REPORT) } if (bankAccountFiles.length > 0) { await storeVendorFiles(tx, newVendor.id, bankAccountFiles, FILE_TYPES.BANK_ACCOUNT_COPY) } // Insert contacts with new fields for (const contact of contacts) { await tx.insert(vendorContacts).values({ vendorId: newVendor.id, contactName: contact.contactName, contactPosition: contact.contactPosition || null, contactDepartment: contact.contactDepartment || null, contactTask: contact.contactTask || null, contactEmail: contact.contactEmail, contactPhone: contact.contactPhone || null, isPrimary: contact.isPrimary ?? false, }) } }) revalidateTag("vendors") return NextResponse.json( { message: '벤더 등록이 완료되었습니다.' }, { status: 201 } ) } catch (error) { console.error('Vendor creation error:', error) return NextResponse.json( { error: getErrorMessage(error) }, { status: 500 } ) } }