> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# BadRequestException

> 잘못된 요청 처리를 위한 400 예외

`BadRequestException`은 클라이언트의 요청이 잘못된 경우 사용하는 예외입니다. HTTP 400 상태 코드를 반환하며, 유효성 검증 실패, 잘못된 매개변수, 요청 형식 오류 등에 사용됩니다.

## 기본 사용법

```typescript theme={null}
class BadRequestException extends SoException {
  constructor(message: LocalizedString, payload?: unknown);
}
```

**간단한 예시:**

```typescript theme={null}
@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 });
}
```

## 실용 예제

### 유효성 검증

```typescript theme={null}
@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 });
}
```

### 비즈니스 규칙 검증

```typescript theme={null}
@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);
}
```

### 파일 업로드 검증

```typescript theme={null}
@upload()
async uploadProfileImage(ctx: Context) {
  const { bufferedFiles } = Sonamu.getContext();
  const file = bufferedFiles?.[0]; // 첫 번째 파일 사용

  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
      }
    );
  }

  // 이미지 저장
  const key = `profile-images/${Date.now()}-${file.filename}`;
  return file.saveToDisk("s3", key);
}
```

### 날짜/시간 검증

```typescript theme={null}
@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 });
}
```

### 배열/객체 검증

```typescript theme={null}
@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`을 사용합니다:

```typescript theme={null}
@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 활용 패턴

### 필드별 오류 목록

```typescript theme={null}
throw new BadRequestException("입력 데이터 검증 실패", {
  errors: [
    { field: "email", code: "INVALID_FORMAT" },
    { field: "password", code: "TOO_SHORT", minLength: 8 },
  ],
});
```

### 제한 사항 정보

```typescript theme={null}
throw new BadRequestException("파일 크기 초과", {
  size: file.size,
  limit: 5242880, // 5MB in bytes
  unit: "bytes",
});
```

### 재시도 가능 정보

```typescript theme={null}
throw new BadRequestException("일일 요청 한도 초과", {
  limit: 100,
  used: 100,
  resetsAt: new Date("2024-01-01T00:00:00Z"),
});
```

## 클라이언트 응답 예시

### 기본 응답

```json theme={null}
{
  "statusCode": 400,
  "message": "제목은 필수입니다"
}
```

### payload 포함 응답

```json theme={null}
{
  "statusCode": 400,
  "message": "입력 데이터가 유효하지 않습니다",
  "payload": {
    "errors": [
      {
        "field": "email",
        "message": "유효하지 않은 이메일 형식입니다"
      },
      {
        "field": "age",
        "message": "나이는 0~150 사이여야 합니다"
      }
    ]
  }
}
```

## 관련 문서

* [SoException](/ko/api-reference/exceptions/sonamu-error)
* [커스텀 예외](/ko/api-reference/exceptions/custom-errors)
* [Zod 검증](/ko/api-development/validation)
