feat(bms): BMS 옵션 사전 검증 추가 (cherry-pick from master)#156
feat(bms): BMS 옵션 사전 검증 추가 (cherry-pick from master)#156Palbahngmiyine merged 5 commits intosolapi:betafrom
Conversation
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>
There was a problem hiding this comment.
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.
| 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; | ||
| }; | ||
| }; |
There was a problem hiding this comment.
BmsConstraintInput 타입 정의에서 실제 스키마(baseBmsSchema)에 존재하는 여러 필드가 누락되어 있습니다.
- 최상위
adult및video필드 subWideItemList아이템 내의imageId필드carousel.list아이템 내의imageId필드
이 타입은 kakaoOption.ts에서 실제 데이터를 validator에 전달할 때 사용되므로, 누락된 필드가 있으면 validator 내에서 해당 필드에 접근할 때 타입 오류가 발생하거나 검증 로직을 작성하기 어려워집니다. 모든 유효한 필드를 포함하도록 타입을 보강해 주세요.
| head.linkPc !== undefined || | ||
| head.linkAndroid !== undefined || | ||
| head.linkIos !== undefined; | ||
| return hasOther && head.linkMobile === undefined |
There was a problem hiding this comment.
linkMobile의 존재 여부를 확인할 때 undefined만 체크하고 있습니다. bmsButton.ts의 bmsAppButtonSchema 필터(61-65라인)와 동일하게, 빈 문자열("")이나 공백만 있는 경우도 "값이 없는 것"으로 간주하도록 trim() !== '' 체크를 추가하는 것이 일관성 및 정확성 측면에서 좋습니다.
| return hasOther && head.linkMobile === undefined | |
| return hasOther && (head.linkMobile === undefined || head.linkMobile.trim() === '') |
| ) { | ||
| return newlineError(type, 'content', contentNL); | ||
| } | ||
|
|
| export const validateImageIdLength = ( | ||
| bms: BmsConstraintInput, | ||
| ): true | string => { | ||
| if (bms.chatBubbleType !== 'IMAGE') return true; |
Summary
master에 머지된 BMS(Brand Message) 옵션 사전 검증 기능을 beta로 sync합니다.
BMS_FREE발송 시 서버에서 접수 후 발생하던 1011류 검증 오류(대표적으로 "쿠폰 설명은 최대 12자 이하로 입력해주세요.")를 Effect Schema 단계에서 즉시 차단하여 개발자 경험을 개선합니다.kakaoOption.ts의Schema.filter에 validator 체인을 fail-fast로 적용bmsChatBubbleTypeSchema를bms/폴더로 분리해 순환 참조 제거새로 사전 차단하는 항목
쿠폰 / 커머스
버튼
텍스트 / 줄바꿈 / 필드
링크 / 캐러셀
변경 파일
신규
수정
비범위 (후속)
Origin
Test plan
🤖 Generated with Claude Code