Skip to content

refactor(common): 남은 인라인 예외 메시지를 카탈로그로 이관 + 회귀 가드 - #342

Merged
chanwoo7 merged 2 commits into
develop-msafrom
refactor/inline-error-messages
Sep 14, 2026
Merged

chanwoo7 merged 2 commits into
develop-msafrom
refactor/inline-error-messages

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Sep 14, 2026

Copy link
Copy Markdown
Member

배경

#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로 일원화 — 같은 판정이 두 곳에 흩어져 있었다.
  • 영어로 남아 있던 동적 메시지 2건을 한국어로.

신설 코드 21개:

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
CLOSURE_DATE_REQUIRED  CAPACITY_DATE_REQUIRED  UNSUPPORTED_OIDC_PROVIDER

회귀 가드

error-catalog.spec.ts"인라인 예외 메시지 금지" 검사를 추가했다. src 전체를 훑어 new XxxException('...') 형태를 잡고, ${} 조립이 실제로 필요한 파일만 허용 목록에 둔다.

가드 자체가 썩지 않도록 허용 목록의 각 파일이 정말로 ${}를 쓰는지까지 같은 스펙에서 검사한다 — 목록에 넣고 나중에 정적 문구로 바뀌면 그 파일이 통째로 무검사 구역이 된다.

이 가드는 만든 직후 실제로 수동 grep이 놓친 여러 줄 throw 2건을 잡아냈다. "현재 코드에서 통과"가 아니라 막아야 할 걸 막는지 확인된 상태다.

리뷰 대응 — 탐지기 AST 전환 + 전수 스윕

첫 버전의 정규식이 작은따옴표만 보고 있어 큰따옴표·템플릿 리터럴이 그대로 빠져나갔다. 탐지기를 TypeScript AST로 교체하고, 다시 훑어 나온 자리를 전부 닫았다.

대상
AST로 훑은 파일 300+ (src 전체, spec·test 제외)
새로 잡힌 자리 9
고친 것 9
허용 목록으로 남긴 것 0
자리 처리
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 ×3 user-error-messages.ts 빌더로. 전부 영문이었어서 한국어화
text-cleaner.ts ×2 같은 모듈 maxLengthMessage()
s3.service.ts ×2 storage.constants.ts 빌더 2개

허용 목록이 비었다. 규칙이 "throw 자리에서 메시지 문자열을 직접 쓰지 않는다"로 절대화됐고, 런타임 값이 섞이는 문구는 상수 모듈의 이름 붙은 빌더를 거친다 — 인자가 CallExpression이라 탐지기를 통과하되, 상수와 같은 파일에 모여 따로 검토된다.

닉네임 길이·전화번호 형식·limit 상한을 카탈로그 고정 문구로 박지 않은 이유: 숫자가 user.constants의 상수라 문구에 박으면 상수를 바꿨을 때 메시지만 거짓말이 된다.

반증 표는 it.each 8행으로 고정했다 — 작은따옴표 · 큰따옴표 · 템플릿 2형 · 여러 줄 인자는 잡히고, domainError · 빌더 호출 · 상수 참조는 안 잡힌다. 실제 프로덕션 파일에 템플릿 리터럴 throw를 심어 파일:줄까지 잡히는 것도 확인했다.

검증

yarn validate 그린 — 274 suites / 2,828 tests.

FE-BREAKING

타입 변경 없음. 다만 위 31개 자리의 에러 문구가 바뀌고 extensions.errorCode가 새로 실린다. 문구로 분기하는 FE 코드가 있으면 코드로 옮기는 게 맞다. 대부분 마이페이지·프로필 수정·최근 검색어 경로다.

#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건을 실제로 잡아냈다.
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 45e9c196-3e64-42d8-bd84-6fb896f786fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🩺 NestJS Doctor — 89/100 (Good)

진단 385건 (error 0).

Category error warning info
architecture 0 0 14
correctness 0 237 0
performance 0 26 17
schema 0 0 75
security 0 16 0
architecture / security 상위 항목
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'IAuditLogRepository'.
  • warning security/security/no-exposed-env-vars: Direct 'process.env.NODE_ENV' access in 'AuthController'. Use ConfigService instead.
  • warning security/security/require-guards-on-endpoints: Endpoint 'start' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'callback' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'refresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'logout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'devIssueToken' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'adminLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'adminRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'adminLogout' has no @UseGuards() at class or method level.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/conversation/repositories/conversation.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'ConversationRepository'.

오탐 포함 가능 · 기준 docs/guide/architecture-conventions.md

@github-actions

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

Unresolved imports (225)
Unused exports (1)
Unused exported types (4)
Duplicate exports (1)
전체 리포트
Unresolved imports (225)
@/generated/prisma/client  prisma/seed.ts:14:30                                                              
@/generated/prisma/client  src/common/utils/decimal-parser.ts:2:24                                           
@/generated/prisma/client  src/features/admin/repositories/admin.repository.ts:20:8                          
@/generated/prisma/client  src/features/admin/resolvers/admin-account.resolver.spec.ts:9:35                  
@/generated/prisma/client  src/features/admin/resolvers/admin-content.resolver.spec.ts:9:35                  
@/generated/prisma/client  src/features/admin/resolvers/admin-dashboard.resolver.spec.ts:9:35                
@/generated/prisma/client  src/features/admin/resolvers/admin-moderation.resolver.spec.ts:9:35               
@/generated/prisma/client  src/features/admin/resolvers/admin-notification.resolver.spec.ts:8:35             
@/generated/prisma/client  src/features/admin/resolvers/admin-order.resolver.spec.ts:10:35                   
@/generated/prisma/client  src/features/admin/resolvers/admin-product.resolver.spec.ts:9:35                  
@/generated/prisma/client  src/features/admin/resolvers/admin-region.resolver.spec.ts:9:35                   
@/generated/prisma/client  src/features/admin/resolvers/admin-seller.resolver.spec.ts:9:35                   
@/generated/prisma/client  src/features/admin/resolvers/admin-store.resolver.spec.ts:9:35                    
@/generated/prisma/client  src/features/admin/resolvers/admin-taxonomy.resolver.spec.ts:9:35                 
@/generated/prisma/client  src/features/admin/resolvers/admin-user.resolver.spec.ts:9:35                     
@/generated/prisma/client  src/features/admin/services/admin-account.service.spec.ts:13:35                   
@/generated/prisma/client  src/features/admin/services/admin-audit.service.spec.ts:5:35                      
@/generated/prisma/client  src/features/admin/services/admin-banner.service.spec.ts:7:35                     
@/generated/prisma/client  src/features/admin/services/admin-banner.service.ts:39:8                          
@/generated/prisma/client  src/features/admin/services/admin-base.service.ts:4:29                            
@/generated/prisma/client  src/features/admin/services/admin-content-mappers.helper.ts:2:29                  
@/generated/prisma/client  src/features/admin/services/admin-dashboard.service.spec.ts:8:35                  
@/generated/prisma/client  src/features/admin/services/admin-media-url-ownership.spec.ts:6:35                
@/generated/prisma/client  src/features/admin/services/admin-moderation.service.spec.ts:7:35                 
@/generated/prisma/client  src/features/admin/services/admin-notification.service.spec.ts:7:35               
@/generated/prisma/client  src/features/admin/services/admin-notification.service.ts:23:50                   
@/generated/prisma/client  src/features/admin/services/admin-order.service.spec.ts:8:35                      
@/generated/prisma/client  src/features/admin/services/admin-order.service.ts:33:29                          
@/generated/prisma/client  src/features/admin/services/admin-product.service.spec.ts:7:35                    
@/generated/prisma/client  src/features/admin/services/admin-product.service.ts:28:50                        
@/generated/prisma/client  src/features/admin/services/admin-region.service.spec.ts:8:35                     
@/generated/prisma/client  src/features/admin/services/admin-region.service.ts:30:8                          
@/generated/prisma/client  src/features/admin/services/admin-seller.service.spec.ts:9:35                     
@/generated/prisma/client  src/features/admin/services/admin-seller.service.ts:48:50                         
@/generated/prisma/client  src/features/admin/services/admin-store-mappers.helper.ts:6:28                    
@/generated/prisma/client  src/features/admin/services/admin-store.service.spec.ts:7:35                      
@/generated/prisma/client  src/features/admin/services/admin-store.service.ts:35:8                           
@/generated/prisma/client  src/features/admin/services/admin-taxonomy.service.spec.ts:7:35                   
@/generated/prisma/client  src/features/admin/services/admin-taxonomy.service.ts:43:8                        
@/generated/prisma/client  src/features/admin/services/admin-user.service.spec.ts:11:35                      
@/generated/prisma/client  src/features/admin/services/admin-user.service.ts:30:8                            
@/generated/prisma/client  src/features/admin/types/admin-output.type.ts:14:8                                
@/generated/prisma/client  src/features/audit-log/repositories/audit-log.repository.interface.ts:6:8         
@/generated/prisma/client  src/features/audit-log/repositories/audit-log.repository.spec.ts:2:35             
@/generated/prisma/client  src/features/audit-log/repositories/audit-log.repository.ts:11:8                  
@/generated/prisma/client  src/features/auth/repositories/account-credential.repository.interface.ts:1:29    
@/generated/prisma/client  src/features/auth/repositories/account-credential.repository.spec.ts:2:35         
@/generated/prisma/client  src/features/auth/repositories/account.repository.interface.ts:1:47               
@/generated/prisma/client  src/features/auth/repositories/account.repository.spec.ts:6:34                    
@/generated/prisma/client  src/features/auth/repositories/account.repository.ts:17:8                         
@/generated/prisma/client  src/features/auth/repositories/refresh-session.repository.interface.ts:1:41       
@/generated/prisma/client  src/features/auth/repositories/refresh-session.repository.spec.ts:5:35            
@/generated/prisma/client  src/features/auth/repositories/refresh-session.repository.ts:6:49                 
@/generated/prisma/client  src/features/auth/services/credential-auth.service.interface.ts:3:36              
@/generated/prisma/client  src/features/auth/services/credential-auth.service.spec.ts:27:29                  
@/generated/prisma/client  src/features/auth/services/credential-auth.service.ts:30:50                       
@/generated/prisma/client  src/features/auth/services/oidc-client.service.spec.ts:5:34                       
@/generated/prisma/client  src/features/auth/services/oidc-client.service.ts:13:34                           
@/generated/prisma/client  src/features/auth/strategies/jwt-bearer.strategy.spec.ts:7:35                     
@/generated/prisma/client  src/features/conversation/repositories/conversation.repository.spec.ts:2:35       
@/generated/prisma/client  src/features/conversation/repositories/conversation.repository.ts:7:8             
@/generated/prisma/client  src/features/conversation/resolvers/conversation-center.resolver.spec.ts:10:35    
@/generated/prisma/client  src/features/conversation/resolvers/conversation-inquiry.resolver.spec.ts:9:35    
@/generated/prisma/client  src/features/conversation/services/conversation-center.service.spec.ts:5:35       
@/generated/prisma/client  src/features/conversation/services/conversation-inquiry.service.spec.ts:12:35     
@/generated/prisma/client  src/features/conversation/services/conversation-inquiry.service.ts:29:8           
@/generated/prisma/client  src/features/conversation/services/conversation-subscription.service.spec.ts:12:35
@/generated/prisma/client  src/features/conversation/types/conversation-output.type.ts:4:8                   
@/generated/prisma/client  src/features/notification/constants/notification-messages.ts:1:29                 
@/generated/prisma/client  src/features/notification/services/notification-payloads.helper.spec.ts:9:8       
@/generated/prisma/client  src/features/notification/services/notification-payloads.helper.ts:10:8           
@/generated/prisma/client  src/features/order/policies/order-status-transition.policy.spec.ts:4:29           
@/generated/prisma/client  src/features/order/policies/order-status-transition.policy.ts:4:29                
@/generated/prisma/client  src/features/order/repositories/order.repository.spec.ts:2:35                     
@/generated/prisma/client  src/features/order/repositories/order.repository.ts:10:8                          
@/generated/prisma/client  src/features/order/resolvers/order-checkout-mutation.resolver.spec.ts:9:35        
@/generated/prisma/client  src/features/order/services/order-checkout.service.spec.ts:23:8                   
@/generated/prisma/client  src/features/order/services/order-checkout.service.ts:15:24                       
@/generated/prisma/client  src/features/order/types/create-order-output.type.ts:1:34                         
@/generated/prisma/client  src/features/product/repositories/product-review.repository.ts:4:24               
@/generated/prisma/client  src/features/product/repositories/product.repository.spec.ts:2:35                 
@/generated/prisma/client  src/features/product/repositories/product.repository.ts:8:8                       
@/generated/prisma/client  src/features/product/resolvers/product-category-query.resolver.spec.ts:4:35       
@/generated/prisma/client  src/features/product/resolvers/product-detail-query.resolver.spec.ts:6:35         
@/generated/prisma/client  src/features/product/resolvers/product-home-query.resolver.spec.ts:7:35           
@/generated/prisma/client  src/features/product/resolvers/product-review-query.resolver.spec.ts:7:35         
@/generated/prisma/client  src/features/product/resolvers/product-search-query.resolver.spec.ts:6:35         
@/generated/prisma/client  src/features/product/resolvers/product-storefront-query.resolver.spec.ts:4:35     
@/generated/prisma/client  src/features/product/services/product-best-seller.service.spec.ts:10:8            
@/generated/prisma/client  src/features/product/services/product-category.service.spec.ts:3:35               
@/generated/prisma/client  src/features/product/services/product-detail.service.spec.ts:5:35                 
@/generated/prisma/client  src/features/product/services/product-home.service.spec.ts:6:51                   
@/generated/prisma/client  src/features/product/services/product-review.service.spec.ts:6:52                 
@/generated/prisma/client  src/features/product/services/product-search.service.spec.ts:7:51                 
@/generated/prisma/client  src/features/product/services/product-storefront.service.spec.ts:3:35             
@/generated/prisma/client  src/features/region/resolvers/region-query.resolver.spec.ts:6:35                  
@/generated/prisma/client  src/features/region/services/region.service.spec.ts:5:35                          
@/generated/prisma/client  src/features/review/repositories/review-listing.repository.ts:10:24               
@/generated/prisma/client  src/features/review/types/review-listing.type.ts:1:38                             
@/generated/prisma/client  src/features/search/repositories/search.repository.spec.ts:2:35                   
@/generated/prisma/client  src/features/search/repositories/search.repository.ts:4:24                        
@/generated/prisma/client  src/features/search/resolvers/search-entry-query.resolver.spec.ts:12:35           
@/generated/prisma/client  src/features/search/resolvers/search-result-query.resolver.spec.ts:10:35          
@/generated/prisma/client  src/features/search/services/search-entry.service.spec.ts:7:35                    
@/generated/prisma/client  src/features/search/services/search-keyword-rank.service.spec.ts:6:35             
@/generated/prisma/client  src/features/search/services/search-result.service.spec.ts:11:35                  
@/generated/prisma/client  src/features/seller/repositories/seller.repository.spec.ts:5:48                   
@/generated/prisma/client  src/features/seller/repositories/seller.repository.ts:8:8                         
@/generated/prisma/client  src/features/seller/resolvers/seller-content.resolver.spec.ts:12:35               
@/generated/prisma/client  src/features/seller/resolvers/seller-conversation.resolver.spec.ts:12:35          
@/generated/prisma/client  src/features/seller/resolvers/seller-order.resolver.spec.ts:10:35                 
@/generated/prisma/client  src/features/seller/resolvers/seller-product.resolver.spec.ts:36:35               
@/generated/prisma/client  src/features/seller/resolvers/seller-store.resolver.spec.ts:14:35                 
@/generated/prisma/client  src/features/seller/services/seller-audit.service.spec.ts:8:43                    
@/generated/prisma/client  src/features/seller/services/seller-audit.service.ts:22:33                        
@/generated/prisma/client  src/features/seller/services/seller-base.service.spec.ts:11:35                    
@/generated/prisma/client  src/features/seller/services/seller-base.service.ts:11:24                         
@/generated/prisma/client  src/features/seller/services/seller-content-mappers.helper.ts:9:46                
@/generated/prisma/client  src/features/seller/services/seller-conversation.service.spec.ts:10:35            
@/generated/prisma/client  src/features/seller/services/seller-conversation.service.ts:43:8                  
@/generated/prisma/client  src/features/seller/services/seller-custom-template.service.spec.ts:8:44          
@/generated/prisma/client  src/features/seller/services/seller-custom-template.service.ts:31:50              
@/generated/prisma/client  src/features/seller/services/seller-faq.service.spec.ts:7:35                      
@/generated/prisma/client  src/features/seller/services/seller-faq.service.ts:21:50                          
@/generated/prisma/client  src/features/seller/services/seller-media-url-ownership.spec.ts:10:35             
@/generated/prisma/client  src/features/seller/services/seller-option.service.spec.ts:8:44                   
@/generated/prisma/client  src/features/seller/services/seller-option.service.ts:37:50                       
@/generated/prisma/client  src/features/seller/services/seller-order.service.spec.ts:12:48                   
@/generated/prisma/client  src/features/seller/services/seller-order.service.ts:25:29                        
@/generated/prisma/client  src/features/seller/services/seller-product-image.service.spec.ts:8:35            
@/generated/prisma/client  src/features/seller/services/seller-product-image.service.ts:27:50                
@/generated/prisma/client  src/features/seller/services/seller-product-lifecycle.service.spec.ts:8:35        
@/generated/prisma/client  src/features/seller/services/seller-product-lifecycle.service.ts:36:8             
@/generated/prisma/client  src/features/seller/services/seller-product-query.service.spec.ts:12:35           
@/generated/prisma/client  src/features/seller/services/seller-product-taxonomy.service.spec.ts:8:35         
@/generated/prisma/client  src/features/seller/services/seller-product-taxonomy.service.ts:18:50             
@/generated/prisma/client  src/features/seller/services/seller-store-hours.service.spec.ts:7:35              
@/generated/prisma/client  src/features/seller/services/seller-store-hours.service.ts:35:50                  
@/generated/prisma/client  src/features/seller/services/seller-store-mappers.helper.ts:7:24                  
@/generated/prisma/client  src/features/seller/services/seller-store-policy.service.spec.ts:7:35             
@/generated/prisma/client  src/features/seller/services/seller-store-policy.service.ts:39:50                 
@/generated/prisma/client  src/features/seller/services/seller-store-profile.service.spec.ts:8:35            
@/generated/prisma/client  src/features/seller/services/seller-store-profile.service.ts:15:50                
@/generated/prisma/client  src/features/store/repositories/store-review.repository.ts:4:24                   
@/generated/prisma/client  src/features/store/repositories/store-wishlist.repository.ts:3:24                 
@/generated/prisma/client  src/features/store/repositories/store.repository.ts:5:47                          
@/generated/prisma/client  src/features/store/resolvers/store-detail-query.resolver.spec.ts:8:35             
@/generated/prisma/client  src/features/store/resolvers/store-pickup-schedule-query.resolver.spec.ts:5:35    
@/generated/prisma/client  src/features/store/resolvers/store-query.resolver.spec.ts:6:35                    
@/generated/prisma/client  src/features/store/resolvers/store-review-query.resolver.spec.ts:5:35             
@/generated/prisma/client  src/features/store/resolvers/store-search-query.resolver.spec.ts:8:35             
@/generated/prisma/client  src/features/store/resolvers/store-today-pickup-query.resolver.spec.ts:8:35       
@/generated/prisma/client  src/features/store/resolvers/store-wishlist-mutation.resolver.spec.ts:8:35        
@/generated/prisma/client  src/features/store/resolvers/store-wishlist-query.resolver.spec.ts:8:35           
@/generated/prisma/client  src/features/store/services/store-basic-info.helper.spec.ts:4:24                  
@/generated/prisma/client  src/features/store/services/store-basic-info.helper.ts:21:47                      
@/generated/prisma/client  src/features/store/services/store-detail-mappers.helper.spec.ts:6:24              
@/generated/prisma/client  src/features/store/services/store-detail.service.spec.ts:7:35                     
@/generated/prisma/client  src/features/store/services/store-listing.service.spec.ts:5:35                    
@/generated/prisma/client  src/features/store/services/store-pickup-schedule.service.spec.ts:11:8            
@/generated/prisma/client  src/features/store/services/store-review-mappers.helper.spec.ts:3:24              
@/generated/prisma/client  src/features/store/services/store-review.service.spec.ts:6:35                     
@/generated/prisma/client  src/features/store/services/store-search.service.spec.ts:9:42                     
@/generated/prisma/client  src/features/store/services/store-today-pickup.service.spec.ts:7:42               
@/generated/prisma/client  src/features/store/services/store-wishlist.service.spec.ts:11:35                  
@/generated/prisma/client  src/features/user/dto/inputs/my-orders.input.spec.ts:7:29                         
@/generated/prisma/client  src/features/user/dto/inputs/my-orders.input.ts:4:29                              
@/generated/prisma/client  src/features/user/repositories/review-report.repository.ts:7:8                    
@/generated/prisma/client  src/features/user/repositories/review.repository.ts:4:46                          
@/generated/prisma/client  src/features/user/repositories/user.repository.spec.ts:3:35                       
@/generated/prisma/client  src/features/user/repositories/user.repository.ts:10:8                            
@/generated/prisma/client  src/features/user/resolvers/user-engagement.resolver.spec.ts:6:35                 
@/generated/prisma/client  src/features/user/resolvers/user-mypage.resolver.spec.ts:6:35                     
@/generated/prisma/client  src/features/user/resolvers/user-notification.resolver.spec.ts:7:35               
@/generated/prisma/client  src/features/user/resolvers/user-order.resolver.spec.ts:6:35                      
@/generated/prisma/client  src/features/user/resolvers/user-profile.resolver.spec.ts:7:35                    
@/generated/prisma/client  src/features/user/resolvers/user-recent-view.resolver.spec.ts:9:35                
@/generated/prisma/client  src/features/user/resolvers/user-report.resolver.spec.ts:7:35                     
@/generated/prisma/client  src/features/user/resolvers/user-review.resolver.spec.ts:6:35                     
@/generated/prisma/client  src/features/user/resolvers/user-search.resolver.spec.ts:7:35                     
@/generated/prisma/client  src/features/user/resolvers/user-wishlist.resolver.spec.ts:9:35                   
@/generated/prisma/client  src/features/user/services/user-account-policy.helper.ts:1:29                     
@/generated/prisma/client  src/features/user/services/user-base.service.spec.ts:9:35                         
@/generated/prisma/client  src/features/user/services/user-engagement.service.spec.ts:11:35                  
@/generated/prisma/client  src/features/user/services/user-mypage.service.spec.ts:5:29                       
@/generated/prisma/client  src/features/user/services/user-notification.service.spec.ts:9:35                 
@/generated/prisma/client  src/features/user/services/user-order.service.spec.ts:5:35                        
@/generated/prisma/client  src/features/user/services/user-order.service.ts:12:29                            
@/generated/prisma/client  src/features/user/services/user-profile.service.spec.ts:10:35                     
@/generated/prisma/client  src/features/user/services/user-recent-view.service.spec.ts:7:35                  
@/generated/prisma/client  src/features/user/services/user-report.service.spec.ts:11:35                      
@/generated/prisma/client  src/features/user/services/user-report.service.ts:19:35                           
@/generated/prisma/client  src/features/user/services/user-review.service.spec.ts:11:35                      
@/generated/prisma/client  src/features/user/services/user-review.service.ts:19:46                           
@/generated/prisma/client  src/features/user/services/user-search.service.spec.ts:5:35                       
@/generated/prisma/client  src/features/user/services/user-wishlist.service.spec.ts:7:35                     
@/generated/prisma/client  src/features/user/types/user-mypage-output.type.ts:1:34                           
@/generated/prisma/client  src/features/user/types/user-order-output.type.ts:1:34                            
@/generated/prisma/client  src/features/user/types/user-output.type.ts:6:8                                   
@/generated/prisma/client  src/prisma/prisma.service.ts:3:30                                                 
@/generated/prisma/client  src/prisma/soft-delete.middleware.spec.ts:4:24                                    
@/generated/prisma/client  src/prisma/soft-delete.middleware.ts:2:24                                         
@/generated/prisma/client  src/test/db/db-clock.ts:1:35                                                      
@/generated/prisma/client  src/test/db/prisma-test-client.ts:8:30                                            
@/generated/prisma/client  src/test/factories/account.factory.ts:6:8                                         
@/generated/prisma/client  src/test/factories/auth.factory.ts:9:8                                            
@/generated/prisma/client  src/test/factories/category.factory.ts:5:8                                        
@/generated/prisma/client  src/test/factories/notification.factory.ts:6:8                                    
@/generated/prisma/client  src/test/factories/order.factory.ts:7:8                                           
@/generated/prisma/client  src/test/factories/product.factory.ts:1:44                                        
@/generated/prisma/client  src/test/factories/recent-product-view.factory.ts:4:8                             
@/generated/prisma/client  src/test/factories/region.factory.ts:1:43                                         
@/generated/prisma/client  src/test/factories/review-report.factory.ts:6:8                                   
@/generated/prisma/client  src/test/factories/review.factory.ts:1:43                                         
@/generated/prisma/client  src/test/factories/search-event.factory.ts:1:48                                   
@/generated/prisma/client  src/test/factories/search-history.factory.ts:1:50                                 
@/generated/prisma/client  src/test/factories/search-keyword-rank-snapshot.factory.ts:1:35                   
@/generated/prisma/client  src/test/factories/seller.factory.ts:6:8                                          
@/generated/prisma/client  src/test/factories/store-wishlist.factory.ts:4:8                                  
@/generated/prisma/client  src/test/factories/store.factory.ts:1:42                                          
@/generated/prisma/client  src/test/factories/tag.factory.ts:1:40                                            
@/generated/prisma/client  src/test/factories/user-profile.factory.ts:1:48                                   
@/generated/prisma/client  src/test/modules/testing-module.builder.ts:4:35                                   
@/generated/prisma/client  src/test/sample.real-db.spec.ts:3:35                                              
@/generated/prisma/client  test/app.e2e-spec.ts:12:29                                                        
Unused exports (1)
RANKING_VALID_ORDER_STATUSES  src/features/store/index.ts:7:3
Unused exported types (4)
ReviewLikeRankRow  type  src/features/review/index.ts:10:3
ReviewLikesScope   type  src/features/review/index.ts:11:3
ReviewStat         type  src/features/review/index.ts:13:3
ReviewStatKey      type  src/features/review/index.ts:14:3
Duplicate exports (1)
KEYWORD_RANK_SNAPSHOT_SIZE|MAX_POPULAR_KEYWORDS_LIMIT  src/features/search/constants/search.constants.ts

청소 후보(오탐 가능) · 기준 docs/guide/architecture-conventions.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/common/errors/error-catalog.spec.ts Outdated
Comment on lines +177 to +178
/** `new XxxException('...')` — 작은따옴표 리터럴 메시지 */
const INLINE_MESSAGE = /new [A-Za-z]+Exception\(\s*'/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

유효. 정규식이 작은따옴표만 보고 있었고, 지목한 두 자리(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

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.63014% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/global/storage/s3.service.ts 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.62% 7754/7943
🟢 Branches 92.44% 2961/3203
🟢 Functions 96.14% 1521/1582
🟢 Lines 98.21% 7081/7210

Test suite run success

2828 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의
상수여서다 — 문구에 박으면 상수를 바꿨을 때 메시지만 거짓말이 된다.
@chanwoo7
chanwoo7 merged commit 2f9e738 into develop-msa Sep 14, 2026
10 checks passed
@chanwoo7
chanwoo7 deleted the refactor/inline-error-messages branch September 14, 2026 22:30
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