BadRequestException์ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ด ์๋ชป๋ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ ์์ธ์
๋๋ค. HTTP 400 ์ํ ์ฝ๋๋ฅผ ๋ฐํํ๋ฉฐ, ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ, ์๋ชป๋ ๋งค๊ฐ๋ณ์, ์์ฒญ ํ์ ์ค๋ฅ ๋ฑ์ ์ฌ์ฉ๋ฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๋ณต์ฌ
class BadRequestException extends SoException {
constructor(
public message = "Bad Request",
public payload?: unknown,
);
}
๋ณต์ฌ
@api()
async createPost(title: string, content: string) {
if (!title || title.trim().length === 0) {
throw new BadRequestException("์ ๋ชฉ์ ํ์์
๋๋ค");
}
if (content.length > 10000) {
throw new BadRequestException("๋ณธ๋ฌธ์ 10,000์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค");
}
return this.create({ title, content });
}
์ค์ฉ ์์
์ ํจ์ฑ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async updateUser(
userId: number,
email?: string,
age?: number,
phoneNumber?: string
) {
const errors: Array<{ field: string; message: string }> = [];
// ์ด๋ฉ์ผ ๊ฒ์ฆ
if (email && !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
errors.push({
field: "email",
message: "์ ํจํ์ง ์์ ์ด๋ฉ์ผ ํ์์
๋๋ค"
});
}
// ๋์ด ๊ฒ์ฆ
if (age !== undefined && (age < 0 || age > 150)) {
errors.push({
field: "age",
message: "๋์ด๋ 0~150 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค"
});
}
// ์ ํ๋ฒํธ ๊ฒ์ฆ
if (phoneNumber && !phoneNumber.match(/^\d{3}-\d{3,4}-\d{4}$/)) {
errors.push({
field: "phoneNumber",
message: "์ ํ๋ฒํธ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค (์: 010-1234-5678)"
});
}
if (errors.length > 0) {
throw new BadRequestException("์
๋ ฅ ๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ์์ต๋๋ค", {
errors
});
}
return this.update(userId, { email, age, phoneNumber });
}
๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ
๋ณต์ฌ
@api()
async transferMoney(
ctx: Context,
fromAccountId: number,
toAccountId: number,
amount: number
) {
// ๊ธ์ก ๊ฒ์ฆ
if (amount <= 0) {
throw new BadRequestException(
"์ด์ฒด ๊ธ์ก์ 0๋ณด๋ค ์ปค์ผ ํฉ๋๋ค",
{ amount }
);
}
if (amount > 10000000) {
throw new BadRequestException(
"1ํ ์ด์ฒด ํ๋๋ ์ฒ๋ง์์
๋๋ค",
{ amount, limit: 10000000 }
);
}
// ๊ณ์ข ๊ฒ์ฆ
if (fromAccountId === toAccountId) {
throw new BadRequestException("๋์ผํ ๊ณ์ข๋ก๋ ์ด์ฒดํ ์ ์์ต๋๋ค");
}
const fromAccount = await AccountModel.findById(fromAccountId);
if (fromAccount.userId !== ctx.user!.id) {
throw new BadRequestException("๋ณธ์ธ ๊ณ์ข์์๋ง ์ด์ฒดํ ์ ์์ต๋๋ค");
}
if (fromAccount.balance < amount) {
throw new BadRequestException(
"์์ก์ด ๋ถ์กฑํฉ๋๋ค",
{
balance: fromAccount.balance,
requested: amount,
shortage: amount - fromAccount.balance
}
);
}
// ์ด์ฒด ์คํ
return this.executeTransfer(fromAccountId, toAccountId, amount);
}
ํ์ผ ์ ๋ก๋ ๊ฒ์ฆ
๋ณต์ฌ
@api()
@upload({ mode: "single" })
async uploadProfileImage(ctx: Context) {
const { file } = Sonamu.getUploadContext();
if (!file) {
throw new BadRequestException("ํ์ผ์ด ํ์ํฉ๋๋ค");
}
// ํ์ผ ํฌ๊ธฐ ๊ฒ์ฆ (5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
throw new BadRequestException(
"ํ์ผ ํฌ๊ธฐ๋ 5MB๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค",
{
size: file.size,
maxSize,
sizeMB: (file.size / 1024 / 1024).toFixed(2),
maxSizeMB: 5
}
);
}
// ํ์ผ ํ์ ๊ฒ์ฆ
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!allowedTypes.includes(file.mimetype)) {
throw new BadRequestException(
"์ง์ํ์ง ์๋ ์ด๋ฏธ์ง ํ์์
๋๋ค",
{
provided: file.mimetype,
allowed: allowedTypes
}
);
}
// ์ด๋ฏธ์ง ์ ์ฅ
return Sonamu.storage.save({
file: file.buffer,
filename: file.filename,
bucket: "profile-images"
});
}
๋ ์ง/์๊ฐ ๊ฒ์ฆ
๋ณต์ฌ
@api()
async createReservation(
userId: number,
startDate: Date,
endDate: Date,
guestCount: number
) {
const now = new Date();
// ๊ณผ๊ฑฐ ๋ ์ง ์ฒดํฌ
if (startDate < now) {
throw new BadRequestException(
"์์ฝ ์์์ผ์ ํ์ฌ ์๊ฐ ์ดํ์ฌ์ผ ํฉ๋๋ค",
{ startDate, now }
);
}
// ์์์ผ/์ข
๋ฃ์ผ ์์ ์ฒดํฌ
if (startDate >= endDate) {
throw new BadRequestException(
"์ข
๋ฃ์ผ์ ์์์ผ๋ณด๋ค ์ดํ์ฌ์ผ ํฉ๋๋ค",
{ startDate, endDate }
);
}
// ์ต๋ ์์ฝ ๊ธฐ๊ฐ ์ฒดํฌ (30์ผ)
const maxDays = 30;
const daysDiff = Math.ceil(
(endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
);
if (daysDiff > maxDays) {
throw new BadRequestException(
`์์ฝ ๊ธฐ๊ฐ์ ์ต๋ ${maxDays}์ผ๊น์ง ๊ฐ๋ฅํฉ๋๋ค`,
{ requestedDays: daysDiff, maxDays }
);
}
// ์ธ์ ์ฒดํฌ
if (guestCount < 1 || guestCount > 10) {
throw new BadRequestException(
"์์ฝ ์ธ์์ 1~10๋ช
์ฌ์ด์ฌ์ผ ํฉ๋๋ค",
{ guestCount }
);
}
return this.create({ userId, startDate, endDate, guestCount });
}
๋ฐฐ์ด/๊ฐ์ฒด ๊ฒ์ฆ
๋ณต์ฌ
@api()
async createOrder(
userId: number,
items: Array<{ productId: number; quantity: number }>
) {
// ๋น ์ฃผ๋ฌธ ์ฒดํฌ
if (!items || items.length === 0) {
throw new BadRequestException("์ต์ 1๊ฐ ์ด์์ ์ํ์ด ํ์ํฉ๋๋ค");
}
// ์ต๋ ์ฃผ๋ฌธ ์๋ ์ฒดํฌ
if (items.length > 50) {
throw new BadRequestException(
"ํ ๋ฒ์ ์ต๋ 50๊ฐ ์ํ๊น์ง ์ฃผ๋ฌธํ ์ ์์ต๋๋ค",
{ itemCount: items.length, maxItems: 50 }
);
}
// ๊ฐ ํญ๋ชฉ ๊ฒ์ฆ
const errors: Array<{ index: number; message: string }> = [];
items.forEach((item, index) => {
if (!item.productId || item.productId <= 0) {
errors.push({
index,
message: "์ ํจํ์ง ์์ ์ํ ID์
๋๋ค"
});
}
if (!item.quantity || item.quantity <= 0) {
errors.push({
index,
message: "์๋์ 1๊ฐ ์ด์์ด์ด์ผ ํฉ๋๋ค"
});
}
if (item.quantity > 999) {
errors.push({
index,
message: "์ํ๋น ์ต๋ 999๊ฐ๊น์ง ์ฃผ๋ฌธํ ์ ์์ต๋๋ค"
});
}
});
if (errors.length > 0) {
throw new BadRequestException("์ฃผ๋ฌธ ํญ๋ชฉ์ด ์ ํจํ์ง ์์ต๋๋ค", {
errors
});
}
return this.createOrder(userId, items);
}
Zod ๊ฒ์ฆ๊ณผ์ ์กฐํฉ
Sonamu๋ API ํ๋ผ๋ฏธํฐ๋ฅผ ์๋์ผ๋ก Zod ์คํค๋ง๋ก ๊ฒ์ฆํ์ง๋ง, ์ถ๊ฐ ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ์๋BadRequestException์ ์ฌ์ฉํฉ๋๋ค:
๋ณต์ฌ
@api()
async createProduct(
name: string, // Zod๋ก ์๋ ๊ฒ์ฆ: ๋ฌธ์์ด ํ์
price: number, // Zod๋ก ์๋ ๊ฒ์ฆ: ์ซ์ ํ์
categoryId: number // Zod๋ก ์๋ ๊ฒ์ฆ: ์ซ์ ํ์
) {
// ์ถ๊ฐ ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ
if (price < 0) {
throw new BadRequestException("๊ฐ๊ฒฉ์ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค");
}
const category = await CategoryModel.findById(categoryId);
if (!category) {
throw new BadRequestException(
"์กด์ฌํ์ง ์๋ ์นดํ
๊ณ ๋ฆฌ์
๋๋ค",
{ categoryId }
);
}
if (!category.isActive) {
throw new BadRequestException(
"๋นํ์ฑํ๋ ์นดํ
๊ณ ๋ฆฌ์๋ ์ํ์ ์ถ๊ฐํ ์ ์์ต๋๋ค",
{ categoryId, categoryName: category.name }
);
}
return this.create({ name, price, categoryId });
}
payload ํ์ฉ ํจํด
ํ๋๋ณ ์ค๋ฅ ๋ชฉ๋ก
๋ณต์ฌ
throw new BadRequestException("์
๋ ฅ ๋ฐ์ดํฐ ๊ฒ์ฆ ์คํจ", {
errors: [
{ field: "email", code: "INVALID_FORMAT" },
{ field: "password", code: "TOO_SHORT", minLength: 8 }
]
});
์ ํ ์ฌํญ ์ ๋ณด
๋ณต์ฌ
throw new BadRequestException("ํ์ผ ํฌ๊ธฐ ์ด๊ณผ", {
size: file.size,
limit: 5242880, // 5MB in bytes
unit: "bytes"
});
์ฌ์๋ ๊ฐ๋ฅ ์ ๋ณด
๋ณต์ฌ
throw new BadRequestException("์ผ์ผ ์์ฒญ ํ๋ ์ด๊ณผ", {
limit: 100,
used: 100,
resetsAt: new Date("2024-01-01T00:00:00Z")
});
ํด๋ผ์ด์ธํธ ์๋ต ์์
๊ธฐ๋ณธ ์๋ต
๋ณต์ฌ
{
"statusCode": 400,
"message": "์ ๋ชฉ์ ํ์์
๋๋ค"
}
payload ํฌํจ ์๋ต
๋ณต์ฌ
{
"statusCode": 400,
"message": "์
๋ ฅ ๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ์์ต๋๋ค",
"payload": {
"errors": [
{
"field": "email",
"message": "์ ํจํ์ง ์์ ์ด๋ฉ์ผ ํ์์
๋๋ค"
},
{
"field": "age",
"message": "๋์ด๋ 0~150 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค"
}
]
}
}