Conversation
- GET /api/alarms 에서 remaingOffCount 제거 - GET /api/alarms/off-count 구현
Walkthrough알람 끄기 잔여 횟수(remainingOffCount) 필드를 알람 목록 미리보기 응답에서 제거하고, 별도의 DTO와 API 엔드포인트로 분리하는 리팩터링이 이루어졌습니다. 관련 서비스, 유스케이스, 컨트롤러에 새로운 메서드와 엔드포인트가 추가되었습니다. FCM 토큰 등록 요청의 유효성 메시지도 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AlarmController
participant AlarmUseCase
participant AlarmQueryService
participant AlarmQueryServiceImpl
Client->>AlarmController: GET /api/alarms/off-count
AlarmController->>AlarmUseCase: getWeeklyRemainingOffCount(memberId)
AlarmUseCase->>AlarmQueryService: getWeeklyRemainingOffCount(memberId)
AlarmQueryService->>AlarmQueryServiceImpl: getWeeklyRemainingOffCount(memberId)
AlarmQueryServiceImpl-->>AlarmQueryService: AlarmRemainingOffCountResponse
AlarmQueryService-->>AlarmUseCase: AlarmRemainingOffCountResponse
AlarmUseCase-->>AlarmController: AlarmRemainingOffCountResponse
AlarmController-->>Client: ApplicationResponse<AlarmRemainingOffCountResponse>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Semgrep (1.128.1)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]} src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]} Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 8
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmInfoPreviewResponse.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java(2 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java(2 hunks)src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java(2 hunks)src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.java
📄 CodeRabbit Inference Engine (AGENT.md)
**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.
Files:
src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.javasrc/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmInfoPreviewResponse.javasrc/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.javasrc/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.javasrc/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
**/*{Controller,Service,Repository}.java
📄 CodeRabbit Inference Engine (AGENT.md)
Controller/Service는
getUser,createUser,removeUser등 동사 기반으로 명명하고, Repository는findByUser,insertUser,deleteUser처럼 명확히 구분합니다.
Files:
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.javasrc/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
🔇 Additional comments (5)
src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java (1)
10-10: 검증 메시지 명시 추가, 좋습니다.사용자 피드백 측면에서 더 친절해졌습니다. 변경 자체는 안전합니다.
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmInfoPreviewResponse.java (1)
49-49: 레코드 컴포넌트 정합성 OK마지막 컴포넌트 처리(후행 쉼표 없음) 정상입니다. 변경 의도와도 부합합니다.
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (3)
5-5: 새 DTO import 추가 적절새로운 응답 DTO 분리에 맞춘 import 추가가 정확합니다. 불필요한 import도 없습니다.
89-109: PR 목적과 일치: 잔여 횟수 조회 분리 및 로직 구성 적절회원 검증 → 주차 시작~현재 시간 범위 계산 → 주간 OFF 로그 count → 잔여치 계산 → DTO 반환 흐름이 명확합니다. 기존 미리보기 응답에서 필드 제거와의 일관성도 확보되었습니다.
100-102: createdAt BETWEEN 범위·타임존 검증 필요AlarmOffLogEntity가 상속하는 BaseTimeEntity의
createdAt타입과 JPA 변환 규칙에 따라
BETWEEN(start, end)절의 포함 범위·타임존 처리가 달라질 수 있습니다.
현재 의도(월요일 00:00:00 포함 ~ 현재 시각 포함)와 DB 질의가 정확히 일치하는지 추가 검증이 필요합니다.검토 포인트:
- BaseTimeEntity의
createdAt필드 타입(예: LocalDateTime vs OffsetDateTime) 및@CreatedDate등 어노테이션- DB 컬럼 타입(TIMESTAMP vs TIMESTAMP WITH TIME ZONE)과 Hibernate의 매핑 규칙
- JPA
between절이 양 끝점을 모두 포함하는지 여부 (JpaSpecificationBuilder 기준 모두 포함)다음 스크립트로 BaseTimeEntity의 선언을 확인해 주세요:
#!/bin/bash set -euo pipefail # BaseTimeEntity 파일 경로 조회 fd -e java BaseTimeEntity # BaseTimeEntity에서 createdAt 선언 및 어노테이션 확인 rg -n "class\s+BaseTimeEntity" -A 20 rg -n --no-heading -A 3 -B 3 "createdAt|@CreatedDate" $(fd -e java BaseTimeEntity)
| package akuma.whiplash.domains.alarm.application.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| @Schema(description = "남은 알람 끄기 횟수 응답") | ||
| public record AlarmRemainingOffCountResponse( | ||
| @Schema( | ||
| description = "회원의 남은 알람 끄기 횟수(회원당 매주 2회 부여, 매주 월요일 초기화)", | ||
| example = "1" | ||
| ) | ||
| Long remainingOffCount | ||
| ) {} No newline at end of file |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
remainingOffCount는 원시형(int) 사용을 권장 — 널 회피 및 의미상 적합
주간 잔여 횟수는 음수가 아니고 값의 범위가 작습니다. Long 대신 int(원시형)를 사용하면 널 가능성을 제거하고 의미에도 부합합니다. 코드베이스 내 일관성(다른 사용처에서 Long 기대 여부)도 함께 확인해 주세요.
-@Builder
@Schema(description = "남은 알람 끄기 횟수 응답")
public record AlarmRemainingOffCountResponse(
@Schema(
description = "회원의 남은 알람 끄기 횟수(회원당 매주 2회 부여, 매주 월요일 초기화)",
example = "1"
)
- Long remainingOffCount
+ int remainingOffCount
) {}참고: 단일 필드 레코드에서는 @Builder는 과하므로 제거 가능(선호도에 따름). 만약 API 전반에서 Builder 사용을 표준화했다면 유지해도 무방합니다. 이 변경을 적용할 경우, 컴파일 타임에 해당 타입 변경이 전파되므로 사용처 점검 부탁드립니다.
아래 스크립트를 통해 AlarmRemainingOffCountResponse의 사용처와 다른 DTO 응답 레코드들의 필드 타입 현황을 확인해주세요.
🏁 Script executed:
#!/bin/bash
# 1) AlarmRemainingOffCountResponse 사용 위치 검색
rg "AlarmRemainingOffCountResponse" -n
# 2) 다른 Response 레코드들의 선언 및 필드 타입 확인
rg "public record .*Response" -n -A 3Length of output: 5875
아래 스크립트로 AlarmRemainingOffCountResponse 빌더 사용부와 컨트롤러에서의 필드 접근 방식을 확인해주세요.
🏁 Script executed:
#!/bin/bash
# 1) AlarmRemainingOffCountResponse.builder() 사용 위치 확인
sed -n '80,140p' src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
# 2) 컨트롤러에서 remainingOffCount 접근 방식 확인
sed -n '100,140p' src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
# 3) remainingOffCount 호출 검색
rg "remainingOffCount" -nLength of output: 4231
원시형(int)으로 변경하여 널 가능성 제거 및 일관성 확보
아래 항목을 수정해주세요.
- AlarmRemainingOffCountResponse.java
Long remainingOffCount→int remainingOffCount
- AlarmQueryServiceImpl.java (src/main/java/…/AlarmQueryServiceImpl.java)
long count = Math.max(0, 2 - offCount);→int count = (int) Math.max(0, 2 - offCount);- builder 호출은 그대로 유지 (
.remainingOffCount(count))
- AlarmCommandServiceImpl.java (src/main/java/…/AlarmCommandServiceImpl.java)
remainingCount변수 타입을int로 변경하고, builder 호출부(.remainingOffCount(remainingCount))가 컴파일되도록 동기화
- 기타:
AlarmOffResultResponse등 다른 DTO는 이미int remainingOffCount를 사용 중이므로 일관성 유지
예시 diff:
--- a/src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java
+++ b/src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java
@@ public record AlarmRemainingOffCountResponse(
- Long remainingOffCount
+ int remainingOffCount
) {}--- a/src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
+++ b/src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
@@ public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
- long count = Math.max(0, 2 - offCount);
+ int count = (int) Math.max(0, 2 - offCount);
return AlarmRemainingOffCountResponse.builder()
.remainingOffCount(count)
.build();Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java
lines 1 to 14, change the type of remainingOffCount from Long to int to remove
nullability and ensure consistency. Then, in AlarmQueryServiceImpl.java, cast
the result of Math.max(0, 2 - offCount) to int when assigning to count, keeping
the builder call unchanged. In AlarmCommandServiceImpl.java, update the
remainingCount variable type to int and ensure the builder call with
remainingOffCount(remainingCount) compiles correctly. This aligns with other
DTOs like AlarmOffResultResponse that already use int for remainingOffCount.
| import akuma.whiplash.domains.alarm.application.dto.request.AlarmRegisterRequest; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmInfoPreviewResponse; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmOffResultResponse; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
UseCase에서 DTO 매핑으로 역할 분리 제안
현재 구조도 무방하지만, 도메인 레이어 의존성 축소를 위해 도메인 서비스는 원시 타입을 반환하고(예: long), UseCase에서 DTO로 감싸는 방식을 권장합니다.
도메인 서비스 시그니처를 long getRemainingOffCount(Long memberId)로 바꾼 경우, 본 메서드는 다음처럼 매핑합니다:
- public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
- return alarmQueryService.getRemainingOffCount(memberId);
- }
+ public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
+ long count = alarmQueryService.getRemainingOffCount(memberId);
+ return new AlarmRemainingOffCountResponse(count);
+ // 또는 Lombok Builder 사용 시:
+ // return AlarmRemainingOffCountResponse.builder().remainingOffCount(count).build();
+ }참고: 팀 컨벤션에 따라 본 메서드에 @Transactional(readOnly = true)를 부여하는 것도 고려할 수 있습니다(읽기 전용 쿼리).
Also applies to: 47-49
🏁 Script executed:
#!/bin/bash
echo "== AlarmUseCase.java 스니펫 확인 =="
rg "class AlarmUseCase" -A30 -B10 src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java
echo "== getRemainingOffCount 메서드 정의 및 호출 확인 =="
rg "getRemainingOffCount" -tjava -A5 -B5 srcLength of output: 6619
도메인 서비스는 원시 타입 반환, UseCase에서 DTO 매핑으로 책임 분리
현재 AlarmQueryService가 AlarmRemainingOffCountResponse DTO를 직접 반환하며 도메인 계층이 애플리케이션 레이어 DTO에 의존하고 있습니다. 다음과 같이 리팩토링을 권장합니다.
• AlarmQueryService 인터페이스와 구현체에서 DTO 대신 long 반환
• AlarmUseCase#getRemainingOffCount에서 반환된 long을 AlarmRemainingOffCountResponse로 매핑
• 필요 시 읽기 전용 메서드에 @Transactional(readOnly = true) 추가 검토
수정 대상:
- src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
- src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
- src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java
// AlarmQueryService.java
-public interface AlarmQueryService {
- AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId);
-}
+public interface AlarmQueryService {
+ long getRemainingOffCount(Long memberId);
+}// AlarmQueryServiceImpl.java
-@Override
-public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
- // ... 계산 로직
- return new AlarmRemainingOffCountResponse(count);
-}
+@Override
+public long getRemainingOffCount(Long memberId) {
+ // ... 계산 로직
+ return count;
+}// AlarmUseCase.java
-public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
- return alarmQueryService.getRemainingOffCount(memberId);
-}
+@Transactional(readOnly = true)
+public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) {
+ long count = alarmQueryService.getRemainingOffCount(memberId);
+ return new AlarmRemainingOffCountResponse(count);
+}이렇게 하면 도메인 레이어의 의존성을 줄이고, UseCase가 DTO 매핑을 책임지도록 분리할 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; | |
| public interface AlarmQueryService { | |
| long getRemainingOffCount(Long memberId); | |
| } |
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; | |
| @Override | |
| public long getRemainingOffCount(Long memberId) { | |
| // ... 계산 로직 | |
| return count; | |
| } |
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; | |
| @Transactional(readOnly = true) | |
| public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) { | |
| long count = alarmQueryService.getRemainingOffCount(memberId); | |
| return new AlarmRemainingOffCountResponse(count); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
and AlarmQueryServiceImpl.java around line 7, refactor the method that currently
returns AlarmRemainingOffCountResponse DTO to instead return a primitive long
value. Then, in
src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java,
update the getRemainingOffCount method to call the domain service returning long
and map this long value to an AlarmRemainingOffCountResponse DTO before
returning it. Also consider adding @Transactional(readOnly = true) to the
UseCase method if it is a read-only operation. This separates domain logic from
DTO mapping and reduces domain layer dependencies on application layer DTOs.
|
|
||
| import akuma.whiplash.domains.alarm.application.dto.etc.OccurrencePushInfo; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmInfoPreviewResponse; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
🛠️ Refactor suggestion
Domain 서비스가 Application DTO에 의존하고 있습니다 — 계층 분리 권장
domain.service 인터페이스가 application.dto.response를 직접 반환하면 계층 역참조가 발생합니다. 도메인 서비스는 원시 타입(예: long) 또는 도메인 VO를 반환하고, UseCase에서 DTO로 매핑하는 구조가 바람직합니다.
아래처럼 반환 타입을 단순화하면 계층 결합을 낮출 수 있습니다.
-import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse;
+// 애플리케이션 DTO 의존 제거
...
- AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId);
+ long getRemainingOffCount(Long memberId);추가로, 구현체(AlarmQueryServiceImpl)와 UseCase에서 해당 변경에 맞춰 매핑을 적용해 주세요.
Also applies to: 12-12
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
at line 5 and also around line 12, the domain service interface is directly
returning an Application DTO, causing unwanted layer coupling. To fix this,
change the return type in the domain service interface to a primitive type like
long or a domain value object instead of the DTO. Then, update the
implementation class AlarmQueryServiceImpl and the UseCase layer to handle the
mapping from the domain return type to the Application DTO accordingly, ensuring
proper separation of concerns between layers.
| @Override | ||
| public AlarmRemainingOffCountResponse getRemainingOffCount(Long memberId) { | ||
| memberRepository | ||
| .findById(memberId) | ||
| .orElseThrow(() -> ApplicationException.from(MemberErrorCode.MEMBER_NOT_FOUND)); | ||
|
|
||
| LocalDate today = LocalDate.now(); | ||
| LocalDate monday = today.with(DayOfWeek.MONDAY); | ||
| LocalDateTime weekStart = monday.atStartOfDay(); | ||
| LocalDateTime now = LocalDateTime.now(); | ||
|
|
||
| long offCount = alarmOffLogRepository.countByMemberIdAndCreatedAtBetween( | ||
| memberId, weekStart, now | ||
| ); | ||
|
|
||
| long count = Math.max(0, 2 - offCount); | ||
|
|
||
| return AlarmRemainingOffCountResponse.builder() | ||
| .remainingOffCount(count) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
메서드 설명 주석(의도·정책 명시) 추가 제안
- “기준 주차: 월요일 00:00~현재”, “주간 OFF 허용치: 2회” 같은 정책을 Javadoc에 명시하면 유지보수에 도움이 됩니다.
- 엔드포인트 명칭은 주간 기준이므로 필요시
getWeeklyRemainingOffCount등으로의 리네이밍도 고려해볼 만합니다(비호환 변경이므로 선택 사항).
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
around lines 89 to 109, add a Javadoc comment above the getRemainingOffCount
method to clearly state the method's intent and policy, including that the
reference week is from Monday 00:00 to the current time and the weekly OFF
allowance is 2 times. Optionally, consider renaming the method to
getWeeklyRemainingOffCount to better reflect its weekly scope, but this is not
mandatory due to potential backward compatibility issues.
| LocalDate today = LocalDate.now(); | ||
| LocalDate monday = today.with(DayOfWeek.MONDAY); | ||
| LocalDateTime weekStart = monday.atStartOfDay(); | ||
| LocalDateTime now = LocalDateTime.now(); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
now/주차 기준 개선: Clock 주입 및 Monday 계산 명시화 권장
- 테스트 가능성과 타임존 일관성을 위해
Clock주입 사용을 권장합니다. today.with(DayOfWeek.MONDAY)대신TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)로 의도를 명시하면 가독성이 좋아집니다.
적용 예시(diff는 현재 범위 내 변경만 포함):
- LocalDate today = LocalDate.now();
- LocalDate monday = today.with(DayOfWeek.MONDAY);
- LocalDateTime weekStart = monday.atStartOfDay();
- LocalDateTime now = LocalDateTime.now();
+ LocalDate today = LocalDate.now(clock);
+ LocalDate monday = today.with(java.time.temporal.TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
+ LocalDateTime weekStart = monday.atStartOfDay();
+ LocalDateTime now = LocalDateTime.now(clock);범위 밖 변경(참고):
// 필드 주입
private final java.time.Clock clock;
// 구성 클래스 등지에서 Bean 제공 (예: KST 고정이 필요하다면)
@Bean
public Clock clock() {
return Clock.system(ZoneId.of("Asia/Seoul"));
}🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
around lines 95 to 99, replace the direct calls to LocalDate.now() and
LocalDateTime.now() with calls that use an injected Clock instance to improve
testability and timezone consistency. Also, change the calculation of Monday
from today.with(DayOfWeek.MONDAY) to use
TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY) for clearer intent. Inject
the Clock as a final field and use it in the date/time calls accordingly.
| memberId, weekStart, now | ||
| ); | ||
|
|
||
| long count = Math.max(0, 2 - offCount); |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
매직 넘버 제거 및 명시적 long 사용(가독성/오류 예방)
주간 제한치 2는 상수로 추출하고, 변수명도 의미를 드러내도록 변경하면 명확합니다. 또한 Math.max 인자 타입을 long으로 명시해 혼동을 줄입니다.
- long count = Math.max(0, 2 - offCount);
+ long remainingOffCount = Math.max(0L, WEEKLY_OFF_LIMIT - offCount);
...
- .remainingOffCount(count)
+ .remainingOffCount(remainingOffCount)범위 밖 변경(참고):
private static final long WEEKLY_OFF_LIMIT = 2L;Also applies to: 107-107
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
at lines 104 and 107, replace the magic number 2 with a clearly named constant
like WEEKLY_OFF_LIMIT declared as a static final long. Also, ensure the
arguments passed to Math.max are explicitly long types to improve readability
and prevent type confusion.
| import akuma.whiplash.domains.alarm.application.dto.request.AlarmRegisterRequest; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmInfoPreviewResponse; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmOffResultResponse; | ||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
신규 엔드포인트 설계 적절 — 오류 코드 문서화 및 테스트 보강 제안
구현은 명확하고 일관적입니다. 두 가지 보강점을 제안합니다.
- PERMISSION_DENIED 문서화 여부 확인: 다른 엔드포인트 일부는
authErrorCodes = {PERMISSION_DENIED}를 명시합니다. 현재getAlarms와 동일하게 생략되어 있는데, 전역 예외 매핑 정책에 따른 의도인지 확인 부탁드립니다. - 테스트 추가:
/api/alarms/off-count에 대한 MVC/통합 테스트(성공 케이스, 회원 없음, 인증 실패 등) 추가를 권장합니다.
원하시면 해당 엔드포인트용 Spring MVC 테스트 템플릿을 생성해 드리겠습니다.
Also applies to: 110-118
🏁 Script executed:
#!/bin/bash
# Display the controller definition and imports
rg -n "class AlarmController" -A15 -B15 src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
# Show the new /off-count endpoint snippet
rg -n "off-count" -A5 -B5 src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
# Show the existing getAlarms endpoint for comparison
rg -n "getAlarms" -A5 -B5 src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
# Find occurrences of PERMISSION_DENIED across the codebase
rg -n "PERMISSION_DENIED"
# Check for authErrorCodes usage in controllers
rg -n "authErrorCodes" -A3 -B3 src/main/java/akuma/whiplash/domainsLength of output: 8965
🏁 Script executed:
#!/bin/bash
# Search for any controller tests referencing the /off-count endpoint
rg -n "/off-count" -g "src/test/java/**/*.java"
# Search for tests referencing getRemainingOffCount method
rg -n "getRemainingOffCount" -g "src/test/java/**/*.java"
# Search for tests referencing AlarmController directly
rg -n "AlarmController" -g "src/test/java/**/*.java"Length of output: 158
/api/alarms/off-count 엔드포인트: PERMISSION_DENIED 생략은 의도된 패턴, 테스트 추가 필요
getAlarms와 동일하게 자신의 리소스만 조회하기 때문에 authErrorCodes = {PERMISSION_DENIED}를 명시하지 않은 것이 일관된 설계입니다. 별도 변경은 필요하지 않습니다.
다만, 현재 src/test/java 경로에 해당 엔드포인트를 검증하는 Spring MVC/통합 테스트가 존재하지 않습니다. 아래 시나리오를 커버하는 테스트를 추가해 주세요:
- 정상 조회 (인증된 회원의 off-count 반환)
- 인증 실패 (무효 토큰 또는 비인증)
- MEMBER_NOT_FOUND (인증 토큰은 유효하나 회원이 DB에 없는 경우)
필요하시면 Spring MVC 테스트 템플릿을 제공해 드리겠습니다.
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
at line 13, no code change is needed for the /api/alarms/off-count endpoint
regarding permission handling, but you must add Spring MVC or integration tests
under src/test/java to cover this endpoint. Implement tests for successful
retrieval by an authenticated user, authentication failure due to invalid or
missing token, and the case where the token is valid but the member is not found
in the database.
| @Schema(description = "FCM 토큰", example = "djkhsa01whjas") | ||
| @NotBlank | ||
| @NotBlank(message = "FCM 토큰을 입력해주세요.") | ||
| String fcmToken |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
국제화/일관성: 하드코딩 메시지 대신 메시지 코드 사용을 고려해주세요.
팀에서 MessageSource(예: messages.properties)을 사용한다면, 직접 문자열 대신 메시지 코드를 사용하는 편이 일관성과 다국어 확장성에 유리합니다.
아래처럼 변경을 제안드립니다:
- @NotBlank(message = "FCM 토큰을 입력해주세요.")
+ @NotBlank(message = "{validation.fcmToken.notBlank}")(messages.properties 예시)
validation.fcmToken.notBlank=FCM 토큰을 입력해주세요.
원하시면 messages 파일 생성/추가까지 포함한 패치도 드리겠습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Schema(description = "FCM 토큰", example = "djkhsa01whjas") | |
| @NotBlank | |
| @NotBlank(message = "FCM 토큰을 입력해주세요.") | |
| String fcmToken | |
| @Schema(description = "FCM 토큰", example = "djkhsa01whjas") | |
| @NotBlank(message = "{validation.fcmToken.notBlank}") | |
| String fcmToken |
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/auth/application/dto/request/RegisterFcmTokenRequest.java
around lines 9 to 11, the @NotBlank annotation uses a hardcoded validation
message string. To improve internationalization and consistency, replace the
hardcoded message with a message code key that refers to an entry in
messages.properties (e.g., "validation.fcmToken.notBlank"). Update the
annotation to use the message code instead of the direct string, and add the
corresponding key-value pair in the messages.properties file.
- 주간 끄기 제한 횟수 상수로 변경
There was a problem hiding this comment.
Actionable comments posted: 3
🔭 Outside diff range comments (1)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (1)
107-109: validAlarmOwner 인자 순서 일관성 정리동일 클래스 내 다른 사용처(Line 181-182)와 달리 여기서는 인자 순서가 반대로 전달됩니다. 검증 자체는 대칭이지만, 가독성과 일관성을 위해 아래처럼 통일을 권장합니다.
- validAlarmOwner(findAlarm.getMember().getId(), memberId); + validAlarmOwner(memberId, findAlarm.getMember().getId());
♻️ Duplicate comments (5)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java (1)
5-6: 도메인 서비스가 Application DTO에 의존 — 반환 타입을 원시 타입으로 분리하세요도메인 계층이
application.dto.response에 의존하지 않도록long등 원시 타입으로 반환하고, DTO 매핑은 UseCase에서 처리하는 구조가 바람직합니다.예시 diff:
-import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; +// Application DTO 의존 제거 @@ public interface AlarmQueryService { @@ - AlarmRemainingOffCountResponse getWeeklyRemainingOffCount(Long memberId); + long getWeeklyRemainingOffCount(Long memberId); }추가 작업:
- AlarmQueryServiceImpl: 계산 로직은 유지하되
long반환으로 수정- AlarmUseCase:
long→AlarmRemainingOffCountResponse로 매핑필요하시면 Impl/UseCase까지 반영된 전체 패치 제안을 드리겠습니다.
Also applies to: 12-13
src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java (1)
47-49: UseCase에서 DTO 매핑 책임을 가지도록 변경도메인 서비스가
long을 반환하도록 리팩터링할 경우, 본 메서드는 다음처럼 매핑하면 계층 분리가 선명해집니다.- public AlarmRemainingOffCountResponse getWeeklyRemainingOffCount(Long memberId) { - return alarmQueryService.getWeeklyRemainingOffCount(memberId); - } + public AlarmRemainingOffCountResponse getWeeklyRemainingOffCount(Long memberId) { + long count = alarmQueryService.getWeeklyRemainingOffCount(memberId); + return new AlarmRemainingOffCountResponse((int) count); + // 팀 컨벤션상 Builder 유지 시: + // return AlarmRemainingOffCountResponse.builder().remainingOffCount((int) count).build(); + }선택 사항:
- 읽기 전용 메서드에
@Transactional(readOnly = true)부여 검토.src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (3)
43-44: 타입 일관성 및 네이밍 개선(미세 최적화)
- offCount가 long이므로, 상수와 계산도 long으로 맞추면 캐스팅 없이 읽기 쉬워집니다.
- 변수명은 의미가 드러나도록 remainingOffCount로 제안합니다.
적용 예시:
- private static final int WEEKLY_OFF_LIMIT = 2; + private static final long WEEKLY_OFF_LIMIT = 2L; ... - int count = (int) Math.max(0, WEEKLY_OFF_LIMIT - offCount); + long remainingOffCount = Math.max(0L, WEEKLY_OFF_LIMIT - offCount); ... - return AlarmRemainingOffCountResponse.builder() - .remainingOffCount(count) - .build(); + return AlarmRemainingOffCountResponse.builder() + .remainingOffCount((int) remainingOffCount) // DTO 필드가 long이면 캐스팅 제거 + .build();Also applies to: 106-110
97-101: now/주차 기준 개선: Clock 주입 및 Monday 계산 명시화 권장
- 테스트 가능성과 타임존 일관성을 위해 Clock 주입 사용을 권장합니다.
- Monday 계산은 TemporalAdjusters.previousOrSame로 의도를 명시하면 가독성이 좋아집니다.
범위 내 변경 예시:
- LocalDate today = LocalDate.now(); - LocalDate monday = today.with(DayOfWeek.MONDAY); + LocalDate today = LocalDate.now(clock); + LocalDate monday = today.with(java.time.temporal.TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); LocalDateTime weekStart = monday.atStartOfDay(); - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = LocalDateTime.now(clock);범위 밖(참고):
- 필드:
private final java.time.Clock clock;- import 추가:
import java.time.Clock;,import java.time.temporal.TemporalAdjusters;- 구성에서 KST 고정이 필요하면
Clock.system(ZoneId.of("Asia/Seoul"))주입 고려.
91-93: 메서드 정책 Javadoc 추가 제안주간 한도(2회), 기준 주차(월요일 00:00~현재), 경계 포함 여부를 Javadoc에 명시하면 유지보수에 유리합니다. 필요시 엔드포인트/메서드명이 주간 기준임을 드러내도록 선택적 리네이밍 고려.
예시:
/** * 회원의 이번 주(월요일 00:00 ~ 현재) 알람 끄기(OFF) 잔여 횟수를 조회합니다. * - 주간 OFF 허용치: 2회 * - 조회 구간 경계: [weekStart, now] (양 끝단 포함/제외 여부를 명시) * - 타임존: Clock 기준(예: Asia/Seoul) */
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.java(2 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java(2 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java(3 hunks)src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*{Controller,Service,Repository}.java
📄 CodeRabbit Inference Engine (AGENT.md)
Controller/Service는
getUser,createUser,removeUser등 동사 기반으로 명명하고, Repository는findByUser,insertUser,deleteUser처럼 명확히 구분합니다.
Files:
src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java
**/*.java
📄 CodeRabbit Inference Engine (AGENT.md)
**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.
Files:
src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.javasrc/main/java/akuma/whiplash/domains/alarm/application/usecase/AlarmUseCase.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.javasrc/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java
🧬 Code Graph Analysis (3)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (2)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandService.java (1)
AlarmCommandService(10-18)src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java (1)
Entity(27-89)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryService.java (1)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOffLogRepository.java (1)
AlarmOffLogRepository(10-24)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (1)
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOffLogRepository.java (2)
countByMemberIdAndCreatedAtBetween(11-15)AlarmOffLogRepository(10-24)
🔇 Additional comments (3)
src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java (1)
110-118: LGTM — 엔드포인트 계약 및 에러 코드 구성이 기존 패턴과 일치합니다
getAlarms와 동일한 패턴으로authErrorCodes생략 유지, 문서화도 적절합니다.src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (2)
5-5: 새 DTO import 적절합니다신규 응답 DTO 분리 의도와 일치합니다. 추가적인 포맷/정렬 변경은 불필요합니다.
102-104: BETWEEN 경계 포함성/타임존 정합성 확인 필요
- Spring Data JPA의 Between은 양 끝단 포함입니다. weekStart(월 00:00) 포함, now 포함이 의도인지 확인이 필요합니다.
- DB 저장 타임존(UTC/KST)과 애플리케이션 Clock의 Zone이 불일치하면 주차 경계(월요일 00:00)에서 off-by-one 이슈가 날 수 있습니다.
권장 검증:
- 경계 케이스 단위 테스트 추가
- 일: 월요일 00:00:00 정확히 기록 시 포함되는지
- 일: 직전 일요일 23:59:59.xxx 기록 시 제외되는지
- now 시점과 동일 타임스탬프 기록 포함 여부 확인(의도 결정 후 테스트 고정)
| import lombok.Builder; | ||
|
|
||
| @Builder |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
단일 필드 record에서 @builder는 과합니다 + 최소값 스키마 명시 제안
- 단일 필드 record에서는
@Builder없이도 가독성과 생성 용이성이 충분합니다. 팀 컨벤션에 특별한 이유가 없다면 제거를 권장합니다. remainingOffCount는 음수가 되지 않으므로 스키마에 최소값을 명시하면 API 문서 품질이 좋아집니다.
예시 diff:
-import lombok.Builder;
+// import 제거
-@Builder
@Schema(description = "남은 알람 끄기 횟수 응답")
public record AlarmRemainingOffCountResponse(
@Schema(
description = "회원의 남은 알람 끄기 횟수(회원당 매주 2회 부여, 매주 월요일 초기화)",
- example = "1"
+ example = "1",
+ minimum = "0"
)
int remainingOffCount
) {}Also applies to: 9-13
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java
lines 4 to 6 and 9 to 13, remove the @Builder annotation from the single-field
record to simplify the code and improve readability. Additionally, add a minimum
value constraint to the remainingOffCount field to ensure it cannot be negative,
enhancing the API schema documentation quality.
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
Show resolved
Hide resolved
| @CustomErrorCodes(memberErrorCodes = {MEMBER_NOT_FOUND}) | ||
| @Operation(summary = "남은 알람 끄기 횟수 조회", description = "회원의 이번 주 남은 알람 끄기 횟수를 조회합니다.") | ||
| @GetMapping("/off-count") | ||
| public ApplicationResponse<AlarmRemainingOffCountResponse> getWeeklyRemainingOffCount( | ||
| @AuthenticationPrincipal MemberContext memberContext | ||
| ) { | ||
| AlarmRemainingOffCountResponse response = alarmUseCase.getWeeklyRemainingOffCount(memberContext.memberId()); | ||
| return ApplicationResponse.onSuccess(response); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
신규 엔드포인트 설계는 명확 — 테스트 커버리지 추가 필요
계약, 에러코드, 문서화 모두 일관적입니다. 다음 시나리오의 MVC/통합 테스트를 추가해 주세요:
- 정상 조회(인증된 사용자의 off-count 반환)
- 인증 실패(무효 토큰/비인증)
- MEMBER_NOT_FOUND(유효 토큰이나 회원 미존재)
검증용 스크립트:
원하시면 MockMvc 기반 테스트 템플릿을 제공합니다.
🏁 Script executed:
#!/bin/bash
# /off-count 엔드포인트 테스트 존재 여부 점검
rg -n "/api/alarms/off-count" -g "src/test/java/**/*.java" -A2 -B2 || trueLength of output: 73
테스트 추가 필요: /api/alarms/off-count 엔드포인트
현재 이 엔드포인트에 대한 테스트가 없습니다. 아래 시나리오를 MockMvc 기반 MVC/통합 테스트로 보강해 주세요:
- 정상 조회
- 인증된 사용자의 off-count 반환 검증
- 인증 실패
- 무효 토큰 또는 비인증 요청 시 401 응답 확인
- MEMBER_NOT_FOUND
- 유효 토큰이지만 회원이 존재하지 않을 때 정의된 에러 코드 및 메시지 검증
추가 대상 예시 위치:
- src/test/java/.../alarm/presentation/AlarmControllerTest.java
- 또는 통합 테스트 패키지 내 적절한 클래스
원하시면 MockMvc 테스트 템플릿을 제공합니다.
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/domains/alarm/presentation/AlarmController.java
around lines 110 to 118, the /api/alarms/off-count endpoint lacks tests. Add
MockMvc-based MVC/integration tests covering: successful retrieval of off-count
for authenticated users, 401 responses for invalid or unauthenticated requests,
and handling of MEMBER_NOT_FOUND error with proper error code and message. Place
these tests in src/test/java/.../alarm/presentation/AlarmControllerTest.java or
an appropriate integration test class.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java (1)
4-6: 단일 필드 record의 @builder 제거 권장(중복 제안)단일 필드 응답 DTO에서 @builder는 과합니다. 팀 컨벤션상 빌더 강제 사용이 아니라면 제거해 단순화해 주세요. import도 함께 정리 가능합니다.
-import lombok.Builder; - -@Builder @Schema(description = "남은 알람 끄기 횟수 응답") public record AlarmRemainingOffCountResponse(src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (1)
166-167: Math.max를 이용한 음수 방지 및 상수 사용 적용 LGTM
WEEKLY_OFF_LIMIT기반 계산으로 하드코딩 제거- 음수 방지 로직 반영
현 구현은 의도에 부합합니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java(1 hunks)src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java(3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
📄 CodeRabbit Inference Engine (AGENT.md)
**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.
Files:
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.javasrc/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java
🔇 Additional comments (1)
src/main/java/akuma/whiplash/domains/alarm/application/dto/response/AlarmRemainingOffCountResponse.java (1)
9-15: int 타입 + 최소값 명시 LGTM
- 필드 타입을 int로 두고, 스키마에 minimum="0"을 명시한 점이 명확하고 일관적입니다.
| private String sheetRange; | ||
|
|
||
| private static final double CHECKIN_RADIUS_METERS = 100.0; | ||
| private static final int WEEKLY_OFF_LIMIT = 2; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
WEEKLY_OFF_LIMIT 상수의 단일 출처화 제안
한 곳(예: domain 상수 클래스 또는 설정값)으로 상수 출처를 통일하세요. 커맨드/쿼리 양쪽에서 동일 상수를 참조하면 변경 비용과 불일치 리스크를 줄일 수 있습니다.
예시(새 상수 클래스 추가):
// src/main/java/akuma/whiplash/domains/alarm/domain/constant/AlarmLimits.java
package akuma.whiplash.domains.alarm.domain.constant;
public final class AlarmLimits {
private AlarmLimits() {}
public static final int WEEKLY_OFF_LIMIT = 2;
}이 파일 적용:
- private static final int WEEKLY_OFF_LIMIT = 2;
+ // TODO: 상수 단일 출처화 완료 후 로컬 상수 제거 예정
+ // private static final int WEEKLY_OFF_LIMIT = 2;그리고 사용부:
import static akuma.whiplash.domains.alarm.domain.constant.AlarmLimits.WEEKLY_OFF_LIMIT;🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
at line 70, the WEEKLY_OFF_LIMIT constant should be centralized to avoid
duplication and inconsistency. Move the WEEKLY_OFF_LIMIT constant to a new
dedicated constants class, for example, AlarmLimits in the domain.constant
package. Then, update this file and any other usage locations to import and
reference WEEKLY_OFF_LIMIT from that new constants class.
| // 5. 제한 초과 시 예외 발생 | ||
| if (weeklyOffCount >= 2) { | ||
| if (weeklyOffCount >= WEEKLY_OFF_LIMIT) { | ||
| throw ApplicationException.from(ALARM_OFF_LIMIT_EXCEEDED); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
주간 OFF 제한 체크의 경쟁 조건(race condition) 가능성
동일 회원에 대한 동시 요청 두 건이 같은 주 내에 거의 동시에 들어오면, 둘 다 weeklyOffCount를 낮게 읽고 제한을 통과해 초과 허용될 수 있습니다. 단일 트랜잭션이어도 별 트랜잭션 간에는 상호 가시성이 없어 방지되지 않습니다.
권장 대응(택1 또는 병행):
- DB 잠금 기반 직렬화: 알람 끄기 처리 시작 시 회원 레코드(또는 회원-주 단위 집계 레코드)에 PESSIMISTIC_WRITE 락을 걸어 체크/기록을 직렬화.
- 사후 검증 롤백: 저장 직후 재집계하여 한도를 초과하면 예외를 던져 전체 트랜잭션 롤백. 단, 격리수준/잠금 없이는 동시 커밋 간 틈이 남습니다. 잠금과 병행 권장.
예시(Repository + 사용부):
// MemberRepository.java
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select m from MemberEntity m where m.id = :id")
Optional<MemberEntity> findByIdForUpdate(@Param("id") Long id);// AlarmCommandServiceImpl.alarmOff(...)
MemberEntity lockedMember = memberRepository.findByIdForUpdate(memberId)
.orElseThrow(() -> ApplicationException.from(MemberErrorCode.MEMBER_NOT_FOUND));
// 이후 weeklyOffCount 조회/검증/저장 수행또는 회원-주 키를 가지는 별도 테이블(예: member_weekly_off_counter)에 unique(member_id, week_start) + 남은 횟수 컬럼을 두고, 행 잠금으로 원자적 감소를 수행하는 방식도 견고합니다.
검증 요청: 동시성 테스트(동일 memberId로 병렬 호출 3회)에서 초과가 허용되지 않는지 확인 부탁드립니다.
아래 스크립트로 동시성 제약(락/트랜잭션) 적용 여부를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# AlarmCommandServiceImpl 내 weeklyOffCount 사용 위치 확인
rg "weeklyOffCount" -n src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
# 알람 끄기 메서드 시그니처 및 트랜잭션 어노테이션 확인
rg -n -A2 "class AlarmCommandServiceImpl" src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
rg -n "@Transactional" src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
# MemberRepository 에서 PESSIMISTIC_WRITE 락 메서드 정의 여부 확인
rg -n "interface MemberRepository" -A5 src/main/java/akuma/whiplash/domains
rg -n "@Lock" src/main/java/akuma/whiplash/domainsLength of output: 1373
경쟁 조건 방지를 위해 PESSIMISTIC_WRITE 락 또는 원자적 감소 로직 적용 필요
AlarmCommandServiceImpl.java (115–124행)에서 트랜잭션 기본 격리 수준(READ_COMMITTED) 하에 weeklyOffCount 조회 → 검증 → 기록이 이루어지므로, 동일 회원에 대해 병렬 요청 시 둘 다 낮은 카운트를 읽고 한도를 초과 허용할 수 있습니다. 현재 MemberRepository에는 락 메서드가 없습니다.
수정 제안 (택1 또는 병행):
- MemberRepository에 PESSIMISTIC_WRITE 락 메서드 추가
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select m from MemberEntity m where m.id = :id") Optional<MemberEntity> findByIdForUpdate(@Param("id") Long id);
- AlarmCommandServiceImpl.alarmOff(...) 시작부에서 위 메서드로 회원 조회 후 한도 체크·저장 직렬화
MemberEntity lockedMember = memberRepository.findByIdForUpdate(memberId) .orElseThrow(() -> ApplicationException.from(MemberErrorCode.MEMBER_NOT_FOUND)); // 이후 weeklyOffCount 조회/검증/로그 저장
또는
member_weekly_off_counter엔티티(또는 테이블)에 unique(member_id, week_start) + 남은횟수 컬럼을 두고
UPDATE … SET remaining_count = remaining_count - 1 WHERE member_id = :id AND week_start = :start AND remaining_count > 0
처럼 원자적 감소 방식 적용
동일 memberId로 병렬 3회 호출 시 한도가 절대 초과되지 않는지 확인하는 동시성 테스트도 병행해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
around lines 115 to 124, the current logic reads and checks weeklyOffCount
without locking, causing race conditions under concurrent requests. To fix this,
add a PESSIMISTIC_WRITE lock method in MemberRepository to fetch the member
entity with a database lock, then use this locked entity in alarmOff(...) to
perform the count check and update atomically. Alternatively, implement an
atomic decrement query on a dedicated member_weekly_off_counter table with a
unique constraint on member_id and week_start to ensure the limit is never
exceeded. Also, add concurrency tests to verify the fix.
📄 PR 요약
✍🏻 PR 상세
GET /api/alarms에서 remaingOffCount 제거GET /api/alarms/off-count구현👀 참고사항
✅ 체크리스트
🚪 연관된 이슈 번호
Closes #47
Summary by CodeRabbit
신규 기능
버그 수정
스타일