Skip to content

[feat] FCM 기반 푸쉬알림 기능 구현#293

Merged
buzz0331 merged 95 commits intodevelopfrom
feat/#289-fcm-notification
Sep 8, 2025
Merged

[feat] FCM 기반 푸쉬알림 기능 구현#293
buzz0331 merged 95 commits intodevelopfrom
feat/#289-fcm-notification

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Sep 4, 2025

#️⃣ 연관된 이슈

closes #289
closes #111

📝 작업 내용

  1. 구현 방식
    아키텍처 계층 분리
    • EventCommandPort(FeedEventCommandPort, RoomEventCommandPort)를 통해 이벤트 발행 추상화

    • PublisherAdapter에서 Spring ApplicationEventPublisher를 이용해 이벤트 발행

    • EventListener에서 @TransactionalEventListener(AFTER_COMMIT) + @async로 비동기 이벤트 처리

    • DispatchService에서 이벤트를 FCM 메시지로 변환 후 FirebaseMessagingPort를 통해 발송

    테스트 환경 분리
    • local/test 프로필에서 실제 FCM 대신 FakeFirebaseAdapter 동작 (로그 출력 전용)

    • 운영 환경에서는 FirebaseConfig를 통해 FirebaseApp, FirebaseMessaging 초기화

  2. 동작 흐름

    1. 도메인 서비스 / UseCase 계층에서 특정 이벤트 발생 시 EventCommandPort 호출
      → 예: 피드 작성 시 FeedEventCommandPort.publishFolloweeNewPostEvent(...)
    2. PublisherAdapter가 ApplicationEventPublisher를 통해 이벤트 객체 발행
    3. EventListener가 트랜잭션 커밋 이후(AFTER_COMMIT) 이벤트를 감지하고,
      대응되는 DispatchService 메서드를 호출
    4. DispatchService에서 FcmTokenLoadPort로 대상 사용자 FCM 토큰/디바이스 ID 조회
    5. 메시지(com.google.firebase.messaging.Message) 생성 시 Notification + putData로 라우팅 정보 포함
    6. FirebaseMessagingPort로 위임하여 FCM 서버로 전송
  3. 푸쉬알림 이벤트 발행 케이스
    피드(Feed)
    • 팔로워 알림

    • 피드 댓글

    • 댓글의 답글

    • 팔로우한 사람의 새 게시물

    • 피드 좋아요

    • 댓글/답글 좋아요

    모임(Room)
    • 내 게시글(기록/투표)에 댓글

    • 투표 시작

    • 새로운 기록 작성

    • 모집 조기 마감 / 활동 시작

    • 모임 참여 알림 (방장 전용)

    • 댓글/답글 좋아요

    • 기록/투 좋아요

    • 댓글/답글 작성

-> 자세한 내용은 노션 참고해주세요!!

📸 스크린샷

💬 리뷰 요구사항

추가적으로 방 상태 변경 스케줄러도 같이 도입하였습니다! (방 활동 시작을 하려면 이벤트 발행하는 트리거가 필요해서 상태 변경시에 발행되도록 구현하였습니다~)

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • New Features
    • 앱 푸시 알림 전면 도입: 팔로우, 피드·모임 글/댓글/좋아요, 답글, 투표 시작·기록 생성·가입 요청 등 알림 제공
    • 사용자용 알림 API 추가: FCM 토큰 등록·삭제 및 기기별 푸시 사용 설정
    • 예약된 스케줄러: 자정 기준 모임 상태 자동 전환 및 시작 알림 발송
  • Improvements
    • 푸시 전송 안정성 향상 및 비동기 작업 전용 실행기 분리
    • JWT 서명 오류 로깅 강화
  • Chores
    • CI/CD에 Firebase 키 주입 자동화 및 키 파일 Git 무시 규칙 추가
    • Firebase SDK 및 관련 설정 추가

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)

25-25: 메서드 시그니처 간소화
불필요한 exceptStatus 파라미터를 제거하고, 항상 RoomStatus.EXPIRED로만 처리하도록 인터페이스와 구현체를 함께 수정하세요.

-    int updateRoomStateToExpired(RoomStatus exceptStatus);
+    int updateRoomStateToExpired();
🧹 Nitpick comments (12)
src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java (3)

13-16: 포트 계층에서 공용 ErrorCode 예외를 직접 던지는 기본 메서드 결합도

포트가 구체 예외/에러코드에 의존하면 상위 계층으로의 재사용성이 떨어집니다. getByDeviceIdOrThrow는 애플리케이션 서비스 쪽 헬퍼로 이동하거나, 도메인 표준 예외(예: NoSuchElementException)로 한정하는 방안을 고려해 주세요.


24-24: 삭제 결과를 반환해 TOCTOU 레이스를 검출하세요

void deleteByUserIdAndDeviceId(...)는 실제 삭제 여부를 알 수 없어 검증-삭제 사이 소유자 변경 등의 레이스를 놓칠 수 있습니다. 삭제된 행 수(또는 boolean)를 반환하도록 계약을 바꾸는 것을 권장합니다.

적용 예시:

-    void deleteByUserIdAndDeviceId(Long userId, String deviceId);
+    /**
+     * @return true if exactly one row deleted; false otherwise.
+     */
+    boolean deleteByUserIdAndDeviceId(Long userId, String deviceId);

20-20: deviceId 불변성 계약을 포트에 명시

update(FcmToken)는 mutable 필드만 갱신하고 deviceId는 절대 변경되지 않음을 Javadoc에 명시해 주세요. 구현체/매퍼에서 실수로 동기화되지 않도록 가이드가 필요합니다.

예시:

-    void update(FcmToken fcmToken);
+    /**
+     * Updates mutable fields only (deviceId is immutable and must NOT change).
+     */
+    void update(FcmToken fcmToken);
src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java (1)

21-22: 중복 검증 여부 확인

validateFcmOwner로 소유 검증 후 deleteByUserIdAndDeviceId(userId, deviceId)로 한 번 더 필터링합니다. 보안상 안전하지만 중복일 수 있으니 유지 의도라면 주석으로 남겨 주세요.

src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java (3)

20-20: 클래스명 오타: Persistence 중복

FcmTokenPersistencePersistenceAdapterFcmTokenPersistenceAdapter로 정정해 주세요. 파일명도 클래스명과 일치 필요.

적용 예시:

-public class FcmTokenPersistencePersistenceAdapter implements FcmTokenPersistencePort {
+public class FcmTokenPersistenceAdapter implements FcmTokenPersistencePort {

43-52: updateFrom에서 deviceId 미변경 보장 필요 + save 중복 호출 가능성

  • 학습 메모에 따라 updateFromdeviceId를 변경하지 않는지 재확인해 주세요. 변경 시 무결성/소유권 붕괴 리스크가 큽니다.
  • 영속 상태 엔티티에 필드만 갱신하면 flush 시 반영되므로 save(...) 호출은 중복일 수 있습니다. 트래픽 경로라면 제거 검토.

55-58: 알림 발송 우선순위를 위한 정렬 추가 제안

findEnabledByUserId 결과를 lastUsedDate DESC 등으로 정렬하면 가장 최근 사용 디바이스 우선 정책을 쉽게 적용할 수 있습니다.

예: findByUserIdAndIsEnabledTrueOrderByLastUsedDateDesc(Long userId)

src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (2)

27-27: 범용 전이 메서드는 도메인 제약을 약화합니다

updateRoomState(fromStatus, toStatus)는 잘못된 전이(예: IN_PROGRESS→RECRUITING)를 유도할 수 있습니다. 스케줄러 목적이 “RECRUITING→IN_PROGRESS”로 고정이라면 전용 메서드로 좁히는 것을 권장합니다.

-    int updateRoomState(RoomStatus fromStatus, RoomStatus toStatus);
+    int updateRecruitingToInProgress();

29-29: findProgressTargetRooms의 status 파라미터 제거 및 네이밍 정리 권장

실사용이 고정 상태(대개 RECRUITING)라면 파라미터를 제거하고 네이밍을 더 구체화하는 편이 안전합니다. 또한 본 인터페이스 전반에서 “State/Status” 용어가 혼용되니 통일을 제안합니다.

-    List<Room> findProgressTargetRooms(RoomStatus status);
+    List<Room> findProgressTargetRooms();
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (3)

31-36: DB current_date 의존 — 타임존/자정 경계 오동작 가능성

current_date는 DB 타임존에 종속됩니다. 애플리케이션 Clock(KST 등)과 불일치하면 경계 시각에 잘못된 전이가 발생할 수 있습니다. 애플리케이션에서 LocalDate today를 주입받도록 파라미터화하는 것을 권장합니다.

-            where r.endDate < current_date
+            where r.endDate < :today
...
-            where r.startDate <= current_date
-              and r.endDate >= current_date
+            where r.startDate <= :today
+              and r.endDate   >= :today
...
-              and r.endDate   >= current_date
+              and r.endDate   >= :today

메서드 시그니처(예):

-int updateRoomStatusToExpired(@Param("status") RoomStatus status);
+int updateRoomStatusToExpired(@Param("status") RoomStatus status, @Param("today") LocalDate today);

-int updateRoomStatus(@Param("fromStatus") RoomStatus fromStatus, @Param("toStatus") RoomStatus toStatus);
+int updateRoomStatus(@Param("fromStatus") RoomStatus fromStatus,
+                     @Param("toStatus") RoomStatus toStatus,
+                     @Param("today") LocalDate today);

-List<RoomJpaEntity> findProgressTargetIds(@Param("status") RoomStatus status);
+List<RoomJpaEntity> findProgressTargetIds(@Param("status") RoomStatus status,
+                                          @Param("today") LocalDate today);

검증 포인트:

  • 운영 DB 타임존이 애플리케이션 타임존과 동일하게 설정돼 있는지 확인.
  • 스케줄러가 Clock을 주입받아 today를 전달하는지 확인.

Also applies to: 45-49, 56-58


53-60: 메서드명-반환타입 불일치(Ids vs Entity) — 혼란 유발

findProgressTargetIdsList<RoomJpaEntity>를 반환합니다. 네이밍을 findProgressTargets(또는 실제로 ID만 필요하다면 select r.id로 변경)로 정리하세요.

-    List<RoomJpaEntity> findProgressTargetIds(@Param("status") RoomStatus status);
+    List<RoomJpaEntity> findProgressTargets(@Param("status") RoomStatus status);

26-60: 일괄 업데이트 경로 최적화를 위한 인덱스 권장

다음 패턴을 자주 타므로 복합 인덱스 추가를 고려하세요.

  • (room_status, end_date)
  • (room_status, start_date, end_date)

주의: <> 조건은 인덱스 활용이 제한적일 수 있으나, 날짜 범위 조건과 결합 시 플랜에 따라 이득이 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e012be4 and 9ad3d9a.

📒 Files selected for processing (12)
  • src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java (1 hunks)
  • src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java (1 hunks)
  • src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java
  • src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java
  • src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java
  • src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java
  • src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-05T05:26:24.607Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java:44-51
Timestamp: 2025-09-05T05:26:24.607Z
Learning: FcmTokenJpaEntity에서 deviceId는 디바이스를 식별하는 불변 값으로, updateFrom 메서드에서 동기화할 필요가 없음

Applied to files:

  • src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java
📚 Learning: 2025-09-05T04:11:01.419Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.419Z
Learning: RoomStateScheduler의 changeRoomStateToExpired()와 changeRoomStateToProgress()는 서로 다른 날짜 조건으로 완전히 다른 방들을 대상으로 하므로(end_date < today vs start_date <= today AND end_date >= today), 비동기 실행 순서와 관계없이 데이터 정합성에 문제가 없다.

Applied to files:

  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
📚 Learning: 2025-09-05T04:11:01.419Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.419Z
Learning: RoomStateScheduler에서 changeRoomStateToExpired()와 changeRoomStateToProgress()는 각각 다른 트랜잭션에서 실행되며, 날짜 기반으로 서로 다른 방들을 대상으로 하므로 실행 순서가 바뀌어도 데이터 정합성에 문제가 없다.

Applied to files:

  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java
📚 Learning: 2025-08-14T09:27:36.450Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.450Z
Learning: AttendanceCheckJpaRepository의 countByUserIdAndCreatedAtBetween 메서드는 메서드명과 달리 사용자 정의 JPQL 쿼리를 사용한다. "a.createdAt >= :startOfDay AND a.createdAt < :endOfDay"로 반열림구간 [startOfDay, endOfDay)을 정확히 구현하고 있어서 경계값 버그가 없다. 메서드명만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.

Applied to files:

  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
🧬 Code graph analysis (1)
src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java (2)
src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java (1)
  • Service (11-27)
src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java (1)
  • Service (13-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java (1)

11-12: deviceId 단일성(UNIQUE) 및 인덱스 보장

findByDeviceId를 핵심 식별 경로로 사용하므로 DB에 device_id UNIQUE INDEX를 강제하세요. 동시 등록 경쟁 시 중복 레코드 생성을 방지합니다.

마이그레이션 예시:

ALTER TABLE fcm_tokens
  ADD CONSTRAINT uk_fcm_tokens_device UNIQUE (device_id);
CREATE INDEX idx_fcm_tokens_user_enabled ON fcm_tokens (user_id, is_enabled);
⛔ Skipped due to learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java:21-0
Timestamp: 2025-09-05T04:38:38.301Z
Learning: FcmTokenJpaEntity의 deviceId 필드는 JPA Column(unique = true)와 데이터베이스 레벨 UNIQUE 제약이 모두 설정되어 있어 중복 방지가 보장됨
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java:21-0
Timestamp: 2025-09-05T04:38:38.301Z
Learning: FcmTokenJpaEntity의 deviceId 필드는 Column(unique = true) 제약과 데이터베이스 레벨 UNIQUE 제약이 모두 설정되어 있음
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java:44-51
Timestamp: 2025-09-05T05:26:24.607Z
Learning: FcmTokenJpaEntity에서 deviceId는 디바이스를 식별하는 불변 값으로, updateFrom 메서드에서 동기화할 필요가 없음
src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java (1)

20-24: deleteByUserIdAndDeviceId 반환 타입 변경 및 삭제 실패 정책 결정 필요

  • FcmTokenPersistencePort.deleteByUserIdAndDeviceId 시그니처 void → boolean으로 변경, 구현체 및 JpaRepository 쿼리도 반환값 반영
  • 삭제 결과가 false(0건 삭제)일 때 EntityNotFoundException 발생 vs idempotent(204) 허용 중 정책 선택
  • API 소비자 측에서 “이미 삭제된 경우” 어떤 응답을 기대하는지 확인 요망
-        fcmTokenPersistencePort.deleteByUserIdAndDeviceId(command.userId(), command.deviceId());
+        boolean deleted = fcmTokenPersistencePort.deleteByUserIdAndDeviceId(command.userId(), command.deviceId());
+        if (!deleted) {
+            throw new konkuk.thip.common.exception.EntityNotFoundException(
+                konkuk.thip.common.exception.code.ErrorCode.FCM_TOKEN_NOT_FOUND
+            );
+        }
src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java (1)

27-31: LGTM: 조회 경로 단순·명확

findByDeviceId → Optional 매핑이 깔끔합니다.

src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)

4-4: 의존성 방향 바로잡음 — 도메인 RoomStatus 사용 확인

adapter 타입 참조 제거하고 도메인 value로 교체한 점 좋습니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (1)

39-52: from/to 분리로 논리 오류 해소됨

IN_PROGRESS 전환에서 대상/결과 상태 분리가 명확합니다. 업데이트 조건도 의도에 부합합니다.

Comment on lines +34 to +40
public FcmToken save(FcmToken token) {
UserJpaEntity user = userJpaRepository.findByUserId(token.getUserId())
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND));

var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user));
return fcmTokenMapper.toDomainEntity(saved);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동시 등록(중복 키) 대비 업서트 가드

save 경로에서 동시성으로 device_id UNIQUE 충돌이 날 수 있습니다. 제약 추가와 함께 DataIntegrityViolationException을 잡아 업데이트로 전환(upsert)하는 방식을 고려해 주세요.

예시:

+import org.springframework.dao.DataIntegrityViolationException;
@@
-        var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user));
-        return fcmTokenMapper.toDomainEntity(saved);
+        try {
+            var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user));
+            return fcmTokenMapper.toDomainEntity(saved);
+        } catch (DataIntegrityViolationException e) {
+            // deviceId UNIQUE 충돌 시 기존 행 갱신 시도
+            return findByDeviceId(token.getDeviceId())
+                    .map(existing -> {
+                        existing.updateToken(
+                            token.getFcmToken(), token.getPlatformType(), token.getLastUsedDate(), token.getUserId()
+                        );
+                        update(existing);
+                        return existing;
+                    })
+                    .orElseThrow(() -> e);
+        }

주: 저장소에 UNIQUE 제약이 선행되어야 의미가 있습니다.

📝 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.

Suggested change
public FcmToken save(FcmToken token) {
UserJpaEntity user = userJpaRepository.findByUserId(token.getUserId())
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND));
var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user));
return fcmTokenMapper.toDomainEntity(saved);
}
// at the top of the file
import org.springframework.dao.DataIntegrityViolationException;
public FcmToken save(FcmToken token) {
UserJpaEntity user = userJpaRepository.findByUserId(token.getUserId())
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND));
try {
var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user));
return fcmTokenMapper.toDomainEntity(saved);
} catch (DataIntegrityViolationException e) {
// deviceId UNIQUE 충돌 시 기존 행 갱신 시도
return findByDeviceId(token.getDeviceId())
.map(existing -> {
existing.updateToken(
token.getFcmToken(),
token.getPlatformType(),
token.getLastUsedDate(),
token.getUserId()
);
update(existing);
return existing;
})
.orElseThrow(() -> e);
}
}

Comment on lines +61 to +63
public void deleteByUserIdAndDeviceId(Long userId, String deviceId) {
fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 결과 반환으로 레이스 검출

포트 제안과 일치하도록 삭제된 행 수를 기반으로 boolean 반환을 권장합니다. 저장소 메서드는 @Modifying + 카운트 반환이 필요합니다.

적용 예시(저장소가 long deleteByUserIdAndDeviceId(...)를 반환한다고 가정):

-    public void deleteByUserIdAndDeviceId(Long userId, String deviceId) {
-        fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId);
-    }
+    public boolean deleteByUserIdAndDeviceId(Long userId, String deviceId) {
+        return fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId) == 1L;
+    }
📝 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.

Suggested change
public void deleteByUserIdAndDeviceId(Long userId, String deviceId) {
fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId);
}
public boolean deleteByUserIdAndDeviceId(Long userId, String deviceId) {
return fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId) == 1L;
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java
around lines 61-63, the deleteByUserIdAndDeviceId method currently returns void
which prevents detecting races; change it to return boolean and call the
repository method that returns the count of deleted rows (assume long), then
return (deletedCount > 0). Update the repository method to use @Modifying and
return long so the adapter can make the boolean decision and surface whether a
record was actually removed.

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

수정해주신 코드 확인했습니다!

그런데 여전히 RoomCommandPort 에
int updateRoomStateToExpired(RoomStatus exceptStatus);

메서드가 정의되어 있는데, 이 부분은 아직 수정이 덜 된 부분일까요??

메서드 네이밍이 수정되던지, 아니면 메서드의 파라미터를 없애고 영속성 어댑터 내부에서 RoomStatus.EXPIRED 를 주입하는게 좋을 것 같습니다!

Comment on lines 68 to 69
public int updateRoomStateToInProgress(RoomStatus fromStatus, RoomStatus toStatus) {
return roomJpaRepository.updateRoomStatusToInProgress(fromStatus, toStatus);
public int updateRoomState(RoomStatus fromStatus, RoomStatus toStatus) {
return roomJpaRepository.updateRoomStatus(fromStatus, toStatus);
Copy link
Collaborator

Choose a reason for hiding this comment

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

아 이렇게 메서드 네이밍을 변경해주셨군요! 이것도 좋습니다!

hd0rable
hd0rable previously approved these changes Sep 7, 2025
Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

pr 볼륨이 정말 장난아니네요,, 혼자 api 구현 3개하는거랑 맞먹는 수준이네요.. 너무 수고하셨습니다!! 적절한 주석처리랑 코드 너무 깔끔해서 술술읽히네요,, 늦게 pr확인하니 성준띠니가 예리하게 다 잡아주셧네요 굿입니닷 👍🏻👍🏻👍🏻


// 매일 0시 실행
@Async
@Async("schedulerAsyncExecutor")
Copy link
Member

Choose a reason for hiding this comment

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

옷 감사합니닷

return feedCommandPort.getPostQueryDtoById(postId);
}

return recordCommandPort.getPostQueryDtoById(postId);
Copy link
Member

Choose a reason for hiding this comment

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

엇 근데 혹시 모임방의 내 게시글(기록&투표)에 대한 댓글알림을 전송할때 필요한 정보들을 찾는 메서드를 따로 추가로 정의하신거같은데 지금코드에서는 투표가 누락되어있는거같은데 맞나요..??

--> 아 메서드 타고 넘어가니 투표도 처리되는거 확인했습니다!! recordCommandPort라 기록만 처리하는줄 알고 헷갈렸네요.. roomPostCommandPort가 없으니 헷갈렸네요,,

@Transactional
public void changeRoomStateToExpired() {
int updated = roomCommandPort.updateRoomStateToExpired(RoomStatus.EXPIRED);
log.info("[RoomState] EXPIRED로 변경된 건수={}", updated);
Copy link
Member

Choose a reason for hiding this comment

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

오오 꼼꼼한 로깅까지 좋네요

);
return feedCommandPort.save(feed);

// 4. 피드 영속화
Copy link
Member

Choose a reason for hiding this comment

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

LGTM


public class RoomEvents {

// 댓글 대상이 "기록/투표" 모두 가능하므로 통합 스키마 사용
Copy link
Member

Choose a reason for hiding this comment

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

떰즈업

@Transactional
public void deleteToken(FcmTokenDeleteCommand command) {
FcmToken fcmToken = fcmTokenPersistencePort.getByDeviceIdOrThrow(command.deviceId());
fcmToken.validateFcmOwner(command.userId());
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

@buzz0331
Copy link
Contributor Author

buzz0331 commented Sep 8, 2025

@seongjunnoh @hd0rable 리뷰 모두 반영했습니다! 확인부탁드릴게요!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (1)

16-17: @Profile 표현식 OR 사용 방식 수정 필요

@Profile("prod | dev")는 Spring 버전에 따라 인식되지 않을 수 있습니다. OR 조건은 배열로 명시하는 편이 가장 호환성이 높습니다.

아래처럼 변경을 권장합니다.

-@Profile("prod | dev")
+@Profile({"prod", "dev"})

참고: 동일 패턴("local | test")을 사용 중인 FakeFirebaseAdapter도 함께 정리해 주세요.

🧹 Nitpick comments (18)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java (1)

15-23: exists 쿼리는 파생 메서드로 단순화 + DISTINCT로 불필요 스캔 축소 제안

  • 존재 여부는 파생 메서드가 더 간결하며 벤더에 따라 더 효율적인 SQL이 생성됩니다.
  • 선택적으로 DISTINCT를 추가해 잠재적 중복 스캔을 줄일 수 있습니다(Set 반환이라 논리 결과는 동일).
-    @Query("SELECT p.postJpaEntity.postId FROM PostLikeJpaEntity p WHERE p.userJpaEntity.userId = :userId AND p.postJpaEntity.postId IN :postIds")
-    Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds,
-                                     @Param("userId") Long userId);
+    @Query("SELECT DISTINCT p.postJpaEntity.postId FROM PostLikeJpaEntity p " +
+           "WHERE p.userJpaEntity.userId = :userId AND p.postJpaEntity.postId IN :postIds")
+    Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds,
+                                     @Param("userId") Long userId);
@@
-    @Query("SELECT CASE WHEN COUNT(pl) > 0 THEN true ELSE false END " +
-            "FROM PostLikeJpaEntity pl " +
-            "WHERE pl.userJpaEntity.userId = :userId AND pl.postJpaEntity.postId = :postId")
-    boolean existsByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);
+    boolean existsByUserJpaEntity_UserIdAndPostJpaEntity_PostId(Long userId, Long postId);

참고: 호출부에서 빈 postIds(Set.isEmpty())는 조기에 빈 결과를 반환하도록 가드하는 편이 안전합니다(JPA 구현별 IN () 처리 차이).

src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java (1)

16-19: 빈 postIds 입력 가드로 불필요한 DB 호출 방지

호출자가 빈 Set을 전달할 수 있다면, 조기에 반환하면 안전하고 빠릅니다.

     @Override
     public Set<Long> findPostIdsLikedByUser(Set<Long> postIds, Long userId) {
-        return postLikeJpaRepository.findPostIdsLikedByUser(postIds, userId);
+        if (postIds == null || postIds.isEmpty()) {
+            return java.util.Collections.emptySet();
+        }
+        return postLikeJpaRepository.findPostIdsLikedByUser(postIds, userId);
     }
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1)

17-20: 중복 static import 정리 제안

와일드카드와 개별 상수 import가 혼재되어 있습니다. 코드스타일 및 가독성을 위해 개별 상수만 명시하세요.

-import static konkuk.thip.common.exception.code.ErrorCode.*;
-import static konkuk.thip.common.exception.code.ErrorCode.RECORD_NOT_FOUND;
-import static konkuk.thip.common.exception.code.ErrorCode.VOTE_NOT_FOUND;
+import static konkuk.thip.common.exception.code.ErrorCode.FEED_NOT_FOUND;
+import static konkuk.thip.common.exception.code.ErrorCode.RECORD_NOT_FOUND;
+import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND;
+import static konkuk.thip.common.exception.code.ErrorCode.VOTE_NOT_FOUND;
src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (1)

357-362: 주석 오타 수정 제안: '유선' → '유저'
가독성 보완을 위해 주석의 오타를 교정해 주세요.

-                 * 특정 유선 공개 피드 최신순 조회
+                 * 특정 유저 공개 피드 최신순 조회
src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (5)

63-66: 배치 일부 실패 시 예외 전파 정책 재고

현재는 failureCount > 0이면 예외를 던집니다. PR 토론 결론(실패 시 재시도/개별 처리 없음, 로깅+모니터링 위주)과 맞지 않아 상위 흐름을 불필요하게 error로 만들 수 있습니다. WARN 로그만 남기고 정상 종료하는 것을 제안합니다.

-            if (batchResponse.getFailureCount() > 0) {
-                log.warn("[FCM:BATCH] 일부 메시지 전송 실패: {}/{}", batchResponse.getFailureCount(), messages.size());
-                throw new FirebaseException();
-            }
+            if (batchResponse.getFailureCount() > 0) {
+                log.warn("[FCM:BATCH] 일부 메시지 전송 실패: {}/{}", batchResponse.getFailureCount(), messages.size());
+                // 정책상 재시도/개별 보정 없음 → 예외 미전파
+            }

38-40: 리스트 크기 검증 메시지에 실제 크기 포함

운영 중 원인 파악을 쉽게 하려면 실제 크기를 함께 남겨 주세요.

-throw new FirebaseException(new IllegalArgumentException("메시지, FCM 토큰, 디바이스 ID 리스트의 크기는 같아야 합니다."));
+throw new FirebaseException(new IllegalArgumentException(
+        String.format("리스트 크기 불일치: messages=%d, tokens=%d, devices=%d",
+                messages.size(), fcmTokens.size(), deviceIds.size())));

28-33: 로깅 수준/내용은 합리적이나 공통 마스킹 유틸로의 이관을 고려

현재 마스킹 로직이 어댑터 내부에 있어 재사용이 어렵습니다. 공통 로깅/보안 유틸로 이동하면 일관성 관리가 쉬워집니다.

Also applies to: 51-59, 68-70


91-105: 마스킹 길이 최적화(로그 부하/가독성)

절반 마스킹은 토큰이 길 때 로그 라인이 과도하게 길어질 수 있습니다. 앞 6/뒤 4 노출과 고정 길이 별표(예: 8~12개)를 권장합니다. 운영 가독성↑, 비용↓.

-    private String maskHalf(String s) {
+    private String maskHalf(String s) {
         if (s == null || s.isEmpty()) return "null";
-        int len = s.length();
-        if (len <= 4) return "*".repeat(len); // 너무 짧으면 전부 마스킹
-
-        int maskLen = len / 2;                 // 절반 마스킹
-        int visible = len - maskLen;           // 보이는 길이
-        int left = visible / 2;                // 앞쪽 보이는 길이
-        int right = visible - left;            // 뒤쪽 보이는 길이
-
-        String prefix = s.substring(0, left);
-        String stars = "*".repeat(maskLen);
-        String suffix = s.substring(len - right);
-        return prefix + stars + suffix;
+        if (s.length() <= 4) return "****";
+        int left = Math.min(6, Math.max(2, s.length() / 4));
+        int right = Math.min(4, Math.max(2, s.length() / 5));
+        String prefix = s.substring(0, left);
+        String suffix = s.substring(s.length() - right);
+        return prefix + "********" + suffix; // 8자 고정
     }

42-71: 운영 가시성 향상을 위한 간단한 메트릭 훅 제안

카운터(성공/실패 수)만 기록해도 경향 파악에 유용합니다. Micrometer 사용 시 도입 비용 낮습니다.

예: Counter.builder("fcm.send.success"), Counter.builder("fcm.send.fail")send/sendBatch 지점에 추가.

src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java (1)

6-9: null 반환 계약 명확화(Optional 권장)

조회 실패 시 fetchOne()null을 돌려줄 수 있습니다. NPE 방지를 위해 반환 타입을 Optional<PostQueryDto>로 바꾸거나 @Nullable로 계약을 명시하는 것을 권장합니다.

가능한 변경 예:

 package konkuk.thip.post.adapter.out.persistence.repository;

 import konkuk.thip.post.application.port.out.dto.PostQueryDto;
+import java.util.Optional;

 public interface PostQueryRepository {
-    PostQueryDto getPostQueryDtoByFeedId(Long feedId);
+    Optional<PostQueryDto> getPostQueryDtoByFeedId(Long feedId);

-    PostQueryDto getPostQueryDtoByRecordId(Long recordId);
-    PostQueryDto getPostQueryDtoByVoteId(Long voteId);
+    Optional<PostQueryDto> getPostQueryDtoByRecordId(Long recordId);
+    Optional<PostQueryDto> getPostQueryDtoByVoteId(Long voteId);
 }
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java (1)

7-8: @repository 애너테이션은 생략 가능

Spring Data 스캐닝이 이미 설정되어 있다면 인터페이스에 @Repository는 선택사항입니다. 불필요한 빈 스캔을 줄이려면 제거해도 됩니다.

-@Repository
 public interface PostJpaRepository extends JpaRepository<PostJpaEntity, Long>, PostQueryRepository {
src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1)

9-11: 조회 전용 트랜잭션 권장

읽기 전용 트랜잭션을 적용하면 플러시 방지 및 약간의 성능 이점이 있습니다.

 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;

 @Repository
 @RequiredArgsConstructor
+@Transactional(readOnly = true)
 public class PostQueryPersistenceAdapter implements PostQueryPort {
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (2)

36-39: 중복 조건 제거 가능(가독성)

from(feed)를 사용하는 이상 feed.dtype = FEED 조건은 중복일 가능성이 높습니다. 유지 의도가 없다면 제거해도 됩니다.

-                .where(feed.dtype.eq(FEED.getType()))

27-39: 파라미터 유효성 검증 고려

feedIdnull이면 런타임 시 쿼리 빌드 실패가 날 수 있습니다. 조기 검증 또는 requireNonNull 적용을 검토하세요.

src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)

6-6: 상태 전이용 Port 시그니처 추가는 적절합니다. 다만 Finder는 Query 포트로 분리 제안

  • updateRoomStateToExpired / updateRoomStateFromRecruitingToProgress: 배치 전이 의도가 명확해 좋습니다.
  • findProgressTargetRooms: Command 포트보다는 Query 포트로의 책임 분리가 더 자연스럽습니다(오용 방지).

Also applies to: 24-28

src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (1)

62-78: 배치 전이 위임 구현 OK + 네이밍 일치성 제안

  • Repository 위임/매핑 흐름 적절합니다.
  • repository 메서드명이 findProgressTargetIds인데 실제 반환은 엔티티 리스트입니다. 의미 혼동을 줄이기 위해 메서드명을 Entities/Rooms로 교정 권장합니다.

제안 diff(레포지토리와 함께 변경):

-        List<RoomJpaEntity> roomJpaEntities = roomJpaRepository.findProgressTargetIds();
+        List<RoomJpaEntity> roomJpaEntities = roomJpaRepository.findProgressTargetRooms();
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (1)

52-59: 메서드 네이밍/타입 일치성 및 날짜 파라미터화 제안

  • 반환이 엔티티인데 메서드명이 Ids입니다. 의미 일치로 교정 권장.
  • current_date는 DB 타임존 의존성이 있으므로 today 파라미터 주입을 고려하세요.
-    List<RoomJpaEntity> findProgressTargetIds();
+    List<RoomJpaEntity> findProgressTargetRooms();

(선택) 날짜 파라미터화:

-            where r.startDate <= current_date
-              and r.endDate   >= current_date
+            where r.startDate <= :today
+              and r.endDate   >= :today
-              and r.roomStatus = 'RECRUITING'
+              and r.roomStatus = :status
@@
-    List<RoomJpaEntity> findProgressTargetRooms();
+    List<RoomJpaEntity> findProgressTargetRooms(@Param("today") LocalDate today,
+                                                @Param("status") RoomStatus status);
src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java (1)

52-60: N+1 조회와 대량 이벤트 발행 비용 — 배치/스트리밍 전환 제안

  • 방별 participants 개별 조회는 N+1입니다. roomIds 단건 리스트로 한번에 가져오고(roomParticipantCommandPort.findAllByRoomIdIn), 메모리에서 groupBy 후 발행하세요.
  • 수천 건 규모 대비를 위해 per-room 단위 배치(예: 500명씩 chunk) 또는 단일 이벤트에 userIds 리스트를 싣는 형태도 고려해 주세요.
  • 알림/수신자 건수 로깅을 추가하면 운영 가시성에 유용합니다.

예시(서비스 측 단편):

-        List<Room> targetRooms = roomCommandPort.findProgressTargetRooms();
-        for (Room room : targetRooms) {
-            List<RoomParticipant> targetUsers = roomParticipantCommandPort.findAllByRoomId(room.getId());
-            for (RoomParticipant participant : targetUsers) {
-                roomEventCommandPort.publishRoomActivityStartedEvent(participant.getUserId(), room.getId(), room.getTitle());
-            }
-        }
+        List<Room> rooms = roomCommandPort.findProgressTargetRooms();
+        List<Long> roomIds = rooms.stream().map(Room::getId).toList();
+        List<RoomParticipant> participants = roomParticipantCommandPort.findAllByRoomIdIn(roomIds);
+        Map<Long, List<Long>> userIdsByRoomId = participants.stream()
+            .collect(Collectors.groupingBy(RoomParticipant::getRoomId,
+                     Collectors.mapping(RoomParticipant::getUserId, Collectors.toList())));
+        for (Room room : rooms) {
+            List<Long> userIds = userIdsByRoomId.getOrDefault(room.getId(), List.of());
+            // 필요시 chunk 처리
+            for (Long userId : userIds) {
+                roomEventCommandPort.publishRoomActivityStartedEvent(userId, room.getId(), room.getTitle());
+            }
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ad3d9a and 738d5cb.

📒 Files selected for processing (32)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (0 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (3 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java
✅ Files skipped from review due to trivial changes (3)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java
  • src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java
  • src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java
  • src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java
  • src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.

Applied to files:

  • src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
📚 Learning: 2025-09-05T04:11:01.456Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.456Z
Learning: RoomStateScheduler에서 changeRoomStateToExpired()와 changeRoomStateToProgress()는 각각 다른 트랜잭션에서 실행되며, 날짜 기반으로 서로 다른 방들을 대상으로 하므로 실행 순서가 바뀌어도 데이터 정합성에 문제가 없다.

Applied to files:

  • src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java
  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
📚 Learning: 2025-09-05T04:11:01.456Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.456Z
Learning: RoomStateScheduler의 changeRoomStateToExpired()와 changeRoomStateToProgress()는 서로 다른 날짜 조건으로 완전히 다른 방들을 대상으로 하므로(end_date < today vs start_date <= today AND end_date >= today), 비동기 실행 순서와 관계없이 데이터 정합성에 문제가 없다.

Applied to files:

  • src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java
  • src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
📚 Learning: 2025-09-05T05:26:24.628Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java:44-51
Timestamp: 2025-09-05T05:26:24.628Z
Learning: FcmTokenJpaEntity에서 deviceId는 디바이스를 식별하는 불변 값으로, updateFrom 메서드에서 동기화할 필요가 없음

Applied to files:

  • src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java
📚 Learning: 2025-09-05T04:38:38.325Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java:21-0
Timestamp: 2025-09-05T04:38:38.325Z
Learning: FcmTokenJpaEntity의 deviceId 필드는 JPA Column(unique = true)와 데이터베이스 레벨 UNIQUE 제약이 모두 설정되어 있어 중복 방지가 보장됨

Applied to files:

  • src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java
📚 Learning: 2025-09-05T04:38:38.325Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java:21-0
Timestamp: 2025-09-05T04:38:38.325Z
Learning: FcmTokenJpaEntity의 deviceId 필드는 Column(unique = true) 제약과 데이터베이스 레벨 UNIQUE 제약이 모두 설정되어 있음

Applied to files:

  • src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java
📚 Learning: 2025-08-14T09:27:36.450Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.450Z
Learning: AttendanceCheckJpaRepository의 countByUserIdAndCreatedAtBetween 메서드는 메서드명과 달리 사용자 정의 JPQL 쿼리를 사용한다. "a.createdAt >= :startOfDay AND a.createdAt < :endOfDay"로 반열림구간 [startOfDay, endOfDay)을 정확히 구현하고 있어서 경계값 버그가 없다. 메서드명만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.

Applied to files:

  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
🧬 Code graph analysis (5)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java (2)
src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1)
  • Repository (9-30)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (1)
  • Repository (17-73)
src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java (1)
src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java (1)
  • Slf4j (9-24)
src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (1)
  • Repository (17-73)
src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (2)
src/main/java/konkuk/thip/common/exception/FirebaseException.java (1)
  • FirebaseException (5-21)
src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java (1)
  • Slf4j (12-27)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (1)
src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1)
  • Repository (9-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (22)
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java (1)

1-1: 패키지 경로 리팩터링 적절합니다

리포지토리 전용 하위 패키지로 이동해 모듈 경계가 명확해졌습니다. 빈 스캐닝에도 영향 없어 보입니다.

src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java (1)

3-3: 리포지토리 경로 변경 반영 OK

주요 동작 영향 없이 의존 경로만 정리되었습니다.

src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1)

4-4: 리포지토리 경로 변경 반영 OK

기능 영향 없이 import만 업데이트되었습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)

9-9: 테스트 import 경로 변경 반영 OK

패키지 리팩터링과 일관됩니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java (1)

8-8: 테스트 import 경로 변경 반영 OK

리팩터링에 맞춘 경로 수정만 있습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1)

12-12: 테스트 import 경로 변경 반영 OK

기능 영향 없습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java (1)

11-11: 테스트 import 경로 변경 반영 OK

다른 테스트들과 일관성 있습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (1)

9-9: 테스트 import 경로 변경 및 잔여 참조 검증 완료 잔여 구 경로 참조가 없음을 확인했습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (1)

9-9: PostLikeJpaRepository 패키지 경로 검증 완료

변경된 패키지 위치 일치, 구 경로 참조 잔존 없음, 스캔 범위 제한 설정 없음 확인됨.

src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (3)

25-34: 단건 전송 예외 처리 정책은 LGTM

성공은 DEBUG, 실패는 WARN + 예외 전파로 상위 계층에서 모니터링하기 적절합니다.


42-44: sendEach 메서드 사용은 올바릅니다 – 수정 불필요
com.google.firebase:firebase-admin:9.3.0은 9.2.0부터 sendEach API를 제공하며 sendAll은 deprecated 상태입니다. (firebase.google.cn, firebase.google.com)

Likely an incorrect or invalid review comment.


22-24: @value("${server.profile}") 기본값 미지정 시 빈 생성 실패 위험
server.profile 프로퍼티가 정의되지 않으면 애플리케이션 시작 중 빈 생성이 즉시 실패합니다.

  • 최소 변경: 기본값 빈 문자열 추가

    -    @Value("${server.profile}")
    +    @Value("${server.profile:}")
         private String profile;
  • 권장: Spring 표준 Environment·Profiles 사용해 중복 설정 제거

    +    private final Environment env;
    
    -    @Value("${server.profile:}")
    -    private String profile;
    
    -    private boolean isDev() {
    -        return profile != null && profile.trim().equalsIgnoreCase("dev");
    -    }
    +    private boolean isDev() {
    +        return env.acceptsProfiles(Profiles.of("dev"));
    +    }
  • application.properties/yml에서 server.profile 정의 여부 확인 필요

src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java (1)

5-10: CQRS 분리 방향 LGTM

조회 전용 포트/레포지토리로의 분리는 THIP의 CQRS 컨벤션과도 잘 맞습니다.

src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java (2)

5-10: 포트 정의 방향 LGTM

도메인 레벨에서 조회 전용 포트를 분리한 점 좋습니다.


6-9: null 처리 계약 확인 요청

어댑터/리포지토리에서 조회 실패 시 null이 전파될 수 있습니다. 호출부 계약에 맞춰 Optional 반환으로 통일하거나, null 허용을 문서화해 주세요.

src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java (1)

7-9: Spring Data 조합 구조 적절

PostJpaRepositoryPostQueryRepository를 확장하는 구조는 커스텀 프래그먼트 조합에 적합합니다.

src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java (1)

16-28: 조회 실패 시 null 전파 가능성 확인

postJpaRepository.getPostQueryDtoByXXX()가 결과 없음이면 null일 수 있습니다. 상위 계층에서 안전하게 처리되는지 확인해 주세요(무시/로그/예외).

src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java (1)

43-71: 타입 캐스팅(treat) 사용 적절 — 주석 유지 권장

상속 매핑에 대한 treat 사용이 적절합니다. 유지보수를 위해 현재와 같은 주석을 유지하는 것에 동의합니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

4-5: Import 정리만 발생 — OK

패키지 경로/정렬 변경만 있으며 동작 영향 없습니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (1)

29-37: roomStatus Enum 매핑 확인 및 DB 타임존 검증 필요

  • src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java:56 에서 @Enumerated(EnumType.STRING) 적용으로 EnumType.STRING 매핑이 확인됨.
  • application-*.yml/properties에 timezone 설정이 보이지 않습니다. JDBC URL 또는 Spring 설정에서 DB 타임존이 Asia/Seoul로 설정되었는지 검증 필요.
src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java (2)

30-36: 만료 전이 로직 OK

비동기 + 트랜잭션 경계 적절, 배치 업데이트 카운트 로깅도 좋습니다.


41-50: 진행 전이 순서(알림→상태변경) 합리적

학습 메모대로(EXPIRED/PROGRESS 대상 분리) 동시 실행에 따른 데이터 정합성 문제 없습니다. 현재 순서가 중복 알림을 줄이는 데 유리합니다.

Comment on lines +12 to +19
import org.springframework.stereotype.Repository;

import static com.querydsl.jpa.JPAExpressions.treat;
import static konkuk.thip.post.domain.PostType.*;

@Repository
@RequiredArgsConstructor
public class PostQueryRepositoryImpl implements PostQueryRepository {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

커스텀 구현에 @repository 제거 권장(프래그먼트 충돌 방지)

레포지토리 프래그먼트 구현 클래스는 보통 스프링 빈으로 등록하지 않습니다. 현재 상태에선 PostQueryRepository 타입 주입 시 PostJpaRepository 프록시와 이 구현 클래스가 둘 다 후보가 되어 충돌할 수 있습니다.

-import org.springframework.stereotype.Repository;
+// (스프링 빈 등록 제거)

-@Repository
 @RequiredArgsConstructor
 public class PostQueryRepositoryImpl implements PostQueryRepository {
📝 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.

Suggested change
import org.springframework.stereotype.Repository;
import static com.querydsl.jpa.JPAExpressions.treat;
import static konkuk.thip.post.domain.PostType.*;
@Repository
@RequiredArgsConstructor
public class PostQueryRepositoryImpl implements PostQueryRepository {
// (스프링 빈 등록 제거)
import static com.querydsl.jpa.JPAExpressions.treat;
import static konkuk.thip.post.domain.PostType.*;
@RequiredArgsConstructor
public class PostQueryRepositoryImpl implements PostQueryRepository {
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java
around lines 12 to 19, the custom repository implementation is annotated with
@Repository which can cause bean candidate conflicts with the Spring Data JPA
repository proxy; remove the @Repository annotation from this implementation
class (and any unused import of org.springframework.stereotype.Repository) so
only the Spring Data generated proxy for PostQueryRepository is registered as
the bean.

private final RecordCommandPort recordCommandPort;
private final VoteCommandPort voteCommandPort;

private final PostQueryPort portQueryPort;
Copy link

@coderabbitai coderabbitai bot Sep 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

오타: 필드명 portQueryPortpostQueryPort

가독성과 자동완성(검색)에 치명적이므로 즉시 수정 권장.

-    private final PostQueryPort portQueryPort;
+    private final PostQueryPort postQueryPort;
📝 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.

Suggested change
private final PostQueryPort portQueryPort;
private final PostQueryPort postQueryPort;
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
around line 24, the field name is misspelled as "portQueryPort" and should be
"postQueryPort"; rename the field declaration to postQueryPort, update the
constructor parameter and assignment, and refactor all usages inside this class
(and any external references or tests) to use postQueryPort so compilation and
autocompletion work correctly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@buzz0331 오타있네요ㅎ

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 42 to 48
public PostQueryDto getPostQueryDto(PostType type, Long postId) {
return switch (type) {
case FEED -> portQueryPort.getPostQueryDtoByFeedId(postId);
case RECORD -> portQueryPort.getPostQueryDtoByRecordId(postId);
case VOTE -> portQueryPort.getPostQueryDtoByVoteId(postId);
};
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

필드명 변경 반영 및 null 방어

필드명 오타 수정에 따라 참조도 함께 변경 필요. 또한 조회 실패 시 null일 수 있으니 호출부에서 방어하거나 포트 반환을 Optional로 승격하세요.

-    public PostQueryDto getPostQueryDto(PostType type, Long postId) {
+    public PostQueryDto getPostQueryDto(PostType type, Long postId) {
         return switch (type) {
-            case FEED -> portQueryPort.getPostQueryDtoByFeedId(postId);
-            case RECORD -> portQueryPort.getPostQueryDtoByRecordId(postId);
-            case VOTE -> portQueryPort.getPostQueryDtoByVoteId(postId);
+            case FEED -> postQueryPort.getPostQueryDtoByFeedId(postId);
+            case RECORD -> postQueryPort.getPostQueryDtoByRecordId(postId);
+            case VOTE -> postQueryPort.getPostQueryDtoByVoteId(postId);
         };
     }

추가로 PostHandler 클래스 내 필드명과 생성자 주입부를 확인합니다.


🏁 Script executed:

#!/bin/bash
# 클래스 선언부 확인
rg -n 'class PostHandler' -C5 src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
# 생성자 시그니처 확인
rg -n 'public PostHandler' -C5 src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
# QueryPort 관련 필드 검색
rg -n 'QueryPort' -C3 src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java

Length of output: 1373


🏁 Script executed:

#!/bin/bash
# portQueryPort 사용 위치 검색
rg -nC3 '\bportQueryPort\b' src/main/java/konkuk/thip/post

# PostQueryPort 인터페이스 메서드 반환 타입 확인
sed -n '1,200p' src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java

Length of output: 2175


PostQueryPort 필드명 및 null 방어 로직 수정

  • 필드명 수정: PostHandler.java 24번 줄의 private final PostQueryPort portQueryPort;
    private final PostQueryPort postQueryPort;로 변경하고, 44~46번 줄의 메서드 호출도 portQueryPortpostQueryPort로 함께 수정하세요.
  • null 방어 추가: postQueryPort.getPostQueryDtoBy… 호출 결과가 null일 수 있으므로, 반환 타입을 Optional<PostQueryDto>로 승격하거나 호출부에서 NPE 방어 로직을 구현하세요.
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
(field at ~line 24 and method body around lines 42-48): rename the field private
final PostQueryPort portQueryPort; to private final PostQueryPort postQueryPort;
and update all usages in the method to call
postQueryPort.getPostQueryDtoByFeedId/postQueryPort.getPostQueryDtoByRecordId/postQueryPort.getPostQueryDtoByVoteId;
additionally add NPE protection by capturing the call result into a local
variable and either (a) change the method signature to return
Optional<PostQueryDto> and return Optional.ofNullable(result) or (b) keep the
current return type and if result is null throw a suitable runtime exception
(e.g. NotFoundException) or return a safe default; make the chosen null-handling
consistent across all three switch branches.

Comment on lines +29 to +37
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = 'EXPIRED'
where r.endDate < current_date
and r.roomStatus <> 'EXPIRED'
""")
int updateRoomStatusToExpired();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enum 필드에 문자열 리터럴 비교/대입 — 타입 불일치로 런타임 오류 위험

roomStatus가 Enum이면 JPQL에서 'EXPIRED' 문자열은 매칭되지 않습니다. Enum 파라미터 또는 enum literal을 사용하세요.

옵션 A(권장: 파라미터화)

-    @Query("""
-           update RoomJpaEntity r
-              set r.roomStatus = 'EXPIRED'
-            where r.endDate < current_date
-              and r.roomStatus <> 'EXPIRED'
-           """)
-    int updateRoomStatusToExpired();
+    @Query("""
+           update RoomJpaEntity r
+              set r.roomStatus = :toStatus
+            where r.endDate < current_date
+              and r.roomStatus <> :toStatus
+           """)
+    int updateRoomStatusToExpired(@Param("toStatus") RoomStatus toStatus);

옵션 B(enum literal 사용)

-              set r.roomStatus = 'EXPIRED'
+              set r.roomStatus = konkuk.thip.room.adapter.out.jpa.RoomStatus.EXPIRED
...
-              and r.roomStatus <> 'EXPIRED'
+              and r.roomStatus <> konkuk.thip.room.adapter.out.jpa.RoomStatus.EXPIRED
📝 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.

Suggested change
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = 'EXPIRED'
where r.endDate < current_date
and r.roomStatus <> 'EXPIRED'
""")
int updateRoomStatusToExpired();
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = :toStatus
where r.endDate < current_date
and r.roomStatus <> :toStatus
""")
int updateRoomStatusToExpired(@Param("toStatus") RoomStatus toStatus);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
around lines 29–37, the query sets and compares roomStatus using the string
literal 'EXPIRED' which will not match an Enum column and can cause runtime
errors; fix by replacing string literals with an Enum value — preferred:
parameterize the enum (e.g. set r.roomStatus = :status and where r.roomStatus <>
:status, add a method parameter RoomStatus status and pass RoomStatus.EXPIRED
when calling); alternative: use a JPQL enum literal (fully qualified enum name
like com.example.RoomStatus.EXPIRED) in place of 'EXPIRED' if you must keep it
inline.

Comment on lines +42 to +51
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = 'IN_PROGRESS'
where r.startDate <= current_date
and r.endDate >= current_date
and r.roomStatus = 'RECRUITING'
""")
int updateRoomStatusFromRecruitingToProgress();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

RECRUITING→IN_PROGRESS 전이도 문자열 리터럴 사용 — 동일 이슈

Enum 파라미터 2개(from/to)로 명확히 전이 정의하세요.

-    @Query("""
-           update RoomJpaEntity r
-              set r.roomStatus = 'IN_PROGRESS'
-            where r.startDate <= current_date
-              and r.endDate >= current_date
-              and r.roomStatus = 'RECRUITING'
-           """)
-    int updateRoomStatusFromRecruitingToProgress();
+    @Query("""
+           update RoomJpaEntity r
+              set r.roomStatus = :toStatus
+            where r.startDate <= current_date
+              and r.endDate   >= current_date
+              and r.roomStatus = :fromStatus
+           """)
+    int updateRoomStatusFromRecruitingToProgress(@Param("fromStatus") RoomStatus fromStatus,
+                                                 @Param("toStatus")   RoomStatus toStatus);
📝 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.

Suggested change
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = 'IN_PROGRESS'
where r.startDate <= current_date
and r.endDate >= current_date
and r.roomStatus = 'RECRUITING'
""")
int updateRoomStatusFromRecruitingToProgress();
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update RoomJpaEntity r
set r.roomStatus = :toStatus
where r.startDate <= current_date
and r.endDate >= current_date
and r.roomStatus = :fromStatus
""")
int updateRoomStatusFromRecruitingToProgress(@Param("fromStatus") RoomStatus fromStatus,
@Param("toStatus") RoomStatus toStatus);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java
around lines 42-51, the JPQL uses string literals for roomStatus transitions;
change the method to accept two RoomStatus enum parameters (e.g., fromStatus and
toStatus), update the @Query to set r.roomStatus = :toStatus and use where
r.roomStatus = :fromStatus, and add @Param annotations on the method parameters
so the enum values are bound into the query; keep the @Modifying and return type
(int) as-is.

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

확인했습니다!! 오타부분 확인해주시면 감사하겠습니다!

Comment on lines +73 to +77
/**
* 프로파일별 마스킹 정책
* - dev: 원문 그대로 노출
* - prod: 전체 길이의 절반을 '*'로 치환(치환된 개수만큼 별표가 보이도록), 나머지는 앞/뒤를 균등하게 노출
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

private final RecordCommandPort recordCommandPort;
private final VoteCommandPort voteCommandPort;

private final PostQueryPort portQueryPort;
Copy link
Collaborator

Choose a reason for hiding this comment

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

@buzz0331 오타있네요ㅎ

seongjunnoh
seongjunnoh previously approved these changes Sep 8, 2025
@buzz0331 buzz0331 merged commit 57d2a57 into develop Sep 8, 2025
3 of 4 checks passed
@buzz0331 buzz0331 deleted the feat/#289-fcm-notification branch September 8, 2025 17:24
@buzz0331 buzz0331 changed the title FCM 기반 푸쉬알림 기능 구현 [feat] FCM 기반 푸쉬알림 기능 구현 Sep 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-333] [chore] FCM 기반 푸쉬알림 기능 구현 [THIP2025-166] [feat] 스케줄러 필요한 기능

3 participants