Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`);
18 changes: 18 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -442,20 +442,38 @@ 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

user User @relation(fields: [user_id], references: [user_id], onDelete: Cascade)

@@index([bank_code])
@@index([account_number])
@@index([status])
}

model Review {
Expand Down
33 changes: 33 additions & 0 deletions src/middlewares/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
166 changes: 166 additions & 0 deletions src/settlements/controllers/settlement.seller.controller.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
};
21 changes: 20 additions & 1 deletion src/settlements/dtos/settlement.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,23 @@ export interface UpdateAccountRequestDto {
bank: string;
accountNumber: string;
holderName: string;
}
}

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;
}
25 changes: 24 additions & 1 deletion src/settlements/repositories/settlement.repository.ts
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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,
}})
}
};
Loading