Skip to content

Conversation

@Daae-Kim
Copy link
Member

@Daae-Kim Daae-Kim commented Jan 4, 2026

📌 관련 이슈

closes #294

✒️ 작업 내용

  • FCM 의존성 추가
  • notification 도메인 생성 및 entity 생성
  • 사용자 알림 토큰 저장 및 삭제 API 구현

스크린샷 🏞️ (선택)

💬 REVIEWER에게 요구사항 💬

Summary by CodeRabbit

  • 새로운 기능

    • 회원별 푸시 토큰 등록, 개별 삭제, 전체 삭제 REST API 추가 (POST/DELETE /api/pushToken 등)
    • FCM 등 다수 알림 제공자 지원 및 토큰 중복 방지·갱신으로 안정적 동작 보장
  • 보안

    • 푸시 토큰 관련 API에 인증 적용
  • 잡무(Chore)

    • Firebase Admin SDK 추가 (FCM 연동 준비)

✏️ Tip: You can customize this high-level summary in your review settings.

@Daae-Kim Daae-Kim requested review from kssumin and rlajm1203 January 4, 2026 09:18
@Daae-Kim Daae-Kim self-assigned this Jan 4, 2026
@Daae-Kim Daae-Kim added the feature 기능 개발 label Jan 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

Walkthrough

FCM(Firebase Admin) 의존성 추가 및 회원 푸시 토큰 관리 기능(엔티티, 리포지토리, 서비스, 컨트롤러, DTO, 예외, 변환기, API 문서)이 멀티레이어로 추가되었고 보안 설정에 /api/pushToken 경로가 인증 매처로 등록되었습니다.

Changes

Cohort / File(s) 변경 요약
의존성 및 버전
\eeos/build.gradle.kts`, `eeos/gradle/libs.versions.toml``
Firebase Admin SDK(libs.firebase.admin, version 9.2.0) 의존성 및 버전 항목 추가
보안 설정
\eeos/src/main/java/.../SecurityFilterChainConfig.java``
인증 체인에 /api/pushToken/api/pushToken/** 매처 추가
DTOs
\eeos/src/main/java/.../notification/application/dto/`CreateMemberPushTokenRequest.java`, `DeleteMemberPushTokenRequest.java``
푸시 토큰 생성/삭제 요청 DTO 추가 (@NotBlank 검증 포함)
예외들
\eeos/src/main/java/.../notification/application/exception/*Exception.java``
DeniedDeletePushTokenException, NotFoundNotificationProviderException, NotFoundPushTokenException 추가
도메인 모델 / enum
\eeos/src/main/java/.../notification/application/model/`MemberPushTokenModel.java`, `NotificationProvider.java``
MemberPushTokenModel 및 NotificationProvider(enum, FCM/ETC, find 메서드) 추가
엔티티 & JPA 리포지토리
\eeos/src/main/java/.../notification/persistence/`MemberPushTokenEntity.java`, `JpaMemberPushTokenRepository.java``
member_push_token JPA 엔티티 추가, JPA 리포지토리 조회/삭제/삭제ByUpdatedDateBefore 메서드 추가
응용 리포지토리 구현
\eeos/src/main/java/.../notification/persistence/`MemberPushTokenRepositoryImpl.java``
JPA 리포지토리와 엔티티↔모델 변환을 연결하는 구현체 추가
엔티티 변환기
\eeos/src/main/java/.../model/converter/`MemberPushTokenEntityConverter.java``
Entity ↔ Model 양방향 변환기 추가
서비스(유스케이스) 계층
\eeos/src/main/java/.../application/service/`NotificationTokenService.java`, .../usecase/*.java``
토큰 생성(멱등성: 기존 토큰은 lastActiveAt 갱신), 단건 삭제(소유권 검증), 전체 삭제 유스케이스 구현 및 인터페이스 추가
프레젠테이션 / 컨트롤러 / API 문서
\eeos/src/main/java/.../presentation/controller/`MemberPushTokenController.java`, .../presentation/docs/`MemberPushTokenApi.java``
POST /api/pushToken, DELETE /api/pushToken, DELETE /api/pushToken/all 컨트롤러 및 Swagger 계약 추가

Sequence Diagram(s)

%%{init: {"themeVariables":{"actorBorder":"#2b6cb0","actorBackground":"#ebf8ff","sequenceNumber":"#666"}}}%%
sequenceDiagram
    participant Client as Client
    participant Controller as MemberPushTokenController
    participant Service as NotificationTokenService
    participant Provider as NotificationProvider
    participant Repo as MemberPushTokenRepository
    participant DB as Database

    rect rgb(220,235,255)
    Note over Client,Controller: 푸시 토큰 생성 흐름
    Client->>Controller: POST /api/pushToken (memberId, pushToken, provider)
    Controller->>Service: create(memberId, request)
    Service->>Provider: NotificationProvider.find(provider)
    alt provider 존재
        Provider-->>Service: provider enum
        Service->>Repo: findByPushToken(pushToken)
        alt 토큰 없음
            Repo-->>Service: empty
            Service->>Repo: save(model)
            Repo->>DB: INSERT
            DB-->>Repo: OK
            Repo-->>Service: saved model
        else 토큰 존재
            Repo-->>Service: existing model -> renew/skip save
        end
    else provider 없음
        Provider-->>Service: throw NotFoundNotificationProviderException
    end
    Service-->>Controller: void
    Controller-->>Client: 201 CREATED
    end

    rect rgb(255,240,230)
    Note over Client,Controller: 푸시 토큰 삭제 흐름
    Client->>Controller: DELETE /api/pushToken (memberId, pushToken)
    Controller->>Service: delete(memberId, request)
    Service->>Repo: findByPushToken(pushToken)
    alt 토큰 없음
        Repo-->>Service: empty -> throw NotFoundPushTokenException
    else 토큰 존재
        Repo-->>Service: model
        alt memberId 일치
            Service->>Repo: deleteByPushToken(pushToken)
            Repo->>DB: DELETE
            DB-->>Repo: OK
        else 불일치
            Service-->>Service: throw DeniedDeletePushTokenException
        end
    end
    Service-->>Controller: void
    Controller-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rlajm1203
  • kssumin

Poem

🐰 푸시토큰 한 움큼 주워왔네,
바늘처럼 예민한 검증 달고,
주인만 지우게 문지기 세웠네,
FCM 깃털로 알림 살랑살랑,
당근 한 조각, 코드에 웃음 가득 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 풀 리퀘스트 제목은 '알림 토큰 생성, 삭제 API 구현'으로 변경 사항의 핵심인 알림 토큰 API 기능 구현을 명확하게 설명합니다.
Linked Issues check ✅ Passed 모든 요구사항이 충족되었습니다: FCM 의존성 추가, FCM 토큰 엔티티 생성, 중복 토큰 시 lastActiveAt 갱신, 토큰 저장/개별 삭제/전체 삭제 API 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항은 알림 토큰 생성 및 삭제 API 구현에 범위 내이며, SecurityFilterChainConfig 수정도 /api/pushToken 엔드포인트 보호에 필요한 관련 변경입니다.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f568b5e and 17bafe1.

📒 Files selected for processing (4)
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/repository/MemberPushTokenRepository.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/JpaMemberPushTokenRepository.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/JpaMemberPushTokenRepository.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kssumin
Repo: JNU-econovation/EEOS-BE PR: 218
File: eeos/src/main/java/com/blackcompany/eeos/target/presentation/controller/AttendWeightPolicyController.java:19-20
Timestamp: 2025-03-01T17:43:11.312Z
Learning: 사용자 kssumin은 토큰 기반의 ROLE 검증 기능을 별도 이슈로 해결할 예정이며, 관리자 권한 검증 관련 TODO 항목은 현재 PR에서 다루지 않기로 했습니다.
📚 Learning: 2025-03-01T17:43:11.312Z
Learnt from: kssumin
Repo: JNU-econovation/EEOS-BE PR: 218
File: eeos/src/main/java/com/blackcompany/eeos/target/presentation/controller/AttendWeightPolicyController.java:19-20
Timestamp: 2025-03-01T17:43:11.312Z
Learning: 사용자 kssumin은 토큰 기반의 ROLE 검증 기능을 별도 이슈로 해결할 예정이며, 관리자 권한 검증 관련 TODO 항목은 현재 PR에서 다루지 않기로 했습니다.

Applied to files:

  • eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java
🧬 Code graph analysis (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java (1)
  • DeniedDeletePushTokenException (6-17)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java (1)
  • NotFoundPushTokenException (6-19)
⏰ 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). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (4)
eeos/src/main/java/com/blackcompany/eeos/notification/application/repository/MemberPushTokenRepository.java (1)

9-26: LGTM!

리포지토리 인터페이스가 깔끔하게 설계되었습니다. findByMemberIdAndPushToken 메서드 추가로 회원별 토큰 중복 확인이 가능해졌고, 삭제 메서드들의 반환 타입도 적절합니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)

30-47: provider 변수가 기존 토큰 갱신 시 사용되지 않습니다.

Line 31에서 provider를 조회하지만, 기존 토큰이 존재할 경우 renew(memberId) 호출 시 이 값이 사용되지 않습니다. 사용자가 다른 provider로 요청을 보내도 기존 provider가 유지됩니다.

의도된 동작인지 확인해 주세요. 만약 provider 변경도 허용해야 한다면 renew 메서드에 provider를 전달하는 것을 고려해 주세요.


49-53: LGTM!

간단하고 명확한 구현입니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java (1)

14-67: LGTM!

리포지토리 구현이 깔끔합니다. Entity-Model 변환 로직이 일관되게 적용되어 있고, deleteByUpdatedDateBeforeLocalDateTimeTimestamp 변환도 적절합니다.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 4, 2026

Test Results

101 tests  ±0   98 ✅ ±0   5s ⏱️ ±0s
 40 suites ±0    3 💤 ±0 
 40 files   ±0    0 ❌ ±0 

Results for commit 17bafe1. ± Comparison against base commit 8ff8078.

♻️ This comment has been updated with latest results.

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: 10

🧹 Nitpick comments (6)
eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/CreateMemberPushTokenUsecase.java (1)

6-7: 인터페이스의 @component 어노테이션 제거 권장

인터페이스에 @Component 어노테이션을 사용하는 것은 Spring의 일반적인 관례가 아닙니다. 스테레오타입 어노테이션(@Component, @Service 등)은 일반적으로 구현 클래스에 적용됩니다.

현재 구조에서는 구현체인 NotificationTokenService가 적절한 어노테이션(@Service 등)을 가지고 있으므로, 인터페이스의 @Component는 제거하는 것이 명확합니다.

🔎 제안하는 수정
-@Component
 public interface CreateMemberPushTokenUsecase {
 	void create(Long memberId, CreateMemberPushTokenRequest request);
 }
eeos/gradle/libs.versions.toml (1)

13-13: Firebase Admin SDK 버전 업데이트 권장

Firebase Admin SDK 9.2.0 버전은 현재 최신 버전인 9.7.0(2025년 9월 24일 릴리스)보다 뒤처져 있습니다. 9.2.0에는 알려진 보안 취약점(CVE)이 없지만, 최신 버전으로 업데이트하여 버그 수정과 개선사항을 받을 것을 권장합니다.

eeos/build.gradle.kts (1)

67-68: Firebase Admin SDK 버전 업그레이드 권고

현재 사용 중인 Firebase Admin SDK 9.2.0은 알려진 보안 취약점이 없으나, 최신 버전 9.7.0 (2025년 9월 24일 출시)이 있습니다. 최신 버전으로 업그레이드하면 버그 수정 및 의존성 업데이트의 이점을 얻을 수 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/JpaMemberPushTokenRepository.java (1)

27-27: java.time.LocalDateTime 사용을 고려해보세요.

java.sql.Timestamp 대신 java.time.LocalDateTime을 파라미터로 직접 사용하는 것이 최신 Java 날짜/시간 API를 활용하는 더 나은 방법입니다. Spring Data JPA는 LocalDateTime을 자동으로 변환해줍니다.

🔎 제안하는 개선안
-int deleteByUpdatedDateBefore(Timestamp updatedDate);
+int deleteByUpdatedDateBefore(LocalDateTime updatedDate);

단, 이 경우 MemberPushTokenRepositoryImpl의 변환 로직도 제거할 수 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)

46-50: 삭제된 토큰 수 반환 고려

현재 메서드는 void를 반환하므로 실제로 토큰이 삭제되었는지 알 수 없습니다. 디버깅, 로깅, 또는 API 응답을 위해 삭제된 토큰의 수를 반환하는 것을 고려해보세요.

🔎 제안하는 수정안
 @Override
 @Transactional
-public void deleteAllMemberPushTokens(Long memberId) {
-	memberPushTokenRepository.deleteByMemberId(memberId);
+public int deleteAllMemberPushTokens(Long memberId) {
+	int deletedCount = memberPushTokenRepository.deleteByMemberId(memberId);
+	log.debug("Deleted {} push tokens for member {}", deletedCount, memberId);
+	return deletedCount;
 }

참고: 이를 위해서는 DeleteAllMemberPushTokensUsecase 인터페이스와 MemberPushTokenRepository.deleteByMemberId() 메서드의 반환 타입도 함께 수정해야 합니다.


52-63: 삭제 로직 최적화 고려

현재 구현은 두 단계로 동작합니다:

  1. Line 55-58: 토큰을 조회하여 소유권 확인
  2. Line 62: pushToken 문자열로 토큰 삭제

잠재적 개선 사항:

  1. TOCTOU 이슈: 소유권 확인(Line 59)과 삭제(Line 62) 사이에 다른 트랜잭션이 토큰을 삭제할 수 있습니다. 트랜잭션 격리 수준에 따라 조용히 실패할 수 있습니다.
  2. 성능: Line 62에서 pushToken 문자열로 다시 검색하는 대신, 이미 조회한 모델의 ID를 사용하여 삭제할 수 있습니다.
🔎 제안하는 수정안

Repository에 ID 기반 삭제 메서드가 있다면:

 @Override
 @Transactional
 public void delete(Long memberId, DeleteMemberPushTokenRequest request) {
 	MemberPushTokenModel model =
 			memberPushTokenRepository
 					.findByPushToken(request.getPushToken())
 					.orElseThrow(() -> new NotFoundPushTokenException(request.getPushToken()));
 	if (!model.getMemberId().equals(memberId)) {
 		throw new DeniedDeletePushTokenException();
 	}
-	memberPushTokenRepository.deleteByPushToken(request.getPushToken());
+	memberPushTokenRepository.deleteById(model.getId());
 }

이렇게 하면:

  • 두 번째 조회를 피할 수 있습니다
  • TOCTOU 위험이 약간 감소합니다 (ID가 더 구체적이므로)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff8078 and b7c8b39.

📒 Files selected for processing (21)
  • eeos/build.gradle.kts
  • eeos/gradle/libs.versions.toml
  • eeos/src/main/java/com/blackcompany/eeos/config/security/SecurityFilterChainConfig.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/DeleteMemberPushTokenRequest.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundNotificationProviderException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/model/NotificationProvider.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/model/converter/MemberPushTokenEntityConverter.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/respository/MemberPushTokenRepository.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/CreateMemberPushTokenUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteAllMemberPushTokensUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteMemberPushTokenUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/JpaMemberPushTokenRepository.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/presentation/controller/MemberPushTokenController.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/presentation/docs/MemberPushTokenApi.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-16T14:22:46.594Z
Learnt from: rlajm1203
Repo: JNU-econovation/EEOS-BE PR: 256
File: eeos/src/main/java/com/blackcompany/eeos/config/security/UnknownEndpointFilter.java:29-41
Timestamp: 2025-07-16T14:22:46.594Z
Learning: EEOS-BE 프로젝트에서는 여러 개의 SecurityFilterChain을 등록하고, 모든 API 엔드포인트들을 시큐리티 필터에 매칭시켜 놓았습니다. UnknownEndpointFilter는 SecurityMatcher에 걸리지 않는 요청들만 처리하는 catch-all 역할을 하므로, 정상적인 API 요청들이 이 필터에 도달하지 않습니다.

Applied to files:

  • eeos/src/main/java/com/blackcompany/eeos/config/security/SecurityFilterChainConfig.java
🧬 Code graph analysis (7)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/DeleteMemberPushTokenRequest.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java (1)
  • Getter (9-16)
eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java (1)
  • DeniedDeletePushTokenException (6-17)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java (1)
  • NotFoundPushTokenException (6-21)
eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)
  • Getter (9-18)
eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/CreateMemberPushTokenUsecase.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/converter/MemberPushTokenEntityConverter.java (1)
  • Component (8-31)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/DeleteMemberPushTokenRequest.java (1)
  • Getter (9-15)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)
  • Getter (20-45)
eeos/src/main/java/com/blackcompany/eeos/notification/presentation/docs/MemberPushTokenApi.java (1)
eeos/src/main/java/com/blackcompany/eeos/common/presentation/response/ApiResponseBody.java (1)
  • ApiResponseBody (7-24)
⏰ 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: test
🔇 Additional comments (14)
eeos/src/main/java/com/blackcompany/eeos/config/security/SecurityFilterChainConfig.java (1)

90-91: LGTM!

인증된 사용자의 푸시 토큰 관리 엔드포인트가 올바르게 등록되었습니다. 기존 /api/calendars/** 패턴과 일관성 있게 구현되어 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/respository/MemberPushTokenRepository.java (1)

9-24: 메서드 시그니처 잘 설계됨

리포지토리 메서드들이 적절하게 설계되었습니다:

  • Optional 사용으로 null-safety 보장
  • 삭제 메서드의 int 반환으로 영향받은 행 수 추적 가능
  • 다양한 조회 조건 지원 (토큰, 멤버 ID, 프로바이더, 날짜 기반)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundNotificationProviderException.java (1)

6-20: 구현이 올바릅니다.

예외 클래스 구조가 적절하고, HTTP 404 상태 코드가 존재하지 않는 provider 조회에 맞습니다. 에러 메시지 포맷팅도 명확합니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/model/NotificationProvider.java (1)

7-24: LGTM!

열거형 구조와 find() 메서드 구현이 적절합니다. 대소문자 무관 검색으로 클라이언트 요청을 안전하게 처리합니다.

eeos/src/main/java/com/blackcompany/eeos/notification/presentation/docs/MemberPushTokenApi.java (1)

12-25: LGTM!

API 인터페이스 정의가 명확하고 Swagger 문서화가 잘 되어 있습니다. 일관된 응답 타입과 적절한 유효성 검증 어노테이션이 적용되어 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/model/converter/MemberPushTokenEntityConverter.java (1)

8-31: LGTM!

엔티티와 모델 간의 양방향 변환이 올바르게 구현되었습니다. 빌더 패턴을 일관되게 사용하고 있으며, 필드명 차이(provider/notificationProvider)도 적절히 매핑되었습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)

9-18: 잘 구현되었습니다!

모델 클래스가 프로젝트 컨벤션을 잘 따르고 있으며, Lombok 어노테이션이 적절하게 사용되었습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/JpaMemberPushTokenRepository.java (2)

14-19: 표준 쿼리 메서드가 올바르게 정의되었습니다.

Spring Data JPA의 메서드 네이밍 규칙을 잘 따르고 있습니다.


21-23: 트랜잭션 처리는 이미 올바르게 구현되어 있습니다.

NotificationTokenServicedeleteAllMemberPushTokens 메서드(line 47)는 이미 @Transactional 어노테이션을 적용하고 있으며, 이 메서드 내에서 deleteByMemberId를 호출합니다. 클래스 레벨의 @Transactional(readOnly = true) 설정을 메서드 레벨의 @Transactional로 적절히 오버라이드하고 있으므로 추가 작업은 필요하지 않습니다.

Likely an incorrect or invalid review comment.

eeos/src/main/java/com/blackcompany/eeos/notification/presentation/controller/MemberPushTokenController.java (2)

32-38: 토큰 생성 엔드포인트가 올바르게 구현되었습니다.

REST 규칙을 잘 따르고 있으며, 적절한 상태 코드(201 Created)와 요청 검증이 적용되어 있습니다.


40-53: 삭제 엔드포인트들이 잘 설계되었습니다.

단일 삭제와 전체 삭제 엔드포인트가 명확하게 분리되어 있으며, 멤버 인증이 적절하게 적용되어 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)

20-27: 엔티티 구조가 프로젝트 컨벤션을 잘 따르고 있습니다.

Lombok 어노테이션과 상속 구조가 적절하게 사용되었습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java (2)

22-38: 조회 메서드들이 올바르게 구현되었습니다.

JPA 레포지토리로의 위임과 엔티티-모델 변환이 적절하게 처리되고 있습니다.


40-61: 변경 메서드들이 잘 구현되었습니다.

레포지토리 위임과 엔티티-모델 변환 로직이 적절하며, 저장 메서드가 양방향 변환을 올바르게 처리하고 있습니다.

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: 0

♻️ Duplicate comments (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (1)

31-44: 중복 토큰 생성 가능성 (기존 리뷰 내용)

이전 리뷰에서 이미 지적된 동시성 문제가 여전히 존재합니다:

  • Line 34의 findByPushToken 체크와 Line 42의 save 사이에 경쟁 조건이 발생할 수 있습니다
  • 데이터베이스 레벨의 unique constraint가 없어 중복 토큰이 생성될 수 있습니다

이전 리뷰의 제안사항을 참고하여 수정해주세요.

🧹 Nitpick comments (2)
eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)

21-21: 미사용 @slf4j 어노테이션 제거 고려

코드 내에서 log를 사용하지 않고 있습니다. 불필요한 의존성을 제거하는 것을 권장합니다.

🔎 제안하는 수정
-@Slf4j
 public class NotificationTokenService

46-50: 삭제된 토큰 개수 반환 고려

현재 리포지토리의 deleteByMemberId는 삭제된 레코드 개수를 반환하지만, 서비스 메서드에서는 이를 활용하지 않습니다. 디버깅과 모니터링을 위해 삭제 개수를 반환하거나 로깅하는 것을 고려해보세요.

🔎 제안하는 수정
 @Override
 @Transactional
-public void deleteAllMemberPushTokens(Long memberId) {
-	memberPushTokenRepository.deleteByMemberId(memberId);
+public int deleteAllMemberPushTokens(Long memberId) {
+	return memberPushTokenRepository.deleteByMemberId(memberId);
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7c8b39 and ae39fe3.

📒 Files selected for processing (10)
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/repository/MemberPushTokenRepository.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/CreateMemberPushTokenUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteAllMemberPushTokensUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteMemberPushTokenUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteMemberPushTokenUsecase.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/CreateMemberPushTokenUsecase.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-02-22T16:03:14.347Z
Learnt from: rlajm1203
Repo: JNU-econovation/EEOS-BE PR: 213
File: eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java:33-36
Timestamp: 2025-02-22T16:03:14.347Z
Learning: When suggesting additional validation checks, first verify if the underlying components (like TokenExtractor and TokenResolver in auth interceptors) already implement those checks to avoid redundancy.

Applied to files:

  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
🧬 Code graph analysis (3)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/DeleteMemberPushTokenRequest.java (1)
  • Getter (9-15)
eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (2)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/DeniedDeletePushTokenException.java (1)
  • DeniedDeletePushTokenException (6-17)
eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java (1)
  • NotFoundPushTokenException (6-19)
eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)
  • Getter (9-18)
⏰ 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). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (8)
eeos/src/main/java/com/blackcompany/eeos/notification/application/dto/CreateMemberPushTokenRequest.java (1)

14-15: 이전 리뷰 피드백이 반영되어 있습니다!

pushTokenprovider 필드 모두 @NotBlank를 사용하여 일관성 있는 검증을 적용하고 있습니다. DeleteMemberPushTokenRequest와 동일한 검증 전략을 사용하여 빈 문자열과 공백만 있는 문자열을 적절히 방어하고 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)

43-44: 이전 리뷰 피드백들이 모두 반영되었습니다!

pushToken 필드에 대해 제기된 두 가지 이슈가 모두 해결되었습니다:

  1. 유니크 제약 조건 추가: unique = true 속성이 추가되어 동시성 환경에서도 중복 토큰 저장을 데이터베이스 레벨에서 방지합니다.
  2. 컬럼명 네이밍 규칙 통일: "_push_token"으로 변경되어 다른 컬럼들과 일관되게 snake_case를 사용합니다.
eeos/src/main/java/com/blackcompany/eeos/notification/application/usecase/DeleteAllMemberPushTokensUsecase.java (1)

3-5: 이전 리뷰 피드백이 반영되었습니다!

인터페이스에서 @Component 애노테이션이 제거되었습니다. 이제 Spring의 권장 사항에 따라 인터페이스는 순수한 계약 정의만 담고 있으며, 구현체에서 @Service 또는 @Component로 빈 등록이 이루어지는 올바른 구조입니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/exception/NotFoundPushTokenException.java (1)

10-18: 이전 보안 이슈가 해결되었습니다!

민감한 푸시 토큰 값이 예외 메시지에서 제거되었습니다:

  1. pushToken 필드가 완전히 제거되어 민감 정보 저장을 방지합니다.
  2. getMessage()가 일반적인 메시지만 반환하여 로그나 모니터링 시스템에 토큰이 노출되지 않습니다.

이는 중요한 보안 개선입니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/repository/MemberPushTokenRepository.java (1)

9-24: 잘 설계된 리포지토리 인터페이스입니다!

메서드 시그니처가 적절하게 설계되어 있습니다:

  • 조회 메서드들은 OptionalList를 적절히 사용하여 결과의 존재 여부를 명확히 표현합니다.
  • 삭제 메서드들은 영향받은 행 수(int)를 반환하여 호출자가 작업 결과를 확인할 수 있습니다.
  • deleteByUpdatedDateBefore 메서드는 오래된 토큰 정리에 유용한 설계입니다.

모든 메서드 명명이 Spring Data 규칙을 따르고 있으며, 도메인 로직을 명확히 표현하고 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (1)

52-63: 소유권 검증 로직이 올바르게 구현되었습니다

토큰 삭제 전 소유권을 검증하는 보안 로직이 적절하게 구현되어 있습니다. Long 타입 비교에 .equals()를 사용한 것도 정확합니다.

eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenRepositoryImpl.java (2)

14-38: 리포지토리 어댑터 패턴이 깔끔하게 구현되었습니다

JPA 리포지토리와 애플리케이션 레이어 사이의 어댑터 구현이 일관성 있고 명확합니다. 엔티티/모델 변환 로직도 적절하게 분리되어 있습니다.


50-54: JPA 리포지토리 인터페이스가 명시적으로 Timestamp 타입을 요구합니다. JpaMemberPushTokenRepository.deleteByUpdatedDateBefore(Timestamp)의 메서드 시그니처를 보면 현재의 LocalDateTime 에서 Timestamp 로의 변환이 필요합니다. 공개 인터페이스에서 Java 8 시간 API를 사용하고 JPA 계층에서 JDBC Timestamp를 사용하는 것은 적절한 추상화 계층 패턴이므로 변환을 제거할 수 없습니다.

토큰 활동상태를 의미하는 lastActiveAt 필드를 추가했습니다.
토큰 중복의 경우 토큰 활동 시간을 의미하는 lastActiveAt 필드를 변경하는 로직으로 수정하였습니다.
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

Fix all issues with AI Agents 🤖
In
@eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java:
- Around line 57-66: The delete method has a TOCTOU race between validating
ownership via memberPushTokenRepository.findByPushToken(...) and calling
memberPushTokenRepository.deleteByPushToken(...); replace this two-step check
with a single conditional delete that uses both memberId and pushToken to
atomically remove the row: call a repository method like
deleteByMemberIdAndPushToken(memberId, request.getPushToken()) and check its
returned deleted count, throwing NotFoundPushTokenException when 0; remove the
separate ownership check and DeniedDeletePushTokenException branch so deletion
is safe against concurrent reassignments.
- Around line 30-47: The create method in NotificationTokenService currently
calls existingModel.renew(memberId) which silently transfers token ownership
when memberPushTokenRepository.findByPushToken(...) finds an existing token;
confirm intended behavior and either document/log it or prevent reassignment:
update NotificationTokenService.create to check the existing model's memberId
from memberPushTokenRepository.findByPushToken(request.getPushToken()) and if it
differs from the incoming memberId, either (A) log an audit message and proceed
with existingModel.renew(memberId) (and persist) if reassignment is intended, or
(B) throw a specific exception (e.g., IllegalStateException or a custom
TokenConflictException) to reject re-registration and return/do not call renew;
ensure you reference existingModel.renew,
CreateMemberPushTokenRequest.getPushToken/getProvider, and
memberPushTokenRepository.save when implementing the chosen path and add a brief
comment explaining the business decision.

In
@eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java:
- Around line 21-49: MemberPushTokenEntity defines a new table but no DB
migration exists, so add a Flyway/V1-style migration (e.g.
V1.00.0.2__add_member_push_token_table.sql) that creates table member_push_token
with columns matching the entity: member_push_token_id PK,
member_push_token_member_id (FK to members table), member_push_token_provider,
member_push_token_push_token (with UNIQUE constraint),
member_push_token_last_activate_at, plus the BaseEntity default columns
(created_date, updated_date, is_deleted); ensure column names use the
ENTITY_PREFIX convention from MemberPushTokenEntity and add the FK constraint
and indexes as appropriate so Hibernate validate mode succeeds on startup.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 711cc76 and f568b5e.

📒 Files selected for processing (4)
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/model/converter/MemberPushTokenEntityConverter.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java
  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: kssumin
Repo: JNU-econovation/EEOS-BE PR: 218
File: eeos/src/main/java/com/blackcompany/eeos/target/presentation/controller/AttendWeightPolicyController.java:19-20
Timestamp: 2025-03-01T17:43:11.312Z
Learning: 사용자 kssumin은 토큰 기반의 ROLE 검증 기능을 별도 이슈로 해결할 예정이며, 관리자 권한 검증 관련 TODO 항목은 현재 PR에서 다루지 않기로 했습니다.
📚 Learning: 2025-02-22T16:03:14.347Z
Learnt from: rlajm1203
Repo: JNU-econovation/EEOS-BE PR: 213
File: eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java:33-36
Timestamp: 2025-02-22T16:03:14.347Z
Learning: When suggesting additional validation checks, first verify if the underlying components (like TokenExtractor and TokenResolver in auth interceptors) already implement those checks to avoid redundancy.

Applied to files:

  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
📚 Learning: 2025-02-22T16:03:14.347Z
Learnt from: rlajm1203
Repo: JNU-econovation/EEOS-BE PR: 213
File: eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java:33-36
Timestamp: 2025-02-22T16:03:14.347Z
Learning: When reviewing auth-related code in EEOS, check both TokenExtractor and TokenResolver implementations as they might already include necessary validation and error handling logic.

Applied to files:

  • eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java
🧬 Code graph analysis (2)
eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)
  • Getter (10-24)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)
eeos/src/main/java/com/blackcompany/eeos/notification/persistence/MemberPushTokenEntity.java (1)
  • Getter (21-49)
⏰ 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). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (3)
eeos/src/main/java/com/blackcompany/eeos/notification/application/model/converter/MemberPushTokenEntityConverter.java (1)

8-33: LGTM!

양방향 변환 로직이 정확하며, 필드 매핑이 일관성 있게 구현되어 있습니다. 프로젝트의 AbstractEntityConverter 패턴을 올바르게 따르고 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/service/NotificationTokenService.java (1)

49-53: LGTM!

사용자의 모든 토큰을 삭제하는 로직이 명확하게 구현되어 있습니다.

eeos/src/main/java/com/blackcompany/eeos/notification/application/model/MemberPushTokenModel.java (1)

10-24: LGTM!

도메인 모델이 깔끔하게 구현되어 있습니다. renew 메서드는 memberId를 업데이트하는데, 이는 NotificationTokenService에서 토큰 재할당에 사용되는 핵심 로직입니다.

해당 재할당 로직의 의도에 대한 검토는 NotificationTokenService 리뷰 코멘트를 참고해주세요.

Copy link
Contributor

@rlajm1203 rlajm1203 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 +14 to +24
public class MemberPushTokenModel implements AbstractModel {
private Long id;
private Long memberId;
private NotificationProvider notificationProvider;
private String pushToken;
private LocalDateTime lastActiveAt;

public MemberPushTokenModel renew(Long memberId) {
return this.toBuilder().memberId(memberId).lastActiveAt(LocalDateTime.now()).build();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

private LocalDateTime lastActiveAt 은 왜 필요한가요?

Copy link
Member Author

@Daae-Kim Daae-Kim Jan 5, 2026

Choose a reason for hiding this comment

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

말이 길어졌습니다..ㅎ
요약 : 토큰 신선도 관리를 위해서 도입했고, 토큰 사용시마다 lastActiveAt 값을 현재시간으로 변경, 저장하기위해 도입했습니다!

알림 토큰의 경우 생성시 타임스탬프를 기록해서 신선도 관리가 필요합니다.
기존에 base entity의 update_date를 사용하려고 했었는데 동일한 토큰에 대해서 post 요청이 오는 상황이 있어요 (로그인 -> fcm에서 이전 발급한 토큰을 그대로 반환 -> db 에 저장된것과 동일한 토큰 post 요청 및 사용 날짜 기록)

이 경우에 마지막으로 사용한 날짜를 update_date 로 하면 jpa dirty checking 에 걸리지 않을 수도 있겠다 생각했습니다 (기존 저장된 토큰과 비교해서 변경사항이 없기 때문)

이때문에 lastActiveAt 이라는 필드를 만들게 되었고, 사용자가 로그인 후 기존 알림 토큰을 사용할때 lastActiveAt 값이 현재시간으로 업데이트되어 마지막으로 토큰을 사용한 시간을 기록하도록 구현했습니다!

@Daae-Kim Daae-Kim merged commit d6ac04a into develop Jan 5, 2026
8 checks passed
@Daae-Kim Daae-Kim deleted the feat/#294/FCM branch January 5, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 일정 FCM 알림 토큰 API 개발

3 participants