From 6013fe51293ea067400e6b3b26691705608eba22 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Fri, 15 Aug 2025 08:45:48 +0000 Subject: (김준회) 1. resolve signup-with-vendor api route problem 2. shi-api 기반 유저 동기화 로직 개선 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/signup-with-vendor.tsx | 491 ------------------------------- app/api/auth/signup-with-vendor/route.ts | 491 +++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+), 491 deletions(-) delete mode 100644 app/api/auth/signup-with-vendor.tsx create mode 100644 app/api/auth/signup-with-vendor/route.ts (limited to 'app/api') diff --git a/app/api/auth/signup-with-vendor.tsx b/app/api/auth/signup-with-vendor.tsx deleted file mode 100644 index 1274d59b..00000000 --- a/app/api/auth/signup-with-vendor.tsx +++ /dev/null @@ -1,491 +0,0 @@ -// app/api/auth/signup-with-vendor/route.ts -import { NextRequest, NextResponse } from 'next/server' -import { unstable_noStore } from 'next/cache' -import { revalidateTag } from 'next/cache' -import { eq, and } from 'drizzle-orm' -import { PgTransaction } from 'drizzle-orm/pg-core' - -import db from '@/db/db' -import { - users, - vendors, - vendorContacts, - vendorAttachments, - userConsents, - consentLogs, - policyVersions -} from '@/db/schema' -import { insertVendor } from '@/lib/vendors/repository' -import { getErrorMessage } from '@/lib/handle-error' -import { saveFile, SaveFileResult } from '@/lib/file-stroage' - -// Types -interface AccountData { - name: string - email: string - phone: string - country: string -} - -interface VendorData { - 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 - contacts: ContactData[] -} - -interface ContactData { - contactName: string - contactPosition?: string - contactDepartment?: string - contactTask?: string - contactEmail: string - contactPhone?: string - isPrimary?: boolean -} - -interface ConsentData { - privacy_policy: { - agreed: boolean - version: string - } - terms_of_service: { - agreed: boolean - version: string - } - marketing: { - agreed: boolean - version: string - } -} - -interface CompleteSignupData { - account: AccountData - vendor: VendorData - consents: ConsentData -} - -// 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, - userId?: number -) { - const vendorDirectory = `vendors/${vendorId}` - - for (const file of files) { - console.log(`📄 업체 파일 저장 시작: ${file.name} (타입: ${attachmentType})`) - - // 보안 강화된 파일 저장 - const saveResult: SaveFileResult = await saveFile({ - file, - directory: vendorDirectory, - originalName: file.name, - userId: userId?.toString(), - }) - - if (!saveResult.success) { - throw new Error(`파일 저장 실패 (${file.name}): ${saveResult.error}`) - } - - // 파일 정보 DB에 저장 - await tx.insert(vendorAttachments).values({ - vendorId, - fileName: saveResult.originalName || file.name, - filePath: saveResult.publicPath!, - attachmentType, - }) - - console.log(`✅ 업체 파일 저장 완료: ${saveResult.fileName}`) - } -} - -// 사용자 계정 생성 함수 (승인 대기 상태) -async function createUserAccount( - tx: PgTransaction, - accountData: AccountData, - vendorId?: number -) { - console.log(`👤 사용자 계정 생성: ${accountData.email} (승인 대기 상태)`) - - // 국가코드에 따른 언어 설정 - const language = accountData.country === 'KR' ? 'ko' : 'en' - - // 사용자 생성 (승인 대기 상태로) - const [newUser] = await tx.insert(users).values({ - name: accountData.name, - email: accountData.email.toLowerCase(), - phone: accountData.phone, - domain: 'partners', // 파트너 도메인 - companyId: vendorId || null, - language, // 🌍 국가코드 기반 언어 설정 (KR → 'ko', 그외 → 'en') - - // 🔒 승인 대기 상태로 설정 - isActive: false, // 관리자 승인 후 활성화 예정 - - // 보안 관련 초기 설정 - mfaEnabled: false, - isLocked: false, - failedLoginAttempts: 0, - passwordChangeRequired: false, // 패스워드 링크로 설정 예정 - - // 동의 관련 초기 설정 - requiresConsentUpdate: false, - lastConsentUpdate: new Date(), - }).returning() - - console.log(`✅ 사용자 계정 생성 완료: ${newUser.email} (ID: ${newUser.id}, 언어: ${language})`) - return newUser -} - -// 동의 정보 저장 함수 -async function saveUserConsents( - tx: PgTransaction, - userId: number, - consents: ConsentData, - clientIP?: string, - userAgent?: string -) { - const timestamp = new Date() - - // 각 동의 타입에 대해 처리 - const consentTypes = [ - { type: 'privacy_policy' as const, data: consents.privacy_policy }, - { type: 'terms_of_service' as const, data: consents.terms_of_service }, - { type: 'marketing' as const, data: consents.marketing } - ] - - for (const { type, data } of consentTypes) { - // 동의 상태 저장 - await tx.insert(userConsents).values({ - userId, - consentType: type, - consentStatus: data.agreed, - policyVersion: data.version, - consentedAt: timestamp, - ipAddress: clientIP || null, - userAgent: userAgent || null, - }) - - // 동의 로그 저장 - await tx.insert(consentLogs).values({ - userId, - consentType: type, - action: 'consent', - oldStatus: null, // 신규 사용자이므로 이전 상태 없음 - newStatus: data.agreed, - policyVersion: data.version, - ipAddress: clientIP || null, - userAgent: userAgent || null, - actionTimestamp: timestamp, - additionalData: { - source: 'signup', - initialConsent: true - } - }) - } - - // users 테이블의 동의 관련 필드 업데이트 - await tx.update(users) - .set({ - lastConsentUpdate: timestamp, - consentVersion: consents.privacy_policy.version, // 대표 버전으로 privacy policy 사용 - requiresConsentUpdate: false, - updatedAt: timestamp - }) - .where(eq(users.id, userId)) -} - -// 유효성 검사 함수들 -function validateAccountData(account: AccountData): string[] { - const errors: string[] = [] - - if (!account.name?.trim()) errors.push('이름은 필수입니다.') - if (!account.email?.trim()) errors.push('이메일은 필수입니다.') - if (!account.phone?.trim()) errors.push('전화번호는 필수입니다.') - if (!account.country?.trim()) errors.push('국가는 필수입니다.') - - // 이메일 형식 검증 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (account.email && !emailRegex.test(account.email)) { - errors.push('유효한 이메일 형식이 아닙니다.') - } - - return errors -} - -function validateVendorData(vendor: VendorData): string[] { - const errors: string[] = [] - - if (!vendor.vendorName?.trim()) errors.push('업체명은 필수입니다.') - if (!vendor.vendorTypeId) errors.push('업체 유형은 필수입니다.') - if (!vendor.items?.trim()) errors.push('공급품목은 필수입니다.') - if (!vendor.taxId?.trim()) errors.push('사업자등록번호는 필수입니다.') - if (!vendor.country?.trim()) errors.push('국가는 필수입니다.') - if (!vendor.phone?.trim()) errors.push('대표 전화번호는 필수입니다.') - if (!vendor.email?.trim()) errors.push('대표 이메일은 필수입니다.') - - // 연락처 검증 - if (!vendor.contacts?.length) { - errors.push('최소 1명의 담당자 정보가 필요합니다.') - } else { - vendor.contacts.forEach((contact, index) => { - if (!contact.contactName?.trim()) { - errors.push(`담당자 ${index + 1}의 이름은 필수입니다.`) - } - if (!contact.contactEmail?.trim()) { - errors.push(`담당자 ${index + 1}의 이메일은 필수입니다.`) - } - }) - } - - return errors -} - -function validateConsents(consents: ConsentData): string[] { - const errors: string[] = [] - - // 필수 동의 항목 확인 - if (!consents.privacy_policy?.agreed) { - errors.push('개인정보 처리방침에 동의해주세요.') - } - if (!consents.terms_of_service?.agreed) { - errors.push('서비스 이용약관에 동의해주세요.') - } - - // 버전 정보 확인 - if (!consents.privacy_policy?.version) { - errors.push('개인정보 처리방침 버전 정보가 없습니다.') - } - if (!consents.terms_of_service?.version) { - errors.push('서비스 이용약관 버전 정보가 없습니다.') - } - - return errors -} - -export async function POST(request: NextRequest) { - unstable_noStore() - - try { - const formData = await request.formData() - - // 완전한 가입 데이터 파싱 - const completeDataString = formData.get('completeData') as string - if (!completeDataString) { - return NextResponse.json( - { error: '가입 정보가 누락되었습니다.' }, - { status: 400 } - ) - } - - const completeData: CompleteSignupData = JSON.parse(completeDataString) - const { account, vendor, consents } = completeData - - // 유효성 검사 - const accountErrors = validateAccountData(account) - const vendorErrors = validateVendorData(vendor) - const consentErrors = validateConsents(consents) - - const allErrors = [...accountErrors, ...vendorErrors, ...consentErrors] - if (allErrors.length > 0) { - return NextResponse.json( - { error: allErrors.join('\n') }, - { status: 400 } - ) - } - - // 파일 추출 - 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[] - - // 필수 파일 검증 - 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 (vendor.country !== "KR" && bankAccountFiles.length === 0) { - return NextResponse.json( - { error: '대금지급 통장사본을 업로드해주세요.' }, - { status: 400 } - ) - } - - // 중복 검사 - const existingUser = await db - .select({ id: users.id }) - .from(users) - .where(eq(users.email, account.email.toLowerCase())) - .limit(1) - - if (existingUser.length > 0) { - return NextResponse.json( - { error: '이미 등록된 이메일입니다.' }, - { status: 400 } - ) - } - - const existingVendor = await db - .select({ id: vendors.id }) - .from(vendors) - .where(eq(vendors.taxId, vendor.taxId)) - .limit(1) - - if (existingVendor.length > 0) { - return NextResponse.json( - { error: '이미 등록된 사업자등록번호입니다.' }, - { status: 400 } - ) - } - - // 클라이언트 정보 추출 - const clientIP = request.headers.get('x-forwarded-for') || - request.headers.get('x-real-ip') || - request.ip || - '127.0.0.1' - const userAgent = request.headers.get('user-agent') || 'Unknown' - - let newUser: any - let newVendor: any - - // 트랜잭션으로 모든 데이터 생성 - await db.transaction(async (tx) => { - // 1. 업체 생성 - [newVendor] = await insertVendor(tx, { - vendorName: vendor.vendorName, - vendorCode: vendor.vendorCode || null, - address: vendor.address || null, - country: vendor.country || null, - phone: vendor.phone || null, - email: vendor.email, - website: vendor.website || null, - status: "PENDING_REVIEW", // 관리자 승인 대기 - taxId: vendor.taxId, - vendorTypeId: vendor.vendorTypeId, - items: vendor.items || null, - // 한국 사업자 정보 - representativeName: vendor.representativeName || null, - representativeBirth: vendor.representativeBirth || null, - representativeEmail: vendor.representativeEmail || null, - representativePhone: vendor.representativePhone || null, - corporateRegistrationNumber: vendor.corporateRegistrationNumber || null, - representativeWorkExpirence: vendor.representativeWorkExpirence || false, - }) - - // 2. 사용자 계정 생성 (업체 ID와 연결) - newUser = await createUserAccount(tx, account, newVendor.id) - - // 3. 동의 정보 저장 - await saveUserConsents(tx, newUser.id, consents, clientIP, userAgent) - - // 4. 파일 저장 (보안 강화된 파일 저장 함수 사용) - if (businessRegistrationFiles.length > 0) { - await storeVendorFiles(tx, newVendor.id, businessRegistrationFiles, FILE_TYPES.BUSINESS_REGISTRATION, newUser.id) - } - - if (isoCertificationFiles.length > 0) { - await storeVendorFiles(tx, newVendor.id, isoCertificationFiles, FILE_TYPES.ISO_CERTIFICATION, newUser.id) - } - - if (creditReportFiles.length > 0) { - await storeVendorFiles(tx, newVendor.id, creditReportFiles, FILE_TYPES.CREDIT_REPORT, newUser.id) - } - - if (bankAccountFiles.length > 0) { - await storeVendorFiles(tx, newVendor.id, bankAccountFiles, FILE_TYPES.BANK_ACCOUNT_COPY, newUser.id) - } - - // 5. 담당자 정보 저장 - for (const [index, contact] of vendor.contacts.entries()) { - 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: index === 0, // 첫 번째 담당자를 주담당자로 설정 - }) - } - }) - - // 캐시 무효화 - revalidateTag("vendors") - revalidateTag("users") - - console.log(`🎉 통합 회원가입 완료:`) - console.log(` - 사용자: ${newUser.email} (ID: ${newUser.id})`) - console.log(` - 업체: ${newVendor.vendorName} (ID: ${newVendor.id})`) - console.log(` - 상태: 승인 대기 (isActive: false)`) - console.log(` - 다음 단계: 관리자 승인 → 패스워드 설정 링크 발송`) - - return NextResponse.json({ - message: '회원가입 및 업체 등록이 완료되었습니다.', - notice: '관리자 승인 후 계정이 활성화됩니다. 승인 완료 시 패스워드 설정 링크가 이메일로 전송됩니다.', - data: { - userId: newUser.id, - vendorId: newVendor.id, - email: newUser.email, - status: 'PENDING_APPROVAL', // 승인 대기 상태 - isActive: false - } - }, { status: 201 }) - - } catch (error) { - console.error('통합 회원가입 처리 오류:', error) - return NextResponse.json( - { - error: '회원가입 및 업체 등록 처리 중 오류가 발생했습니다.', - details: getErrorMessage(error), - notice: '문제가 지속될 경우 관리자에게 문의해주세요.' - }, - { status: 500 } - ) - } -} diff --git a/app/api/auth/signup-with-vendor/route.ts b/app/api/auth/signup-with-vendor/route.ts new file mode 100644 index 00000000..1274d59b --- /dev/null +++ b/app/api/auth/signup-with-vendor/route.ts @@ -0,0 +1,491 @@ +// app/api/auth/signup-with-vendor/route.ts +import { NextRequest, NextResponse } from 'next/server' +import { unstable_noStore } from 'next/cache' +import { revalidateTag } from 'next/cache' +import { eq, and } from 'drizzle-orm' +import { PgTransaction } from 'drizzle-orm/pg-core' + +import db from '@/db/db' +import { + users, + vendors, + vendorContacts, + vendorAttachments, + userConsents, + consentLogs, + policyVersions +} from '@/db/schema' +import { insertVendor } from '@/lib/vendors/repository' +import { getErrorMessage } from '@/lib/handle-error' +import { saveFile, SaveFileResult } from '@/lib/file-stroage' + +// Types +interface AccountData { + name: string + email: string + phone: string + country: string +} + +interface VendorData { + 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 + contacts: ContactData[] +} + +interface ContactData { + contactName: string + contactPosition?: string + contactDepartment?: string + contactTask?: string + contactEmail: string + contactPhone?: string + isPrimary?: boolean +} + +interface ConsentData { + privacy_policy: { + agreed: boolean + version: string + } + terms_of_service: { + agreed: boolean + version: string + } + marketing: { + agreed: boolean + version: string + } +} + +interface CompleteSignupData { + account: AccountData + vendor: VendorData + consents: ConsentData +} + +// 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, + userId?: number +) { + const vendorDirectory = `vendors/${vendorId}` + + for (const file of files) { + console.log(`📄 업체 파일 저장 시작: ${file.name} (타입: ${attachmentType})`) + + // 보안 강화된 파일 저장 + const saveResult: SaveFileResult = await saveFile({ + file, + directory: vendorDirectory, + originalName: file.name, + userId: userId?.toString(), + }) + + if (!saveResult.success) { + throw new Error(`파일 저장 실패 (${file.name}): ${saveResult.error}`) + } + + // 파일 정보 DB에 저장 + await tx.insert(vendorAttachments).values({ + vendorId, + fileName: saveResult.originalName || file.name, + filePath: saveResult.publicPath!, + attachmentType, + }) + + console.log(`✅ 업체 파일 저장 완료: ${saveResult.fileName}`) + } +} + +// 사용자 계정 생성 함수 (승인 대기 상태) +async function createUserAccount( + tx: PgTransaction, + accountData: AccountData, + vendorId?: number +) { + console.log(`👤 사용자 계정 생성: ${accountData.email} (승인 대기 상태)`) + + // 국가코드에 따른 언어 설정 + const language = accountData.country === 'KR' ? 'ko' : 'en' + + // 사용자 생성 (승인 대기 상태로) + const [newUser] = await tx.insert(users).values({ + name: accountData.name, + email: accountData.email.toLowerCase(), + phone: accountData.phone, + domain: 'partners', // 파트너 도메인 + companyId: vendorId || null, + language, // 🌍 국가코드 기반 언어 설정 (KR → 'ko', 그외 → 'en') + + // 🔒 승인 대기 상태로 설정 + isActive: false, // 관리자 승인 후 활성화 예정 + + // 보안 관련 초기 설정 + mfaEnabled: false, + isLocked: false, + failedLoginAttempts: 0, + passwordChangeRequired: false, // 패스워드 링크로 설정 예정 + + // 동의 관련 초기 설정 + requiresConsentUpdate: false, + lastConsentUpdate: new Date(), + }).returning() + + console.log(`✅ 사용자 계정 생성 완료: ${newUser.email} (ID: ${newUser.id}, 언어: ${language})`) + return newUser +} + +// 동의 정보 저장 함수 +async function saveUserConsents( + tx: PgTransaction, + userId: number, + consents: ConsentData, + clientIP?: string, + userAgent?: string +) { + const timestamp = new Date() + + // 각 동의 타입에 대해 처리 + const consentTypes = [ + { type: 'privacy_policy' as const, data: consents.privacy_policy }, + { type: 'terms_of_service' as const, data: consents.terms_of_service }, + { type: 'marketing' as const, data: consents.marketing } + ] + + for (const { type, data } of consentTypes) { + // 동의 상태 저장 + await tx.insert(userConsents).values({ + userId, + consentType: type, + consentStatus: data.agreed, + policyVersion: data.version, + consentedAt: timestamp, + ipAddress: clientIP || null, + userAgent: userAgent || null, + }) + + // 동의 로그 저장 + await tx.insert(consentLogs).values({ + userId, + consentType: type, + action: 'consent', + oldStatus: null, // 신규 사용자이므로 이전 상태 없음 + newStatus: data.agreed, + policyVersion: data.version, + ipAddress: clientIP || null, + userAgent: userAgent || null, + actionTimestamp: timestamp, + additionalData: { + source: 'signup', + initialConsent: true + } + }) + } + + // users 테이블의 동의 관련 필드 업데이트 + await tx.update(users) + .set({ + lastConsentUpdate: timestamp, + consentVersion: consents.privacy_policy.version, // 대표 버전으로 privacy policy 사용 + requiresConsentUpdate: false, + updatedAt: timestamp + }) + .where(eq(users.id, userId)) +} + +// 유효성 검사 함수들 +function validateAccountData(account: AccountData): string[] { + const errors: string[] = [] + + if (!account.name?.trim()) errors.push('이름은 필수입니다.') + if (!account.email?.trim()) errors.push('이메일은 필수입니다.') + if (!account.phone?.trim()) errors.push('전화번호는 필수입니다.') + if (!account.country?.trim()) errors.push('국가는 필수입니다.') + + // 이메일 형식 검증 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (account.email && !emailRegex.test(account.email)) { + errors.push('유효한 이메일 형식이 아닙니다.') + } + + return errors +} + +function validateVendorData(vendor: VendorData): string[] { + const errors: string[] = [] + + if (!vendor.vendorName?.trim()) errors.push('업체명은 필수입니다.') + if (!vendor.vendorTypeId) errors.push('업체 유형은 필수입니다.') + if (!vendor.items?.trim()) errors.push('공급품목은 필수입니다.') + if (!vendor.taxId?.trim()) errors.push('사업자등록번호는 필수입니다.') + if (!vendor.country?.trim()) errors.push('국가는 필수입니다.') + if (!vendor.phone?.trim()) errors.push('대표 전화번호는 필수입니다.') + if (!vendor.email?.trim()) errors.push('대표 이메일은 필수입니다.') + + // 연락처 검증 + if (!vendor.contacts?.length) { + errors.push('최소 1명의 담당자 정보가 필요합니다.') + } else { + vendor.contacts.forEach((contact, index) => { + if (!contact.contactName?.trim()) { + errors.push(`담당자 ${index + 1}의 이름은 필수입니다.`) + } + if (!contact.contactEmail?.trim()) { + errors.push(`담당자 ${index + 1}의 이메일은 필수입니다.`) + } + }) + } + + return errors +} + +function validateConsents(consents: ConsentData): string[] { + const errors: string[] = [] + + // 필수 동의 항목 확인 + if (!consents.privacy_policy?.agreed) { + errors.push('개인정보 처리방침에 동의해주세요.') + } + if (!consents.terms_of_service?.agreed) { + errors.push('서비스 이용약관에 동의해주세요.') + } + + // 버전 정보 확인 + if (!consents.privacy_policy?.version) { + errors.push('개인정보 처리방침 버전 정보가 없습니다.') + } + if (!consents.terms_of_service?.version) { + errors.push('서비스 이용약관 버전 정보가 없습니다.') + } + + return errors +} + +export async function POST(request: NextRequest) { + unstable_noStore() + + try { + const formData = await request.formData() + + // 완전한 가입 데이터 파싱 + const completeDataString = formData.get('completeData') as string + if (!completeDataString) { + return NextResponse.json( + { error: '가입 정보가 누락되었습니다.' }, + { status: 400 } + ) + } + + const completeData: CompleteSignupData = JSON.parse(completeDataString) + const { account, vendor, consents } = completeData + + // 유효성 검사 + const accountErrors = validateAccountData(account) + const vendorErrors = validateVendorData(vendor) + const consentErrors = validateConsents(consents) + + const allErrors = [...accountErrors, ...vendorErrors, ...consentErrors] + if (allErrors.length > 0) { + return NextResponse.json( + { error: allErrors.join('\n') }, + { status: 400 } + ) + } + + // 파일 추출 + 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[] + + // 필수 파일 검증 + 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 (vendor.country !== "KR" && bankAccountFiles.length === 0) { + return NextResponse.json( + { error: '대금지급 통장사본을 업로드해주세요.' }, + { status: 400 } + ) + } + + // 중복 검사 + const existingUser = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.email, account.email.toLowerCase())) + .limit(1) + + if (existingUser.length > 0) { + return NextResponse.json( + { error: '이미 등록된 이메일입니다.' }, + { status: 400 } + ) + } + + const existingVendor = await db + .select({ id: vendors.id }) + .from(vendors) + .where(eq(vendors.taxId, vendor.taxId)) + .limit(1) + + if (existingVendor.length > 0) { + return NextResponse.json( + { error: '이미 등록된 사업자등록번호입니다.' }, + { status: 400 } + ) + } + + // 클라이언트 정보 추출 + const clientIP = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + request.ip || + '127.0.0.1' + const userAgent = request.headers.get('user-agent') || 'Unknown' + + let newUser: any + let newVendor: any + + // 트랜잭션으로 모든 데이터 생성 + await db.transaction(async (tx) => { + // 1. 업체 생성 + [newVendor] = await insertVendor(tx, { + vendorName: vendor.vendorName, + vendorCode: vendor.vendorCode || null, + address: vendor.address || null, + country: vendor.country || null, + phone: vendor.phone || null, + email: vendor.email, + website: vendor.website || null, + status: "PENDING_REVIEW", // 관리자 승인 대기 + taxId: vendor.taxId, + vendorTypeId: vendor.vendorTypeId, + items: vendor.items || null, + // 한국 사업자 정보 + representativeName: vendor.representativeName || null, + representativeBirth: vendor.representativeBirth || null, + representativeEmail: vendor.representativeEmail || null, + representativePhone: vendor.representativePhone || null, + corporateRegistrationNumber: vendor.corporateRegistrationNumber || null, + representativeWorkExpirence: vendor.representativeWorkExpirence || false, + }) + + // 2. 사용자 계정 생성 (업체 ID와 연결) + newUser = await createUserAccount(tx, account, newVendor.id) + + // 3. 동의 정보 저장 + await saveUserConsents(tx, newUser.id, consents, clientIP, userAgent) + + // 4. 파일 저장 (보안 강화된 파일 저장 함수 사용) + if (businessRegistrationFiles.length > 0) { + await storeVendorFiles(tx, newVendor.id, businessRegistrationFiles, FILE_TYPES.BUSINESS_REGISTRATION, newUser.id) + } + + if (isoCertificationFiles.length > 0) { + await storeVendorFiles(tx, newVendor.id, isoCertificationFiles, FILE_TYPES.ISO_CERTIFICATION, newUser.id) + } + + if (creditReportFiles.length > 0) { + await storeVendorFiles(tx, newVendor.id, creditReportFiles, FILE_TYPES.CREDIT_REPORT, newUser.id) + } + + if (bankAccountFiles.length > 0) { + await storeVendorFiles(tx, newVendor.id, bankAccountFiles, FILE_TYPES.BANK_ACCOUNT_COPY, newUser.id) + } + + // 5. 담당자 정보 저장 + for (const [index, contact] of vendor.contacts.entries()) { + 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: index === 0, // 첫 번째 담당자를 주담당자로 설정 + }) + } + }) + + // 캐시 무효화 + revalidateTag("vendors") + revalidateTag("users") + + console.log(`🎉 통합 회원가입 완료:`) + console.log(` - 사용자: ${newUser.email} (ID: ${newUser.id})`) + console.log(` - 업체: ${newVendor.vendorName} (ID: ${newVendor.id})`) + console.log(` - 상태: 승인 대기 (isActive: false)`) + console.log(` - 다음 단계: 관리자 승인 → 패스워드 설정 링크 발송`) + + return NextResponse.json({ + message: '회원가입 및 업체 등록이 완료되었습니다.', + notice: '관리자 승인 후 계정이 활성화됩니다. 승인 완료 시 패스워드 설정 링크가 이메일로 전송됩니다.', + data: { + userId: newUser.id, + vendorId: newVendor.id, + email: newUser.email, + status: 'PENDING_APPROVAL', // 승인 대기 상태 + isActive: false + } + }, { status: 201 }) + + } catch (error) { + console.error('통합 회원가입 처리 오류:', error) + return NextResponse.json( + { + error: '회원가입 및 업체 등록 처리 중 오류가 발생했습니다.', + details: getErrorMessage(error), + notice: '문제가 지속될 경우 관리자에게 문의해주세요.' + }, + { status: 500 } + ) + } +} -- cgit v1.2.3