Skip to content

Commit d7c55c0

Browse files
authored
Merge pull request #435 from PromptPlace/feat/#430
Feat/#430
2 parents 4c04c69 + fc9fef9 commit d7c55c0

10 files changed

Lines changed: 787 additions & 6 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[business_number]` on the table `SettlementAccount` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE `SettlementAccount` ADD COLUMN `business_license_url` TEXT NULL,
9+
ADD COLUMN `business_number` VARCHAR(30) NULL,
10+
ADD COLUMN `company_name` VARCHAR(100) NULL,
11+
ADD COLUMN `representative_name` VARCHAR(100) NULL,
12+
ADD COLUMN `seller_type` ENUM('INDIVIDUAL', 'BUSINESS') NOT NULL DEFAULT 'INDIVIDUAL',
13+
ADD COLUMN `status` ENUM('PENDING', 'APPROVED', 'REJECTED') NOT NULL DEFAULT 'APPROVED';
14+
15+
-- CreateIndex
16+
CREATE UNIQUE INDEX `SettlementAccount_business_number_key` ON `SettlementAccount`(`business_number`);
17+
18+
-- CreateIndex
19+
CREATE INDEX `SettlementAccount_status_idx` ON `SettlementAccount`(`status`);

prisma/schema.prisma

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,20 +442,38 @@ model Settlement {
442442
@@index([user_id])
443443
}
444444

445+
enum SellerType {
446+
INDIVIDUAL
447+
BUSINESS
448+
}
449+
450+
enum ApprovalStatus {
451+
PENDING // 심사 대기중
452+
APPROVED // 승인 완료
453+
REJECTED // 승인 거절
454+
}
455+
445456
model SettlementAccount {
446457
id Int @id @default(autoincrement())
447458
user_id Int @unique // 한 유저당 하나의 계좌만 가질 수 있도록 강제 (1:1 관계)
448459
bank_code String @db.VarChar(50)
449460
account_number String @db.VarChar(30)
450461
account_holder String @db.VarChar(100)
462+
seller_type SellerType @default(INDIVIDUAL)
463+
status ApprovalStatus @default(APPROVED) // 개인은 즉시 승인, 사업자는 PENDING으로 생성
451464
is_active Boolean @default(true)
465+
representative_name String? @db.VarChar(100)
466+
company_name String? @db.VarChar(100)
467+
business_number String? @unique @db.VarChar(30)
468+
business_license_url String? @db.Text
452469
created_at DateTime @default(now())
453470
updated_at DateTime @updatedAt
454471
455472
user User @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
456473
457474
@@index([bank_code])
458475
@@index([account_number])
476+
@@index([status])
459477
}
460478

461479
model Review {

src/middlewares/upload.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,36 @@ export const upload = multer({
3838
fileSize: 5 * 1024 * 1024, // 5MB 제한
3939
},
4040
});
41+
42+
const businessLicenseFileFilter = (
43+
req: Request,
44+
file: Express.Multer.File,
45+
cb: multer.FileFilterCallback
46+
) => {
47+
const allowedTypes = /jpeg|jpg|png|pdf/;
48+
const mimetype = allowedTypes.test(file.mimetype);
49+
const extname = allowedTypes.test(
50+
path.extname(file.originalname).toLowerCase()
51+
);
52+
53+
if (mimetype && extname) {
54+
return cb(null, true);
55+
}
56+
57+
cb(
58+
new AppError(
59+
"지원하지 않는 파일 형식입니다. (jpg, jpeg, png, pdf만 가능)",
60+
415,
61+
"InvalidFileType"
62+
)
63+
);
64+
};
65+
66+
// 사업자등록증 전용 업로드 미들웨어 (20MB 제한)
67+
export const uploadBusinessLicense = multer({
68+
storage: storage, // 기존 메모리 스토리지 재활용
69+
fileFilter: businessLicenseFileFilter,
70+
limits: {
71+
fileSize: 20 * 1024 * 1024,
72+
},
73+
});

src/settlements/controllers/settlement.controller.ts renamed to src/settlements/controllers/settlement.account.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Request, Response } from 'express';
2-
import { verifyAndSaveAccount, getAccountInfo } from '../services/settlement.service';
2+
import { verifyAndSaveAccount, getAccountInfo } from '../services/settlement.account.service';
33
import { VerifyAccountRequestDto, ViewAccountResponseDto} from '../dtos/settlement.dto';
44

55
export const verifyAccount = async (req: Request, res: Response) => {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Request, Response } from 'express';
2+
import { registerIndividualSeller, registerBusinessSeller } from '../services/settlement.seller.service';
3+
import multer from 'multer';
4+
import { uploadBusinessLicense } from '../../middlewares/upload';
5+
import { uploadBusinessLicenseFile } from '../services/settlement.seller.service';
6+
import { AppError } from '../../errors/AppError';
7+
8+
export const registerIndividual = async (req: Request, res: Response) => {
9+
try {
10+
const user = req.user;
11+
12+
if (!user) {
13+
return res.status(401).json({
14+
error: 'Unauthorized',
15+
message: '로그인이 필요합니다.',
16+
statusCode: 401,
17+
});
18+
}
19+
20+
const userId = (req.user as { user_id: number }).user_id;
21+
const result = await registerIndividualSeller(userId, req.body);
22+
23+
return res.status(200).json({
24+
message: result.message,
25+
statusCode: 200,
26+
});
27+
28+
} catch (error: any) {
29+
if (error.name === 'ValidationError') {
30+
return res.status(400).json({
31+
error: 'ValidationError',
32+
message: error.message,
33+
statusCode: 400,
34+
});
35+
}
36+
37+
if (error.name === 'AlreadyRegistered') {
38+
return res.status(409).json({
39+
error: 'AlreadyRegistered',
40+
message: error.message,
41+
statusCode: 409,
42+
});
43+
}
44+
45+
return res.status(500).json({
46+
error: 'InternalServerError',
47+
message: '서버 오류가 발생했습니다.',
48+
statusCode: 500,
49+
});
50+
}
51+
};
52+
53+
export const uploadLicense = async (req: Request, res: Response) => {
54+
const uploadSingle = uploadBusinessLicense.single('file');
55+
56+
uploadSingle(req, res, async (err: any) => {
57+
try {
58+
if (err instanceof multer.MulterError) {
59+
if (err.code === 'LIMIT_FILE_SIZE') {
60+
return res.status(413).json({
61+
error: 'FileTooLarge',
62+
message: '파일 크기는 최대 20MB까지만 허용됩니다.',
63+
statusCode: 413,
64+
});
65+
}
66+
}
67+
68+
if (err instanceof AppError && err.statusCode === 415) {
69+
return res.status(415).json({
70+
error: err.name,
71+
message: err.message,
72+
statusCode: 415,
73+
});
74+
} else if (err) {
75+
throw err;
76+
}
77+
78+
const user = req.user as { user_id: number } | undefined;
79+
if (!user) {
80+
return res.status(401).json({
81+
error: 'Unauthorized',
82+
message: '로그인이 필요합니다.',
83+
statusCode: 401,
84+
});
85+
}
86+
87+
if (!req.file) {
88+
return res.status(400).json({
89+
error: 'ValidationError',
90+
message: '업로드할 파일이 첨부되지 않았습니다.',
91+
statusCode: 400,
92+
});
93+
}
94+
95+
const result = await uploadBusinessLicenseFile(user.user_id, req.file);
96+
97+
return res.status(200).json({
98+
message: result.message,
99+
fileUrl: result.fileUrl,
100+
statusCode: 200,
101+
});
102+
103+
} catch (error: any) {
104+
console.error('사업자등록증 업로드 중 에러 발생:', error);
105+
return res.status(500).json({
106+
error: 'InternalServerError',
107+
message: '알 수 없는 오류가 발생했습니다.',
108+
statusCode: 500,
109+
});
110+
}
111+
});
112+
};
113+
114+
export const registerBusiness = async (req: Request, res: Response) => {
115+
try {
116+
const user = req.user as { user_id: number } | undefined;
117+
118+
if (!user) {
119+
return res.status(401).json({
120+
error: 'Unauthorized',
121+
message: '로그인이 필요합니다.',
122+
statusCode: 401,
123+
});
124+
}
125+
126+
const userId = user.user_id;
127+
const result = await registerBusinessSeller(userId, req.body);
128+
129+
return res.status(200).json({
130+
message: result.message,
131+
statusCode: 200,
132+
});
133+
134+
} catch (error: any) {
135+
if (error.name === 'ValidationError') {
136+
return res.status(400).json({
137+
error: 'ValidationError',
138+
message: error.message,
139+
statusCode: 400,
140+
});
141+
}
142+
143+
if (error.name === 'AlreadyRegistered') {
144+
return res.status(409).json({
145+
error: 'AlreadyRegistered',
146+
message: error.message,
147+
statusCode: 409,
148+
});
149+
}
150+
151+
if (error.name === 'DuplicateBusinessNumber') {
152+
return res.status(409).json({
153+
error: 'DuplicateBusinessNumber',
154+
message: error.message,
155+
statusCode: 409,
156+
});
157+
}
158+
159+
console.error('사업자 판매자 등록 중 에러 발생:', error);
160+
return res.status(500).json({
161+
error: 'InternalServerError',
162+
message: '서버 오류가 발생했습니다.',
163+
statusCode: 500,
164+
});
165+
}
166+
};

src/settlements/dtos/settlement.dto.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,23 @@ export interface UpdateAccountRequestDto {
2222
bank: string;
2323
accountNumber: string;
2424
holderName: string;
25-
}
25+
}
26+
27+
export interface RegisterIndividualSellerRequestDto {
28+
name: string;
29+
bank: string;
30+
accountNumber: string;
31+
holderName: string;
32+
isTermsAgreed: boolean;
33+
}
34+
35+
export interface RegisterBusinessSellerRequestDto {
36+
representativeName: string;
37+
bank: string;
38+
accountNumber: string;
39+
holderName: string;
40+
businessNumber: string;
41+
companyName: string;
42+
businessLicenseUrl: string;
43+
isTermsAgreed: boolean;
44+
}

src/settlements/repositories/settlement.repository.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import prisma from '../../config/prisma';
22
import { SettlementAccount } from '@prisma/client';
3-
import { VerifyAccountRequestDto } from '../dtos/settlement.dto';
3+
import { VerifyAccountRequestDto, RegisterBusinessSellerRequestDto} from '../dtos/settlement.dto';
44

55
export const SettlementRepository = {
66
upsertSettlementAccount: async (
@@ -28,5 +28,28 @@ export const SettlementRepository = {
2828
return await prisma.settlementAccount.findUnique({
2929
where: { user_id: userId},
3030
});
31+
},
32+
33+
findAccountByBusinessNumber: async (businessNumber: string) => {
34+
return await prisma.settlementAccount.findFirst({
35+
where: { business_number: businessNumber },
36+
});
37+
},
38+
39+
createBusinessAccount: async (userId: number, dto: RegisterBusinessSellerRequestDto) => {
40+
return await prisma.settlementAccount.create({
41+
data: {
42+
user_id: userId,
43+
bank_code: dto.bank,
44+
account_number: dto.accountNumber,
45+
account_holder: dto.holderName,
46+
business_number: dto.businessNumber,
47+
company_name: dto.companyName,
48+
representative_name: dto.representativeName,
49+
business_license_url: dto.businessLicenseUrl,
50+
seller_type: 'BUSINESS',
51+
status: 'PENDING',
52+
is_active: false,
53+
}})
3154
}
3255
};

0 commit comments

Comments
 (0)