refactor(common): 남은 인라인 예외 메시지를 카탈로그로 이관 + 회귀 가드 - #342
Conversation
#334·#340·#341에 이어 남은 인라인 문자열 예외를 전부 정리한다. `new XxxException('리터럴')` 형태가 31곳 남아 있었고, 상수 객체로만 존재하던 STORAGE_ERRORS·RECENT_VIEW_ERRORS도 카탈로그 밖이라 errorCode가 붙지 않았다. - 인라인 throw 31건 → `domainError(code)` 치환. 카탈로그 코드 18개 신설 (INVALID_ID, INVALID_DATE_VALUE, REQUIRED_TEXT_EMPTY, OFFSET_NEGATIVE, DEV_ENDPOINT_DISABLED, NAME_REQUIRED, NAME_CANNOT_BE_EMPTY, NO_FIELDS_TO_UPDATE, NICKNAME_TAKEN, NICKNAME_INVALID_CHARACTERS, INVALID_BIRTH_DATE, BIRTH_DATE_TOO_OLD, BIRTH_DATE_IN_FUTURE, SEARCH_HISTORY_NOT_FOUND, CANNOT_LIKE_OWN_REVIEW, NOT_OWNED_UPLOAD_URL, INVALID_CONTENT_LENGTH, S3_PRESIGN_FAILED). - STORAGE_ERRORS는 허용 타입·최대 용량이 런타임에 붙는 2건(INVALID_CONTENT_TYPE, FILE_TOO_LARGE)만 남기고 나머지는 카탈로그로. RECENT_VIEW_ERRORS는 제거. - user-profile.service의 업로드 URL 소유 검증을 s3Service.assertOwnedUploadUrl로 일원화 — 같은 판정이 두 곳에 흩어져 있었다. - 영어로 남아 있던 동적 메시지 2건을 한국어로. 회귀 방어: error-catalog.spec에 "인라인 예외 메시지 금지" 가드를 추가했다. `new XxxException('...')` 정규식으로 src 전체를 훑고, `${}` 조립이 실제로 필요한 파일만 허용 목록에 둔다. 허용 목록이 비대해지지 않도록 "목록의 각 파일이 정말로 `${}`를 쓰는지"까지 같은 스펙에서 검사한다. 이 가드가 수동 grep이 놓친 여러 줄 throw 2건을 실제로 잡아냈다.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🩺 NestJS Doctor — 89/100 (Good)진단 385건 (error 0).
architecture / security 상위 항목
|
🧹 knip — dead-code 리포트전체 리포트
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bce97a9aef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /** `new XxxException('...')` — 작은따옴표 리터럴 메시지 */ | ||
| const INLINE_MESSAGE = /new [A-Za-z]+Exception\(\s*'/; |
There was a problem hiding this comment.
Detect all inline exception message literals
Expand this matcher beyond single-quoted literals. Template-literal exceptions already exist outside the declared allowlist—for example date-parser.ts:26 and oidc-provider.type.ts:20—and double-quoted literals would likewise evade this regex, so the new regression test passes even though production exceptions can still lack an errorCode. Match all string/template literal forms, preferably through the TypeScript AST, and then apply the dynamic-message allowlist explicitly.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
유효. 정규식이 작은따옴표만 보고 있었고, 지목한 두 자리(date-parser.ts:26, oidc-provider.type.ts:20)가 실제로 그 구멍으로 빠져나가고 있었다.
탐지기를 TypeScript AST로 교체했다. 판정 기준은 예외 생성자의 첫 인자가 문자열/템플릿 리터럴인가 — StringLiteral · NoSubstitutionTemplateLiteral · TemplateExpression 전부 잡는다. 반증 표를 it.each 8행으로 고정했다(작은따옴표 · 큰따옴표 · 템플릿 2형 · 여러 줄 인자 → 잡힘 / domainError · 빌더 호출 · 상수 참조 → 안 잡힘). 실제 프로덕션 파일에 템플릿 리터럴 throw를 심어 파일:줄까지 잡히는 것도 확인했다.
AST로 다시 훑으니 9곳이 나왔다. 허용 목록으로 덮지 않고 전부 닫았다.
| 자리 | 처리 |
|---|---|
date-parser.ts ${field} is required. |
toDateRequired(raw, code: ErrorCode)로 시그니처 변경. CLOSURE_DATE_REQUIRED · CAPACITY_DATE_REQUIRED 신설 |
oidc-provider.type.ts Unsupported OIDC provider: ${raw} |
UNSUPPORTED_OIDC_PROVIDER. 입력값 되비추기도 같이 제거 |
user-base.service.ts ×3 (닉네임 길이 · 전화번호 형식 · limit 상한) |
user-error-messages.ts 빌더로. 전부 영문이었어서 한국어화 |
text-cleaner.ts ×2 (최대 길이) |
같은 모듈의 maxLengthMessage() 빌더로 |
s3.service.ts ×2 (허용 타입 · 최대 용량) |
storage.constants.ts의 빌더 2개로 |
결과적으로 허용 목록이 비었다. 규칙이 "throw 자리에서 메시지 문자열을 직접 쓰지 않는다"로 절대화됐고, 런타임 값이 섞이는 문구는 상수 모듈의 이름 붙은 빌더를 거친다 — 그 자리는 인자가 CallExpression이라 탐지기를 통과하고, 대신 상수와 같은 파일에 모여 따로 검토된다.
닉네임 길이·전화번호 형식·limit 상한을 카탈로그 고정 문구로 박지 않은 이유: 숫자가 user.constants의 상수라 문구에 박으면 상수를 바꿨을 때 메시지만 거짓말이 된다.
yarn validate 그린 — 274 suites / 2,828 tests.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Coverage report
Test suite run success2828 tests passing in 274 suites. Report generated by 🧪jest coverage report action from 1f2c22c |
Codex 리뷰 반영. 정규식이 작은따옴표만 보고 있어 큰따옴표·템플릿 리터럴이 그대로 빠져나갔다 — 지적대로 실제로 새던 자리가 있었다. 탐지기를 TypeScript AST로 바꿨다. 판정 기준은 예외 생성자의 첫 인자가 문자열/템플릿 리터럴인가이고, StringLiteral·NoSubstitutionTemplateLiteral· TemplateExpression을 전부 잡는다. 반증 표를 it.each 8행으로 고정했다. AST로 다시 훑어 나온 9곳을 허용 목록으로 덮지 않고 전부 닫았다. - date-parser: `toDateRequired(raw, code: ErrorCode)`로 시그니처 변경. CLOSURE_DATE_REQUIRED·CAPACITY_DATE_REQUIRED 신설. - oidc-provider.type: UNSUPPORTED_OIDC_PROVIDER. 입력값을 문구로 되비추던 것도 제거. - user-base.service ×3(닉네임 길이·전화번호 형식·limit 상한): user-error-messages 빌더로. 셋 다 영문이었어서 한국어화. - text-cleaner ×2, s3.service ×2: 같은/인접 상수 모듈의 이름 붙은 빌더로. 허용 목록이 비었다. 규칙이 "throw 자리에서 메시지 문자열을 직접 쓰지 않는다"로 절대화됐고, 런타임 값이 섞이는 문구는 상수 모듈의 빌더를 거친다. 닉네임 길이 등을 카탈로그 고정 문구로 박지 않은 이유는 숫자가 user.constants의 상수여서다 — 문구에 박으면 상수를 바꿨을 때 메시지만 거짓말이 된다.
배경
#334에서 에러 코드 카탈로그를 도입하고 #340(auth·conversation) · #341(seller·admin)로 이관을 이어왔다. 남은 건 도메인 상수 파일을 거치지 않고 코드에 직접 박혀 있던 문자열이다. 이 자리들은
extensions.errorCode가 아예 안 실린다.변경
new XxxException(<리터럴>)38곳 →domainError(code)또는 이름 붙은 빌더. 카탈로그 코드 21개 신설.STORAGE_ERRORS는 허용 타입 목록·최대 용량이 런타임에 붙는 2건(INVALID_CONTENT_TYPE·FILE_TOO_LARGE)만 남기고 카탈로그로 이관.RECENT_VIEW_ERRORS는 제거.user-profile.service의 업로드 URL 소유 검증을s3Service.assertOwnedUploadUrl로 일원화 — 같은 판정이 두 곳에 흩어져 있었다.신설 코드 21개:
회귀 가드
error-catalog.spec.ts에 "인라인 예외 메시지 금지" 검사를 추가했다.src전체를 훑어new XxxException('...')형태를 잡고,${}조립이 실제로 필요한 파일만 허용 목록에 둔다.가드 자체가 썩지 않도록 허용 목록의 각 파일이 정말로
${}를 쓰는지까지 같은 스펙에서 검사한다 — 목록에 넣고 나중에 정적 문구로 바뀌면 그 파일이 통째로 무검사 구역이 된다.이 가드는 만든 직후 실제로 수동 grep이 놓친 여러 줄
throw2건을 잡아냈다. "현재 코드에서 통과"가 아니라 막아야 할 걸 막는지 확인된 상태다.리뷰 대응 — 탐지기 AST 전환 + 전수 스윕
첫 버전의 정규식이 작은따옴표만 보고 있어 큰따옴표·템플릿 리터럴이 그대로 빠져나갔다. 탐지기를 TypeScript AST로 교체하고, 다시 훑어 나온 자리를 전부 닫았다.
src전체, spec·test 제외)date-parser.ts`${field} is required.`toDateRequired(raw, code: ErrorCode)로 시그니처 변경 + 코드 2개 신설oidc-provider.type.ts`Unsupported OIDC provider: ${raw}`UNSUPPORTED_OIDC_PROVIDER. 입력값 되비추기도 제거user-base.service.ts×3user-error-messages.ts빌더로. 전부 영문이었어서 한국어화text-cleaner.ts×2maxLengthMessage()s3.service.ts×2storage.constants.ts빌더 2개허용 목록이 비었다. 규칙이 "throw 자리에서 메시지 문자열을 직접 쓰지 않는다"로 절대화됐고, 런타임 값이 섞이는 문구는 상수 모듈의 이름 붙은 빌더를 거친다 — 인자가
CallExpression이라 탐지기를 통과하되, 상수와 같은 파일에 모여 따로 검토된다.닉네임 길이·전화번호 형식·limit 상한을 카탈로그 고정 문구로 박지 않은 이유: 숫자가
user.constants의 상수라 문구에 박으면 상수를 바꿨을 때 메시지만 거짓말이 된다.반증 표는
it.each8행으로 고정했다 — 작은따옴표 · 큰따옴표 · 템플릿 2형 · 여러 줄 인자는 잡히고,domainError· 빌더 호출 · 상수 참조는 안 잡힌다. 실제 프로덕션 파일에 템플릿 리터럴 throw를 심어 파일:줄까지 잡히는 것도 확인했다.검증
yarn validate그린 — 274 suites / 2,828 tests.FE-BREAKING
타입 변경 없음. 다만 위 31개 자리의 에러 문구가 바뀌고
extensions.errorCode가 새로 실린다. 문구로 분기하는 FE 코드가 있으면 코드로 옮기는 게 맞다. 대부분 마이페이지·프로필 수정·최근 검색어 경로다.