Skip to content

Feat/295 assigned coupon#296

Merged
YEJIRYOO merged 7 commits into
mainfrom
feat/295-assigned-coupon
May 16, 2026
Merged

Feat/295 assigned coupon#296
YEJIRYOO merged 7 commits into
mainfrom
feat/295-assigned-coupon

Conversation

@YEJIRYOO

@YEJIRYOO YEJIRYOO commented May 16, 2026

Copy link
Copy Markdown
Member

🔎 Resolved Issue

  • ✨ [FEAT] 지정 커피 쿠폰 도메인 추가 #295 지정 커피 쿠폰 도메인 추가
  • 사전 등록된 대상자에게만 발급 가능한 신규 쿠폰 흐름 구현
  • 기존 event 도메인과 기능 혼동 방지를 위해 도메인 분리 → 차후 컨벤션 논의 필요

✅ Title

  • feat: 지정 커피 쿠폰(assigned-coupon) 도메인 추가

📄 Content

  • AssignedCouponController 추가
    • GET /api/v2/assigned-coupons/eligibility — 보관함 진입 시 발급 자격 확인 (이름 + 전화번호 이중 검증)
    • POST /api/v2/assigned-coupons/coupons — 매장 PIN 인증 + 쿠폰 발급 (=사용 처리)
    • POST /api/v2/assigned-coupons/admin/register — 관리자: 대상자 일괄 등록
    • POST /api/v2/assigned-coupons/admin/register/one — 관리자: 단건 등록
  • AssignedCouponService 추가
    • checkEligibility(username) — Redis Hash assigned-coupon:target:{phoneNum} 조회 후 이름 일치까지 확인 → phoneNum 만으로 매칭되지 않게 이중 검증
    • issueCoupon(username, storePin) — PIN 검증 → User 조회 → Redisson 분산락 획득 → 락 안 재검증 (TOCTOU 방어) → INCR atomic 으로 쿠폰 번호 발급 → 상태를 USED 로 단일 갱신 → S3 로깅 이벤트 발행
    • registerTargets(targets) — 전화번호 정규화 후 Redis Hash 등록, 중복/실패 카운트 반환
  • Redis 자료구조 설계
    • assigned-coupon:target:{phoneNum} (Hash) — name, status, couponNumber, issuedAt, usedAt, claimedBy, registeredAtHSET putAll 로 부분 갱신, ziplist 인코딩으로 메모리 효율
    • assigned-coupon:seq (String) — INCR atomic 으로 쿠폰 번호 유일성 보장 (AC-%04d 포맷)
    • assigned-coupon:used:count (String) — 발급 통계용 카운터
    • lock:assigned-coupon:issue:{phoneNum} (Redisson RLock) — phoneNum 단위 fine-grained locking 으로 동시 발급 차단하면서 처리량 유지
  • 동시성 제어 - Lock
    • 발급 = 단일 사용 → 재사용 불가 (상태: TARGETED → USED)
    • tryLock(3, 3, SECONDS) — waitTime/leaseTime 분리, 클라이언트 장애 시 자동 해제로 deadlock 방어
    • 락 안에서 HGETALL 재실행 → Time-Of-Check, Time-Of-Use 갭 동안 다른 발급에 의해 상태가 바뀌었을 가능성 차단
    • 같은 phoneNum 의 동시 발급은 직렬화, 서로 다른 phoneNum 은 병렬 처리
  • 보안 검증
    • phoneNum 신뢰성: 기존 UserService.updateUser 흐름에서 SMS 인증 강제됨 → User.phoneNum 은 신뢰 가능한 값
    • name + phoneNum 이중 매칭: phoneNum 단독 매칭 시 우연/악의적 일치 위험 → 사전 등록 명단의 nameUser.name 모두 일치해야 통과
    • usernameAuthentication 에서 추출한 OAuth identity → 클라이언트 조작 불가
    • claimedBy 필드에 username 기록 → 누가 발급받았는지 추적 가능
  • S3 비동기 로깅
    • ApplicationEventPublisherAssignedCouponIssuedEvent 발행 → @Async @EventListener 로 비동기 처리
    • S3 업로드 경로 event-logs/assigned-coupon/assigned-coupon-{couponNumber}_{timestamp}.json
    • S3 업로드 실패해도 발급 트랜잭션은 성공 (예외 swallow + 로그) → 쿠폰 발급과 S3 업로드 가용성 분리
  • GlobalErrorCode 추가
    • ASSIGNED_COUPON_PHONE_NOT_SET (400) — User.phoneNum 미등록
    • ASSIGNED_COUPON_NOT_TARGET (404) — 사전 등록 명단에 없음 또는 이름 불일치
    • ASSIGNED_COUPON_ALREADY_ISSUED (409) — 이미 발급(=사용) 처리됨
    • EVENT_PIN_MISMATCH, EVENT_CONCURRENCY_ERROR 는 기존 event 도메인 코드 재사용
  • 테스트
    • AssignedCouponServiceTest 18개 — @NestedRegisterTargets / CheckEligibility / IssueCoupon 그룹화
    • 정상 흐름 외에 PIN 불일치, phoneNum null, 락 timeout, 대상자 없음, 이름 불일치, 이미 발급, TOCTOU 방어 등 모든 분기 검증
    • ArgumentCaptor 로 발행된 AssignedCouponIssuedEvent 의 username/name/phoneNum/couponNumber 모두 검증
    • AssignedCouponIssueEventListenerTest 3개 — 정상 S3 업로드 경로 검증, 직렬화/업로드 실패 시 예외 swallow 검증

Summary by CodeRabbit

  • New Features

    • 축제 지정 쿠폰 기능 추가: 사용자 자격 조회, 매장 PIN 기반 쿠폰 발급, 관리자 대상자 일괄/단건 등록 지원
    • 발급 이벤트 비동기 로깅: 발급 시 이벤트를 클라우드 스토리지에 저장
  • Errors

    • 지정 쿠폰 관련 오류 코드 추가(미대상·이미발급·전화번호 미등록 등)
  • Tests

    • 쿠폰 서비스 및 이벤트 처리 단위 테스트 추가

Review Change Stack

@YEJIRYOO YEJIRYOO requested a review from jinuklee777 May 16, 2026 07:14
@YEJIRYOO YEJIRYOO self-assigned this May 16, 2026
@YEJIRYOO YEJIRYOO added the ✨ feat New feature or request label May 16, 2026
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@YEJIRYOO has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 56 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c9707606-abd5-4ee6-8941-f8b409b9fdd2

📥 Commits

Reviewing files that changed from the base of the PR and between db4d8ab and a4ad598.

📒 Files selected for processing (1)
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java

Walkthrough

지정 쿠폰 도메인을 새로 추가하여 대상자 등록, 자격 확인, 동시성 제어 기반 발급 처리를 구현합니다. Redis 해시와 Redisson 분산락을 사용하며, 발급 이벤트를 S3로 비동기 로깅하고 REST API 및 단위 테스트를 제공합니다.

Changes

Assigned Coupon Feature

Layer / File(s) Summary
Data Contracts & Response Types
src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/*, src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssuedEvent.java, src/main/java/com/soongsil/CoffeeChat/global/exception/GlobalErrorCode.java
AssignedCouponCheckResponse, AssignedCouponResponse, AssignedCouponTargetRequest, AssignedCouponRegisterResult, AssignedCouponIssueRequest, AssignedCouponIssuedEvent 등을 추가하고 관련 오류 코드 3개를 GlobalErrorCode에 등록합니다.
Service Core Logic
src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java
관리자 대상자 등록(registerTargets), 사용자 자격검사(checkEligibility), PIN 검증 및 분산락 기반 쿠폰 발급(issueCoupon) 로직을 구현합니다(시퀀스 증가, Redis 해시 갱신, 이벤트 발행 포함).
Event Listener & S3 Logging
src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListener.java
AssignedCouponIssuedEvent@Async @EventListener``로 비동기 처리해 JSON 직렬화 후 event-logs/assigned-coupon 경로에 S3 업로드하고 예외를 로깅하되 전파하지 않습니다.
REST API Endpoints
src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/controller/AssignedCouponController.java
/api/v2/assigned-coupons에 사용자 GET /eligibility, POST /coupons와 관리자 POST /admin/register, POST /admin/register/one 엔드포인트를 추가해 서비스 호출 결과를 ApiResponse로 감싸 반환합니다.
Service Integration Tests
src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponServiceTest.java
대상자 등록(신규/중복/실패), 자격 확인(미존재/불일치/발급 여부), 발급 흐름(PIN, 락, 상태 갱신, 시퀀스/카운트 증가, 이벤트 발행, 락 해제) 시나리오를 Mockito로 검증합니다.
Event Listener Tests
src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListenerTest.java
리스너의 직렬화 결과가 S3 업로드에 전달되는지, 직렬화/업로드 예외 상황에서 예외가 전파되지 않는지를 검증합니다.
Import & Formatting Adjustments
src/main/java/com/soongsil/CoffeeChat/global/exception/GlobalExceptionAdvice.java, src/main/java/com/soongsil/CoffeeChat/CoffeeChatApplication.java, src/main/java/com/soongsil/CoffeeChat/global/dev/DevDataInitializer.java, src/main/java/com/soongsil/CoffeeChat/domain/possibleDate/entity/PossibleDate.java, src/main/java/com/soongsil/CoffeeChat/domain/chat/entity/ChatRoomUser.java
경미한 import 재배치 및 포맷/줄바꿈 정리 변경입니다(동작 변경 없음).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • PR #295: 동일한 AssignedCoupon 도메인 추가(컨트롤러, 서비스, DTO/레코드, 이벤트/리스너, Redisson 락, 단위 테스트)를 다루고 있어 관련이 있습니다.

Poem

🐰 Redis에 명단을 담고, 락으로 순서를 지키며,
발급 한 장씩 번호 매겨 S3에 남기네,
이벤트는 비동기로 살며시 날아가,
테스트는 모든 경로를 하나씩 확인하고,
축제의 컵 한 잔, 준비 끝! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 'Feat/295 assigned coupon'은 assigned coupon 도메인 추가라는 주요 변경사항을 명확하게 반영하고 있으나, 작은 대문자 규칙 위반과 'Feat/' 접두사 스타일이 표준 관례와 일치하지 않습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/295-assigned-coupon

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/controller/AssignedCouponController.java`:
- Around line 94-99: The registerOneTarget method in AssignedCouponController
can NPE when target is null because List.of(target) throws; add a null guard in
AssignedCouponController.registerOneTarget to return a 400 response instead of
letting an exception bubble (e.g., check if target == null and return
ApiResponse.onErrorBadRequest(...) or ResponseEntity.badRequest() with an
appropriate ApiResponse), or annotate the request with validation (`@Valid` and
add `@NotNull` on AssignedCouponTargetRequest) and ensure validation is enabled so
assignedCouponService.registerTargets is only called with a non-null list.
- Around line 71-77: The controller currently reads sensitive storePin via
`@RequestParam` in AssignedCouponController.issueCoupon; change the API to accept
a JSON request body instead: create a small DTO (e.g., IssueCouponRequest with a
storePin field, add validation like `@NotBlank`), replace the method parameter
from `@RequestParam` String storePin to `@RequestBody` `@Valid` IssueCouponRequest
request and pass request.getStorePin() into
assignedCouponService.issueCoupon(authentication.getName(),
request.getStorePin()); update any controller annotations/docs/tests that assert
a query parameter to reflect the new JSON body, and adjust the service signature
if your API layer previously relied on framework binding semantics.

In
`@src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java`:
- Around line 66-71: In AssignedCouponService inside the loop over targets, add
null-safety and stricter validation: skip and record targets that are null
before calling target.phoneNum(), treat empty or blank target.name() (after
trimming) as invalid, and ensure normalizePhoneNum(target.phoneNum()) handles
nulls or is only called after null check; when validation fails, add the
original target identifier to failed and continue. Update logic around the
for-loop and the use of normalizePhoneNum, target.phoneNum(), target.name(), and
the failed collection so no NullPointerException is thrown and blank names are
rejected (optionally log the reason).
- Around line 165-167: AssignedCouponService uses lock.tryLock(3, 3,
TimeUnit.SECONDS) which disables Redisson's watchdog (fixed 3s lease) and risks
early expiry; change the call on the lock object to the wait-only overload
(e.g., lock.tryLock(3, TimeUnit.SECONDS)) so the watchdog/auto-renewal remains
enabled during processing, ensure the same lock variable and surrounding
try/finally that unlocks the lock remain intact (update any uses in methods like
the coupon issuance path that currently call tryLock(3, 3, TimeUnit.SECONDS)).

In
`@src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListenerTest.java`:
- Around line 85-97: The test handleEvent_swallowsS3UploadException currently
only asserts no exception; also verify that upload was actually attempted by
adding a Mockito verify call for amazonS3Service.uploadJsonFile with the
expected arguments (use the same JSON from objectMapper.writeValueAsString and
the event identifiers) so the test fails if the upload call is omitted; locate
this in AssignedCouponIssueEventListenerTest near
handleAssignedCouponIssuedEvent and add
verify(amazonS3Service).uploadJsonFile(...) referencing
amazonS3Service.uploadJsonFile and the AssignedCouponIssuedEvent values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1f614779-cc1f-4ed3-906b-63891b3e2d56

📥 Commits

Reviewing files that changed from the base of the PR and between 81ffd92 and 5c433d9.

📒 Files selected for processing (12)
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/controller/AssignedCouponController.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponCheckResponse.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponRegisterResult.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponResponse.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponTargetRequest.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListener.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssuedEvent.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java
  • src/main/java/com/soongsil/CoffeeChat/global/exception/GlobalErrorCode.java
  • src/main/java/com/soongsil/CoffeeChat/global/exception/GlobalExceptionAdvice.java
  • src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListenerTest.java
  • src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponServiceTest.java

Comment on lines +85 to +97
void handleEvent_swallowsS3UploadException() throws Exception {
// given
AssignedCouponIssuedEvent event =
new AssignedCouponIssuedEvent(
"u1", "n", "01000000000", "AC-0001", LocalDateTime.now());
given(objectMapper.writeValueAsString(event)).willReturn("{}");
given(amazonS3Service.uploadJsonFile(anyString(), anyString(), anyString()))
.willThrow(new RuntimeException("S3 error"));

// when & then
assertThatCode(() -> listener.handleAssignedCouponIssuedEvent(event))
.doesNotThrowAnyException();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

S3 실패 테스트에서 “업로드 시도” 자체를 함께 검증해 주세요.

현재는 예외 미전파만 확인해서, 업로드 호출이 누락돼도 테스트가 통과할 수 있습니다. verify(amazonS3Service).uploadJsonFile(...)를 추가해 경로를 고정해 주세요.

보강 예시
 `@Test`
 `@DisplayName`("S3 업로드 실패해도 예외를 외부로 던지지 않음 (로깅만)")
 void handleEvent_swallowsS3UploadException() throws Exception {
@@
     assertThatCode(() -> listener.handleAssignedCouponIssuedEvent(event))
             .doesNotThrowAnyException();
+
+    verify(amazonS3Service).uploadJsonFile(anyString(), anyString(), anyString());
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/message/AssignedCouponIssueEventListenerTest.java`
around lines 85 - 97, The test handleEvent_swallowsS3UploadException currently
only asserts no exception; also verify that upload was actually attempted by
adding a Mockito verify call for amazonS3Service.uploadJsonFile with the
expected arguments (use the same JSON from objectMapper.writeValueAsString and
the event identifiers) so the test fails if the upload call is omitted; locate
this in AssignedCouponIssueEventListenerTest near
handleAssignedCouponIssuedEvent and add
verify(amazonS3Service).uploadJsonFile(...) referencing
amazonS3Service.uploadJsonFile and the AssignedCouponIssuedEvent values.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java`:
- Around line 231-235: The catch block in AssignedCouponService that currently
catches InterruptedException, restores the interrupt flag, and throws an
IllegalStateException should instead throw a domain-level exception that maps to
your 503 concurrency error so the API contract remains consistent; update the
catch to keep Thread.currentThread().interrupt(), then throw your domain
exception (e.g., AssignedCouponException or ConcurrencyDomainException used in
your project) with the appropriate CONCURRENCY/LOCKING error code and pass the
InterruptedException as the cause so the stacktrace is preserved and the error
mapper will return 503.
- Around line 74-101: In AssignedCouponService replace the raw target.name()
usage with a trimmed value: compute a trimmedName (e.g., target.name() == null ?
null : target.name().trim()), use trimmedName for the blank/null checks (instead
of the current name variable) and pass trimmedName into the Redis hash putAll
call (replace target.name() in the Map) so stored names match later exact-match
lookups; ensure you guard trimming against null to avoid NPEs and update any
other places in this method that still reference target.name() to use
trimmedName.
- Around line 168-170: normalizePhoneNum can return an empty string and you must
guard against creating shared keys for multiple users; after calling
normalizePhoneNum(user.getPhoneNum()) in AssignedCouponService, validate that
phoneNum is non-empty and if it is empty either throw a clear exception (e.g.,
IllegalArgumentException) or return an error response, and avoid creating
targetKey ("assigned-coupon:target:" + phoneNum) or the related lock key when
phoneNum is blank; update the code around the normalizePhoneNum call and any
lock/key creation to perform this check and include a descriptive log message
referencing phoneNum, normalizePhoneNum, targetKey and the lock key variable.

In
`@src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponServiceTest.java`:
- Line 387: The test in AssignedCouponServiceTest currently only verifies that
valueOperations.increment(...) was never called, but doesn't assert that the
Redis value-access path wasn't entered; update the test to also verify that
redisTemplate.opsForValue() was never invoked (e.g., add a verify(redisTemplate,
never()).opsForValue() check) so both the opsForValue() call and
valueOperations.increment(...) are asserted as not executed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0a0be90d-4153-4510-94cc-afb7f157a087

📥 Commits

Reviewing files that changed from the base of the PR and between 5c433d9 and db4d8ab.

📒 Files selected for processing (11)
  • src/main/java/com/soongsil/CoffeeChat/CoffeeChatApplication.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/controller/AssignedCouponController.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponCheckResponse.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponIssueRequest.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponResponse.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponTargetRequest.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java
  • src/main/java/com/soongsil/CoffeeChat/domain/chat/entity/ChatRoomUser.java
  • src/main/java/com/soongsil/CoffeeChat/domain/possibleDate/entity/PossibleDate.java
  • src/main/java/com/soongsil/CoffeeChat/global/dev/DevDataInitializer.java
  • src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponServiceTest.java
💤 Files with no reviewable changes (2)
  • src/main/java/com/soongsil/CoffeeChat/domain/chat/entity/ChatRoomUser.java
  • src/main/java/com/soongsil/CoffeeChat/CoffeeChatApplication.java
✅ Files skipped from review due to trivial changes (2)
  • src/main/java/com/soongsil/CoffeeChat/domain/possibleDate/entity/PossibleDate.java
  • src/main/java/com/soongsil/CoffeeChat/global/dev/DevDataInitializer.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponTargetRequest.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponResponse.java
  • src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/dto/AssignedCouponCheckResponse.java

Comment on lines +74 to +101
String name = target.name();

// name, phoneNum blank 검증
if (phoneNum == null
|| phoneNum.isBlank()
|| name == null
|| name.isBlank()) {
failed.add(target.phoneNum());
continue;
}

String targetKey = "assigned-coupon:target:" + phoneNum;

try {
// hash 등록 시도 -> ticket TARGETED 로 초기화
if (redisTemplate.hasKey(targetKey)) {
dupCount++;
continue;
}

redisTemplate
.opsForHash()
.putAll(
targetKey,
Map.of(
"name", target.name(),
"status", "TARGETED",
"registeredAt", LocalDateTime.now().toString()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

이름 공백 정리 누락으로 자격 조회가 오탐될 수 있습니다.

Line 74에서 name을 검증만 하고 저장 시(Line 99) 원본 target.name()을 그대로 써서, 앞뒤 공백이 포함된 대상자는 Line 133의 정확 일치 비교에서 탈락할 수 있습니다. 입력 단계에서 trim 후 동일 값을 저장하세요.

수정 예시
-            String name = target.name();
+            String name = target.name() == null ? null : target.name().trim();

             // name, phoneNum blank 검증
             if (phoneNum == null
                     || phoneNum.isBlank()
                     || name == null
                     || name.isBlank()) {
                 failed.add(target.phoneNum());
                 continue;
             }
@@
                 redisTemplate
                         .opsForHash()
                         .putAll(
                                 targetKey,
                                 Map.of(
-                                        "name", target.name(),
+                                        "name", name,
                                         "status", "TARGETED",
                                         "registeredAt", LocalDateTime.now().toString()));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java`
around lines 74 - 101, In AssignedCouponService replace the raw target.name()
usage with a trimmed value: compute a trimmedName (e.g., target.name() == null ?
null : target.name().trim()), use trimmedName for the blank/null checks (instead
of the current name variable) and pass trimmedName into the Redis hash putAll
call (replace target.name() in the Map) so stored names match later exact-match
lookups; ensure you guard trimming against null to avoid NPEs and update any
other places in this method that still reference target.name() to use
trimmedName.

Comment on lines +168 to +170
String phoneNum = normalizePhoneNum(user.getPhoneNum());
String targetKey = "assigned-coupon:target:" + phoneNum;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

정규화된 전화번호 빈값 가드가 없어 락/키 충돌 위험이 있습니다.

Line 168에서 숫자 외 문자를 제거한 결과가 빈 문자열일 수 있는데, 현재는 그대로 Line 172 lock key와 Line 169 target key를 생성합니다. 잘못된 사용자 데이터가 들어오면 서로 다른 사용자가 동일 빈 key를 공유할 수 있습니다.

수정 예시
         String phoneNum = normalizePhoneNum(user.getPhoneNum());
+        if (phoneNum == null || phoneNum.isBlank()) {
+            throw new GlobalException(GlobalErrorCode.ASSIGNED_COUPON_PHONE_NOT_SET);
+        }
         String targetKey = "assigned-coupon:target:" + phoneNum;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponService.java`
around lines 168 - 170, normalizePhoneNum can return an empty string and you
must guard against creating shared keys for multiple users; after calling
normalizePhoneNum(user.getPhoneNum()) in AssignedCouponService, validate that
phoneNum is non-empty and if it is empty either throw a clear exception (e.g.,
IllegalArgumentException) or return an error response, and avoid creating
targetKey ("assigned-coupon:target:" + phoneNum) or the related lock key when
phoneNum is blank; update the code around the normalizePhoneNum call and any
lock/key creation to perform this check and include a descriptive log message
referencing phoneNum, normalizePhoneNum, targetKey and the lock key variable.


verify(rLock).unlock();
verifyNoInteractions(eventPublisher);
verify(valueOperations, never()).increment(anyString());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

재발급 차단 검증에서 opsForValue() 미호출도 함께 검증해 주세요.

현재 검증은 valueOperations.increment만 확인해서, 값 연산 경로 진입 자체를 더 명확히 막지는 못합니다.

수정 예시
 verify(rLock).unlock();
 verifyNoInteractions(eventPublisher);
 verify(valueOperations, never()).increment(anyString());
+verify(redisTemplate, never()).opsForValue();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/soongsil/CoffeeChat/domain/assignedcoupon/service/AssignedCouponServiceTest.java`
at line 387, The test in AssignedCouponServiceTest currently only verifies that
valueOperations.increment(...) was never called, but doesn't assert that the
Redis value-access path wasn't entered; update the test to also verify that
redisTemplate.opsForValue() was never invoked (e.g., add a verify(redisTemplate,
never()).opsForValue() check) so both the opsForValue() call and
valueOperations.increment(...) are asserted as not executed.

@YEJIRYOO YEJIRYOO merged commit 27f1139 into main May 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant