Refactor/#102 improve 10s interval push#104
Conversation
오늘이 반복 요일이고, 알람이 울리는 시간이 현재 시각보다 뒤라면 오늘 날짜를 반환
testcontainer 1.21.x 버전에서 docker-java 라이브러리의 기본 버전은 1.32를 사용하고 있으나 도커 엔진 v29부터는 docker-java 1.44를 최소 버전으로 요구함.
WalkthroughRedis 기반 울리는 알람 상태 관리 시스템을 도입하여 기존 DB 쿼리 방식을 대체합니다. 새로운 Redis 저장소, 스케줄러 수정, 도메인 서비스 업데이트, 날짜 유틸리티 확장 및 포괄적인 테스트를 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant AlarmCmd as AlarmCommandService
participant Redis as RingingAlarmRedisRepository
participant Log as AlarmRingingLogRepo
participant Scheduler as AlarmRingingNotificationScheduler
participant FCM as FCM Service
rect rgba(100, 200, 150, 0.5)
Note over AlarmCmd,Redis: 1. 알람 울림 (Ring)
AlarmCmd->>Log: 알람 울림 로그 저장
Log-->>AlarmCmd: 저장 완료
AlarmCmd->>Redis: add(alarmId, memberId, epochMillis)
Redis-->>AlarmCmd: Redis Sorted Set 등록
end
rect rgba(100, 150, 200, 0.5)
Note over Scheduler,Redis: 2. 스케줄러 폴링
Scheduler->>Redis: getRingingAlarms()
Redis-->>Scheduler: 현재 울려야 할 알람 목록
end
rect rgba(200, 150, 100, 0.5)
Note over Scheduler,FCM: 3. 푸시 알림 전송
Scheduler->>FCM: 푸시 알림 발송
FCM-->>Scheduler: 전송 완료
Scheduler->>Redis: remove(alarmId, memberId)
Redis-->>Scheduler: 알람 제거
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. 🔧 OpenGrep (1.16.1)src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmRingingNotificationScheduler.javaOpenGrep SIGABRT (exit code 2) src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.javaOpenGrep SIGABRT (exit code 2) src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmOccurrenceBatchService.javaOpenGrep SIGABRT (exit code 2)
🔧 Semgrep (1.152.0)src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmRingingNotificationScheduler.java/usr/local/lib/python3.11/dist-packages/requests/init.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (6.0.0.post1)/charset_normalizer (3.4.4) doesn't match a supported version! src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmOccurrenceBatchService.java/usr/local/lib/python3.11/dist-packages/requests/init.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (6.0.0.post1)/charset_normalizer (3.4.4) doesn't match a supported version! src/main/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepository.java/usr/local/lib/python3.11/dist-packages/requests/init.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (6.0.0.post1)/charset_normalizer (3.4.4) doesn't match a supported version!
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.claude/CLAUDE.md:
- Around line 250-260: Add a trailing newline at the end of the file so the
"Claude Code 관련 질문 처리" section (the block containing the curl example `curl
https://code.claude.com/docs/ko/overview.md` and the documentation URL pattern)
ends with a single Unix-style newline character; update the file so the final
line is followed by one newline to satisfy POSIX/newline conventions.
- Around line 208-230: Add a language identifier to the commit message code
block so syntax highlighting is explicit; update the fenced code block
containing "[`#이슈번호`] :Emoji: <type>: <subject>" to use a language tag (e.g.,
```text) so the template at that snippet is rendered consistently and clearly.
- Around line 28-74: The fenced code blocks in the architecture doc lack
language specifiers which triggers markdown lint warnings; update both blocks
(the layer structure block containing "presentation (Controller) ..." and the
package tree block starting with "akuma.whiplash/" that mentions
RingingAlarmRedisRepository) to use a language tag (e.g., change ``` to ```text)
so the linter stops flagging them.
- Around line 110-207: Fix the fenced code block lint warnings in
.claude/CLAUDE.md by adding explicit language identifiers and ensuring a blank
line immediately after the opening fence for the example blocks (the structure
diagram containing "{원본클래스}Test" and the Java example starting with
"@DisplayName(\"AlarmCommandService Unit Test\")"); update the opening fences to
use ```text and ```java respectively and insert the required blank line so the
static analyzer stops flagging those blocks.
In
`@src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java`:
- Around line 199-200: AlarmCommandServiceImpl currently calls Redis operations
(e.g., ringingAlarmRedisRepository.remove and the other Redis calls around the
same spots) directly inside the DB transaction which can lead to DB/Redis state
divergence on rollback; change these to run only after a successful commit by
deferring them via a transaction synchronization or after-commit event: remove
the direct calls to ringingAlarmRedisRepository.remove (and the other Redis
calls in the same methods) and instead register a post-commit action (e.g.,
TransactionSynchronizationManager.registerSynchronization(...) with
afterCommit() or publish a domain event and handle it with
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT)) that
performs the same repository calls; keep the same method names and parameters
when invoking the repository in the after-commit handler so the logic is
identical but executed only after DB commit.
In
`@src/main/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepository.java`:
- Around line 38-41: Rename the repository methods that use vague verbs (add,
getRingingAlarms, remove) to explicit persistence-style names to match the
coding guidelines: change add(...) to insertRingingAlarm(...),
getRingingAlarms(...) to findRingingAlarms(...), and remove(...) to
deleteRingingAlarm(...) (and apply the same pattern for the other similar
methods referenced around the file). Update all callers to use the new method
names and ensure JavaDoc/signatures reflect the new intent; leave internal logic
(e.g., use of redisTemplate.opsForZSet) unchanged.
- Around line 56-63: The stream mapping in RingingAlarmRedisRepository that
converts members into RingingPushInfo using the lambda (member -> { String[]
parts = member.split(":"); return RingingPushInfo.builder()... }) will throw on
malformed entries and must defensively skip them; update the mapping to first
validate parts.length == 2 and safely parse alarmId/memberId catching
NumberFormatException (or use a parse helper) and on parse failure log/debug the
bad member and return an empty Optional or filter it out so only valid
RingingPushInfo instances are collected, ensuring the scheduler won’t fail due
to a single bad member string.
In
`@src/test/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceTest.java`:
- Around line 67-70: Add verification that
ringingAlarmRedisRepository.remove(...) is called in the successful checkinAlarm
and removeAlarm test cases: update AlarmCommandServiceTest to assert
ringingAlarmRedisRepository.remove(...) is invoked (with the expected alarm id
or key) in the success paths of the test methods that exercise checkinAlarm and
removeAlarm, so the new mock is validated and Redis-sync removal is covered.
In `@src/test/java/akuma/whiplash/global/util/date/DateUtilTest.java`:
- Line 13: 현재 DateUtilTest 클래스의 `@DisplayName`("DateUtil Unit Test")는 규칙에 맞지 않으므로
`@DisplayName` 값을 문장형 도메인 설명으로 바꾸세요; 예컨대 DateUtil의 동작을 설명하는 문장(예: "DateUtil이 날짜를
지정된 포맷으로 변환한다" 또는 "날짜 파싱 시 유효하지 않은 입력을 예외 처리한다")처럼 테스트 의도를 한 문장으로 서술하도록
DateUtilTest 클래스의 `@DisplayName` 어노테이션을 수정하세요.
In
`@src/test/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepositoryTest.java`:
- Line 19: Update the `@DisplayName` annotations in
RingingAlarmRedisRepositoryTest so they use sentence-style, domain-term phrasing
(e.g., "알람이 저장되면 레디스에 존재한다" or "등록된 알람을 삭제하면 레디스에서 제거된다") instead of
title-style; locate the class-level `@DisplayName` on
RingingAlarmRedisRepositoryTest and the remaining `@DisplayName` annotations
within the same test file (the other occurrences in this class) and rewrite each
to a full-sentence, domain-focused description per the src/test/** guidelines.
- Around line 57-71: The test success_idempotent currently only checks count;
update it to assert the score was actually updated by calling
ringingAlarmRedisRepository.add(42L, 7L, updatedScore) and then retrieving
List<RingingPushInfo> result = ringingAlarmRedisRepository.getRingingAlarms();
assert result has size 1 and that result.get(0).getScore() (or the appropriate
field on RingingPushInfo) equals updatedScore (or is > firstScore) to ensure the
second add replaced the score (use the add and getRingingAlarms methods and the
RingingPushInfo score accessor to locate the fields).
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (2)
logs/app-error.logis excluded by!**/*.loglogs/app.logis excluded by!**/*.log
📒 Files selected for processing (10)
.claude/CLAUDE.mdsrc/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmRingingNotificationScheduler.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.javasrc/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmOccurrenceBatchService.javasrc/main/java/akuma/whiplash/global/util/date/DateUtil.javasrc/main/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepository.javasrc/test/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceTest.javasrc/test/java/akuma/whiplash/global/util/date/DateUtilTest.javasrc/test/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepositoryTest.javasrc/test/resources/docker-java.properties
| ## 아키텍처 | ||
|
|
||
| ### 레이어 구조 (도메인별로 반복) | ||
|
|
||
| ``` | ||
| presentation (Controller) | ||
| ↓ | ||
| application (UseCase) ← 도메인 서비스 조합, DTO 변환 | ||
| ↓ | ||
| domain (CommandService / QueryService) ← 비즈니스 규칙 | ||
| ↓ | ||
| persistence (Repository / Entity) | ||
| ``` | ||
|
|
||
| 각 도메인(`alarm`, `auth`, `member`, `place`)은 위 4개 레이어를 독립적으로 가진다. | ||
|
|
||
| ### 도메인 패키지 구조 | ||
|
|
||
| ``` | ||
| akuma.whiplash/ | ||
| ├── domains/ | ||
| │ └── {domain}/ | ||
| │ ├── application/ | ||
| │ │ ├── dto/ (request, response, etc) | ||
| │ │ ├── mapper/ (Entity ↔ DTO) | ||
| │ │ ├── usecase/ (UseCase 인터페이스 + 구현) | ||
| │ │ └── scheduler/ (alarm 도메인만 존재) | ||
| │ ├── domain/ | ||
| │ │ ├── constant/ (enum) | ||
| │ │ └── service/ (CommandService, QueryService) | ||
| │ ├── persistence/ | ||
| │ │ ├── entity/ | ||
| │ │ └── repository/ | ||
| │ ├── presentation/ | ||
| │ └── exception/ (도메인별 ErrorCode enum) | ||
| │ | ||
| ├── infrastructure/ | ||
| │ ├── redis/ (RedisRepository, RedisService, RingingAlarmRedisRepository) | ||
| │ └── firebase/ (FcmService, MockFcmService) | ||
| │ | ||
| └── global/ | ||
| ├── config/ | ||
| │ ├── security/ (JWT, Spring Security) | ||
| │ └── scheduler/ (스케줄러 ThreadPool) | ||
| ├── exception/ (ApplicationException) | ||
| └── response/code/ (공통 ErrorCode, SuccessCode) | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
아키텍처 문서화가 PR 변경사항과 잘 정렬되어 있습니다
레이어 구조와 패키지 구조가 명확하게 문서화되어 있으며, 특히 Line 65의 RingingAlarmRedisRepository 언급이 이번 PR에서 도입된 Redis 기반 울림 상태 관리 시스템과 정확히 일치합니다.
📝 선택적 개선: Markdown linting 이슈 해결
Static analysis에서 fenced code block에 언어 지정자가 없다는 경고가 있습니다. 엄격한 markdown linting을 적용하려면 아래와 같이 수정할 수 있습니다:
Line 32-40의 레이어 구조 다이어그램:
-```
+```text
presentation (Controller)Line 46-74의 패키지 구조:
-```
+```text
akuma.whiplash/단, 이는 문서 가독성에 영향을 주지 않는 nitpick 수준의 개선사항입니다.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 32-32: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 46-46: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.claude/CLAUDE.md around lines 28 - 74, The fenced code blocks in the
architecture doc lack language specifiers which triggers markdown lint warnings;
update both blocks (the layer structure block containing "presentation
(Controller) ..." and the package tree block starting with "akuma.whiplash/"
that mentions RingingAlarmRedisRepository) to use a language tag (e.g., change
``` to ```text) so the linter stops flagging them.
| ## 테스트 컨벤션 | ||
|
|
||
| ### 어떤 테스트를 선택할까 | ||
|
|
||
| | 목적 | 사용할 어노테이션 | | ||
| |---|---| | ||
| | 컨트롤러 요청-응답 / 검증 / 예외 핸들링 | `@WebMvcTest` | | ||
| | 서비스 비즈니스 로직 / 트랜잭션 경계 | `@ExtendWith(MockitoExtension.class)` | | ||
| | 리포지토리 / 엔티티 / JPQL | `@PersistenceTest` | | ||
| | 보안 + 필터 + DB/Redis 연동 + 전체 플로우 | `@IntegrationTest` | | ||
| | Redis Sorted Set 등 Redis 슬라이스 | `@DataRedisTest` + `RedisContainerInitializer` | | ||
|
|
||
| ### 테스트 어노테이션 상세 | ||
|
|
||
| - **`@ExtendWith(MockitoExtension.class)`**: 서비스 단위 테스트. DB/Redis 없음, Mockito만 사용 | ||
| - **`@WebMvcTest`**: 컨트롤러 슬라이스. 보안 필터 제외 시 `@AutoConfigureMockMvc(addFilters = false)`. 협력 빈은 `@MockitoBean`으로 주입 | ||
| - **`@PersistenceTest`**: MySQL Testcontainer + `@DataJpaTest`. JPA Auditing 필요 시 `@Import(JpaAuditingConfig.class)` | ||
| - **`@IntegrationTest`**: 전체 Spring 컨텍스트, MySQL + Redis Testcontainer, 트랜잭션 롤백 | ||
| - **`@DataRedisTest`** + `@ContextConfiguration(initializers = RedisContainerInitializer.class)`: Redis 슬라이스 | ||
|
|
||
| ### 테스트 구조 규칙 (@Nested) | ||
|
|
||
| 하나의 테스트 클래스는 **클래스의 각 메서드마다 `@Nested` inner class 하나**로 구성한다. | ||
| 각 inner class 안에 성공/실패 케이스를 모두 작성한다. | ||
|
|
||
| ``` | ||
| {원본클래스}Test | ||
| └── @Nested {메서드명}Test ← 메서드마다 inner class | ||
| ├── success() | ||
| ├── fail_{비즈니스실패이유}() | ||
| └── fail_{다른실패이유}() | ||
| ``` | ||
|
|
||
| 예시: | ||
| ```java | ||
| @DisplayName("AlarmCommandService Unit Test") | ||
| @ExtendWith(MockitoExtension.class) | ||
| class AlarmCommandServiceTest { | ||
|
|
||
| @Mock private AlarmRepository alarmRepository; | ||
| @InjectMocks private AlarmCommandServiceImpl alarmCommandService; | ||
|
|
||
| @Nested | ||
| @DisplayName("alarmOff - 알람 끄기(OFF)") | ||
| class AlarmOffTest { | ||
|
|
||
| @Test | ||
| @DisplayName("성공: 주간 OFF 한도 내에서 알람을 끈다") | ||
| void success() { ... } | ||
|
|
||
| @Test | ||
| @DisplayName("실패: 주간 OFF 한도를 초과하면 예외를 던진다") | ||
| void fail_weeklyLimitExceeded() { ... } | ||
| } | ||
|
|
||
| @Nested | ||
| @DisplayName("ringAlarm - 알람 울림") | ||
| class RingAlarmTest { | ||
|
|
||
| @Test | ||
| @DisplayName("성공: alarmRinging=true, 로그 저장, Redis 적재가 모두 수행된다") | ||
| void success() { ... } | ||
|
|
||
| @Test | ||
| @DisplayName("실패: 알람 시간이 되지 않았으면 예외를 던진다") | ||
| void fail_notAlarmTime() { ... } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 클래스 / 메서드 네이밍 | ||
|
|
||
| - 루트 테스트 클래스: `{원본클래스명}Test` | ||
| - inner 테스트 클래스: `{메서드명}Test` (ex. `alarmOff` → `AlarmOffTest`) | ||
| - 테스트 메서드: `success` / `fail_{비즈니스_관점_실패이유}` (ex. `fail_weeklyLimitExceeded`, `fail_memberNotFound`) | ||
|
|
||
| ### DisplayName 규칙 | ||
|
|
||
| - 문장형으로 작성. "~테스트" 금지 | ||
| - 결과까지 기술: `"성공: 주간 OFF 한도 내에서 알람을 끈다"` / `"실패: 한도 초과 시 예외를 던진다"` | ||
| - inner class의 `@DisplayName`: `"{메서드명} - {한글 기능 설명}"` (ex. `"alarmOff - 알람 끄기(OFF)"`) | ||
| - 도메인 용어 사용 (메서드 이름 관점 X, 정책 관점 O) | ||
|
|
||
| ### BDD 스타일 (Given / When / Then) | ||
|
|
||
| 모든 테스트는 `// given`, `// when`, `// then` 주석으로 구분한다. | ||
|
|
||
| ### 테스트 픽스처 | ||
|
|
||
| `src/test/java/akuma/whiplash/common/fixture/`에 enum 기반 픽스처 존재: | ||
| - `MemberFixture` — 테스트용 멤버 (`MEMBER_1` ~ `MEMBER_N`), `toMockEntity()` 제공 | ||
| - `AlarmFixture` — 테스트용 알람 (`ALARM_01` ~ `ALARM_N`), `toMockEntity()` 제공 | ||
| - `AlarmOccurrenceFixture` | ||
|
|
||
| ### FCM 테스트 | ||
|
|
||
| 프로파일 `test`에서는 `MockFcmService`가 자동으로 등록되어 실제 FCM 요청을 보내지 않는다. | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
테스트 컨벤션이 매우 체계적이고 실용적입니다
- 테스트 어노테이션 선택 가이드(Line 114)가 목적별로 명확하게 정리되어 있습니다.
@Nested구조 규칙과 예제(Lines 132-178)가 구체적이어서 일관된 테스트 코드 작성에 큰 도움이 될 것입니다.- DisplayName 규칙("~테스트" 금지, 결과까지 기술)이 명확하고 합리적입니다.
- 테스트 픽스처 enum 패턴과 MockFcmService 설명이 유용합니다.
📝 선택적 개선: Markdown linting 이슈 해결
Static analysis에서 일부 fenced code block 관련 경고가 있습니다:
Line 135의 구조 다이어그램:
-```
+```text
{원본클래스}TestLine 144의 Java 예제 (blank line 추가 + 언어 지정):
예시:
+
-```java
+```java
`@DisplayName`("AlarmCommandService Unit Test")이는 nitpick 수준의 개선사항입니다.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 135-135: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 144-144: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.claude/CLAUDE.md around lines 110 - 207, Fix the fenced code block lint
warnings in .claude/CLAUDE.md by adding explicit language identifiers and
ensuring a blank line immediately after the opening fence for the example blocks
(the structure diagram containing "{원본클래스}Test" and the Java example starting
with "@DisplayName(\"AlarmCommandService Unit Test\")"); update the opening
fences to use ```text and ```java respectively and insert the required blank
line so the static analyzer stops flagging those blocks.
| ## 커밋 메시지 컨벤션 | ||
|
|
||
| ``` | ||
| [#이슈번호] :Emoji: <type>: <subject> | ||
|
|
||
| <body> | ||
| - 파일명 | ||
| - 변경 내용 | ||
|
|
||
| <footer> | ||
| - 해결: #이슈번호 | ||
| ``` | ||
|
|
||
| | Type | Emoji | 설명 | | ||
| |---|---|---| | ||
| | Feature | ✨ | 새로운 기능 추가 | | ||
| | Fix | 🐛 | 버그 수정 | | ||
| | Docs | 📝 | 문서 수정 | | ||
| | Style | 🎨 | 코드 포맷팅 (로직 변경 없음) | | ||
| | Refactor | ♻️ | 리팩토링 | | ||
| | Test | ✅ | 테스트 코드 추가/수정 | | ||
| | Chore | 🔧 | 빌드, 패키지 매니저 수정 | | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
커밋 메시지 컨벤션이 명확하게 정의되어 있습니다
이슈 번호 연결, Emoji/Type 분류, Subject 규칙(50자 제한, 마침표 없음)이 모두 명시되어 일관된 커밋 이력 관리가 가능합니다.
📝 선택적 개선: 템플릿 코드 블록 언어 지정
Line 210의 커밋 메시지 템플릿:
-```
+```text
[`#이슈번호`] :Emoji: <type>: <subject>🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 210-210: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.claude/CLAUDE.md around lines 208 - 230, Add a language identifier to the
commit message code block so syntax highlighting is explicit; update the fenced
code block containing "[`#이슈번호`] :Emoji: <type>: <subject>" to use a language tag
(e.g., ```text) so the template at that snippet is rendered consistently and
clearly.
| ## Claude Code 관련 질문 처리 | ||
|
|
||
| claude-code-guide는 틀린 답을 낼 때가 있다. 사용자가 Claude Code 기능에 대해 추가 질문을 하면, 공식 문서를 curl로 직접 참조해서 답한다. | ||
|
|
||
| ```bash | ||
| curl https://code.claude.com/docs/ko/overview.md | ||
| ``` | ||
|
|
||
| 문서 URL 패턴: `https://code.claude.com/docs/ko/{페이지명}.md` | ||
|
|
||
| 답변 후에는 `AskUserQuestion`으로 퀴즈를 내서 사용자가 직접 따라해보도록 안내한다. No newline at end of file |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Claude Code 질문 처리 가이드가 유용합니다
공식 문서를 curl로 직접 참조하도록 한 것은 최신 정보를 얻는 실용적인 방법입니다.
📝 선택적 개선: 파일 끝 newline 추가
Unix 표준 관행에 따라 파일 끝에 빈 줄을 추가하세요:
답변 후에는 `AskUserQuestion`으로 퀴즈를 내서 사용자가 직접 따라해보도록 안내한다.
+📝 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.
| ## Claude Code 관련 질문 처리 | |
| claude-code-guide는 틀린 답을 낼 때가 있다. 사용자가 Claude Code 기능에 대해 추가 질문을 하면, 공식 문서를 curl로 직접 참조해서 답한다. | |
| ```bash | |
| curl https://code.claude.com/docs/ko/overview.md | |
| ``` | |
| 문서 URL 패턴: `https://code.claude.com/docs/ko/{페이지명}.md` | |
| 답변 후에는 `AskUserQuestion`으로 퀴즈를 내서 사용자가 직접 따라해보도록 안내한다. | |
| ## Claude Code 관련 질문 처리 | |
| claude-code-guide는 틀린 답을 낼 때가 있다. 사용자가 Claude Code 기능에 대해 추가 질문을 하면, 공식 문서를 curl로 직접 참조해서 답한다. | |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 260-260: Files should end with a single newline character
(MD047, single-trailing-newline)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.claude/CLAUDE.md around lines 250 - 260, Add a trailing newline at the end
of the file so the "Claude Code 관련 질문 처리" section (the block containing the curl
example `curl https://code.claude.com/docs/ko/overview.md` and the documentation
URL pattern) ends with a single Unix-style newline character; update the file so
the final line is followed by one newline to satisfy POSIX/newline conventions.
| // 1-2. 울리고 있을 수 있으므로 Redis Sorted Set에서 먼저 제거 | ||
| ringingAlarmRedisRepository.remove(alarmId, memberId); |
There was a problem hiding this comment.
DB 커밋 전 Redis 반영으로 상태 불일치 위험이 있습니다.
Line [199], Line [266], Line [304]의 Redis 연산이 트랜잭션 내부에서 즉시 실행됩니다. 이후 DB 롤백이 나면 Redis/DB 상태가 어긋납니다. Redis 반영은 afterCommit(또는 outbox)로 미루는 편이 안전합니다.
afterCommit 적용 예시
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
@@
- ringingAlarmRedisRepository.remove(alarmId, memberId);
+ runAfterCommit(() -> ringingAlarmRedisRepository.remove(alarmId, memberId));
@@
- ringingAlarmRedisRepository.remove(alarmId, memberId);
+ runAfterCommit(() -> ringingAlarmRedisRepository.remove(alarmId, memberId));
@@
- ringingAlarmRedisRepository.add(alarmId, memberId, score);
+ runAfterCommit(() -> ringingAlarmRedisRepository.add(alarmId, memberId, score));
@@
+ private static void runAfterCommit(Runnable action) {
+ if (!TransactionSynchronizationManager.isActualTransactionActive()) {
+ action.run();
+ return;
+ }
+ TransactionSynchronizationManager.registerSynchronization(
+ new TransactionSynchronization() {
+ `@Override`
+ public void afterCommit() {
+ action.run();
+ }
+ }
+ );
+ }Also applies to: 264-267, 298-305
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java`
around lines 199 - 200, AlarmCommandServiceImpl currently calls Redis operations
(e.g., ringingAlarmRedisRepository.remove and the other Redis calls around the
same spots) directly inside the DB transaction which can lead to DB/Redis state
divergence on rollback; change these to run only after a successful commit by
deferring them via a transaction synchronization or after-commit event: remove
the direct calls to ringingAlarmRedisRepository.remove (and the other Redis
calls in the same methods) and instead register a post-commit action (e.g.,
TransactionSynchronizationManager.registerSynchronization(...) with
afterCommit() or publish a domain event and handle it with
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT)) that
performs the same repository calls; keep the same method names and parameters
when invoking the repository in the after-commit handler so the logic is
identical but executed only after DB commit.
| return members.stream() | ||
| .map(member -> { | ||
| String[] parts = member.split(":"); | ||
| return RingingPushInfo.builder() | ||
| .alarmId(Long.parseLong(parts[0])) | ||
| .memberId(Long.parseLong(parts[1])) | ||
| .build(); | ||
| }) |
There was a problem hiding this comment.
비정상 member 포맷 1건이 전체 조회를 실패시킬 수 있습니다.
Line [58]~[61]은 포맷 이상 데이터가 들어오면 파싱 예외로 전체 스케줄러 경로를 깨뜨릴 수 있습니다. 잘못된 엔트리는 건너뛰도록 방어가 필요합니다.
방어 로직 예시
import java.util.List;
+import java.util.Optional;
import java.util.Set;
@@
- return members.stream()
- .map(member -> {
- String[] parts = member.split(":");
- return RingingPushInfo.builder()
- .alarmId(Long.parseLong(parts[0]))
- .memberId(Long.parseLong(parts[1]))
- .build();
- })
- .toList();
+ return members.stream()
+ .map(this::parseMemberSafely)
+ .flatMap(Optional::stream)
+ .toList();
}
+
+ private Optional<RingingPushInfo> parseMemberSafely(String member) {
+ String[] parts = member.split(":", 2);
+ if (parts.length != 2) return Optional.empty();
+ try {
+ return Optional.of(
+ RingingPushInfo.builder()
+ .alarmId(Long.parseLong(parts[0]))
+ .memberId(Long.parseLong(parts[1]))
+ .build()
+ );
+ } catch (NumberFormatException e) {
+ return Optional.empty();
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepository.java`
around lines 56 - 63, The stream mapping in RingingAlarmRedisRepository that
converts members into RingingPushInfo using the lambda (member -> { String[]
parts = member.split(":"); return RingingPushInfo.builder()... }) will throw on
malformed entries and must defensively skip them; update the mapping to first
validate parts.length == 2 and safely parse alarmId/memberId catching
NumberFormatException (or use a parse helper) and on parse failure log/debug the
bad member and return an empty Optional or filter it out so only valid
RingingPushInfo instances are collected, ensuring the scheduler won’t fail due
to a single bad member string.
| @Mock | ||
| private RingingAlarmRedisRepository ringingAlarmRedisRepository; | ||
| @Mock | ||
| private ArchiveService archiveService; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Redis 동기화 목적에 맞춰 remove 경로 검증도 추가해 주세요.
새 mock이 추가됐으니 checkinAlarm/removeAlarm 성공 케이스에서 ringingAlarmRedisRepository.remove(...) 호출 검증을 넣으면 회귀 방지가 더 강해집니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceTest.java`
around lines 67 - 70, Add verification that
ringingAlarmRedisRepository.remove(...) is called in the successful checkinAlarm
and removeAlarm test cases: update AlarmCommandServiceTest to assert
ringingAlarmRedisRepository.remove(...) is invoked (with the expected alarm id
or key) in the success paths of the test methods that exercise checkinAlarm and
removeAlarm, so the new mock is validated and Redis-sync removal is covered.
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| @DisplayName("DateUtil Unit Test") |
There was a problem hiding this comment.
클래스 DisplayName을 문장형 도메인 설명으로 수정해 주세요.
Line [13]의 "DateUtil Unit Test"는 규칙상 요구되는 문장형/도메인 관점 표현과 맞지 않습니다.
As per coding guidelines src/test/**: - @DisplayName 문장형, 도메인 용어 사용. **/*Test.java: 테스트명은 @DisplayName을 사용하고, "음료를 추가하면 주문 목록에 담긴다"와 같이 문장형으로 작성합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/akuma/whiplash/global/util/date/DateUtilTest.java` at line 13,
현재 DateUtilTest 클래스의 `@DisplayName`("DateUtil Unit Test")는 규칙에 맞지 않으므로
`@DisplayName` 값을 문장형 도메인 설명으로 바꾸세요; 예컨대 DateUtil의 동작을 설명하는 문장(예: "DateUtil이 날짜를
지정된 포맷으로 변환한다" 또는 "날짜 파싱 시 유효하지 않은 입력을 예외 처리한다")처럼 테스트 의도를 한 문장으로 서술하도록
DateUtilTest 클래스의 `@DisplayName` 어노테이션을 수정하세요.
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| @DisplayName("RingingAlarmRedisRepository Test") |
There was a problem hiding this comment.
테스트 그룹 DisplayName을 문장형으로 통일해 주세요.
현재 표현은 제목형이라 규칙의 문장형 스타일과 다릅니다.
As per coding guidelines src/test/**: - @DisplayName 문장형, 도메인 용어 사용. **/*Test.java: 테스트명은 @DisplayName을 사용하고, "음료를 추가하면 주문 목록에 담긴다"와 같이 문장형으로 작성합니다.
Also applies to: 38-38, 75-75, 111-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepositoryTest.java`
at line 19, Update the `@DisplayName` annotations in
RingingAlarmRedisRepositoryTest so they use sentence-style, domain-term phrasing
(e.g., "알람이 저장되면 레디스에 존재한다" or "등록된 알람을 삭제하면 레디스에서 제거된다") instead of
title-style; locate the class-level `@DisplayName` on
RingingAlarmRedisRepositoryTest and the remaining `@DisplayName` annotations
within the same test file (the other occurrences in this class) and rewrite each
to a full-sentence, domain-focused description per the src/test/** guidelines.
| @Test | ||
| @DisplayName("성공: 동일한 alarmId:memberId로 재호출 시 score만 갱신된다(멱등)") | ||
| void success_idempotent() { | ||
| // given | ||
| long firstScore = System.currentTimeMillis() - 2000; | ||
| long updatedScore = System.currentTimeMillis() - 1000; | ||
| ringingAlarmRedisRepository.add(42L, 7L, firstScore); | ||
|
|
||
| // when | ||
| ringingAlarmRedisRepository.add(42L, 7L, updatedScore); // 동일 member, score만 갱신 | ||
|
|
||
| // then | ||
| List<RingingPushInfo> result = ringingAlarmRedisRepository.getRingingAlarms(); | ||
| assertThat(result).hasSize(1); // 중복 적재 아님 | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
멱등성 테스트에서 score 갱신까지 단언해 주세요.
현재는 건수만 확인해서 “갱신” 보장이 약합니다. 두 번째 add를 미래 score로 바꾼 뒤 조회 결과가 비어야 함을 검증하면 더 정확합니다.
검증 강화 예시
- long updatedScore = System.currentTimeMillis() - 1000;
+ long updatedScore = System.currentTimeMillis() + 60_000;
@@
- assertThat(result).hasSize(1); // 중복 적재 아님
+ assertThat(result).isEmpty(); // score가 갱신되어 due 대상에서 제외🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/akuma/whiplash/infrastructure/redis/RingingAlarmRedisRepositoryTest.java`
around lines 57 - 71, The test success_idempotent currently only checks count;
update it to assert the score was actually updated by calling
ringingAlarmRedisRepository.add(42L, 7L, updatedScore) and then retrieving
List<RingingPushInfo> result = ringingAlarmRedisRepository.getRingingAlarms();
assert result has size 1 and that result.get(0).getScore() (or the appropriate
field on RingingPushInfo) equals updatedScore (or is > firstScore) to ensure the
second add replaced the score (use the add and getRingingAlarms methods and the
RingingPushInfo score accessor to locate the fields).
📄 PR 요약
AS-IS
flowchart LR A["Scheduler (10s)"] --> B["AlarmQueryService"] B --> C["DB: alarm_occurrence 조회"] C --> D["RingingPushInfo 목록"] D --> E["RedisService: FCM 토큰 조회"] E --> F["FCM 전송"] G["알람 울림 상태 변경"] -. 별도 연계 .-> CTO-BE
flowchart LR A["Scheduler (10s)"] --> B["RingingAlarmRedisRepository"] B --> C["Redis ZSET: alarm:ringing<br/>ZRANGEBYSCORE 0..now"] C --> D["RingingPushInfo 목록"] D --> E["RedisService: FCM 토큰 조회"] E --> F["FCM 전송"] G["ringAlarm()"] --> H["ZADD alarm:ringing"] I["checkInAlarm()/deleteAlarm()"] --> J["ZREM alarm:ringing"]✍🏻 PR 상세
AlarmRingingNotificationScheduler에서 DB 조회(AlarmQueryService) 대신RingingAlarmRedisRepository조회로 변경db_query_duration→redis_query_duration에 맞게 정리RingingAlarmRedisRepository신규 추가alarm:ringing, Member:{alarmId}:{memberId}, Score:epochMillisadd/getRingingAlarms/remove구현으로 울림 대상 적재/조회/제거 처리AlarmCommandServiceImpl.ringAlarm()에서 울림 시작 시 Redis ZSET 적재 추가checkInAlarm(),deleteAlarm()에서 비활성화/삭제 시 Redis ZSET 제거 추가Asia/Seoul기준 epoch millis 사용DateUtil.getNextOccurrenceDate(repeatDays, fromDateTime, alarmTime)오버로딩 추가RingingAlarmRedisRepositoryTest신규 작성DateUtilTest신규 작성AlarmCommandServiceTest에서ringAlarm관련 검증(로그 + Redis 적재) 보강src/test/resources/docker-java.properties추가 (api.version=1.44)👀 참고사항
BUILD SUCCESSFUL(bash gradlew test, 2026-03-02 기준)#102로 작성👀 참고사항
✅ 체크리스트
🚪 연관된 이슈 번호
Closes #102
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests