Skip to content

feat(bms): BMS 옵션 사전 검증 추가 (cherry-pick from master)#156

Merged
Palbahngmiyine merged 5 commits intosolapi:betafrom
Palbahngmiyine:sync/bms-prevalidation-to-beta
Apr 20, 2026
Merged

feat(bms): BMS 옵션 사전 검증 추가 (cherry-pick from master)#156
Palbahngmiyine merged 5 commits intosolapi:betafrom
Palbahngmiyine:sync/bms-prevalidation-to-beta

Conversation

@Palbahngmiyine
Copy link
Copy Markdown
Member

Summary

master에 머지된 BMS(Brand Message) 옵션 사전 검증 기능을 beta로 sync합니다. BMS_FREE 발송 시 서버에서 접수 후 발생하던 1011류 검증 오류(대표적으로 "쿠폰 설명은 최대 12자 이하로 입력해주세요.")를 Effect Schema 단계에서 즉시 차단하여 개발자 경험을 개선합니다.

  • 에러 문구는 서버 응답과 동일하게 맞춰 클라/서버 위치 혼동 없음
  • kakaoOption.tsSchema.filter에 validator 체인을 fail-fast로 적용
  • bmsChatBubbleTypeSchemabms/ 폴더로 분리해 순환 참조 제거

새로 사전 차단하는 항목

쿠폰 / 커머스

  • 쿠폰 설명 길이 (비-WIDE 12자, WIDE 계열 18자) — top-level 및 carousel per-item coupon 양쪽
  • 쿠폰 제목 5가지 프리셋 (무료/UP 쿠폰 prefix 공백 금지)
  • 커머스 가격 범위: `regularPrice`/`discountPrice`/`discountFixed` 0–99,999,999, `discountRate` 0–100

버튼

  • 개수 제한 (TEXT/IMAGE 쿠폰 有 4/無 5, WIDE·CAROUSEL·COMMERCE 2, PREMIUM_VIDEO 1)
  • 버튼명 길이 (TEXT/IMAGE 14자, 그 외 8자)
  • 허용 linkType (COMMERCE/CAROUSEL_* → WL/AL 전용)
  • `button.name`에 `#{변수}` 금지

텍스트 / 줄바꿈 / 필드

  • 길이: header 20, content(TEXT/IMAGE 1300, WIDE/PREMIUM_VIDEO 76), additionalContent 34, mainWideItem.title 25, subWideItem.title 30, carousel.head.content 50, list.content 180
  • 줄바꿈 개수: `\r\n|\r|\n` 모두 대응, header 0줄, WIDE/PREMIUM_VIDEO content 1줄 등
  • chatBubbleType별 허용 필드 외 reject
  • IMAGE 타입 `imageId` 공백 제거 후 32자

링크 / 캐러셀

  • URL 형식 (`http(s)://` + `#{변수}` 포함 케이스도 허용, malformed placeholder는 whole-string 재검사로 거부)
  • `carousel.head`에 linkPc/Android/Ios 있으면 `linkMobile` 필수
  • `carousel.tail` 링크에 `#{변수}` 금지
  • 캐러셀 리스트 개수 (head 有 1–5, 無 2–6)
  • per-item coupon 설명·링크도 함께 검증
  • 빈 문자열(`""`)/공백/제어문자/host-less URL 거부

변경 파일

신규

  • `src/models/base/kakao/bms/bmsChatBubbleType.ts`
  • `src/models/base/kakao/bms/bmsConstraints.ts` (상수 테이블 + 검증 함수 11종)
  • `test/models/base/kakao/bms/bmsConstraints.test.ts`

수정

  • `src/models/base/kakao/bms/{bmsCarousel,bmsCommerce,bmsCoupon,index,bmsButton}.ts`
  • `src/models/base/kakao/kakaoOption.ts` — validator 체인 통합
  • `src/models/index.ts` — barrel 경로 정리
  • 관련 테스트 보강

비범위 (후속)

  • `content` 필수 (TEXT/IMAGE/WIDE): 메시지 레벨 `text`와 상호작용으로 `kakaoOption` 단독 검증 불가
  • BMS_TEMPLATE `templateId` 필수: 메시지 type(BMS_FREE vs BMS) 판별 필요
  • 이미지 파일 조회: 서버 API 의존

Origin

Test plan

  • `pnpm lint` — Biome 통과
  • `pnpm test` — 429 tests 모두 통과
  • `pnpm build` — 타입체크 + tsup 빌드 성공
  • 로컬 `BMS_FREE` 발송 시나리오로 재확인 (13자 쿠폰 설명 즉시 실패 등)

🤖 Generated with Claude Code

Palbahngmiyine and others added 5 commits April 20, 2026 12:53
BMS_FREE 메시지 발송 시 서버에서 발생하는 1011류 검증 오류(예: "쿠폰
설명은 최대 12자 이하로 입력해주세요.")를 Effect Schema 단계에서 즉시
차단하도록 `kakaoOption.bms` 검증을 전면 보강했다. 서버 왕복 없이
개발자가 문제 원인을 즉시 확인할 수 있다.

추가된 사전 검증:
- 쿠폰 설명 길이 (WIDE 계열 18자, 그 외 12자) — top-level 및 carousel
  per-item coupon 양쪽 모두
- 쿠폰 제목 5가지 프리셋 (무료/UP 쿠폰 prefix 공백 금지)
- 버튼 개수 / 이름 길이 / 허용 linkType (chatBubbleType별)
- chatBubbleType별 허용 필드 외 reject
- 텍스트 길이 및 줄바꿈 개수 (CRLF/CR 포함)
- 링크 URL 형식 (`http(s)://` + `#{변수}` 케이스 허용)
- 커머스 가격 범위 (0–99,999,999, discountRate 0–100)
- 캐러셀 리스트 개수 (head 有 1–5 / 無 2–6)
- carousel.head `linkMobile` 조건부 필수
- button.name / carousel.tail 링크 변수 금지
- IMAGE `imageId` 공백 제거 후 32자
- per-item carousel coupon의 설명 길이·링크 검증

아키텍처:
- `bmsChatBubbleTypeSchema`를 `bms/` 폴더로 분리해 순환 참조 제거
- `bmsConstraints.ts` 신규 — 상수 테이블 + 11종 순수 검증 함수
- `kakaoOption.ts`의 `Schema.filter`에 validator 체인을 fail-fast로 적용
- 에러 문구는 서버 응답과 동일하게 유지해 개발자 혼동 방지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ultrareview 후 추가 심층 리뷰(Codex + pr-review-toolkit 5종)에서 발견된
이슈 일괄 반영.

Critical 수정:
- checkLinkPair/checkImageLink의 truthy gate로 인한 빈 문자열 silent pass
  제거. required URL 필드에 ""를 보내면 SDK가 통과시키던 버그 차단.
  `!= null` / `!== undefined`로 "present-but-empty"를 검증 대상에 포함.
- isValidLink regex 강화: HTTP_URL_PATTERN에 whole-string anchor(`$`) 추가,
  whitespace/제어문자 거부, 변수 치환형(`#{var}`)은 placeholder 치환 후
  재검사해 `https:///#{var}` 같은 host-less malformed URL 차단.

개선:
- kakaoOption.ts의 `as unknown as BmsConstraintInput` 이중 캐스트 제거
  (구조적 호환으로 직접 대입 가능 — 향후 drift 시 컴파일러가 감지).
- validator 체인 재정렬: 싼 구조 검사(validateCarouselListCount) 우선화.
- BMS_ACCEPTABLE_FIELDS에 `satisfies`로 키/값 타입 제약 (오타 방지).
- BMS_CAROUSEL_LIST_RANGE `as const`.
- BMS_ALLOWED_LINK_TYPES / validateAcceptableFields / BmsConstraintInput
  JSDoc 정확화 (드리프트 위험 명시).

테스트 보강:
- 빈 문자열/공백 포함/malformed placeholder URL 거부
- COMMERCE/CAROUSEL_* min-count(1) 미달 거부
- subWideItem linkMobile 빈 문자열
- carousel.tail linkAndroid/linkIos 변수 금지
- fail-fast 순서 고정 (acceptable → carousel count → …)
- WIDE_ITEM_LIST header 테스트에서 mainWideItem.title 누락 수정(fragile)

Self-explanatory 코드의 과한 주석 제거 (bmsChatBubbleType, bmsCoupon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iteration 2 재리뷰에서 발견된 silent-pass 경로 추가 차단.

수정:
- bmsCarouselHeadSchema: linkPc/Android/Ios가 빈 문자열이면 "제공됨"으로
  취급되어 linkMobile 필수 규칙을 우회하던 문제. `!== undefined` 기반
  체크로 변경하여 present-but-empty도 검증 대상에 포함.
- bmsAppButtonSchema: 세 링크(linkMobile/linkAndroid/linkIos) 전부 빈
  문자열이면 truthy gate에서 pass되던 문제. `trim().length > 0`로 "실제
  값이 있는지" 엄격 검증.

공식 API 문서(카카오 버튼 규칙)와 SDK 버튼 검증 정합성 재점검 결과 BMS
버튼 타입별(WL/AL/AC/BK/MD/BC/BT/BF) 필수 필드·허용 linkType·개수 제약·
이름 길이 모두 부합함을 확인. DS는 알림톡 전용으로 BMS linkType 목록에
의도적으로 제외 유지.

회귀 테스트 2건 추가: carousel.head linkAndroid 빈 문자열 / AL 버튼 전부
빈 문자열.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex iteration-2 리뷰에서 지적된 테스트 커버리지 보강.

추가된 회귀 테스트:
- CAROUSEL_COMMERCE min-count(버튼 1개 미만) 거부
- carousel.head.header 21자 초과 길이 제한
- button.linkPc 빈 문자열 silent-pass 방지
- coupon.linkMobile / mainWideItem.linkMobile / carousel.head.linkMobile
  빈 문자열 케이스
- carousel list item imageLink 빈 문자열
- imageLink의 malformed placeholder(`https:///#{var}`) 거부

로직 변경 없음 — 기존 checkLinkPair/checkImageLink 가드가 모든 호출 경로에서
동일하게 동작함을 증명하는 regression anchor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex iteration-3 리뷰에서 지적된 이중 cast `as unknown as Record<string,
unknown>`을 제거하고 `getField` 헬퍼로 index access cast를 한 곳에 격리.

해당 validator는 BmsConstraintInput에 선언되지 않은 runtime key까지 순회
검사해야 하므로 어떤 형태로든 dynamic access가 불가피하나, cast 지점을
전용 헬퍼로 국한시키고 의도를 주석으로 명시하여 재사용성·가독성을 동시에
확보.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Palbahngmiyine Palbahngmiyine merged commit 0cb9a9c into solapi:beta Apr 20, 2026
6 checks passed
@Palbahngmiyine Palbahngmiyine deleted the sync/bms-prevalidation-to-beta branch April 20, 2026 03:55
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements extensive validation constraints for Kakao BMS options, covering text lengths, button counts, link formats, and price ranges. Key changes include the addition of a centralized bmsConstraints.ts validator and updates to existing schemas to enforce these rules. Review feedback highlights the need to include missing fields in the BmsConstraintInput type, ensure consistent handling of empty strings in link validation, and expand newline and image ID length checks to all applicable fields.

Comment on lines +118 to +150
export type BmsConstraintInput = {
readonly chatBubbleType: BmsChatBubbleType;
readonly content?: string;
readonly header?: string;
readonly additionalContent?: string;
readonly imageId?: string;
readonly imageLink?: string;
readonly coupon?: {readonly description: string} & LinkFields;
readonly buttons?: ReadonlyArray<
{readonly name?: string; readonly linkType: string} & LinkFields
>;
readonly mainWideItem?: {readonly title?: string} & LinkFields;
readonly subWideItemList?: ReadonlyArray<
{readonly title: string} & LinkFields
>;
readonly carousel?: {
readonly list?: ReadonlyArray<{
readonly header?: string;
readonly content?: string;
readonly additionalContent?: string;
readonly imageLink?: string;
readonly buttons?: ReadonlyArray<
{readonly name?: string; readonly linkType: string} & LinkFields
>;
readonly coupon?: {readonly description: string} & LinkFields;
}>;
readonly head?: {
readonly header?: string;
readonly content?: string;
} & LinkFields;
readonly tail?: LinkFields;
};
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

BmsConstraintInput 타입 정의에서 실제 스키마(baseBmsSchema)에 존재하는 여러 필드가 누락되어 있습니다.

  1. 최상위 adultvideo 필드
  2. subWideItemList 아이템 내의 imageId 필드
  3. carousel.list 아이템 내의 imageId 필드

이 타입은 kakaoOption.ts에서 실제 데이터를 validator에 전달할 때 사용되므로, 누락된 필드가 있으면 validator 내에서 해당 필드에 접근할 때 타입 오류가 발생하거나 검증 로직을 작성하기 어려워집니다. 모든 유효한 필드를 포함하도록 타입을 보강해 주세요.

head.linkPc !== undefined ||
head.linkAndroid !== undefined ||
head.linkIos !== undefined;
return hasOther && head.linkMobile === undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

linkMobile의 존재 여부를 확인할 때 undefined만 체크하고 있습니다. bmsButton.tsbmsAppButtonSchema 필터(61-65라인)와 동일하게, 빈 문자열("")이나 공백만 있는 경우도 "값이 없는 것"으로 간주하도록 trim() !== '' 체크를 추가하는 것이 일관성 및 정확성 측면에서 좋습니다.

Suggested change
return hasOther && head.linkMobile === undefined
return hasOther && (head.linkMobile === undefined || head.linkMobile.trim() === '')

) {
return newlineError(type, 'content', contentNL);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

validateNewlineLimits 함수에서 최상위 additionalContent 필드에 대한 줄바꿈 개수 검증이 누락되었습니다. CAROUSEL_COMMERCE 리스트 아이템의 additionalContent에 대해 1개로 제한하고 있는 것(500라인)과 일관성을 맞추기 위해 최상위 필드에 대해서도 검증을 추가하는 것이 좋습니다.

export const validateImageIdLength = (
bms: BmsConstraintInput,
): true | string => {
if (bms.chatBubbleType !== 'IMAGE') return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

validateImageIdLength 함수가 IMAGE 타입인 경우에만 동작하도록 제한되어 있습니다. 하지만 WIDE, COMMERCE, CAROUSEL_*imageId를 사용하는 모든 타입에서 동일한 길이 제한(32자)이 적용될 가능성이 높습니다. 특정 타입에 국한하지 않고 모든 imageId 필드(리스트 아이템 포함)에 대해 길이를 검증하도록 확장하는 것을 권장합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant