Skip to content
Open
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
32 changes: 32 additions & 0 deletions docs/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@
Long id,
```

### 값이 없을 수 있는 응답 필드는 `nullable = true`로 표현한다 (필수)
응답에서 키를 생략하지 않습니다. 값이 없으면 **키는 그대로 실리고 값만 `null`** 입니다.
그래야 스펙(`nullable`)과 실제 응답이 같아지고, 프론트가 "키가 없는 경우"와 "값이 null인 경우"를
따로 처리하지 않아도 됩니다.

```java
// 항상 실리고 값이 없으면 null → 프론트 타입은 logoUrl: string | null
@Schema(description = "로고 이미지 URL",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String logoUrl,
```

- **응답 DTO**: 값이 없을 수 있으면 `REQUIRED` + `nullable = true`. 키가 항상 있으니 `required`에 남습니다.
- **요청 DTO**: 생략해도 되는 입력은 `NOT_REQUIRED`(+ null도 허용하면 `nullable = true`).
요청은 키를 안 보낼 수 있으므로 응답과 규칙이 다릅니다.
- 빈 목록은 `null`이 아니라 `[]`로 줍니다.
- `@JsonInclude(NON_NULL)`을 붙이거나 `spring.jackson.default-property-inclusion`을 건드리지 마세요.
null을 지우면 스펙과 응답이 어긋납니다.

`$ref`로 참조되는 객체 필드(예: `Prefill`)에도 같은 규칙을 씁니다. 다만 OpenAPI 3.0은 `$ref` 옆에
다른 키워드를 둘 수 없어, swagger-core가 `nullable`을 참조 대상 컴포넌트로 밀어 넣습니다(그러면 그
스키마를 쓰는 모든 자리가 nullable이 됩니다). `NullableSchemaConfig`가 이런 필드를
`allOf: [$ref] + nullable`로 바꾸고 컴포넌트에 새어 들어간 `nullable`을 걷어냅니다. 작성자는 평소대로
`nullable = true`만 달면 됩니다.

`ApiResponse` 래퍼의 `data`/`error`/`code`는 래퍼 종류마다 채워지는 칸이 달라
`ResponseWrapperSchemaCustomizer`가 스키마별로 `required`/`nullable`을 붙입니다. 필드에 직접 달지 마세요.

> `OpenApiContractTest`가 nullable 표기와 컴포넌트 오염 여부를 검사합니다.

### 요청 DTO는 검증 어노테이션을 단다
`@NotBlank`, `@Size`, `@NotNull` 등은 자동으로 `required`/`maxLength` 등으로 스펙에 반영됩니다.

Expand Down Expand Up @@ -148,13 +178,15 @@ ApiResponse<TokenResponse> signupGoogle(
- 모든 응답 본문은 `application/json`이어야 하며 `*/*`가 남으면 실패한다
- `SecurityConfig` 인증 대상 API는 `bearerAuth`를 선언하고, 401에는 성공 DTO가 아닌 공통 오류 Schema를 사용해야 한다
- 204 응답에는 `content/schema`가 없어야 한다
- 값이 없을 수 있는 응답 필드는 `nullable`로 노출하고, 그 `nullable`이 공유 컴포넌트로 새지 않아야 한다

---

## 2. 프론트 전달용 — 무엇을 했고 어떻게 최신화하는가

### 백엔드에서 한 작업
- 모든 응답/에러 스키마에 `required`·`nullable`·타입을 명시해 **codegen 타입이 정확**하도록 정리.
값이 없는 필드도 키는 항상 실리고 값만 `null`입니다. 타입은 `field: T | null`로 생성됩니다.
- 공통 응답 래퍼 `ApiResponse<T>` / 에러 `ErrorResponse` 구조를 스펙에 노출.
- 성공: `ApiResponseSampleResponse`, 목록: `ApiResponseListSampleResponse` 형태로 타입 생성됨.
- operationId를 `도메인+동작`으로 안정화 (예: `createSample`, `getSampleById`).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
package chaeso.zip.server.auth.application.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import io.swagger.v3.oas.annotations.media.Schema;

/**
* 구글 인증 진입({@code POST /auth/google}) 응답. 세 분기(로그인/연결 확인 필요/가입 필요) {@code status} 중 한 상태를 보여주고
* 해당 분기에 없는 필드는 응답에서 생략.
* 해당 분기에 없는 필드는 null 로 내려간다.
*/
@Schema(description = "구글 인증 진입 응답")
@JsonInclude(Include.NON_NULL)
public record GoogleAuthResponse(
@Schema(description = "분기 판별값", requiredMode = Schema.RequiredMode.REQUIRED) Status status,
@Schema(description = "액세스 토큰. 로그인 분기에만 존재", nullable = true) String accessToken,
@Schema(description = "리프레시 토큰. 로그인 분기에만 존재", nullable = true) String refreshToken,
@Schema(description = "액세스 토큰 만료(초)", example = "1800", nullable = true) Long accessTokenExpiresIn,
@Schema(description = "리프레시 토큰 만료(초, 고정값X)", example = "1209600", nullable = true) Long refreshTokenExpiresIn,
@Schema(description = "액세스 토큰. 로그인 분기에만 존재",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) String accessToken,
@Schema(description = "리프레시 토큰. 로그인 분기에만 존재",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) String refreshToken,
@Schema(description = "액세스 토큰 만료(초)", example = "1800",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) Long accessTokenExpiresIn,
@Schema(description = "리프레시 토큰 만료(초, 고정값X)", example = "1209600",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) Long refreshTokenExpiresIn,

@Schema(description = "같은 이메일의 로컬 계정이 있어 연결 확인이 필요함. 아직 연결되지 않았다", example = "true", nullable = true)
@Schema(description = "같은 이메일의 로컬 계정이 있어 연결 확인이 필요함. 아직 연결되지 않았다", example = "true",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Boolean linkRequired,
@Schema(description = "연결 대상 계정의 이메일. 어느 계정에 붙는지 사용자가 보고 판단하도록 내려준다", nullable = true)
@Schema(description = "연결 대상 계정의 이메일. 어느 계정에 붙는지 사용자가 보고 판단하도록 내려준다",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String email,

@Schema(description = "가입 이력이 없어 추가정보 입력이 필요함", example = "true", nullable = true)
@Schema(description = "가입 이력이 없어 추가정보 입력이 필요함", example = "true",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Boolean signupRequired,
@Schema(description = "최종가입에 되돌려줄 일회성 티켓", nullable = true) String signupToken,
@Schema(description = "최종가입 폼 프리필 값", requiredMode = Schema.RequiredMode.NOT_REQUIRED) Prefill prefill) {
@Schema(description = "최종가입에 되돌려줄 일회성 티켓",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) String signupToken,
@Schema(description = "최종가입 폼 프리필 값",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) Prefill prefill) {

public enum Status {
LOGIN, LINK_REQUIRED, SIGNUP_REQUIRED
Expand All @@ -35,7 +41,8 @@ public enum Status {
public record Prefill(
@Schema(description = "구글이 인증한 이메일", example = "user@chaeso.zip",
requiredMode = Schema.RequiredMode.REQUIRED) String email,
@Schema(description = "구글 계정 이름. 계정에 이름이 없으면 null", example = "홍길동", nullable = true)
@Schema(description = "구글 계정 이름. 계정에 이름이 없으면 null", example = "홍길동",
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String suggestedNickname) {
}

Expand Down
22 changes: 20 additions & 2 deletions src/main/java/chaeso/zip/server/auth/presentation/AuthApiDocs.java
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,12 @@ ApiResponse<LoginMethodsResponse> loginMethods(
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"accessTokenExpiresIn": 1800,
"refreshTokenExpiresIn": 1209600
"refreshTokenExpiresIn": 1209600,
"linkRequired": null,
"email": null,
"signupRequired": null,
"signupToken": null,
"prefill": null
}
}
""";
Expand All @@ -316,8 +321,15 @@ ApiResponse<LoginMethodsResponse> loginMethods(
"success": true,
"data": {
"status": "LINK_REQUIRED",
"accessToken": null,
"refreshToken": null,
"accessTokenExpiresIn": null,
"refreshTokenExpiresIn": null,
"linkRequired": true,
"email": "user@chaeso.zip"
"email": "user@chaeso.zip",
"signupRequired": null,
"signupToken": null,
"prefill": null
}
}
""";
Expand All @@ -327,6 +339,12 @@ ApiResponse<LoginMethodsResponse> loginMethods(
"success": true,
"data": {
"status": "SIGNUP_REQUIRED",
"accessToken": null,
"refreshToken": null,
"accessTokenExpiresIn": null,
"refreshTokenExpiresIn": null,
"linkRequired": null,
"email": null,
"signupRequired": true,
"signupToken": "0pxJ3n1Q...",
"prefill": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
public record AudienceMetricResponse(
@Schema(description = "지표명", example = "MAU", requiredMode = Schema.RequiredMode.REQUIRED)
String metricName,
@Schema(description = "지표 수치값", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "지표 수치값", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
BigDecimal valueNumeric,
@Schema(description = "지표 텍스트값", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "지표 텍스트값", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String valueText,
@Schema(description = "단위", example = "명", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단위", example = "명", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String unit,
@Schema(description = "집계 기간", example = "월", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "집계 기간", example = "월", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String period) {

public static AudienceMetricResponse from(ChannelAudienceMetric metric) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,38 @@ public record ChannelDetailResponse(
UUID id,
@Schema(description = "채널명", example = "11번가 광고", requiredMode = Schema.RequiredMode.REQUIRED)
String name,
@Schema(description = "로고 이미지 URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "로고 이미지 URL", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String logoUrl,
@Schema(description = "채널 핵심 요약", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "채널 핵심 요약", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String description,
@Schema(description = "대표 업종 코드값", example = "SHOPPING_COMMERCE",
requiredMode = Schema.RequiredMode.REQUIRED)
Category primaryCategory,
@Schema(description = "매체 유형", example = "DISPLAY", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "매체 유형", example = "DISPLAY", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String mediaType,
@Schema(description = "적합 업종 코드값 목록", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "적합 업종 코드값 목록", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<Category> suitableCategories,
@Schema(description = "연령대 코드값 목록", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "연령대 코드값 목록", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<AgeBand> ageBandCodes,
@Schema(description = "대표 연령대", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "대표 연령대", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String primaryAgeBand,
@Schema(description = "대표 성별 코드값", example = "FEMALE", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "대표 성별 코드값", example = "FEMALE", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Gender primaryGender,
@Schema(description = "오디언스 요약", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "오디언스 요약", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String audienceSummary,
@Schema(description = "오디언스 특성", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "오디언스 특성", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String audienceTraits,
@Schema(description = "채널 강점 목록", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "채널 강점 목록", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<String> advantages,
@Schema(description = "최소 예산(원)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "최소 예산(원)", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Integer minBudgetWon,
@Schema(description = "최대 예산(원)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "최대 예산(원)", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Integer maxBudgetWon,
@Schema(description = "집행 방식 코드값", example = "SELF", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "집행 방식 코드값", example = "SELF", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
ExecutionType executionType,
@Schema(description = "지원 광고 형식 목록", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "지원 광고 형식 목록", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<String> adFormats,
@Schema(description = "지원 타게팅 방식 목록", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "지원 타게팅 방식 목록", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<String> targetingMethods,
@Schema(description = "채널 광고 상품 목록(상품 없는 채널은 빈 배열)",
requiredMode = Schema.RequiredMode.REQUIRED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public record ChannelListItemResponse(
@Schema(description = "채널명", example = "11번가 광고", requiredMode = Schema.RequiredMode.REQUIRED)
String name,
@Schema(description = "로고 이미지 URL",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String logoUrl,
@Schema(description = "채널 핵심 요약", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "채널 핵심 요약", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String description,
@Schema(description = "대표 업종 코드값", example = "SHOPPING_COMMERCE",
requiredMode = Schema.RequiredMode.REQUIRED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
public record PricingResponse(
@Schema(description = "과금 모델 코드값", example = "CPM", requiredMode = Schema.RequiredMode.REQUIRED)
PricingModel pricingModel,
@Schema(description = "단가 값", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 값", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
BigDecimal value,
@Schema(description = "단가 상한값(구간형 단가)", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 상한값(구간형 단가)", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
BigDecimal valueMax,
@Schema(description = "단가 적용 단위 기간", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 적용 단위 기간", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String unitPeriod,
@Schema(description = "단가 적용 단위 일수", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 적용 단위 일수", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
BigDecimal unitDays,
@Schema(description = "단가 적용 세그먼트", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 적용 세그먼트", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String segment,
@Schema(description = "가격 유형 코드값", example = "LIST", requiredMode = Schema.RequiredMode.REQUIRED)
PriceType priceType,
Expand All @@ -29,7 +29,7 @@ public record PricingResponse(
Vat vat,
@Schema(description = "통화 코드값", example = "KRW", requiredMode = Schema.RequiredMode.REQUIRED)
CurrencyType currency,
@Schema(description = "단가 유효 기간", requiredMode = Schema.RequiredMode.NOT_REQUIRED, nullable = true)
@Schema(description = "단가 유효 기간", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String validPeriod) {

public static PricingResponse from(ChannelPricing pricing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ public record ProductResponse(
@Schema(description = "상품 식별자", example = "550e8400-e29b-41d4-a716-446655440000",
requiredMode = Schema.RequiredMode.REQUIRED)
UUID id,
@Schema(description = "상품명", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "상품명", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String productName,
@Schema(description = "인벤토리 유형", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "인벤토리 유형", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String inventoryType,
@Schema(description = "지원 광고 목표 코드값 목록", example = "[\"AWARENESS\", \"TRAFFIC\"]",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<CampaignObjective> supportedObjectives,
Comment on lines 18 to 20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n -C 4 '\bsupportedObjectives\b|getSupportedObjectives' src/main/java src/test

Repository: YAPP-Github/chaeso-zip-server

Length of output: 7531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ProductResponse.java ---'
sed -n '1,80p' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java

printf '%s\n' '--- ChannelProduct.java relevant fields and methods ---'
sed -n '1,120p' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- Migrations / SQL for supported_objectives ---'
rg -n -C 3 'supported_objectives|ChannelProduct|channel_products|create.*Product|ALTER TABLE' src/main/resources src/main/java src/test/resources src/test/java || true

printf '%s\n' '--- Tests mentioning response null support / supportedObjectives ---'
rg -n -C 3 'supportedObjectives|ProductResponse|null|지원 광고|광고 목표|requiredMode|nullable' src/test src/main/resources src/main/java/chaeso/zip/server || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- targeted ProductResponse.java ---'
node -e "const fs='fs'; const p='src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java'; const s=fs.readFileSync(p,'utf8'); const m=s.match(/@Schema\\(description = \"지원 광고 목표 코드값 목록\"[\\s\\S]*?List<ChallengeObjective> supportedObjectives,|`@Schema`\\(description = \"지원 광고 목표 코드값 목록\"[\\s\\S]*?List<.*CampaignObjective> supportedObjectives,/g);"

rg -n -C 3 '`@Schema`\(description = "지원 광고 목표 코드값 목록"|List<.*CampaignObjective> supportedObjectives|product.getSupportedObjectives\(\)' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java src/main/java/chaeso/zip/server/channel domain entity ChannelProduct.java || true

printf '%s\n' '--- ChannelProduct exact relevant sections ---'
rg -n -C 5 'supportedObjectives|getSupportedObjectives|ChannelProduct\\(' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- data/resource references only ---'
fd -a '(\.sql|\.xml|schema|migration|migration\.)' src/main/resources src/test/resources src/main/java src/test/java 2>/dev/null | sed 's#^\./##' | head -200
rg -n -C 3 'supported_objectives|ChannelProduct|channel_products|CREATE TABLE.*Product|ALTER TABLE' src/main/resources src/test/resources src/main/java src/test/java || true

printf '%s\n' '--- focused tests around ProductResponse null behavior ---'
rg -n -C 3 'ProductResponse|supportedObjectives|jsonPath.*products.*supportedObjectives|nullValue|isEmpty\\(\\)' src/test/java src/main/java || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 1124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ProductResponse.java exact lines ---'
sed -n '1,55p' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java

printf '%s\n' '--- ChannelProduct.java entity section ---'
sed -n '1,80p' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- supported_objectives references ---'
rg -n -C 4 'supported_objectives|supportedObjectives|supportedObjectives' src/main/resources src/test/resources src/main/java src/test/java || true

printf '%s\n' '--- ProductResponse factory mapping lines ---'
rg -n -C 3 'ProductResponse\\s*\\(|product\\.(getSupportedObjectives|getId|getProductName|getInventoryType|getMinBudgetWon|getMaxBudgetWon|get|getExpected)' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 10008


빈 목록 규약을 supportedObjectives에도 적용하세요.

supported_objectivesnullable = true로 스웨거 스키마에 노출하면 null 응답을 허용하는 타입이 됩니다. ProductResponse.from에서는 product.getSupportedObjectives()를 그대로 전달하므로, 도메인에서 빈 목록 보장 없으면 null이[]가 아니라 null JSON으로 나옵니다. 응답 작성 시 List.of()로 정규화하고 스키마도 nullable = true를 제거하면 계약과 타입이 일치합니다.

수정 예시
     `@Schema`(description = "지원 광고 목표 코드값 목록", example = "[\"AWARENESS\", \"TRAFFIC\"]",
-        requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
+        requiredMode = Schema.RequiredMode.REQUIRED)
     List<CampaignObjective> supportedObjectives,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java`
around lines 18 - 20, Update ProductResponse.from to normalize
product.getSupportedObjectives() to an empty List.of() when null, and remove
nullable = true from the supportedObjectives `@Schema` declaration so the
documented response type matches the normalized non-null list contract.

@Schema(description = "최소 예산(원)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "최소 예산(원)", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Integer minBudgetWon,
@Schema(description = "최대 예산(원)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "최대 예산(원)", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Integer maxBudgetWon,
@Schema(description = "예상 노출수", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "예상 노출수", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Long expectedImpressions,
@Schema(description = "예상 클릭수(예상 노출수 × CTR)",
example = "5250", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
example = "5250", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
Long expectedClicks,
@Schema(description = "예상 집행 기간", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Schema(description = "예상 집행 기간", requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
String expectedPeriod,
@Schema(description = "상품 단가 목록", requiredMode = Schema.RequiredMode.REQUIRED)
List<PricingResponse> pricing) {
Expand Down
Loading
Loading