summaryrefslogtreecommitdiff
path: root/app/api/auth/signup-with-vendor.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
commitcbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (patch)
tree0a26712f7685e4f6511e637b9a81269d90a47c8f /app/api/auth/signup-with-vendor.tsx
parenteb654f88214095f71be142b989e620fd28db3f69 (diff)
(대표님) 입찰, EDP 변경사항 대응, spreadJS 오류 수정, 벤더실사 수정
Diffstat (limited to 'app/api/auth/signup-with-vendor.tsx')
-rw-r--r--app/api/auth/signup-with-vendor.tsx491
1 files changed, 491 insertions, 0 deletions
diff --git a/app/api/auth/signup-with-vendor.tsx b/app/api/auth/signup-with-vendor.tsx
new file mode 100644
index 00000000..1274d59b
--- /dev/null
+++ b/app/api/auth/signup-with-vendor.tsx
@@ -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<any, any, any>,
+ 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<any, any, any>,
+ 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<any, any, any>,
+ 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 }
+ )
+ }
+}