-
Notifications
You must be signed in to change notification settings - Fork 0
알람 울림 API, 푸시 알림 추가 #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
30f31e8
f86ea0e
a8561bd
3e604c4
4797a79
9844df2
e2e9f7d
2a1734f
63213f3
8dc2b8b
b1f5fa0
7fd6a02
c21efdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package akuma.whiplash.domains.alarm.application.dto.etc; | ||
|
|
||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record RingingPushInfo( | ||
| Long alarmId, | ||
| Long memberId | ||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package akuma.whiplash.domains.alarm.application.dto.etc; | ||
|
|
||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record RingingPushTargetDto( | ||
| String token, | ||
| Long alarmId, | ||
| Long memberId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |||||||||||||
| import java.util.Set; | ||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||
| import org.springframework.scheduling.annotation.Scheduled; | ||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||
|
|
||||||||||||||
| @Slf4j | ||||||||||||||
|
|
@@ -27,7 +28,8 @@ public class AlarmReminderScheduler { | |||||||||||||
| private final FcmService fcmService; | ||||||||||||||
| private final AlarmCommandService alarmCommandService; | ||||||||||||||
|
|
||||||||||||||
| // @Scheduled(cron = "0 * * * * *") // 매 분 | ||||||||||||||
| // 매 분 마다 실행 | ||||||||||||||
| @Scheduled(cron = "0 * * * * *") | ||||||||||||||
| public void sendPreAlarmNotifications() { | ||||||||||||||
|
Comment on lines
+31
to
33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 크론에 타임존 누락 — 배치 스케줄과 동일한 Asia/Seoul로 고정 권장
다음과 같이 수정해 주세요. - // 매 분 마다 실행
- @Scheduled(cron = "0 * * * * *")
+ // 매 분 마다 실행 (KST 기준)
+ @Scheduled(cron = "0 * * * * *", zone = "Asia/Seoul")
public void sendPreAlarmNotifications() {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| log.info("[AlarmReminderScheduler.sendPreAlarmNotifications] 알람 울리기 1시간 전 푸시 알림 전송 스케줄러 시작"); | ||||||||||||||
| try { | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package akuma.whiplash.domains.alarm.application.scheduler; | ||
|
|
||
| import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo; | ||
| import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushTargetDto; | ||
| import akuma.whiplash.domains.alarm.domain.service.AlarmQueryService; | ||
| import akuma.whiplash.infrastructure.firebase.FcmService; | ||
| import akuma.whiplash.infrastructure.redis.RedisService; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AlarmRingingNotificationScheduler { | ||
|
|
||
| private final AlarmQueryService alarmQueryService; | ||
| private final RedisService redisService; | ||
| private final FcmService fcmService; | ||
|
|
||
| // 10초 간격으로 실행 | ||
| @Scheduled(fixedRate = 10000) | ||
| public void sendRingingAlarmNotifications() { | ||
| log.info("[AlarmRingingNotificationScheduler.sendRingingAlarmNotifications] 알람 울림 푸시 알림 전송 스케줄러 시작"); | ||
|
||
| try { | ||
| List<RingingPushInfo> infos = alarmQueryService.getRingingNotificationTargets(); | ||
| if (infos.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| List<RingingPushTargetDto> targets = infos.stream() | ||
| .flatMap(info -> redisService.getFcmTokens(info.memberId()).stream() | ||
| .map(token -> RingingPushTargetDto.builder() | ||
| .token(token) | ||
| .alarmId(info.alarmId()) | ||
| .memberId(info.memberId()) | ||
| .build())) | ||
| .toList(); | ||
|
|
||
| if (targets.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| fcmService.sendRingingNotifications(targets); | ||
| } finally { | ||
|
Comment on lines
+33
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain중복 푸시 방지(Idempotency) 설계 필요 10초마다 같은 대상을 반복 수집/전송할 수 있습니다. 사전알림은
의사 코드 예시: 현 구현에서 중복 방지 로직이 존재하는지 확인하려면: 🏁 Script executed: #!/bin/bash
rg -nP --type=java 'ring(ing)?(Push|Notification)|ringIndex|dedup|lastSent' -C2Length of output: 4626 중복 푸시(Idempotency) 처리 로직 추가 필요 현재
이때 동일한 아래 중 한 가지 방안을 반드시 도입해 중복 전송을 방지해 주시기 바랍니다:
예시 의사 코드: 점검 대상:
🤖 Prompt for AI Agents |
||
| log.info("[AlarmRingingNotificationScheduler.sendRingingAlarmNotifications] 알람 울림 푸시 알림 전송 스케줄러 종료"); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,10 @@ public void checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest reques | |
| alarmCommandService.checkinAlarm(memberId, alarmId, request); | ||
| } | ||
|
|
||
| public void ringAlarm(Long memberId, Long alarmId) { | ||
| alarmCommandService.ringAlarm(memberId, alarmId); | ||
| } | ||
|
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) ringAlarm 유스케이스 추가는 타당. 멱등성·서버시간 사용 여부 확인 필요
|
||
|
|
||
| public List<AlarmInfoPreviewResponse> getAlarms(Long memberId) { | ||
| return alarmQueryService.getAlarms(memberId); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,5 +15,6 @@ public interface AlarmCommandService { | |
| AlarmOffResultResponse alarmOff(Long memberId, Long alarmId, LocalDateTime clientNow); | ||
| void removeAlarm(Long memberId, Long alarmId, String reason); | ||
| void checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest request); | ||
| void ringAlarm(Long memberId, Long alarmId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) ringAlarm 시그니처의 시간 기준·트랜잭션/동시성 정책을 명확히 해주세요
🤖 Prompt for AI Agents |
||
| void markReminderSent(Set<Long> occurrenceIds); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |||||||
| import akuma.whiplash.domains.alarm.persistence.entity.AlarmEntity; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.entity.AlarmOccurrenceEntity; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.entity.AlarmOffLogEntity; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.entity.AlarmRingingLogEntity; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.repository.AlarmOccurrenceRepository; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.repository.AlarmOffLogRepository; | ||||||||
| import akuma.whiplash.domains.alarm.persistence.repository.AlarmRepository; | ||||||||
|
|
@@ -245,6 +246,36 @@ public void checkinAlarm(Long memberId, Long alarmId, AlarmCheckinRequest reques | |||||||
| occurrence.checkin(LocalDateTime.now()); | ||||||||
| } | ||||||||
|
|
||||||||
| @Override | ||||||||
| public void ringAlarm(Long memberId, Long alarmId) { | ||||||||
| AlarmEntity alarm = findAlarmById(alarmId); | ||||||||
| validAlarmOwner(memberId, alarm.getMember().getId()); | ||||||||
|
|
||||||||
|
Comment on lines
+249
to
+253
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain소유자 검증 파라미터 순서의 일관성 유지 본 메서드는 레포 전체에서 호출부를 점검하려면: 🏁 Script executed: #!/bin/bash
rg -nP --type=java -C1 '\bvalidAlarmOwner\s*\('Length of output: 2368 validAlarmOwner 호출 인자 순서 일관성 유지 필요 다음 호출부에서 “요청자 ID, 알람 소유자 ID” 순서를 준수하도록 수정해주세요.
🤖 Prompt for AI Agents |
||||||||
| AlarmOccurrenceEntity occurrence = alarmOccurrenceRepository | ||||||||
| .findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDesc( | ||||||||
| alarmId, | ||||||||
| List.of(DeactivateType.NONE) | ||||||||
| ) | ||||||||
| .orElseThrow(() -> ApplicationException.from(ALARM_OCCURRENCE_NOT_FOUND)); | ||||||||
|
|
||||||||
| if (occurrence.getDeactivateType() != DeactivateType.NONE) { | ||||||||
| throw ApplicationException.from(ALREADY_DEACTIVATED); | ||||||||
| } | ||||||||
|
||||||||
| if (occurrence.getDeactivateType() != DeactivateType.NONE) { | |
| throw ApplicationException.from(ALREADY_DEACTIVATED); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
around lines 261-263, the explicit re-check of occurrence.getDeactivateType() !=
DeactivateType.NONE is redundant because the repository query already filters
for DeactivateType.NONE; either remove this duplicate check or keep it but add a
clarifying comment that it exists to guard against concurrent state changes.
Also verify the repository method signature (e.g., findTopBy...InOrderBy...)
actually restricts results to DeactivateType.NONE and, if helpful for clarity,
consider renaming the repository method to make that condition explicit in its
name.
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
동시 호출 시 울림 카운트/로그 중복 가능성 — 잠금 또는 유니크 제약으로 정합성 보강 필요
동일 알람에 대해 ringAlarm이 거의 동시에 호출되면, 두 트랜잭션이 같은 occurrence를 읽고 각각 ring()을 호출하면서 증가분이 꼬이거나(@Version 부재 시) 동일 ringIndex로 로그가 두 번 적재될 리스크가 있습니다. 조회 시 비관적 잠금 또는 엔티티의 낙관적 잠금, 그리고 로그 테이블의 유니크 제약으로 이중 방어를 권장합니다.
- JPA 비관적 잠금으로 최신 발생내역 조회
AlarmOccurrenceEntity에@Version추가(낙관적 잠금)AlarmRingingLogEntity에(alarm_occurrence_id, ring_index)유니크 제약
변경 제안(서비스 메서드 내 잠금 전용 쿼리 사용):
- AlarmOccurrenceEntity occurrence = alarmOccurrenceRepository
- .findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDesc(
+ AlarmOccurrenceEntity occurrence = alarmOccurrenceRepository
+ .findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate(
alarmId,
List.of(DeactivateType.NONE)
)
.orElseThrow(() -> ApplicationException.from(ALARM_OCCURRENCE_NOT_FOUND));레포지토리/엔티티 보강 예시(참고용):
// AlarmOccurrenceRepository
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<AlarmOccurrenceEntity> findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate(
Long alarmId, List<DeactivateType> types);
// AlarmOccurrenceEntity
@Version
private Long version;
// AlarmRingingLogEntity
@Table(
name = "alarm_ringing_log",
uniqueConstraints = @UniqueConstraint(
name = "uk_occurrence_ringindex",
columnNames = {"alarm_occurrence_id", "ring_index"}
)
)다음 스크립트로 잠금/버전/유니크 제약 존재 여부를 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# AlarmOccurrenceEntity @Version 확인
rg -nP --type=java '(?m)class\s+AlarmOccurrenceEntity\b' -C3
rg -nP --type=java '@Version' -C1
# AlarmRingingLogEntity 유니크 제약/Index 확인
rg -nP --type=java '(?m)class\s+AlarmRingingLogEntity\b' -C5
rg -nP --type=java 'UniqueConstraint|uniqueConstraints|@Index' -C2
# 잠금 전용 쿼리 존재 여부 확인
rg -nP --type=java 'findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate'Length of output: 4351
동시성 안전성 보강: 잠금 및 유니크 제약 추가 필요
검증 결과,
- AlarmOccurrenceEntity에 낙관적 잠금을 위한
@Version애노테이션이 없습니다. - AlarmRingingLogEntity에는
(alarm_occurrence_id, ring_index)유니크 제약이 설정되어 있지 않습니다. - PESSIMISTIC_WRITE 잠금을 위한 전용 조회 메서드(
findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate)도 정의되어 있지 않습니다.
위 문제로 인해 ringAlarm이 동시 호출될 경우 동일한 발생내역을 기준으로 두 트랜잭션이 같은 인덱스를 계산·저장하거나, 애노테이션 미설정 시 낙관적 잠금 충돌 없이 무결성이 깨질 리스크가 있습니다.
조치 사항:
- src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java
@Version private Long version;추가 (낙관적 잠금)
- src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmRingingLogEntity.java
@Table(name = "alarm_ringing_log", uniqueConstraints = @UniqueConstraint(name = "uk_occurrence_ringindex", columnNames = {"alarm_occurrence_id","ring_index"}))추가
- src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
- 아래 전용 조회 메서드 추가
@Lock(LockModeType.PESSIMISTIC_WRITE) Optional<AlarmOccurrenceEntity> findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate( Long alarmId, List<DeactivateType> types);
- 아래 전용 조회 메서드 추가
위 보강 작업을 통해 동시 호출 시에도 정확한 인덱스 계산과 로그 중복 방지를 보장할 수 있습니다.
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java
around lines 249-277: the ringAlarm flow is vulnerable to concurrent calls
because AlarmOccurrenceEntity lacks optimistic locking, AlarmRingingLogEntity
lacks a uniqueness constraint on (alarm_occurrence_id, ring_index), and there is
no PESSIMISTIC_WRITE repository lookup; to fix, add @Version private Long
version; to
src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java,
add a unique constraint on (alarm_occurrence_id, ring_index) via @Table(...) to
src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmRingingLogEntity.java,
and add a repository method annotated with @Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<AlarmOccurrenceEntity>
findTopByAlarmIdAndDeactivateTypeInOrderByDateDescTimeDescForUpdate(Long
alarmId, List<DeactivateType> types) to
src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
so the service can perform a for-update lookup before computing/saving ring
indexes.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||
| package akuma.whiplash.domains.alarm.domain.service; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.etc.OccurrencePushInfo; | ||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo; | ||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmInfoPreviewResponse; | ||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.response.AlarmRemainingOffCountResponse; | ||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.domain.constant.DeactivateType; | ||||||||||||||||||||||||||||||||||
|
|
@@ -177,6 +178,11 @@ private AlarmInfoPreviewResponse buildPreviewResponse(AlarmEntity alarm, LocalDa | |||||||||||||||||||||||||||||||||
| .secondUpcomingDayOfWeek(DateUtil.getKoreanDayOfWeek(resolvedSecondUpcomingDate)) | ||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||
| public List<RingingPushInfo> getRingingNotificationTargets() { | ||||||||||||||||||||||||||||||||||
| return alarmOccurrenceRepository.findRingingNotificationTargets(DeactivateType.NONE); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain울림 타겟 조회: 수신 동의 정책·중복 제거 보장 여부 확인
수정 자체는 Repository 단 변경으로 충분합니다(아래 해당 파일 코멘트 참조). 🏁 Script executed: #!/bin/bash
# Locate the repository method for ringing notification targets
rg -n "interface AlarmOccurrenceRepository" -C2 && \
rg -n "findRingingNotificationTargets" -C3 -g "*.java"Length of output: 2267 🏁 Script executed: #!/bin/bash
# Pre-notification 쿼리(라인 76~117) 확인
sed -n '70,130p' src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.java
# pushNotificationPolicy 필터 사용 여부 검색
rg -n "pushNotificationPolicy" -C3 src/main/java/akuma/whiplash/domains/alarm/persistence/repository/AlarmOccurrenceRepository.javaLength of output: 3038 울림 타겟 조회: 수신 동의 필터와 중복 제거 보장 필요 아래 사항들이 확인되었습니다. 수정은 Repository 쿼리 수준에서 처리하는 것이 가장 간단합니다. • 대상 파일
예시 변경안: @@ AlarmOccurrenceRepository.java
- @Query("""
- SELECT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)
- FROM AlarmOccurrenceEntity o
- JOIN o.alarm a
- JOIN a.member m
- WHERE o.alarmRinging = true
- AND o.deactivateType = :status
-""")
+ @Query("""
+ SELECT DISTINCT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)
+ FROM AlarmOccurrenceEntity o
+ JOIN o.alarm a
+ JOIN a.member m
+ WHERE o.alarmRinging = true
+ AND o.deactivateType = :status
+ AND m.pushNotificationPolicy = true
+""")
List<RingingPushInfo> findRingingNotificationTargets(@Param("status") DeactivateType status);이렇게 변경하면 서비스 계층에서 별도 집합화 없이 수신 동의 정책을 준수하고, 중복 반환을 방지할 수 있습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,6 +83,11 @@ public void checkin(LocalDateTime now) { | |
| this.checkinTime = now; | ||
| } | ||
|
|
||
| public int ring() { | ||
| this.alarmRinging = true; | ||
| return ++this.ringingCount; | ||
| } | ||
|
Comment on lines
+86
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 동시성 제어 부재로 ringingCount 증분 손실 위험(레이스 컨디션) 현재 권장 대응:
엔티티 예시(클래스 상단에 필드 추가): // import jakarta.persistence.Version;
@Version
private Long version;서비스 재시도 스케치(의사 코드): int attempts = 0;
while (true) {
try {
alarmCommandService.ringAlarm(memberId, alarmId);
break;
} catch (OptimisticLockException e) {
if (++attempts >= 3) throw e;
// backoff 후 재시도
}
}추가 확인:
필요하시면 |
||
|
|
||
| public void updateReminderSent(boolean reminderSent) { | ||
| this.reminderSent = reminderSent; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package akuma.whiplash.domains.alarm.persistence.repository; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.etc.OccurrencePushInfo; | ||||||||||||||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo; | ||||||||||||||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.domain.constant.DeactivateType; | ||||||||||||||||||||||||||||||||||||||||||||||
| import akuma.whiplash.domains.alarm.persistence.entity.AlarmOccurrenceEntity; | ||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.LocalDate; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,4 +123,14 @@ List<OccurrencePushInfo> findPreNotificationTargetsUntilTime( | |||||||||||||||||||||||||||||||||||||||||||||
| WHERE o.id IN :ids AND o.reminderSent = false | ||||||||||||||||||||||||||||||||||||||||||||||
| """) | ||||||||||||||||||||||||||||||||||||||||||||||
| void markReminderSentIn(@Param("ids") Set<Long> ids); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @Query(""" | ||||||||||||||||||||||||||||||||||||||||||||||
| SELECT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id) | ||||||||||||||||||||||||||||||||||||||||||||||
| FROM AlarmOccurrenceEntity o | ||||||||||||||||||||||||||||||||||||||||||||||
| JOIN o.alarm a | ||||||||||||||||||||||||||||||||||||||||||||||
| JOIN a.member m | ||||||||||||||||||||||||||||||||||||||||||||||
| WHERE o.alarmRinging = true | ||||||||||||||||||||||||||||||||||||||||||||||
| AND o.deactivateType = :status | ||||||||||||||||||||||||||||||||||||||||||||||
| """) | ||||||||||||||||||||||||||||||||||||||||||||||
| List<RingingPushInfo> findRingingNotificationTargets(@Param("status") DeactivateType status); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 대량 전송 대비 배치·인덱스 설계 권고
// AlarmOccurrenceEntity
@Table(
indexes = {
@Index(name = "idx_occurrence_ringing_status_date", columnList = "alarmRinging,deactivateType,date")
}
)🧹 Nitpick (assertive) 🛠️ Refactor suggestion FCM 수신 동의 미반영·중복 가능성·스코프 과다(날짜 미제한) — 쿼리 보강 필요
아래와 같이 보강을 제안합니다. - @Query("""
- SELECT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)
- FROM AlarmOccurrenceEntity o
- JOIN o.alarm a
- JOIN a.member m
- WHERE o.alarmRinging = true
- AND o.deactivateType = :status
- """)
- List<RingingPushInfo> findRingingNotificationTargets(@Param("status") DeactivateType status);
+ @Query("""
+ SELECT DISTINCT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)
+ FROM AlarmOccurrenceEntity o
+ JOIN o.alarm a
+ JOIN a.member m
+ WHERE o.alarmRinging = true
+ AND o.deactivateType = :status
+ AND m.pushNotificationPolicy = true
+ AND o.date = CURRENT_DATE
+ """)
+ List<RingingPushInfo> findRingingNotificationTargets(@Param("status") DeactivateType status);테스트 보강 제안:
📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,7 +63,7 @@ public ApplicationResponse<CreateAlarmResponse> createAlarm(@AuthenticationPrinc | |
| alarmErrorCodes = {ALARM_NOT_FOUND, ALARM_OFF_LIMIT_EXCEEDED, ALREADY_DEACTIVATED, INVALID_CLIENT_DATE}, | ||
| authErrorCodes = {PERMISSION_DENIED} | ||
| ) | ||
| @Operation(summary = "알람 끄기", description = "알람을 끕니다.") | ||
| @Operation(summary = "알람 끄기", description = "알람 목록에 있는 토글을 이용하여 알람을 끌 때, 알람 울림 화면에서 [봐주세요] 버튼을 눌러서 알람을 끌 때 호출하는 API입니다.") | ||
| @PostMapping("/{alarmId}/off") | ||
| public ApplicationResponse<AlarmOffResultResponse> alarmOff(@AuthenticationPrincipal MemberContext memberContext, @PathVariable Long alarmId, @RequestBody @Valid AlarmOffRequest request) { | ||
| AlarmOffResultResponse response = alarmUseCase.alarmOff(memberContext.memberId(), alarmId, request.clientNow()); | ||
|
|
@@ -117,4 +117,18 @@ public ApplicationResponse<AlarmRemainingOffCountResponse> getWeeklyRemainingOff | |
| AlarmRemainingOffCountResponse response = alarmUseCase.getWeeklyRemainingOffCount(memberContext.memberId()); | ||
| return ApplicationResponse.onSuccess(response); | ||
| } | ||
|
|
||
| @CustomErrorCodes( | ||
| alarmErrorCodes = {ALARM_NOT_FOUND, ALARM_OCCURRENCE_NOT_FOUND, ALREADY_DEACTIVATED, NOT_ALARM_TIME}, | ||
| authErrorCodes = {PERMISSION_DENIED} | ||
| ) | ||
| @Operation(summary = "알람 울림", description = "알람이 울릴 때 호출합니다.") | ||
| @PostMapping("/{alarmId}/ring") | ||
| public ApplicationResponse<Void> ringAlarm( | ||
| @AuthenticationPrincipal MemberContext memberContext, | ||
| @PathVariable Long alarmId | ||
| ) { | ||
| alarmUseCase.ringAlarm(memberContext.memberId(), alarmId); | ||
| return ApplicationResponse.onSuccess(); | ||
| } | ||
|
Comment on lines
+121
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain알람 울림 API의 에러 매핑/테스트 정합성 확인 필요
추가로 슬라이스 테스트에 다음 케이스도 보강 권장:
테스트 기대값/매핑 현황을 점검하려면: 🏁 Script executed: #!/bin/bash
rg -nP --type=java '\[POST\]\s*/api/alarms/\{alarmId\}/ring' -C3
rg -nP --type=java 'ALREADY_DEACTIVATED|NOT_ALARM_TIME|ALARM_NOT_FOUND|PERMISSION_DENIED' src/testLength of output: 1119 🏁 Script executed: #!/bin/bash
# Inspect integration tests for ring alarm error cases
sed -n '100,200p' src/test/java/akuma/whiplash/domains/alarm/presentation/AlarmControllerIntegrationTest.java
# Inspect controller slice tests for error mapping expectations
sed -n '150,260p' src/test/java/akuma/whiplash/domains/alarm/presentation/AlarmControllerTest.javaLength of output: 6665 알람 울림 API 에러 매핑 및 테스트 정합성 확인·수정 필요
위 사항을 반영하여 컨트롤러의 에러 매핑 전략과 통합/슬라이스 테스트의 기대값이 완전히 일치하도록 수정해주세요. 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Bean; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.context.annotation.Configuration; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.core.env.Environment; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,6 +28,7 @@ public class SecurityConfig { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private final JwtUtils jwtUtils; | ||||||||||||||||||||||||||||||||||||||||||||||
| private final RequestMatcherHolder requestMatcherHolder; | ||||||||||||||||||||||||||||||||||||||||||||||
| private final Environment env; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @Value("${swagger.server.url}") | ||||||||||||||||||||||||||||||||||||||||||||||
| private String serverUrl; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,7 +51,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti | |||||||||||||||||||||||||||||||||||||||||||||
| .hasAnyAuthority(ADMIN.name()) | ||||||||||||||||||||||||||||||||||||||||||||||
| .anyRequest().authenticated() | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| .addFilterBefore(new JwtAuthenticationFilter(jwtUtils, requestMatcherHolder), | ||||||||||||||||||||||||||||||||||||||||||||||
| .addFilterBefore(new JwtAuthenticationFilter(jwtUtils, requestMatcherHolder, env), | ||||||||||||||||||||||||||||||||||||||||||||||
| UsernamePasswordAuthenticationFilter.class); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 🛠️ Refactor suggestion JwtAuthenticationFilter 인스턴스 생성은 Bean으로 통일하세요. (@component와 new 병행 사용 지양) 현재
다음처럼 Bean 팩토리 메서드로 등록하고 필터 체인에서 그 빈을 사용해 주세요. (동시에 @Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@@
- .addFilterBefore(new JwtAuthenticationFilter(jwtUtils, requestMatcherHolder, env),
- UsernamePasswordAuthenticationFilter.class);
+ .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
@@
+ @Bean
+ public JwtAuthenticationFilter jwtAuthenticationFilter() {
+ return new JwtAuthenticationFilter(jwtUtils, requestMatcherHolder, env);
+ }그리고 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return http.build(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Record에 @builder는 동작하나 과한 표면적 복잡도—실사용 여부 확인 후 제거 권고
이 DTO는 JPQL
new ...RingingPushInfo(a.id, m.id)로 생성됩니다. 이 경우 빌더는 사용되지 않아 노이즈가 됩니다. 실제 빌더 사용처가 없다면 제거해 단순화하세요.아래 스크립트로 빌더 사용 여부를 확인해 주세요.
빌더 사용이 없다면 다음 변경을 권장합니다:
🏁 Script executed:
Length of output: 65
아래 스크립트로
RingingPushInfo의 빌더 및 인스턴스화 사용처를 재검증해 주세요.출력 결과가 없다면 빌더 사용이 없다고 판단할 수 있습니다.
🏁 Script executed:
Length of output: 1321
RingingPushInfo DTO에서 @builder 제거 권고
RingingPushInfo의 Lombok 빌더 호출은 코드베이스 어디에서도 사용되지 않고, JPQL
new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)만 활용되고 있습니다. 불필요한 복잡도를 줄이기 위해@Builder와 관련 import를 제거하세요.확인된 사용처
• AlarmOccurrenceRepository.java (라인 128):
SELECT new akuma.whiplash.domains.alarm.application.dto.etc.RingingPushInfo(a.id, m.id)변경 제안 (src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/RingingPushInfo.java)
🤖 Prompt for AI Agents