Skip to content

fix: 지운 커스텀 문구 슬롯 키 재사용 시 복구 - #519

Merged
chanwoo7 merged 2 commits into
developfrom
fix/custom-text-token-key-reuse
Oct 5, 2026
Merged

chanwoo7 merged 2 commits into
developfrom
fix/custom-text-token-key-reuse

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Oct 5, 2026

Copy link
Copy Markdown
Member

요약

  • 지운 커스텀 문구 슬롯과 같은 tokenKey로 슬롯을 다시 등록하면 500이 나던 버그를 고칩니다. 판매자 앱 상품 관리 구현 중 발견했습니다.
    • 재현: sellerDeleteProductCustomTextToken → 같은 키로 sellerUpsertProductCustomTextToken(tokenId 생략)
    • 원인: soft-delete 행이 uk_product_custom_text_token(template_id, token_key)를 계속 점유해 P2002
  • 등록 경로는 같은 (템플릿, 키)의 삭제 슬롯이 있으면 그 행(같은 id)을 복구하며 입력값으로 갱신하고, 없으면 새로 만듭니다.
    • 관리자 createOrRestoreTag·카테고리 복구와 같은 방식입니다.
    • 감사 로그는 기존대로 등록 경로 CREATE이며, 복구된 경우 afterJson.tokenId가 복구된 id입니다.
  • 등록 경로에서 템플릿 행을 FOR UPDATE로 잠가 같은 템플릿의 슬롯 생성을 직렬화합니다.
    • 잠금이 없으면 같은 삭제 슬롯을 동시에 복구한 2건이 모두 성공해 한쪽 값이 덮입니다(반증으로 확인).
    • 잠금 뒤의 조회가 앞선 커밋을 보므로, 늦게 온 요청은 활성 키 충돌로 떨어집니다.
  • unique 충돌은 새 코드 CUSTOM_TEXT_TOKEN_KEY_TAKEN(409)로 좁힙니다.
    • 대상: 활성 슬롯과 키 중복(동시 등록 경쟁 포함), 수정(tokenId 지정)으로 다른 슬롯의 키로 바꾸는 경우
    • 수정으로 삭제된 슬롯의 키로 바꾸는 경우도 409로 둡니다. 관리자 태그·카테고리 이름 변경과 같은 정책입니다.
    • 고정 메시지라 error-catalog.spec의 RENDER_PARAMS에는 추가하지 않았습니다(파라미터 메시지 전용 표).
  • SDL description만 바꿨습니다(타입·필드 변경 없음). 커밋을 둘로 나눴습니다.
    • fix 커밋: sellerUpsertProductCustomTextToken에 복구·충돌 동작
    • docs 커밋: SellerCustomTextToken·SellerUpsertProductCustomTextTokenInput의 posX·posY·width·height에 좌표 계약
  • 좌표 계약은 판매자 앱 저장 방식(SCALE = 10_000)을 그대로 적었습니다.
    • 기준: 베이스 이미지 한 변을 10000으로 본 정수 비율, posX·posY는 문구 영역 왼쪽 위 모서리
    • 범위: 위치 010000, 크기 110000(width·height는 기존 @Min(1) 검증이 있어 1부터)
    • 서버는 범위를 검사하지 않는다고 posX에 함께 적었고, 범위 검증 추가는 이번 범위 밖입니다.

FE 영향

  • 파괴적 변경이 없습니다. 응답 형태는 그대로이고, 500이던 경우가 성공(복구) 또는 409 CUSTOM_TEXT_TOKEN_KEY_TAKEN이 됩니다.
  • 판매자 앱은 슬롯 키 중복을 클라이언트에서 검사하지 않으므로, 같은 템플릿에서 키가 겹치면 이 코드의 카탈로그 메시지를 받게 됩니다.

테스트

  • product-seller-custom-template.service.spec(real DB)에 회귀 7건을 추가했습니다.
    • 삭제 후 같은 키 재등록: 같은 id 복구, 모든 입력 필드 갱신, 행 1개, 감사 CREATE 2건 모두 같은 tokenId
    • 활성 키 중복 등록: CUSTOM_TEXT_TOKEN_KEY_TAKEN, 기존 행 무변경, 감사 0건
    • 수정으로 활성·삭제 슬롯의 키로 변경: 둘 다 CUSTOM_TEXT_TOKEN_KEY_TAKEN(it.each 2건)
    • 다른 템플릿의 같은 키(활성·삭제)는 무관: 내 템플릿에 새 행 생성, 상대 삭제 행은 삭제 상태 유지
    • 동시 등록 2건(새 키·삭제 슬롯 키, it.each 2건): 1건 성공·1건 CUSTOM_TEXT_TOKEN_KEY_TAKEN, 활성 행 1개, 감사 CREATE 1건
  • 반증을 두 번 돌리고 되돌렸습니다.
    • 복구 분기를 끄면 첫 케이스가 CUSTOM_TEXT_TOKEN_KEY_TAKEN으로 실패합니다(동시 등록의 삭제 키 케이스도 함께 실패).
    • 템플릿 잠금을 지우면 삭제 슬롯 키 동시 등록에서 2건이 모두 성공해 실패합니다.
  • 관련 spec 4개(custom template 서비스·상품 repository·에러 카탈로그·판매자 상품 resolver)를 돌렸습니다.
    • Test Suites 4 passed, Tests 238 passed
  • tsc --noEmit·변경 파일 eslint·prettier·yarn docs:check·yarn dto:check·yarn arch:check가 통과했고, yarn graphql:codegen을 다시 돌렸습니다.

플랜 대조

플랜 항목 상태
판매자 앱 상품 관리 — 커스텀 문구 슬롯 재생성(버그) 한 것
슬롯 좌표 비율 계약 description 한 것

판매자 앱 상품 관리 구현 중 발견. sellerDeleteProductCustomTextToken으로 슬롯을 지운 뒤
같은 tokenKey로 sellerUpsertProductCustomTextToken(tokenId 생략)을 부르면
soft-delete 행이 uk_product_custom_text_token(template_id, token_key)에 걸려 P2002 → 500.

- repository upsertCustomTextToken 등록 경로: 같은 (template, tokenKey)의 삭제 슬롯이 있으면
  그 행(같은 id)을 복구하며 입력값으로 갱신, 없으면 생성. 관리자 createOrRestoreTag·카테고리와 같은 방식.
- 등록 경로는 템플릿 행을 FOR UPDATE로 잠가 같은 템플릿의 슬롯 생성을 직렬화.
  잠금 없이는 같은 삭제 슬롯을 동시에 복구한 2건이 모두 성공한다(반증 확인).
- unique 충돌(활성 키 중복·경쟁, 수정으로 삭제 슬롯 키로 변경)은 CUSTOM_TEXT_TOKEN_KEY_TAKEN(409)로 좁힘.
  수정 경로를 삭제 키로 바꾸는 경우 오류로 두는 것은 관리자 태그·카테고리 이름 변경과 같은 정책.
- 에러 카탈로그에 CUSTOM_TEXT_TOKEN_KEY_TAKEN 추가(파라미터 없는 고정 메시지라 RENDER_PARAMS 불요).
- SDL은 sellerUpsertProductCustomTextToken description에 복구·충돌 동작만 추가.
- 감사 로그는 기존 그대로 등록 경로 CREATE(복구도 CREATE, tokenId는 복구된 id).
- 회귀 테스트 7건(real DB): 삭제 후 같은 키 재등록 → 같은 id 복구·필드 갱신·감사 /
  활성 키 중복 → 409·무변경 / 수정으로 활성·삭제 키로 변경 → 409(2) /
  다른 템플릿의 같은 키 무관 / 동시 등록 2건(새 키·삭제 키) → 1 성공 1 409(2).
  복구 분기를 지우면 첫 케이스가 409로 실패(반증 확인).
판매자 앱이 슬롯 좌표를 베이스 이미지 한 변을 10000으로 본 정수 비율로 저장한다
(SCALE = 10_000, x·y는 왼쪽 위 모서리, 가로·세로 모두 같은 한 변 기준).
SDL description에 이 계약이 없어 클라이언트마다 단위를 추측해야 했음.

- SellerCustomTextToken·SellerUpsertProductCustomTextTokenInput의 posX·posY·width·height
  description에 기준(왼쪽 위 모서리, 한 변 10000)과 범위(위치 0~10000, 크기 1~10000)를 명시.
  width·height는 기존 @min(1) 검증이 있어 1부터로 적음.
- 서버는 범위를 검사하지 않는다는 점을 posX에 함께 적음. 범위 검증 추가는 이번 범위 밖.
- 코드·DTO 변경 없음.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-05T23:38:27.944683Z b371546 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 5, 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: Repository: CaQuick/caquick-be/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 830828dc-84e0-4539-8b5e-727f32fbe16c

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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

Unused exported types (1)
전체 리포트
Unused exported types (1)
RateLimitPolicy  type  src/global/rate-limit/index.ts:4:8

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

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🩺 NestJS Doctor — 90/100 (Excellent)

진단 484건 (error 12).

Category error warning info
architecture 1 1 44
correctness 0 266 0
performance 0 36 28
schema 0 0 76
security 11 21 0
architecture / security 상위 항목
  • error architecture/architecture/no-manual-instantiation: Manual instantiation of 'OutboxRepository' detected. Use dependency injection instead.
  • 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.
  • warning security/security/require-guards-on-endpoints: Endpoint 'getJwks' has no @UseGuards() at class or method level.

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

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Coverage report

St.❔
Category Percentage Covered / Total
🟢 Statements
98% (-0.01% 🔻)
10692/10910
🟢 Branches
92.92% (-0.01% 🔻)
4070/4380
🟢 Functions
97.53% (+0% 🔼)
2133/2187
🟢 Lines
98.57% (-0.01% 🔻)
9728/9869
Show files with reduced coverage 🔻
St.❔
File Statements Branches Functions Lines
🟢
... / product.repository.ts
98.3% (-0.51% 🔻)
94.12% (-1.04% 🔻)
99.03% (+0.01% 🔼)
99.38% (-0.62% 🔻)

Test suite run success

4244 tests passing in 383 suites.

Report generated by 🧪jest coverage report action from b371546

@chanwoo7
chanwoo7 merged commit 71c1fa6 into develop Oct 5, 2026
17 checks passed
@chanwoo7
chanwoo7 deleted the fix/custom-text-token-key-reuse branch October 5, 2026 23:41
@codecov

codecov Bot commented Oct 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...eatures/product/repositories/product.repository.ts 83.33% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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