diff --git a/prisma/migrations/20260308065417_add_business_seller_fields/migration.sql b/prisma/migrations/20260308065417_add_business_seller_fields/migration.sql new file mode 100644 index 0000000..8e36608 --- /dev/null +++ b/prisma/migrations/20260308065417_add_business_seller_fields/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - A unique constraint covering the columns `[business_number]` on the table `SettlementAccount` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE `SettlementAccount` ADD COLUMN `business_license_url` TEXT NULL, + ADD COLUMN `business_number` VARCHAR(30) NULL, + ADD COLUMN `company_name` VARCHAR(100) NULL, + ADD COLUMN `representative_name` VARCHAR(100) NULL, + ADD COLUMN `seller_type` ENUM('INDIVIDUAL', 'BUSINESS') NOT NULL DEFAULT 'INDIVIDUAL', + ADD COLUMN `status` ENUM('PENDING', 'APPROVED', 'REJECTED') NOT NULL DEFAULT 'APPROVED'; + +-- CreateIndex +CREATE UNIQUE INDEX `SettlementAccount_business_number_key` ON `SettlementAccount`(`business_number`); + +-- CreateIndex +CREATE INDEX `SettlementAccount_status_idx` ON `SettlementAccount`(`status`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b4cb69f..f2872e6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -442,13 +442,30 @@ model Settlement { @@index([user_id]) } +enum SellerType { + INDIVIDUAL + BUSINESS +} + +enum ApprovalStatus { + PENDING // 심사 대기중 + APPROVED // 승인 완료 + REJECTED // 승인 거절 +} + model SettlementAccount { id Int @id @default(autoincrement()) user_id Int @unique // 한 유저당 하나의 계좌만 가질 수 있도록 강제 (1:1 관계) bank_code String @db.VarChar(50) account_number String @db.VarChar(30) account_holder String @db.VarChar(100) + seller_type SellerType @default(INDIVIDUAL) + status ApprovalStatus @default(APPROVED) // 개인은 즉시 승인, 사업자는 PENDING으로 생성 is_active Boolean @default(true) + representative_name String? @db.VarChar(100) + company_name String? @db.VarChar(100) + business_number String? @unique @db.VarChar(30) + business_license_url String? @db.Text created_at DateTime @default(now()) updated_at DateTime @updatedAt @@ -456,6 +473,7 @@ model SettlementAccount { @@index([bank_code]) @@index([account_number]) + @@index([status]) } model Review { diff --git a/src/middlewares/upload.ts b/src/middlewares/upload.ts index bbfd820..158cb4c 100644 --- a/src/middlewares/upload.ts +++ b/src/middlewares/upload.ts @@ -38,3 +38,36 @@ export const upload = multer({ fileSize: 5 * 1024 * 1024, // 5MB 제한 }, }); + +const businessLicenseFileFilter = ( + req: Request, + file: Express.Multer.File, + cb: multer.FileFilterCallback +) => { + const allowedTypes = /jpeg|jpg|png|pdf/; + const mimetype = allowedTypes.test(file.mimetype); + const extname = allowedTypes.test( + path.extname(file.originalname).toLowerCase() + ); + + if (mimetype && extname) { + return cb(null, true); + } + + cb( + new AppError( + "지원하지 않는 파일 형식입니다. (jpg, jpeg, png, pdf만 가능)", + 415, + "InvalidFileType" + ) + ); +}; + +// 사업자등록증 전용 업로드 미들웨어 (20MB 제한) +export const uploadBusinessLicense = multer({ + storage: storage, // 기존 메모리 스토리지 재활용 + fileFilter: businessLicenseFileFilter, + limits: { + fileSize: 20 * 1024 * 1024, + }, +}); diff --git a/src/settlements/controllers/settlement.controller.ts b/src/settlements/controllers/settlement.account.controller.ts similarity index 98% rename from src/settlements/controllers/settlement.controller.ts rename to src/settlements/controllers/settlement.account.controller.ts index 1a40ce6..8391a2a 100644 --- a/src/settlements/controllers/settlement.controller.ts +++ b/src/settlements/controllers/settlement.account.controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { verifyAndSaveAccount, getAccountInfo } from '../services/settlement.service'; +import { verifyAndSaveAccount, getAccountInfo } from '../services/settlement.account.service'; import { VerifyAccountRequestDto, ViewAccountResponseDto} from '../dtos/settlement.dto'; export const verifyAccount = async (req: Request, res: Response) => { diff --git a/src/settlements/controllers/settlement.seller.controller.ts b/src/settlements/controllers/settlement.seller.controller.ts new file mode 100644 index 0000000..c79f700 --- /dev/null +++ b/src/settlements/controllers/settlement.seller.controller.ts @@ -0,0 +1,166 @@ +import { Request, Response } from 'express'; +import { registerIndividualSeller, registerBusinessSeller } from '../services/settlement.seller.service'; +import multer from 'multer'; +import { uploadBusinessLicense } from '../../middlewares/upload'; +import { uploadBusinessLicenseFile } from '../services/settlement.seller.service'; +import { AppError } from '../../errors/AppError'; + +export const registerIndividual = async (req: Request, res: Response) => { + try { + const user = req.user; + + if (!user) { + return res.status(401).json({ + error: 'Unauthorized', + message: '로그인이 필요합니다.', + statusCode: 401, + }); + } + + const userId = (req.user as { user_id: number }).user_id; + const result = await registerIndividualSeller(userId, req.body); + + return res.status(200).json({ + message: result.message, + statusCode: 200, + }); + + } catch (error: any) { + if (error.name === 'ValidationError') { + return res.status(400).json({ + error: 'ValidationError', + message: error.message, + statusCode: 400, + }); + } + + if (error.name === 'AlreadyRegistered') { + return res.status(409).json({ + error: 'AlreadyRegistered', + message: error.message, + statusCode: 409, + }); + } + + return res.status(500).json({ + error: 'InternalServerError', + message: '서버 오류가 발생했습니다.', + statusCode: 500, + }); + } +}; + +export const uploadLicense = async (req: Request, res: Response) => { + const uploadSingle = uploadBusinessLicense.single('file'); + + uploadSingle(req, res, async (err: any) => { + try { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(413).json({ + error: 'FileTooLarge', + message: '파일 크기는 최대 20MB까지만 허용됩니다.', + statusCode: 413, + }); + } + } + + if (err instanceof AppError && err.statusCode === 415) { + return res.status(415).json({ + error: err.name, + message: err.message, + statusCode: 415, + }); + } else if (err) { + throw err; + } + + const user = req.user as { user_id: number } | undefined; + if (!user) { + return res.status(401).json({ + error: 'Unauthorized', + message: '로그인이 필요합니다.', + statusCode: 401, + }); + } + + if (!req.file) { + return res.status(400).json({ + error: 'ValidationError', + message: '업로드할 파일이 첨부되지 않았습니다.', + statusCode: 400, + }); + } + + const result = await uploadBusinessLicenseFile(user.user_id, req.file); + + return res.status(200).json({ + message: result.message, + fileUrl: result.fileUrl, + statusCode: 200, + }); + + } catch (error: any) { + console.error('사업자등록증 업로드 중 에러 발생:', error); + return res.status(500).json({ + error: 'InternalServerError', + message: '알 수 없는 오류가 발생했습니다.', + statusCode: 500, + }); + } + }); +}; + +export const registerBusiness = async (req: Request, res: Response) => { + try { + const user = req.user as { user_id: number } | undefined; + + if (!user) { + return res.status(401).json({ + error: 'Unauthorized', + message: '로그인이 필요합니다.', + statusCode: 401, + }); + } + + const userId = user.user_id; + const result = await registerBusinessSeller(userId, req.body); + + return res.status(200).json({ + message: result.message, + statusCode: 200, + }); + + } catch (error: any) { + if (error.name === 'ValidationError') { + return res.status(400).json({ + error: 'ValidationError', + message: error.message, + statusCode: 400, + }); + } + + if (error.name === 'AlreadyRegistered') { + return res.status(409).json({ + error: 'AlreadyRegistered', + message: error.message, + statusCode: 409, + }); + } + + if (error.name === 'DuplicateBusinessNumber') { + return res.status(409).json({ + error: 'DuplicateBusinessNumber', + message: error.message, + statusCode: 409, + }); + } + + console.error('사업자 판매자 등록 중 에러 발생:', error); + return res.status(500).json({ + error: 'InternalServerError', + message: '서버 오류가 발생했습니다.', + statusCode: 500, + }); + } +}; \ No newline at end of file diff --git a/src/settlements/dtos/settlement.dto.ts b/src/settlements/dtos/settlement.dto.ts index 42174af..1bfeac9 100644 --- a/src/settlements/dtos/settlement.dto.ts +++ b/src/settlements/dtos/settlement.dto.ts @@ -22,4 +22,23 @@ export interface UpdateAccountRequestDto { bank: string; accountNumber: string; holderName: string; -} \ No newline at end of file +} + +export interface RegisterIndividualSellerRequestDto { + name: string; + bank: string; + accountNumber: string; + holderName: string; + isTermsAgreed: boolean; +} + +export interface RegisterBusinessSellerRequestDto { + representativeName: string; + bank: string; + accountNumber: string; + holderName: string; + businessNumber: string; + companyName: string; + businessLicenseUrl: string; + isTermsAgreed: boolean; +} diff --git a/src/settlements/repositories/settlement.repository.ts b/src/settlements/repositories/settlement.repository.ts index ba44f3c..f78d1ad 100644 --- a/src/settlements/repositories/settlement.repository.ts +++ b/src/settlements/repositories/settlement.repository.ts @@ -1,6 +1,6 @@ import prisma from '../../config/prisma'; import { SettlementAccount } from '@prisma/client'; -import { VerifyAccountRequestDto } from '../dtos/settlement.dto'; +import { VerifyAccountRequestDto, RegisterBusinessSellerRequestDto} from '../dtos/settlement.dto'; export const SettlementRepository = { upsertSettlementAccount: async ( @@ -28,5 +28,28 @@ export const SettlementRepository = { return await prisma.settlementAccount.findUnique({ where: { user_id: userId}, }); + }, + + findAccountByBusinessNumber: async (businessNumber: string) => { + return await prisma.settlementAccount.findFirst({ + where: { business_number: businessNumber }, + }); +}, + + createBusinessAccount: async (userId: number, dto: RegisterBusinessSellerRequestDto) => { + return await prisma.settlementAccount.create({ + data: { + user_id: userId, + bank_code: dto.bank, + account_number: dto.accountNumber, + account_holder: dto.holderName, + business_number: dto.businessNumber, + company_name: dto.companyName, + representative_name: dto.representativeName, + business_license_url: dto.businessLicenseUrl, + seller_type: 'BUSINESS', + status: 'PENDING', + is_active: false, + }}) } }; \ No newline at end of file diff --git a/src/settlements/routes/settlement.route.ts b/src/settlements/routes/settlement.route.ts index 539cc25..02b73c0 100644 --- a/src/settlements/routes/settlement.route.ts +++ b/src/settlements/routes/settlement.route.ts @@ -1,5 +1,7 @@ import { Router } from "express"; -import { verifyAccount, ViewAccount } from "../controllers/settlement.controller"; +import { verifyAccount, ViewAccount } from "../controllers/settlement.account.controller"; +import { registerIndividual, registerBusiness } from "../controllers/settlement.seller.controller"; +import { uploadLicense } from "../controllers/settlement.seller.controller"; import { authenticateJwt } from "../../config/passport"; const router = Router(); @@ -234,4 +236,402 @@ router.post("/verify-account", authenticateJwt, verifyAccount); */ router.get("/accounts", authenticateJwt, ViewAccount); +/** + * @swagger + * /api/settlements/register/individual: + * post: + * summary: 개인 판매자 등록 + * description: 개인정보 수집 이용 동의 및 계좌 정보를 입력받아 일반 개인 판매자로 등록합니다. + * tags: + * - Settlement + * security: + * - jwt: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - bank + * - accountNumber + * - holderName + * - isTermsAgreed + * properties: + * name: + * type: string + * description: 실명 + * example: 홍길동 + * bank: + * type: string + * description: 포트원 표준 은행 코드 + * example: KOOKMIN + * accountNumber: + * type: string + * description: '-'를 제외한 계좌 번호 + * example: "1234567890" + * holderName: + * type: string + * description: 계좌 예금주명 + * example: 홍길동 + * isTermsAgreed: + * type: boolean + * description: 개인정보 수집 이용 동의 여부 (반드시 true) + * example: true + * responses: + * 200: + * description: 판매자 등록 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: 개인 판매자 등록이 완료되었습니다. + * statusCode: + * type: integer + * example: 200 + * 400: + * description: 검증 실패 - 필수 입력값 누락 또는 약관 미동의 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: ValidationError + * message: + * type: string + * example: 필수 입력값이 누락되었거나 이용 약관에 동의하지 않았습니다. + * statusCode: + * type: integer + * example: 400 + * 401: + * description: 인증 실패 - 로그인하지 않은 사용자 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Unauthorized + * message: + * type: string + * example: 로그인이 필요합니다. + * statusCode: + * type: integer + * example: 401 + * 409: + * description: 충돌 - 이미 등록된 판매자 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: AlreadyRegistered + * message: + * type: string + * example: 이미 판매자로 등록된 회원입니다. + * statusCode: + * type: integer + * example: 409 + * 500: + * description: 서버 오류 - 알 수 없는 예외 발생 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: InternalServerError + * message: + * type: string + * example: 서버 오류가 발생했습니다. + * statusCode: + * type: integer + * example: 500 + */ +router.post("/register/individual", authenticateJwt, registerIndividual); + +/** + * @swagger + * /api/settlements/upload/business-license: + * post: + * summary: 사업자등록증 업로드 (개인/법인 사업자) + * description: 개인 또는 법인 사업자의 사업자등록증 파일(이미지 또는 PDF, 최대 20MB)을 업로드하고 S3 URL을 반환받습니다. + * tags: + * - Settlement + * security: + * - jwt: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - file + * properties: + * file: + * type: string + * format: binary + * description: 업로드할 사업자등록증 파일 (jpg, jpeg, png, pdf) / 최대 20MB + * responses: + * 200: + * description: 파일 업로드 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: 사업자등록증 업로드가 완료되었습니다. + * fileUrl: + * type: string + * example: https://promptplace-storage.s3.ap-northeast-2.amazonaws.com/business-licenses/123-1709865432123.jpg + * statusCode: + * type: integer + * example: 200 + * 400: + * description: 업로드할 파일 누락 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: ValidationError + * message: + * type: string + * example: 업로드할 파일이 첨부되지 않았습니다. + * statusCode: + * type: integer + * example: 400 + * 401: + * description: 인증 실패 - 로그인하지 않은 사용자 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Unauthorized + * message: + * type: string + * example: 로그인이 필요합니다. + * statusCode: + * type: integer + * example: 401 + * 413: + * description: 파일 용량 제한 초과 (20MB) + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: FileTooLarge + * message: + * type: string + * example: 파일 크기는 최대 20MB까지만 허용됩니다. + * statusCode: + * type: integer + * example: 413 + * 415: + * description: 지원하지 않는 파일 형식 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: InvalidFileType + * message: + * type: string + * example: 지원하지 않는 파일 형식입니다. (jpg, jpeg, png, pdf만 가능) + * statusCode: + * type: integer + * example: 415 + * 500: + * description: 서버 오류 - 알 수 없는 예외 발생 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: InternalServerError + * message: + * type: string + * example: 서버 오류가 발생했습니다. + * statusCode: + * type: integer + * example: 500 + */ +router.post("/upload/business-license", authenticateJwt, uploadLicense); + +/** + * @swagger + * /api/settlements/register/business: + * post: + * summary: 사업자 판매자 등록 + * description: 개인/법인 사업자의 정보와 사업자등록증 URL을 입력받아 판매자로 등록 신청합니다. (관리자 승인 대기 상태로 저장됨) + * tags: + * - Settlement + * security: + * - jwt: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - representativeName + * - bank + * - accountNumber + * - holderName + * - businessNumber + * - companyName + * - businessLicenseUrl + * - isTermsAgreed + * properties: + * representativeName: + * type: string + * description: 대표자명 + * example: 김대표 + * bank: + * type: string + * description: 포트원 표준 은행 코드 + * example: SHINHAN + * accountNumber: + * type: string + * description: '-'를 제외한 계좌 번호 + * example: "0987654321" + * holderName: + * type: string + * description: 계좌 예금주명 + * example: 김대표 + * businessNumber: + * type: string + * description: 사업자등록번호 ('-' 제외 숫자만) + * example: "1234567890" + * companyName: + * type: string + * description: 상호명 + * example: (주)프롬프트팩토리 + * businessLicenseUrl: + * type: string + * description: 업로드 API로 발급받은 사업자등록증 이미지 URL + * example: https://promptplace-storage.s3.ap-northeast-2.amazonaws.com/business-licenses/123-1709865432123.jpg + * isTermsAgreed: + * type: boolean + * description: 개인정보 수집 이용 동의 여부 (반드시 true) + * example: true + * responses: + * 200: + * description: 판매자 신청 성공 (승인 대기 상태) + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: 사업자 판매자 신청이 완료되었습니다. 관리자 승인 후 최종 등록됩니다. + * statusCode: + * type: integer + * example: 200 + * 400: + * description: 검증 실패 - 필수 입력값 누락 또는 약관 미동의 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: ValidationError + * message: + * type: string + * example: 필수 입력값이 누락되었거나 이용 약관에 동의하지 않았습니다. + * statusCode: + * type: integer + * example: 400 + * 401: + * description: 인증 실패 - 로그인하지 않은 사용자 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Unauthorized + * message: + * type: string + * example: 로그인이 필요합니다. + * statusCode: + * type: integer + * example: 401 + * 409: + * description: 충돌 - 중복된 사업자등록번호 또는 이미 등록된 유저 + * content: + * application/json: + * schema: + * oneOf: + * - type: object + * properties: + * error: + * type: string + * example: DuplicateBusinessNumber + * message: + * type: string + * example: 이미 등록되었거나 심사 대기 중인 사업자등록번호입니다. + * statusCode: + * type: integer + * example: 409 + * - type: object + * properties: + * error: + * type: string + * example: AlreadyRegistered + * message: + * type: string + * example: 이미 판매자로 등록되었거나 승인 심사 대기 중인 회원입니다. + * statusCode: + * type: integer + * example: 409 + * 500: + * description: 서버 오류 - 알 수 없는 예외 발생 + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: InternalServerError + * message: + * type: string + * example: 서버 오류가 발생했습니다. + * statusCode: + * type: integer + * example: 500 + */ +router.post("/register/business", authenticateJwt, registerBusiness); + export default router; \ No newline at end of file diff --git a/src/settlements/services/settlement.service.ts b/src/settlements/services/settlement.account.service.ts similarity index 97% rename from src/settlements/services/settlement.service.ts rename to src/settlements/services/settlement.account.service.ts index 3ac5010..141b013 100644 --- a/src/settlements/services/settlement.service.ts +++ b/src/settlements/services/settlement.account.service.ts @@ -6,8 +6,6 @@ import { SettlementRepository} from '../repositories/settlement.repository'; export const verifyAndSaveAccount = async (userId: number, dto: VerifyAccountRequestDto) => { const { name, bank, accountNumber, holderName } = dto; - const existingAccount = await SettlementRepository.findAccountByUserId(userId); - // 1. 필수 입력값 검증 (400) if (!name || !bank || !accountNumber || !holderName) { throw { status: 400, type: "ValidationError", message: "필수 입력값(은행, 계좌번호, 실명/대표자명, 예금주명)이 모두 입력되지 않았습니다." }; diff --git a/src/settlements/services/settlement.seller.service.ts b/src/settlements/services/settlement.seller.service.ts new file mode 100644 index 0000000..de7a52f --- /dev/null +++ b/src/settlements/services/settlement.seller.service.ts @@ -0,0 +1,105 @@ +import path from "path"; +import { S3Client, PutObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3"; +import { AppError } from "../../errors/AppError"; +import { RegisterIndividualSellerRequestDto, RegisterBusinessSellerRequestDto} from '../dtos/settlement.dto'; +import { SettlementRepository } from '../repositories/settlement.repository'; + +export const registerIndividualSeller = async (userId: number, dto: RegisterIndividualSellerRequestDto) => { + // 1. 필수값 누락 및 약관 동의 여부 검증 (400) + if (!dto.name || !dto.bank || !dto.accountNumber || !dto.holderName || dto.isTermsAgreed !== true) { + const error = new Error('필수 입력값이 누락되었거나 이용 약관에 동의하지 않았습니다.'); + error.name = 'ValidationError'; + throw error; + } + + // 2. 이미 등록된 판매자인지(계좌가 있는지) 검증 (409) + const existingAccount = await SettlementRepository.findAccountByUserId(userId); + if (existingAccount) { + const error = new Error('이미 판매자로 등록된 회원입니다.'); + error.name = 'AlreadyRegistered'; + throw error; + } + + await SettlementRepository.upsertSettlementAccount(userId, { + name: dto.name, + bank: dto.bank, + accountNumber: dto.accountNumber, + holderName: dto.holderName, + }); + + return { message: '개인 판매자 등록이 완료되었습니다.' }; +}; + +export const s3Client = new S3Client({ + region: process.env.S3_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + }, +}); + +export const uploadFileToS3 = async (key: string, buffer: Buffer, contentType: string) => { + const command = new PutObjectCommand({ + Bucket: process.env.S3_BUCKET!, + Key: key, + Body: buffer, + ContentType: contentType, + }); + + await s3Client.send(command); + + // 업로드된 파일의 S3 URL 반환 + return `https://${process.env.S3_BUCKET!}.s3.${process.env.S3_REGION}.amazonaws.com/${key}`; +}; + +export const uploadBusinessLicenseFile = async (userId: number, file: Express.Multer.File) => { + try { + const ext = path.extname(file.originalname); + + const uniqueKey = `business-licenses/${userId}-${Date.now()}${ext}`; + + const fileUrl = await uploadFileToS3(uniqueKey, file.buffer, file.mimetype); + + return { + message: '사업자등록증 업로드가 완료되었습니다.', + fileUrl: fileUrl + }; + } catch (error) { + console.error("S3 업로드 에러:", error); + throw new AppError("파일 업로드 중 서버 오류가 발생했습니다.", 500, "InternalServerError"); + } +}; + +export const registerBusinessSeller = async (userId: number, dto: RegisterBusinessSellerRequestDto) => { + // 1. 필수값 누락 및 약관 동의 여부 검증 (400) + if ( + !dto.representativeName || !dto.bank || !dto.accountNumber || + !dto.holderName || !dto.businessNumber || !dto.companyName || + !dto.businessLicenseUrl || dto.isTermsAgreed !== true + ) { + const error = new Error('필수 입력값이 누락되었거나 이용 약관에 동의하지 않았습니다.'); + error.name = 'ValidationError'; + throw error; + } + + // 2. 이미 등록되었거나 심사 대기 중인 유저인지 검증 (409) + const existingAccount = await SettlementRepository.findAccountByUserId(userId); + if (existingAccount) { + const error = new Error('이미 판매자로 등록되었거나 승인 심사 대기 중인 회원입니다.'); + error.name = 'AlreadyRegistered'; + throw error; + } + + // 3. 중복된 사업자등록번호인지 검증 (409) + const existingBusiness = await SettlementRepository.findAccountByBusinessNumber(dto.businessNumber); + if (existingBusiness) { + const error = new Error('이미 등록되었거나 심사 대기 중인 사업자등록번호입니다.'); + error.name = 'DuplicateBusinessNumber'; + throw error; + } + + // 4. 검증 통과 시 DB 저장 (상태: PENDING) + await SettlementRepository.createBusinessAccount(userId, dto); + + return { message: '사업자 판매자 신청이 완료되었습니다. 관리자 승인 후 최종 등록됩니다.' }; +}; \ No newline at end of file