Skip to content

Conversation

@unifolio0
Copy link
Contributor

@unifolio0 unifolio0 commented Aug 28, 2025

🚩 Jira Ticket

SCRUM-117

🗣️ 리뷰 요구사항 (선택)

Summary by CodeRabbit

  • 신기능
    • Kafka 기반 알림 소비/전송 파이프라인 및 관련 DTO·엔티티·레포지토리 추가
    • SMTP 메일 설정 및 VAPID 푸시 비밀키 구성 추가
  • 버그 수정
    • 동일 사용자/엔드포인트 웹푸시 구독 시 기존 구독을 최신 값으로 업데이트
  • 리팩터링
    • webpush 패키지를 notification.webpush로 재구성
  • 작업
    • 개발/테스트용 Dev CI/CD 워크플로, 배포 스크립트 및 환경별 설정/비밀 분리 추가
    • .serena/ 디렉터리 Git 무시 규칙 추가

@unifolio0 unifolio0 self-assigned this Aug 28, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 28, 2025

Walkthrough

새 CI/CD 워크플로우와 배포 스크립트 추가, Kafka 의존성 및 소비자/DTO/리포지토리/엔티티 도입, 웹푸시 패키지 이동 및 구독 업데이트 로직 변경, 환경별 YML·시크릿 추가, 테스트 import 경로 정리 및 .gitignore 항목 추가가 포함됩니다.

Changes

Cohort / File(s) Summary
CI/CD 워크플로우
.github/workflows/Dev_CI.yml, .github/workflows/Dev_CD.yml
dev-ci/dev-cd 워크플로우 추가: PR/푸시 트리거, MySQL/Zookeeper/Kafka 서비스, 빌드·테스트 분리, 아티팩트 업/다운로드 및 배포 단계 포함.
배포 스크립트
scripts/dev/replace-new-version.sh
포트 8080 사용 프로세스 종료 후 최신 JAR 백그라운드 실행하는 배포 스크립트 추가.
빌드 의존성
build.gradle
spring-boot-starter-mail, spring-kafka 추가.
Kafka 설정
src/main/java/com/wellmeet/config/KafkaConfig.java, src/main/resources/application-local.yml, src/main/resources/application-test.yml
Kafka ConsumerFactory/Listener 구성 추가 및 local/test 환경용 bootstrap/consumer 설정 추가.
Kafka 소비자/핸들러
src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java
@KafkaListenerNotificationMessage 처리하는 소비자 클래스 추가(토픽: notification, groupId: notification-group).
Kafka DTO/타입
src/main/java/com/wellmeet/notification/consumer/dto/*
MessageHeader, NotificationInfo, NotificationMessage, NotificationPayload(abstract), ReservationCreatedPayload, NotificationType 등 DTO/타입 추가.
알림 처리 컴포넌트 스켈레톤
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java, .../NotificationSender.java
리포지토리 주입된 체크/전송 컴포넌트 골격 추가(구현 미완).
알림 이력/설정 엔터티 및 리포지토리
src/main/java/com/wellmeet/notification/domain/*, src/main/java/com/wellmeet/notification/repository/*
NotificationHistory, OwnerNotificationEnabled, UserNotificationEnabled 엔티티 및 NotificationHistoryRepository, OwnerNotificationEnabledRepository, UserNotificationEnabledRepository 추가.
웹푸시 패키지 이동 및 로직 변경
src/main/java/com/wellmeet/notification/webpush/...
패키지 com.wellmeet.webpushcom.wellmeet.notification.webpush로 이동. WebPushService의 subscribe: 동일 endpoint 시 기존 구독 업데이트 후 반환하도록 동작 변경. 관련 Controller/DTO/Repository/Domain/Infra 경로 업데이트.
테스트 경로/검증 수정
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java, src/test/java/com/wellmeet/webpush/WebPushControllerTest.java, src/test/java/com/wellmeet/Base*Test.java
테스트의 import 경로를 새 패키지로 수정. 동일 endpoint 구독 테스트 기대 동작을 "업데이트"로 변경.
환경/시크릿 파일
src/main/resources/application-dev.yml, src/main/resources/local-secret.yml, src/main/resources/application-local.yml
dev/local/local-secret.yml에 DB, JPA, Mail, Kafka, VAPID, CORS 등 환경별 설정 및 시크릿 참조 추가.
기타
.gitignore
/.serena/ 항목 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Producer as 외부 프로듀서
  participant Kafka as Kafka (notification)
  participant App as Notification Service
  note right of Kafka: 토픽: notification
  Producer->>Kafka: publish(NotificationMessage)
  Kafka-->>App: deliver(NotificationMessage)
  rect rgba(230,245,255,0.5)
    App->>App: @KafkaListener consume(message)
    App->>NotificationChecker: check(recipient, recipientType)
    NotificationChecker-->>App: enabledChannels (List)
    App->>NotificationSender: send(message, enabledChannels)
  end
Loading
sequenceDiagram
  autonumber
  actor Client
  participant API as WebPushController
  participant Svc as WebPushService
  participant Repo as PushSubscriptionRepository

  Client->>API: subscribe(request)
  API->>Svc: subscribe(request)
  Svc->>Repo: findByUserIdAndEndpoint(userId, endpoint)
  alt 기존 구독 있음
    Svc->>Repo: update existing (p256dh/auth 등) → save
    Repo-->>Svc: updated entity
  else 없음
    Svc->>Repo: save(new entity)
    Repo-->>Svc: created entity
  end
  Svc-->>API: SubscribeResponse
  API-->>Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ezcolin2

Poem

토끼가 깡충, 코드 숲을 뛰네 🐇
Kafka 바람 실어 알림이 춤추고,
구독은 톡톡 새 키로 바뀌었네.
CI는 빙글, CD는 문을 열어,
새 JAR에 별 하나 반짝, 배포 축하해! ✨

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 (2 passed)
Check name Status Explanation
Title Check ✅ Passed 제목이 메인 기능인 “식당 주인이 사용자가 예약 시 알림을 받을 수 있음”을 명확하게 요약하고 있어 변경 사항과 잘 부합하며 Jira 티켓 참조도 포함되어 있습니다.
Description Check ✅ Passed PR 설명이 제공된 템플릿 구조를 충실히 따르고 있어 필요한 Jira 티켓 링크를 포함하며 선택 섹션의 비워둔 부분도 허용 범위로 템플릿 요구사항이 충족되었습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SCRUM-117

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.

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

🧹 Nitpick comments (4)
build.gradle (1)

30-31: Kafka/Mail 스타터 추가는 합리적입니다. 테스트 편의성 위해 spring-kafka-test도 함께 추가하세요.

  • 컨슈머 동작·역직렬화 검증을 위해 내장 임베디드 브로커/유틸을 제공하므로 테스트 작성이 쉬워집니다.

다음 의존성을 테스트 스코프로 추가해 주세요:

 dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-starter-mail'
   implementation 'org.springframework.kafka:spring-kafka'
+  testImplementation 'org.springframework.kafka:spring-kafka-test'
 }
src/main/java/com/wellmeet/consumer/NotificationConsumer.java (1)

8-11: @requiredargsconstructor는 현재 불필요합니다.

  • 주입받는 final 필드가 없어 효과가 없습니다. 유지하려면 의존성(메일 발송/알림 서비스 등)을 주입하거나, 제거하세요.
-@Slf4j
-@Service
-@RequiredArgsConstructor
+@Slf4j
+@Service
 public class NotificationConsumer {
src/main/resources/application-local.yml (1)

38-45: Kafka 설정 중복 최소화 및 토픽명 프로퍼티 추가 권장

  • 역직렬화/auto-offset-reset은 Boot 또는 수동 팩토리 한쪽만 단일 소스로 관리하는 것이 좋습니다(중복 방지).
  • 리스너에서 토픽을 프로퍼티로 읽도록 바꿨으므로, 토픽 프로퍼티를 추가해 주세요.
   kafka:
     bootstrap-servers: localhost:9092
     consumer:
       group-id: notification-group
       auto-offset-reset: earliest
       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
       value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+notification:
+  topic: notification

CI에서 사용하는 테스트 프로파일(application-test.yml)이 없다면, 동일한 bootstrap-serversgroup-id를 가진 테스트 전용 설정 파일 추가를 고려해 주세요.

.github/workflows/Dev_CI.yml (1)

86-87: 테스트 단계에 프로파일 지정으로 환경 결정성을 높이세요.

  • Kafka/MySQL 연결 설정을 명확히 하려면 테스트 프로파일을 활성화하는 것이 안전합니다.
-      - name: Run Tests With Gradle
-        run: ./gradlew test
+      - name: Run Tests With Gradle
+        run: SPRING_PROFILES_ACTIVE=test ./gradlew test

원하시면 최소 설정의 application-test.yml 템플릿을 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 2cd6970 and c9e5ccd.

📒 Files selected for processing (5)
  • .github/workflows/Dev_CI.yml (1 hunks)
  • build.gradle (1 hunks)
  • src/main/java/com/wellmeet/config/KafkaConfig.java (1 hunks)
  • src/main/java/com/wellmeet/consumer/NotificationConsumer.java (1 hunks)
  • src/main/resources/application-local.yml (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/wellmeet/consumer/NotificationConsumer.java (1)
src/main/java/com/wellmeet/WellmeetNotificationApplication.java (2)
  • SpringBootApplication (8-16)
  • main (13-15)
🔇 Additional comments (1)
src/main/java/com/wellmeet/config/KafkaConfig.java (1)

20-24: 하드코딩된 groupId와 프로퍼티 동시 사용은 혼란을 유발합니다.

  • 컨슈머 그룹은 프로퍼티만 신뢰하도록 하고, 리스너 애노테이션에서의 하드코딩은 제거하는 편이 안전합니다. NotificationConsumer에서 처리 제안 드립니다(아래 파일 코멘트 참고).

Comment on lines +25 to +29
MYSQL_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: wellmeet_noti_test
ports:
- 3306:3306
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

MySQL DB 이름 불일치 가능성(wellmeet_noti_test vs wellmeet_noti)

  • 워크플로우는 MYSQL_DATABASE: wellmeet_noti_test를 생성하지만, application-local.ymlwellmeet_noti를 가리킵니다. 테스트 프로파일이 없다면 커넥션 실패/빈 스키마 가능성이 있습니다.

테스트 프로파일 유무와 사용 DB를 확인해 주세요:

  • 선택 1: Run Tests 단계에 SPRING_PROFILES_ACTIVE=test를 설정하고 application-test.yml에서 wellmeet_noti_test 사용.
  • 선택 2: 로컬 프로파일 대신 테스트에서 사용하도록 SPRING_PROFILES_ACTIVE=local을 지정.

🏁 Script executed:

#!/bin/bash
# 테스트 프로파일 및 DB URL 탐색
fd -HI "application-test.yml" || echo "⚠️ application-test.yml 없음"
rg -n "spring\.datasource\.url" -C2 src/main/resources || true

Length of output: 133


테스트·로컬 프로파일에서 실제로 사용 중인 DB 이름과 URL을 명확히 파악하기 위해 아래 스크립트를 실행해 주세요:

이 결과로 wellmeet_noti vs wellmeet_noti_test 참조 여부를 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
echo "===== application-local.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application-local.yml || true

echo "===== application-test.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application-test.yml || true

Length of output: 793


Run Tests 단계에서 SPRING_PROFILES_ACTIVE 설정 여부를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
echo "===== Run Tests 섹션 ====="
rg -n "Run Tests" -C5 .github/workflows/Dev_CI.yml || true

echo "===== SPRING_PROFILES_ACTIVE 설정 ====="
rg -n "SPRING_PROFILES_ACTIVE" -C3 .github/workflows/Dev_CI.yml || echo "⚠️ SPRING_PROFILES_ACTIVE 설정 없음"

Length of output: 527


default 프로파일로 실행될 때 로드되는 application.yml의 datasource 설정 유무를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
echo "===== application.yml 파일 위치 확인 ====="
fd -HI src/main/resources/application.yml || echo "⚠️ application.yml 없음"

echo "===== application.yml datasource 설정 ====="
rg -n "datasource" -C3 src/main/resources/application.yml || true

Length of output: 359


Run Tests 단계에 SPRING_PROFILES_ACTIVE=test 설정
application-test.yml은 jdbc:mysql://localhost:3306/wellmeet_noti_test를, application-local.yml은 …/wellmeet_noti를 참조하지만 기본 프로파일(application.yml)이 없어 워크플로우에서 프로파일을 지정하지 않으면 잘못된 DB로 연결되거나 커넥션 실패가 발생합니다.
.github/workflows/Dev_CI.yml의 Run Tests With Gradle 단계에 아래 env 설정을 추가해 주세요:

       - name: Run Tests With Gradle
         run: ./gradlew test
+        env:
+          SPRING_PROFILES_ACTIVE: test
📝 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
MYSQL_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: wellmeet_noti_test
ports:
- 3306:3306
- name: Run Tests With Gradle
run: ./gradlew test
env:
SPRING_PROFILES_ACTIVE: test
🤖 Prompt for AI Agents
In .github/workflows/Dev_CI.yml around lines 25 to 29 the CI job doesn't set
SPRING_PROFILES_ACTIVE, so Gradle tests can pick the wrong profile/DB; add an
environment variable SPRING_PROFILES_ACTIVE=test to the "Run Tests With Gradle"
step's env block so the test profile (and application-test.yml DB URL) is used
during test execution.

Comment on lines +44 to +47
--health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
--health-interval=10s
--health-timeout=10s
--health-retries=5
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Zookeeper 헬스체크 커맨드가 잘못되었습니다.

  • cp-zookeeper 기본 이미지에는 8080 HTTP 엔드포인트가 없습니다. ruok 체크로 교체하세요.
-          --health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
+          --health-cmd="bash -c 'echo ruok | nc -w 2 localhost 2181 | grep imok'"
           --health-interval=10s
           --health-timeout=10s
           --health-retries=5
📝 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
--health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
--health-interval=10s
--health-timeout=10s
--health-retries=5
--health-cmd="bash -c 'echo ruok | nc -w 2 localhost 2181 | grep imok'"
--health-interval=10s
--health-timeout=10s
--health-retries=5
🤖 Prompt for AI Agents
.github/workflows/Dev_CI.yml lines 44-47: the current health-cmd targets a
non-existent HTTP endpoint on port 8080; replace it with a Zookeeper "ruok"
check against the Zookeeper client port (2181). Update the --health-cmd to run a
TCP check that sends "ruok" and verifies the "imok" response (for example use:
echo ruok | nc -w1 localhost 2181 | grep imok || exit 1), keeping the existing
--health-interval, --health-timeout, and --health-retries values.

Comment on lines +50 to +56
image: confluentinc/cp-kafka:7.0.1
env:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Kafka 리스너 바인딩 명시(KAFKA_LISTENERS) 추가 권장

  • 일부 환경에서 KAFKA_ADVERTISED_LISTENERS만 설정 시 부트 실패 사례가 있어, 리스너 바인딩을 명시하는 편이 안전합니다.
       kafka:
         image: confluentinc/cp-kafka:7.0.1
         env:
           KAFKA_BROKER_ID: 1
           KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
           KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
+          KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
           KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
📝 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
image: confluentinc/cp-kafka:7.0.1
env:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
kafka:
image: confluentinc/cp-kafka:7.0.1
env:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
🤖 Prompt for AI Agents
.github/workflows/Dev_CI.yml around lines 50 to 56: the workflow sets
KAFKA_ADVERTISED_LISTENERS but omits KAFKA_LISTENERS which can cause Kafka to
fail to bind in some environments; add an env entry KAFKA_LISTENERS set to
PLAINTEXT://0.0.0.0:9092 (or matching interface and port you intend to bind)
alongside the existing advertised listeners so the broker explicitly binds to
the correct network interface.

Comment on lines 26 to 35
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
return new DefaultKafkaConsumerFactory<>(props);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Boot 자동 구성과 중복 설정 제거 + 에러 핸들러 적용 권장

  • 수동 props 구성은 application-local.yml과 중복이며, Boot의 Kafka 설정(에러 핸들러, 컨슈머 설정 등)을 일부 놓칠 수 있습니다.

다음처럼 Boot의 ConcurrentKafkaListenerContainerFactoryConfigurer를 이용해 공통 설정과 에러 처리(backoff)까지 적용해 주세요. 또한 코드의 AUTO_OFFSET_RESET 하드코딩을 제거하여 프로퍼티만 신뢰하게 만드는 것을 권장합니다.

 import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer;
 import org.springframework.kafka.core.ConsumerFactory;
 import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.DefaultErrorHandler;
+import org.springframework.util.backoff.FixedBackOff;
@@
-    @Bean
-    public ConsumerFactory<String, String> consumerFactory() {
-        Map<String, Object> props = new HashMap<>();
-        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
-        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
-        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
-        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
-        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
-        return new DefaultKafkaConsumerFactory<>(props);
-    }
+    @Bean
+    public ConsumerFactory<String, String> consumerFactory() {
+        Map<String, Object> props = new HashMap<>();
+        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+        // auto.offset.reset은 application-*.yml만 신뢰 (중복 제거)
+        return new DefaultKafkaConsumerFactory<>(props);
+    }
@@
-    @Bean
-    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
-        ConcurrentKafkaListenerContainerFactory<String, String> factory =
-                new ConcurrentKafkaListenerContainerFactory<>();
-        factory.setConsumerFactory(consumerFactory());
-        return factory;
-    }
+    @Bean
+    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
+            ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
+            ConsumerFactory<String, String> consumerFactory) {
+        ConcurrentKafkaListenerContainerFactory<String, String> factory =
+                new ConcurrentKafkaListenerContainerFactory<>();
+        // Boot의 spring.kafka.* 설정을 공통으로 적용
+        configurer.configure(factory, consumerFactory);
+        // 단순 재시도(1s 간격, 최대 3회)
+        factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 3)));
+        return factory;
+    }
📝 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
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
return new DefaultKafkaConsumerFactory<>(props);
}
// src/main/java/com/wellmeet/config/KafkaConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.FixedBackOff;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
@Configuration
public class KafkaConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// auto.offset.reset은 application-*.yml만 신뢰 (중복 제거)
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
ConsumerFactory<String, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
// Boot의 spring.kafka.* 설정을 공통으로 적용
configurer.configure(factory, consumerFactory);
// 단순 재시도(1s 간격, 최대 3회)
factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 3)));
return factory;
}
}

Comment on lines 13 to 16
@KafkaListener(topics = "notification", groupId = "notification-group")
public void consume(String message) {
log.info("Received message from notification topic: {}", message);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토픽/그룹 하드코딩 제거 + 메타데이터 포함 로깅으로 운용성 개선

  • 프로퍼티 기반으로 외부화하여 환경별 유연성을 높이고, 파티션/오프셋까지 로깅하면 장애 추적이 쉬워집니다.
-import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
@@
-    @KafkaListener(topics = "notification", groupId = "notification-group")
-    public void consume(String message) {
-        log.info("Received message from notification topic: {}", message);
+    @KafkaListener(
+        topics = "${notification.topic:notification}",
+        groupId = "${spring.kafka.consumer.group-id}"
+    )
+    public void consume(ConsumerRecord<String, String> record) {
+        log.info("Consumed reservation event: topic={}, partition={}, offset={}, key={}, value={}",
+                 record.topic(), record.partition(), record.offset(), record.key(), record.value());
     }

메일/SMS/웹푸시 전달 로직을 붙이는 구현이 필요하면, 인터페이스와 간단한 어댑터(이벤트→도메인 DTO 변환) 초안을 제공하겠습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@KafkaListener(topics = "notification", groupId = "notification-group")
public void consume(String message) {
log.info("Received message from notification topic: {}", message);
}
import org.springframework.kafka.annotation.KafkaListener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
@KafkaListener(
topics = "${notification.topic:notification}",
groupId = "${spring.kafka.consumer.group-id}"
)
public void consume(ConsumerRecord<String, String> record) {
log.info("Consumed reservation event: topic={}, partition={}, offset={}, key={}, value={}",
record.topic(),
record.partition(),
record.offset(),
record.key(),
record.value());
}

Comment on lines +23 to +37
mail:
host: smtp.gmail.com
port: 587
username: ${secret.mail.username:test@example.com}
password: ${secret.mail.password:testpassword}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
timeout: 5000
connectiontimeout: 5000
writetimeout: 5000
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

메일 설정은 합리적입니다. 로컬 시크릿 유출 방지 확인 및 타임아웃 단위 일관성 체크

  • ${secret.*} 기본값이 있어 안전합니다. 다만 local-secret.yml이 .gitignore에 포함되어 있는지 확인해 주세요.
  • timeout/connectiontimeout/writetimeout 단위(ms) 일관성은 OK.

다음 스크립트로 시크릿 파일이 gitignore 되는지 확인해 주세요:


🏁 Script executed:

#!/bin/bash
rg -n "^local-secret\.yml$" .gitignore || echo "⚠️ .gitignore에 local-secret.yml 항목이 없습니다."

Length of output: 133


local-secret.yml을 .gitignore에 추가하세요
현재 .gitignorelocal-secret.yml이 없어 시크릿 정보가 커밋될 위험이 있습니다. 해당 파일을 .gitignore에 추가해 주시기 바랍니다.

🤖 Prompt for AI Agents
In src/main/resources/application-local.yml around lines 23 to 37, sensitive
mail credentials are being referenced and you must prevent committing
local-secret.yml; add a line "local-secret.yml" to the repository's .gitignore,
then if local-secret.yml was already committed remove it from the index with git
rm --cached local-secret.yml and commit the change; ensure developers create or
update a local-secret.yml from a template placed in the repo (e.g.,
local-secret.yml.example) instead of committing real secrets.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)

44-61: 업데이트 테스트의 의도 강화: “동일 엔티티 재사용”을 단언하세요.
현재 검증은 필드 값 변경만 보장합니다. 새 엔티티를 생성해도 통과할 수 있어 “업데이트” 의미가 약합니다. 기존 엔티티를 재사용했는지와 중복 생성이 없는지를 함께 단언해 주세요.

         SubscribeResponse response = webPushService.subscribe(userId, request);
         PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get();

         assertAll(
+                () -> assertThat(response.subscriptionId()).isEqualTo(pushSubscription.getId()),
                 () -> assertThat(subscription.getUserId()).isEqualTo(userId),
                 () -> assertThat(subscription.getEndpoint()).isEqualTo(endpoint),
                 () -> assertThat(subscription.getP256dh()).isEqualTo(request.p256dh()),
-                () -> assertThat(subscription.getAuth()).isEqualTo(request.auth())
+                () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()),
+                // 같은 유저/엔드포인트에 대해 레코드가 1건인지 확인해 “업데이트”语義를 강화
+                () -> assertThat(pushSubscriptionRepository.count()).isEqualTo(1)
         );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 c9e5ccd and 18b83b3.

📒 Files selected for processing (4)
  • .github/workflows/Dev_CI.yml (1 hunks)
  • .gitignore (1 hunks)
  • src/main/resources/application-test.yml (1 hunks)
  • src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/Dev_CI.yml
🧰 Additional context used
🧬 Code graph analysis (2)
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)
src/test/java/com/wellmeet/webpush/WebPushControllerTest.java (5)
  • Nested (22-83)
  • Test (25-44)
  • WebPushControllerTest (20-164)
  • Test (136-149)
  • Nested (133-163)
src/main/resources/application-test.yml (2)
src/test/java/com/wellmeet/WellmeetNotificationApplicationTests.java (1)
  • SpringBootTest (6-12)
src/test/java/com/wellmeet/BaseServiceTest.java (1)
  • ExtendWith (9-16)
⏰ 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-and-push
🔇 Additional comments (2)
src/main/resources/application-test.yml (1)

23-29: String 메시지 소비 시 StringDeserializer 유지
NotificationConsumer.consume 메서드가 String 타입 메시지를 수신하고 있으므로, 현재 StringDeserializer 설정을 그대로 사용해도 무방합니다. 향후 DTO 타입으로 변경 시 JsonDeserializer 및 trusted packages 설정을 적용하세요.

src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)

58-59: 업데이트 후 필드 값 단언은 적절합니다.
변경된 p256dh/auth가 저장소 값과 일치하는지 검증한 부분은 명확합니다.

Comment on lines +23 to +29
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테스트 안정성을 위해 group-id는 매 실행마다 난수화하고, 브로커 주소는 환경변수로 주입하세요.
고정 group-id(notification-group)는 오프셋이 누적되어 재실행 시 예측 불가 동작을 유발할 수 있습니다. 또한 로컬에서 카프카가 없으면 테스트가 즉시 실패합니다. 아래와 같이 수정하면 CI/로컬 모두에서 플레이키니스가 줄어듭니다.

   kafka:
-    bootstrap-servers: localhost:9092
+    bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
     consumer:
-      group-id: notification-group
+      group-id: wellmeet-test-${random.uuid}
       auto-offset-reset: earliest
       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
       value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+    listener:
+      # CI 부팅 레이스로 토픽이 늦게 생성될 때 실패를 줄입니다(필요 시 조정).
+      missing-topics-fatal: false

추가로, @KafkaListener에 groupId를 하드코딩했다면 프로퍼티로 일원화하는 편이 유지보수에 유리합니다. 예: @KafkaListener(topics="notification", groupId="${spring.kafka.consumer.group-id}")

📝 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
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
consumer:
group-id: wellmeet-test-${random.uuid}
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# CI 부팅 레이스로 토픽이 늦게 생성될 때 실패를 줄입니다(필요 시 조정).
missing-topics-fatal: false
🤖 Prompt for AI Agents
In src/main/resources/application-test.yml around lines 23-29, replace the
hardcoded kafka.bootstrap-servers and consumer.group-id with
environment/property-driven values: make bootstrap-servers read from an
environment variable (e.g. KAFKA_BOOTSTRAP_SERVERS) with a localhost:9092
fallback, and make group-id derive from a property that defaults to a generated
random value (e.g. Spring's random uuid) so each test run uses a unique group
id; also audit code for any @KafkaListener usages that hardcode groupId and
change them to reference the consumer.group-id property to centralize
configuration.

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

🧹 Nitpick comments (11)
src/main/resources/application-dev.yml (6)

8-8: MySQL JDBC URL에 표준 파라미터 추가 및 인코딩/타임존 안정화 제안

한글/이모지, 타임존, SSL 핸드셰이크 이슈 예방을 위해 JDBC URL에 권장 파라미터를 추가하는 것이 안전합니다. YAML에서 & 포함 시 전체를 문자열로 감싸는 걸 잊지 마세요.

-    url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database}
+    url: "jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true"

16-16: Hibernate Dialect 명시값 점검 (Boot 3/Hibernate 6 기준)

org.hibernate.dialect.MySQLDialect는 버전별 추론이 가능하지만, MySQL 8 사용 시 MySQL8Dialect를 명시하거나(또는 Dialect 자체를 제거해 자동감지에 맡기기) 하는 편이 일관됩니다.

-        dialect: org.hibernate.dialect.MySQLDialect
+        dialect: org.hibernate.dialect.MySQL8Dialect
# 또는 아래처럼 완전 제거도 고려
# -        dialect: org.hibernate.dialect.MySQLDialect

11-16: OSIV 비활성화 권장

웹 계층 Lazy 로딩으로 인한 N+1/지연 예외를 줄이기 위해 Dev에서도 OSIV를 꺼두는 것을 권장합니다.

   jpa:
     show-sql: true
     properties:
       hibernate:
         format_sql: true
-        dialect: org.hibernate.dialect.MySQLDialect
+        dialect: org.hibernate.dialect.MySQL8Dialect
+    open-in-view: false

23-37: Gmail SMTP 호환성 보강 (TLS 1.2 강제 권장)

환경에 따라 TLS 프로토콜 미스매치가 간헐적으로 발생할 수 있어 ssl.protocols를 명시하는 것을 권장합니다.

       mail:
         smtp:
           auth: true
           starttls:
             enable: true
             required: true
           timeout: 5000
           connectiontimeout: 5000
           writetimeout: 5000
+          ssl:
+            protocols: TLSv1.2

46-47: CORS 도메인 확장성

단일 Origin만 허용 중입니다. 로컬/프론트 미러 포트(5173/4173 등)나 개발 도메인(예: *.dev)을 추가로 허용할 필요가 있다면 배열 형태로 관리하는 편이 낫습니다. 이 키가 커스텀 설정이면 해당 Bean에서 배열 지원 여부도 확인 바랍니다.


5-5: dev-secret.yml 관리 확인

classpath:dev-secret.yml을 CI에서 주입하는 구조라면, 저장소에는 커밋되지 않도록 .gitignore 포함 여부와 PR의 시크릿 노출 여부를 다시 한 번 점검해 주세요.

scripts/dev/replace-new-version.sh (2)

1-1: 스クリ프트 안전성 강화: 방어적 Bash 옵션 추가

예상치 못한 실패 시 조기에 종료되도록 set -euo pipefail을 추가하는 것을 권장합니다.

 #!/bin/bash
+set -euo pipefail

30-36: 시작 실패 가시성 향상 (간단 헬스체크/포트 확인)

백그라운드 실행 후 포트 리슨 여부를 짧게 확인하면 배포 실패를 빠르게 감지할 수 있습니다.

 echo "애플리케이션이 백그라운드에서 실행되었습니다."
 echo "로그 확인: tail -f $APP_LOG_FILE"
 echo "=== 배포 완료 ==="
+for i in {1..30}; do
+  if lsof -i:8080 -sTCP:LISTEN >/dev/null 2>&1; then
+    echo "서비스가 포트 8080에서 수신 중입니다."
+    break
+  fi
+  sleep 1
+done
.github/workflows/Dev_CD.yml (3)

78-79: 배포 스크립트와 아티팩트 경로/권한 정합성 확인

스크립트는 $HOME/app/app.jar(권장) 또는 /home/ubuntu/app/app.jar(현재) 중 어느 쪽을 가정하는지 일치가 필요합니다. 스크립트 수정안대로 $HOME/app를 쓰면 워크플로의 다운로드 경로(~/app)와 일치합니다. 실행 전 실행 권한을 부여하는 것도 안전합니다.

       - name: Replace application to latest
-        run: sudo sh ~/app/scripts/replace-new-version.sh
+        run: |
+          chmod +x ~/app/scripts/replace-new-version.sh || true
+          sudo bash ~/app/scripts/replace-new-version.sh

24-37: 시크릿 파일 주입 안정성

멀티라인 시크릿의 줄바꿈 손상 방지를 위해 printf 사용이나 heredoc이 더 안전합니다.

-      - name: Setting dev-secret.yml
-        run: |
-          echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
+      - name: Setting dev-secret.yml
+        run: |
+          printf "%s" "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml

15-23: 동시 배포 충돌 방지(concurrency) 권장

동일 브랜치로 연속 푸시 시 배포 경합을 방지하려면 concurrency를 추가해 주세요.

 jobs:
+  concurrency:
+    group: dev-cd-${{ github.ref }}
+    cancel-in-progress: true
   test:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 18b83b3 and 6c0b07c.

📒 Files selected for processing (4)
  • .github/workflows/Dev_CD.yml (1 hunks)
  • .github/workflows/Dev_CI.yml (1 hunks)
  • scripts/dev/replace-new-version.sh (1 hunks)
  • src/main/resources/application-dev.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/Dev_CI.yml
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/Dev_CD.yml

63-63: label "dev-owner" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2022", "windows-2019", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "ubuntu-20.04", "macos-latest", "macos-latest-xl", "macos-latest-xlarge", "macos-latest-large", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14", "macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

🪛 Shellcheck (0.10.0)
scripts/dev/replace-new-version.sh

[warning] 36-36: sudo doesn't affect redirects. Use ..| sudo tee file

(SC2024)

⏰ 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: build-and-push
  • GitHub Check: test / build-and-push
🔇 Additional comments (3)
src/main/resources/application-dev.yml (2)

19-22: schema.sql 항상 실행 설정의 영향 확인

spring.sql.init.mode: always는 Dev 부팅 때마다 스키마 초기화를 시도합니다. 샘플 데이터/DDL 중복 적용, 수동 마이그레이션 충돌 우려가 없는지 확인 바랍니다. Flyway/Liquibase 사용 계획이 있다면 해당 설정과의 우선순위도 점검이 필요합니다.


39-44: Kafka 역직렬화 방식 확인

현재 StringDeserializer로 설정되어 있습니다. 예약 알림 페이로드가 JSON이면 JsonDeserializerspring.json.trusted.packages 설정이 필요합니다. 토픽 스키마/컨슈머 코드와 합치되는지 재확인 바랍니다.

.github/workflows/Dev_CD.yml (1)

61-64: runs-on: dev-owner만으로 해당 라벨의 셀프호스티드 러너에 매칭됩니다. self-hosted 라벨 추가는 권장사항일 뿐 필수 매칭 조건이 아닙니다. (docs.kkgithub.com, docs.github.com)

Likely an incorrect or invalid review comment.

Comment on lines 41 to 46
- name: bootJar with Gradle
run: ./gradlew :api-owner:bootJar --info

- name: Change artifact file name
run: mv build/libs/*.jar build/libs/app.jar

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

bootJar 산출물 경로 불일치

:api-owner:bootJar를 빌드하면 JAR는 api-owner/build/libs에 생성됩니다. 현재 mv build/libs/*.jar ...는 잘못된 경로입니다. 아래처럼 서브모듈 경로를 사용하세요.

-      - name: bootJar with Gradle
-        run: ./gradlew :api-owner:bootJar --info
-
-      - name: Change artifact file name
-        run: mv build/libs/*.jar build/libs/app.jar
+      - name: bootJar with Gradle
+        run: ./gradlew :api-owner:bootJar --info
+
+      - name: Change artifact file name
+        run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar
📝 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
- name: bootJar with Gradle
run: ./gradlew :api-owner:bootJar --info
- name: Change artifact file name
run: mv build/libs/*.jar build/libs/app.jar
- name: bootJar with Gradle
run: ./gradlew :api-owner:bootJar --info
- name: Change artifact file name
run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar
🤖 Prompt for AI Agents
.github/workflows/Dev_CD.yml around lines 41 to 46: the workflow moves the built
JAR from build/libs but the bootJar task for :api-owner produces the artifact
under api-owner/build/libs; update the move command to reference the submodule
path (api-owner/build/libs/*.jar -> api-owner/build/libs/app.jar) so it targets
the actual output location (or alternatively run the mv from the api-owner
directory); replace the incorrect mv build/libs/... with the correct
api-owner/build/libs/... path.

Comment on lines +47 to +53
- name: Upload artifact file
uses: actions/upload-artifact@v4
with:
name: app-artifact
path: ./build/libs/app.jar
if-no-files-found: error

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

업로드 경로도 서브모듈 경로로 정합성 유지

아티팩트 업로드 경로를 실제 위치로 수정해야 합니다.

-      - name: Upload artifact file
+      - name: Upload artifact file
         uses: actions/upload-artifact@v4
         with:
           name: app-artifact
-          path: ./build/libs/app.jar
+          path: ./api-owner/build/libs/app.jar
           if-no-files-found: error
📝 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
- name: Upload artifact file
uses: actions/upload-artifact@v4
with:
name: app-artifact
path: ./build/libs/app.jar
if-no-files-found: error
- name: Upload artifact file
uses: actions/upload-artifact@v4
with:
name: app-artifact
path: ./api-owner/build/libs/app.jar
if-no-files-found: error
🤖 Prompt for AI Agents
In .github/workflows/Dev_CD.yml around lines 47 to 53, the artifact upload path
points to ./build/libs/app.jar which doesn't match the submodule layout; update
the path to the actual artifact location inside the submodule (for example
./your-submodule-name/build/libs/app.jar) so the Upload artifact step points to
the correct relative path, and verify if-no-files-found behavior is appropriate.

Comment on lines +3 to +18
PID=$(lsof -t -i:8080)

# 프로세스 종료
if [ -z "$PID" ]; then
echo "No process is using port 8080."
else
echo "Killing process with PID: $PID"
kill -15 "$PID"

# 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
if [ $? -eq 0 ]; then
echo "Process $PID terminated successfully."
else
echo "Failed to terminate process $PID."
fi
fi
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

종료 대기/강제 종료 루프 추가 필요

kill -15 성공 여부만 확인하고 포트 해제가 보장되지 않습니다. 일정 시간 대기 후 미종료 시 -9로 강제 종료하는 루프를 권장합니다.

-if [ -z "$PID" ]; then
+if [ -z "$PID" ]; then
   echo "No process is using port 8080."
 else
   echo "Killing process with PID: $PID"
   kill -15 "$PID"
 
   # 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
   if [ $? -eq 0 ]; then
     echo "Process $PID terminated successfully."
   else
     echo "Failed to terminate process $PID."
   fi
+  # 포트 해제 대기(최대 30초), 미해제 시 SIGKILL
+  for i in {1..30}; do
+    if ! lsof -t -i:8080 >/dev/null; then
+      break
+    fi
+    sleep 1
+  done
+  if lsof -t -i:8080 >/dev/null; then
+    echo "Force killing process $PID"
+    kill -9 "$PID" || true
+  fi
 fi
📝 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
PID=$(lsof -t -i:8080)
# 프로세스 종료
if [ -z "$PID" ]; then
echo "No process is using port 8080."
else
echo "Killing process with PID: $PID"
kill -15 "$PID"
# 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
if [ $? -eq 0 ]; then
echo "Process $PID terminated successfully."
else
echo "Failed to terminate process $PID."
fi
fi
PID=$(lsof -t -i:8080)
# 프로세스 종료
if [ -z "$PID" ]; then
echo "No process is using port 8080."
else
echo "Killing process with PID: $PID"
kill -15 "$PID"
# 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
if [ $? -eq 0 ]; then
echo "Process $PID terminated successfully."
else
echo "Failed to terminate process $PID."
fi
# 포트 해제 대기(최대 30초), 미해제 시 SIGKILL
for i in {1..30}; do
if ! lsof -t -i:8080 >/dev/null; then
break
fi
sleep 1
done
if lsof -t -i:8080 >/dev/null; then
echo "Force killing process $PID"
kill -9 "$PID" || true
fi
fi

Comment on lines +20 to +23
JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)

echo "JAR 파일 실행: $JAR_FILE"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

하드코딩된 경로 제거 및 아티팩트 명 일치

워크플로는 ~/app로 다운로드하며 파일명은 app.jar입니다. 스크립트는 /home/ubuntu/app/*.jar를 탐색하여 환경에 따라 실패합니다. $HOME 기반 경로와 고정 파일명을 사용하세요. JAR 미존재 시 즉시 실패하도록 체크를 추가합니다.

-JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)
+APP_DIR="${APP_DIR:-$HOME/app}"
+JAR_FILE="$APP_DIR/app.jar"
+if [ ! -f "$JAR_FILE" ]; then
+  echo "JAR not found: $JAR_FILE"
+  exit 1
+fi
-
-echo "JAR 파일 실행: $JAR_FILE"
+echo "JAR 파일 실행: $JAR_FILE"
📝 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
JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)
echo "JAR 파일 실행: $JAR_FILE"
APP_DIR="${APP_DIR:-$HOME/app}"
JAR_FILE="$APP_DIR/app.jar"
if [ ! -f "$JAR_FILE" ]; then
echo "JAR not found: $JAR_FILE"
exit 1
fi
echo "JAR 파일 실행: $JAR_FILE"
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 20 to 23, the script
currently hardcodes /home/ubuntu/app/*.jar which can fail in other environments
and with the known artifact name; change it to use $HOME-based path and the
fixed filename (e.g. $HOME/app/app.jar) and add an existence check that
immediately exits non-zero if the JAR is missing; ensure subsequent code uses
the JAR_FILE variable set to that path and that the script prints the JAR path
before proceeding.

Comment on lines +25 to +29
APP_LOG_DIR="/home/ubuntu/app/logs"
APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"

echo "애플리케이션 로그 파일: $APP_LOG_FILE"

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

로그 디렉터리 생성 누락

로그 디렉터리가 없으면 리다이렉션이 실패합니다. mkdir -p 추가를 권장합니다.

 APP_LOG_DIR="/home/ubuntu/app/logs"
 APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"
 
 echo "애플리케이션 로그 파일: $APP_LOG_FILE"
+mkdir -p "$APP_LOG_DIR"
📝 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
APP_LOG_DIR="/home/ubuntu/app/logs"
APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"
echo "애플리케이션 로그 파일: $APP_LOG_FILE"
APP_LOG_DIR="/home/ubuntu/app/logs"
APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"
echo "애플리케이션 로그 파일: $APP_LOG_FILE"
mkdir -p "$APP_LOG_DIR"
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 25 to 29, the script sets
APP_LOG_DIR and APP_LOG_FILE but doesn't ensure the log directory exists which
can cause redirection failures; add a directory creation step before using
APP_LOG_FILE by running mkdir -p "$APP_LOG_DIR" and check its exit status (exit
with non-zero and a clear error message if creation fails) so the log file can
be created reliably.

Comment on lines +30 to +36
sudo nohup java \
-Dspring.profiles.active=dev \
-Duser.timezone=Asia/Seoul \
-Dserver.port=8080 \
-Ddd.service=wellmeet-notification \
-Ddd.env=dev \
-jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 &
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

SC2024: sudo가 리다이렉션에 영향 없음 — root 권한으로 파일 쓰기 보장 필요

현재 리다이렉션은 호출 쉘 권한으로 수행되어 권한 오류가 날 수 있습니다. sudo sh -c로 전체 커맨드를 승격하세요.

-sudo nohup java \
-    -Dspring.profiles.active=dev \
-    -Duser.timezone=Asia/Seoul \
-    -Dserver.port=8080 \
-    -Ddd.service=wellmeet-notification \
-    -Ddd.env=dev \
-    -jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 &
+sudo sh -c "nohup java \
+    -Dspring.profiles.active=dev \
+    -Duser.timezone=Asia/Seoul \
+    -Dserver.port=8080 \
+    -Ddd.service=wellmeet-notification \
+    -Ddd.env=dev \
+    -jar \"$JAR_FILE\" >> \"$APP_LOG_FILE\" 2>&1 &"
📝 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
sudo nohup java \
-Dspring.profiles.active=dev \
-Duser.timezone=Asia/Seoul \
-Dserver.port=8080 \
-Ddd.service=wellmeet-notification \
-Ddd.env=dev \
-jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 &
sudo sh -c "nohup java \
-Dspring.profiles.active=dev \
-Duser.timezone=Asia/Seoul \
-Dserver.port=8080 \
-Ddd.service=wellmeet-notification \
-Ddd.env=dev \
-jar \"$JAR_FILE\" >> \"$APP_LOG_FILE\" 2>&1 &"
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 36-36: sudo doesn't affect redirects. Use ..| sudo tee file

(SC2024)

🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 30 to 36, the nohup command's
redirection runs under the calling shell (not sudo) which can cause permission
errors when writing the log; wrap the entire command and its redirection in a
privileged shell by using sudo sh -c '... &' so that the redirection is executed
as root, ensure proper quoting of variables inside the single-quoted command or
escape inner quotes as needed, and keep nohup and the trailing ampersand to run
the process in background.

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

🧹 Nitpick comments (22)
.github/workflows/Dev_CD.yml (2)

32-37: Gradle 캐시 활성화로 빌드 시간 단축

setup-java에서 Gradle 캐시를 켜면 CI가 빨라집니다.

       - name: Set up JDK 21
         uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
           java-version: '21'
+          cache: 'gradle'

78-79: 동시 배포 경합 방지(concurrency) 권장

동일 브랜치에서 연속 푸시 시 이전 배포와 경합할 수 있습니다. concurrency 그룹으로 이전 작업 취소를 권장합니다.

예시(파일 상단 jobs 옆에 추가):

concurrency:
  group: dev-cd-${{ github.ref }}
  cancel-in-progress: true
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (3)

20-20: ownerId 무결성(Null/중복) 보장 추가 권장

1 owner 1 row 설계라면 NotNull+Unique 제약을 명시하세요(쿼리 성능/데이터 정합성).

-    private String ownerId;
+    @jakarta.persistence.Column(nullable = false, unique = true)
+    private String ownerId;

운영: 스키마를 DDL 자동생성이 아닌 마이그레이션으로 관리한다면, 해당 Unique 인덱스 추가 작업(Flyway/Liquibase)도 포함해 주세요.


21-26: 플래그 필드 확장성 개선 검토(선택)

이벤트×채널 조합이 늘면 필드 폭증 위험. Enum(NotificationType, Channel) 기반의 설정 맵(예: Map<NotificationKey, Boolean>) 또는 @embeddable로 구조화하는 방안을 고려해 보세요.


11-14: BaseEntity 상속 일관성 적용 권장

PushSubscription은 BaseEntity를 상속하고 있으나 OwnerNotificationEnabled는 상속하지 않습니다 — 생성/수정 시각 등 공통 필드(감사) 관리를 위해 OwnerNotificationEnabled도 BaseEntity를 상속하도록 적용하세요.

참고: BaseEntity: src/main/java/com/wellmeet/common/domain/BaseEntity.java
사용 예: src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (public class PushSubscription extends BaseEntity)
대상: src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java

src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (1)

12-15: Enum 필드 'name'은 혼동 소지 → 'code'로 변경 고려, category는 enum화 권장

  • Enum.name()과 의미 충돌 소지가 있는 필드명 ‘name’ 대신 ‘code’/‘type’ 등이 명확합니다.
  • category를 문자열이 아닌 별도 enum(NotificationCategory: HIGH/…​)으로 두면 오타 방지 및 검증이 용이합니다.

참고 변경안(로컬 범위, 참조처 일괄 수정 필요):

-    private final String name;
-    private final String category;
+    private final String code;
+    private final NotificationCategory category;
     private final String topic;
     private final String source;
src/main/java/com/wellmeet/notification/webpush/WebPushController.java (1)

18-31: userId 유효성 검증 추가 및 @validated 적용 권장

@RequestParam userId에 @notblank 추가 시 입력 검증 강화. 클래스 레벨 @validated로 메서드 파라미터 검증 활성화 권장.

 package com.wellmeet.notification.webpush;

 import com.wellmeet.notification.webpush.dto.SubscribeRequest;
 import com.wellmeet.notification.webpush.dto.SubscribeResponse;
 import com.wellmeet.notification.webpush.dto.TestPushRequest;
 import com.wellmeet.notification.webpush.dto.UnsubscribeRequest;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;

 import lombok.RequiredArgsConstructor;

 import org.springframework.http.HttpStatus;
+import org.springframework.validation.annotation.Validated;
 ...
-@RestController
-@RequiredArgsConstructor
+@RestController
+@RequiredArgsConstructor
+@Validated
 public class WebPushController {
 ...
-            @RequestParam String userId
+            @RequestParam @NotBlank String userId
     ) {
         return webPushService.subscribe(userId, subscribeRequest);
     }
src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (2)

13-16: (선택) 중복 방지를 위한 유니크 제약 추천

userId+endpoint 조합에 DB 유니크 인덱스를 두면 동일 구독 중복 저장을 구조적으로 차단할 수 있습니다(레포지토리에도 existsBy가 존재).

 import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
 ...
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+    uniqueConstraints = @UniqueConstraint(
+        name = "uk_push_subscription_user_endpoint",
+        columnNames = {"userId", "endpoint"}
+    )
+)
 public class PushSubscription extends BaseEntity {

44-47: 업데이트 시 재활성화 여부 반영 고려

update 시 키만 갱신되고 active 상태는 유지됩니다. 재구독 시 활성화를 보장하려면 active=true 설정을 고려하세요.

 public void update(PushSubscription updatedSubscription) {
     this.p256dh = updatedSubscription.p256dh;
     this.auth = updatedSubscription.auth;
+    this.active = true;
 }
src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java (3)

29-31: ObjectMapper 직접 생성 대신 빈 주입 사용 권장

애플리케이션 전역 설정(모듈/날짜/가시성 등)을 공유하려면 Spring 빈으로 주입하세요.

-    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final ObjectMapper objectMapper;

53-67: 요청의 data 필드가 payload에 반영되지 않음

클라이언트가 전달한 data를 기본값과 병합하는 것이 자연스럽습니다.

 private Map<String, Object> getNotificationPayload(TestPushRequest request) {
     Map<String, Object> notificationPayload = new HashMap<>();
     notificationPayload.put("title", request.title());
     notificationPayload.put("body", request.body());
     notificationPayload.put("icon", "/icon-192x192.png");
     notificationPayload.put("badge", "/badge-72x72.png");
     notificationPayload.put("vibrate", new int[]{100, 50, 100});
     notificationPayload.put("requireInteraction", false);

-    Map<String, Object> defaultData = new HashMap<>();
-    defaultData.put("url", "/notifications");
-    defaultData.put("timestamp", System.currentTimeMillis());
-    notificationPayload.put("data", defaultData);
+    Map<String, Object> data = new HashMap<>();
+    if (request.data() != null) {
+        data.putAll(request.data());
+    }
+    data.putIfAbsent("url", "/notifications");
+    data.put("timestamp", System.currentTimeMillis());
+    notificationPayload.put("data", data);
     return notificationPayload;
 }

69-77: InterruptedException 처리 시 인터럽트 상태 복원

스레드 인터럽트가 손실되지 않도록 재설정하세요.

-        } catch (JoseException | GeneralSecurityException | IOException | ExecutionException | InterruptedException e) {
-            throw new WellMeetNotificationException(ErrorCode.WEB_PUSH_SEND_FAILED);
+        } catch (JoseException | GeneralSecurityException | IOException | ExecutionException | InterruptedException e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            throw new WellMeetNotificationException(ErrorCode.WEB_PUSH_SEND_FAILED);
         }
src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java (1)

5-20: 응답에서 p256dh/auth 노출 필요성 재검토

클라이언트가 즉시 재사용하지 않는다면 민감 키를 응답 본문에 포함하지 않는 편이 안전합니다. 최소한 로그(요청/응답 본문)에서는 마스킹하세요.

src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java (1)

8-13: endpoint 형식 검증(옵션) 추가 제안

웹푸시 endpoint는 URL 형태가 일반적입니다. Hibernate Validator의 @url을 함께 적용하면 조기 검증에 유용합니다.

적용 예:

 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
+import org.hibernate.validator.constraints.URL;

 public record SubscribeRequest(
-        @NotBlank
-        String endpoint,
+        @NotBlank @URL
+        String endpoint,
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)

7-14: 헤더 DTO 역직렬화 보강(Setter + unknown 필드 허용)

Kafka JSON 역직렬화를 위해 setter가 필요합니다. unknown 필드 허용도 추가를 권장합니다.

 package com.wellmeet.notification.consumer.dto;

 import java.time.LocalDateTime;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.NoArgsConstructor;

 @Getter
+@Setter
 @NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class MessageHeader {

     private String messageId;
     private LocalDateTime timestamp;
     private String source;
 }
src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)

7-15: Payload DTO도 역직렬화 보강 필요

Setter 부재 시 Jackson 바인딩 실패 위험이 있습니다. unknown 필드 허용도 추가 권장.

 package com.wellmeet.notification.consumer.dto;

 import java.time.LocalDateTime;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.NoArgsConstructor;

 @Getter
+@Setter
 @NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class ReservationCreatedPayload extends NotificationPayload {

     private Long reservationId;
     private String customerName;
     private LocalDateTime reservationTime;
     private int partySize;
 }
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)

46-61: 중복 레코드 방지/식별자 유지까지 함께 검증 권장(옵션)

업데이트 시 기존 엔티티 재사용을 보장하려면 ID 유지도 확인해 두는 편이 좋습니다.

-            PushSubscription pushSubscription = new PushSubscription(userId, endpoint, "p256dh", "auth");
-            pushSubscriptionRepository.save(pushSubscription);
+            PushSubscription pushSubscription = new PushSubscription(userId, endpoint, "p256dh", "auth");
+            PushSubscription saved = pushSubscriptionRepository.save(pushSubscription);
@@
-            PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get();
+            PushSubscription subscription = pushSubscriptionRepository.findById(response.subscriptionId()).get();
@@
             assertAll(
                     () -> assertThat(subscription.getUserId()).isEqualTo(userId),
                     () -> assertThat(subscription.getEndpoint()).isEqualTo(endpoint),
                     () -> assertThat(subscription.getP256dh()).isEqualTo(request.p256dh()),
                     () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()),
+                    () -> assertThat(response.subscriptionId()).isEqualTo(saved.getId())
             );
src/main/java/com/wellmeet/config/KafkaConfig.java (1)

34-34: auto.offset.reset 하드코딩 제거 권장

동일 설정이 application-*.yml에 있다면 코드 중복입니다. 프로퍼티만 신뢰하세요.

-        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
src/main/java/com/wellmeet/notification/webpush/WebPushService.java (4)

27-31: O(n) 조회 → 단건 조회로 단순화

특정 endpoint 존재 여부만 필요하므로 단건 조회 메서드로 대체해 쿼리·메모리 비용을 줄이세요.

-        List<PushSubscription> existingSubscriptions = pushSubscriptionRepository.findByUserId(userId);
-        Optional<PushSubscription> pushSubscription = existingSubscriptions.stream()
-                .filter(subscription -> subscription.isSameEndpoint(request.endpoint()))
-                .findAny();
+        Optional<PushSubscription> pushSubscription =
+                pushSubscriptionRepository.findByUserIdAndEndpoint(userId, request.endpoint());

레포지토리에 아래 시그니처가 있는지 확인/추가 필요:

Optional<PushSubscription> findByUserIdAndEndpoint(String userId, String endpoint);

43-50: 읽기 전용 트랜잭션으로 의도 명확화

조회 후 전송만 수행합니다. 읽기 전용으로 지정해 JPA 플러시 비용을 방지하세요.

-    public void sendTestPush(String userId, TestPushRequest request) {
+    @Transactional(readOnly = true)
+    public void sendTestPush(String userId, TestPushRequest request) {

52-58: exists+delete 두 번 쿼리 → 단일 삭제 결과 체크로 축소

TOCTOU와 불필요한 왕복을 줄이세요. 삭제 결과로 존재 여부 판정이 가능합니다.

-    @Transactional
-    public void unsubscribe(String userId, UnsubscribeRequest request) {
-        if (!pushSubscriptionRepository.existsByUserIdAndEndpoint(userId, request.endpoint())) {
-            throw new WellMeetNotificationException(ErrorCode.SUBSCRIPTION_NOT_FOUND);
-        }
-        pushSubscriptionRepository.deleteByUserIdAndEndpoint(userId, request.endpoint());
-    }
+    @Transactional
+    public void unsubscribe(String userId, UnsubscribeRequest request) {
+        long deleted = pushSubscriptionRepository.deleteByUserIdAndEndpoint(userId, request.endpoint());
+        if (deleted == 0) {
+            throw new WellMeetNotificationException(ErrorCode.SUBSCRIPTION_NOT_FOUND);
+        }
+    }

레포지토리 메서드가 삭제 건수를 반환하도록 시그니처가 long deleteByUserIdAndEndpoint(...)인지 확인/변경이 필요합니다.


25-41: (권장) 유니크 제약으로 중복 구독 보호

동시 구독 요청 시 중복 레코드 생성 방지를 위해 (user_id, endpoint)에 유니크 인덱스를 추가하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c0b07c and e162c4e.

📒 Files selected for processing (24)
  • .github/workflows/Dev_CD.yml (1 hunks)
  • src/main/java/com/wellmeet/config/KafkaConfig.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1 hunks)
  • src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/WebPushController.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/WebPushService.java (2 hunks)
  • src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java (1 hunks)
  • src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java (1 hunks)
  • src/main/resources/local-secret.yml (1 hunks)
  • src/test/java/com/wellmeet/BaseControllerTest.java (1 hunks)
  • src/test/java/com/wellmeet/BaseServiceTest.java (1 hunks)
  • src/test/java/com/wellmeet/webpush/WebPushControllerTest.java (1 hunks)
  • src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (3 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java
  • src/test/java/com/wellmeet/webpush/WebPushControllerTest.java
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java (1)
  • Getter (13-52)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (2)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)
  • Getter (7-14)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java (1)
  • Getter (7-14)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
  • Getter (6-13)
src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java (1)
  • Getter (7-15)
🪛 Gitleaks (8.28.0)
src/main/resources/local-secret.yml

[high] 4-4: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 actionlint (1.7.7)
.github/workflows/Dev_CD.yml

63-63: label "dev" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2022", "windows-2019", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "ubuntu-20.04", "macos-latest", "macos-latest-xl", "macos-latest-xlarge", "macos-latest-large", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14", "macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

⏰ 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 / build-and-push
🔇 Additional comments (14)
.github/workflows/Dev_CD.yml (3)

41-45: bootJar 산출물 경로 불일치 — 서브모듈 경로 사용 필요

이전 리뷰와 동일 이슈입니다. 멀티모듈(:api-owner)이라면 산출물은 api-owner/build/libs 하위입니다.

다음처럼 수정하세요:

-      - name: bootJar with Gradle
-        run: ./gradlew bootJar --info
-
-      - name: Change artifact file name
-        run: mv build/libs/*.jar build/libs/app.jar
+      - name: bootJar with Gradle
+        run: ./gradlew :api-owner:bootJar --info
+
+      - name: Change artifact file name
+        run: mv ./api-owner/build/libs/*.jar ./api-owner/build/libs/app.jar

또한 다중 매칭 시 mv 실패를 방지하려면 Gradle에서 archiveFileName을 app.jar로 고정하는 것을 권장합니다(Gradle 설정 변경).


47-52: 아티팩트 업로드 경로 불일치

서브모듈 산출물 경로로 수정 필요합니다.

       - name: Upload artifact file
         uses: actions/upload-artifact@v4
         with:
           name: app-artifact
-          path: ./build/libs/app.jar
+          path: ./api-owner/build/libs/app.jar
           if-no-files-found: error

62-64: self-hosted 러너 라벨 명시 권장

runs-on: dev는 GitHub 호스티드에는 없으며, self-hosted 러너 라벨 매칭 실패 가능성이 있습니다. self-hosted 및 OS 라벨을 함께 지정하세요.

-    runs-on: dev
+    runs-on: [self-hosted, linux, dev]

dev 러너에 위 라벨이 실제로 부여되어 있는지 확인 부탁드립니다.

src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)

20-20: 용어 일관성 확인 — userId와 ownerId는 분리되어 사용됩니다

webpush 도메인(예: PushSubscription, PushSubscriptionRepository, WebPushController/WebPushService)은 userId를 사용하고, OwnerNotificationEnabled에는 ownerId만 정의되어 있습니다. rg 검색 결과 교차 사용이나 조회/조인 혼동은 발견되지 않았습니다.

src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java (1)

7-16: LGTM

레코드 + Bean Validation 조합 적절합니다.

src/test/java/com/wellmeet/BaseServiceTest.java (1)

3-15: LGTM

패키지 경로 변경에 따른 import 업데이트 적절합니다.

src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java (1)

8-16: LGTM

JPA 시그니처와 파생 쿼리 메서드 정의 적절합니다.

src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java (1)

1-3: 패키지/임포트 경로 리네임 적절

notification.webpush 네임스페이스 정리 방향 좋습니다.

src/test/java/com/wellmeet/BaseControllerTest.java (1)

3-3: 패키지 이동 반영 LGTM

리포지토리 임포트 경로 수정 적절합니다.

src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java (1)

1-3: 패키지/임포트 경로 리네임 LGTM

src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (2)

14-17: 토픽/그룹 하드코딩 제거 + 레코드 메타데이터 로깅으로 운용성 향상

기 존 코멘트와 동일 제안입니다. 프로퍼티로 외부화하고 파티션/오프셋까지 로그에 포함하세요.

 package com.wellmeet.notification.consumer;

 import com.wellmeet.notification.consumer.dto.NotificationMessage;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.kafka.annotation.KafkaListener;
 import org.springframework.stereotype.Service;
+import org.apache.kafka.clients.consumer.ConsumerRecord;

 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class NotificationConsumer {

-    @KafkaListener(topics = "notification", groupId = "notification-group")
-    public void consume(NotificationMessage message) {
-        log.info("Received message from notification topic: {}", message);
+    @KafkaListener(
+        topics = "${notification.topic:notification}",
+        groupId = "${spring.kafka.consumer.group-id}"
+    )
+    public void consume(ConsumerRecord<String, NotificationMessage> record) {
+        log.info("Consumed notification: topic={}, partition={}, offset={}, key={}, payload={}",
+                 record.topic(), record.partition(), record.offset(), record.key(), record.value());
     }
 }

14-17: Kafka JSON 역직렬화 설정 확인 — 다형성 타입 힌트 점검 필요

  • kafkaListenerContainerFactory / ConsumerFactory가 NotificationMessage 타입으로 Bean으로 등록되어 있음 — src/main/java/com/wellmeet/config/KafkaConfig.java (consumerFactory(), kafkaListenerContainerFactory()).
  • JsonDeserializer 설정 확인됨: USE_TYPE_INFO_HEADERS=false, TRUSTED_PACKAGES="com.wellmeet.consumer.dto", VALUE_DEFAULT_TYPE=NotificationMessage.class.
  • 다형성 위험: 코드베이스에서 @JsonTypeInfo/@JsonSubTypes 등 타입 힌트 사용 흔적 없음. NotificationMessage 내부에 다형성 payload(NotificationPayload 등)가 있다면 현재 설정(USE_TYPE_INFO_HEADERS=false + 단일 VALUE_DEFAULT_TYPE)로 하위 타입 식별이 안 되어 역직렬화 실패 가능. 조치(선택 중 하나): payload 클래스에 @JsonTypeInfo/@JsonSubTypes 추가, 또는 USE_TYPE_INFO_HEADERS=true로 헤더 기반 타입 전달 사용(프로듀서도 헤더 포함), 또는 커스텀 Deserializer 적용.
  • group-id는 외부화되어 있음(@value("${spring.kafka.consumer.group-id}")).
src/test/java/com/wellmeet/webpush/WebPushServiceTest.java (1)

46-61: 동일 유저/엔드포인트 재구독 시 업데이트 검사 LGTM

변경된 업데이트 동작(assert p256dh/auth 갱신) 검증이 정확합니다.

src/main/java/com/wellmeet/config/KafkaConfig.java (1)

42-48: Boot 자동 구성 활용 + 에러 핸들러 적용 권장

수동 팩토리 설정 대신 Boot의 configurer를 사용해 spring.kafka.*를 일관 적용하고, 공통 에러 핸들러(백오프 포함)를 설정하세요.

-    @Bean
-    public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory() {
-        ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory =
-                new ConcurrentKafkaListenerContainerFactory<>();
-        factory.setConsumerFactory(consumerFactory());
-        return factory;
-    }
+    @Bean
+    public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory(
+            org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
+            ConsumerFactory<String, NotificationMessage> consumerFactory) {
+        ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory =
+                new ConcurrentKafkaListenerContainerFactory<>();
+        configurer.configure(factory, consumerFactory);
+        factory.setCommonErrorHandler(new org.springframework.kafka.listener.DefaultErrorHandler(
+                new org.springframework.util.backoff.FixedBackOff(1000L, 3)));
+        return factory;
+    }

추가 import:

import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactoryConfigurer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.FixedBackOff;

Comment on lines +28 to +31
- name: Setting dev-secret.yml
run: |
echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

빌드 산출물에 dev-secret.yml 포함 위험

빌드 단계에서 dev-secret.yml을 리소스에 기록하면 업로드되는 JAR 내부에 비밀이 포함됩니다. 이는 아티팩트 유출 리스크입니다. 배포 단계에서만 파일을 생성·주입하도록 이동하세요.

다음처럼 빌드 단계에서 제거하고, 배포 단계에서 생성하도록 변경:

-      - name: Setting dev-secret.yml
-        run: |
-          echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
+      # dev-secret.yml은 빌드 아티팩트에 포함하지 않습니다.

배포 단계에 추가:

     steps:
+      - name: Create dev-secret.yml on runner
+        run: |
+          mkdir -p ~/app/config
+          echo "${{ secrets.DEV_SECRET_YML }}" > ~/app/config/dev-secret.yml
+
       - name: Download artifact file
         uses: actions/download-artifact@v4
         with:
           name: app-artifact
           path: ~/app
+
+      - name: Verify secret file exists
+        run: test -s ~/app/config/dev-secret.yml

애플리케이션이 외부 파일을 읽도록(코드/스크립트 측) JVM 옵션 예:
-Dspring.config.import=optional:file:~/app/config/dev-secret.yml

📝 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
- name: Setting dev-secret.yml
run: |
echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
# dev-secret.yml은 빌드 아티팩트에 포함하지 않습니다.
steps:
- name: Create dev-secret.yml on runner
run: |
mkdir -p ~/app/config
echo "${{ secrets.DEV_SECRET_YML }}" > ~/app/config/dev-secret.yml
- name: Download artifact file
uses: actions/download-artifact@v4
with:
name: app-artifact
path: ~/app
- name: Verify secret file exists
run: test -s ~/app/config/dev-secret.yml
🤖 Prompt for AI Agents
.github/workflows/Dev_CD.yml lines 28-31: the workflow writes dev-secret.yml
into src/main/resources during the build which embeds secrets into the produced
JAR; remove that run step from the build job, and instead add a step in the
deploy job that creates the file on the runner (or injects it as a secret file)
just before deployment; ensure the build uses no local secret file, update
deployment steps to write the secret from the GitHub secret into an external
path (e.g., ~/app/config/dev-secret.yml) and/or set the JVM/SPRING config to
import an external optional file (for example via
-Dspring.config.import=optional:file:~/app/config/dev-secret.yml) so secrets are
injected at deploy time only and not baked into artifacts.

Comment on lines +36 to +39
props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto");
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
return new DefaultKafkaConsumerFactory<>(props);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

JsonDeserializer TRUSTED_PACKAGES 경로 오타로 역직렬화 실패 가능성

DTO 패키지명이 com.wellmeet.notification.consumer.dto인데, 신뢰 패키지가 com.wellmeet.consumer.dto로 되어 있어 역직렬화가 실패할 수 있습니다. 또한 VALUE_DEFAULT_TYPE은 FQCN 문자열로 지정하는 편이 안전합니다.

다음 최소 패치 권장:

-        props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto");
-        props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
+        props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.notification.consumer.dto");
+        props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.wellmeet.notification.consumer.dto.NotificationMessage");
📝 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
props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto");
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
return new DefaultKafkaConsumerFactory<>(props);
props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.notification.consumer.dto");
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.wellmeet.notification.consumer.dto.NotificationMessage");
return new DefaultKafkaConsumerFactory<>(props);
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/config/KafkaConfig.java around lines 36 to 39, the
JsonDeserializer TRUSTED_PACKAGES value is incorrect (com.wellmeet.consumer.dto)
which can cause deserialization failures; change it to the correct package
com.wellmeet.notification.consumer.dto and set
JsonDeserializer.VALUE_DEFAULT_TYPE to the fully-qualified class name string
"com.wellmeet.notification.consumer.dto.NotificationMessage" (rather than the
class literal) so the deserializer can resolve the type reliably; keep
USE_TYPE_INFO_HEADERS = false and return the DefaultKafkaConsumerFactory with
the updated props.

Comment on lines +6 to +12
@Getter
@NoArgsConstructor
public class NotificationInfo {

private String type;
private String category;
private String recipient;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Jackson 역직렬화 호환성 점검 + 타입 안전성 개선 제안

  • 현재 @Getter, @NoArgsConstructor만으로는 기본 ObjectMapper 설정에서 private 필드 역직렬화가 실패할 수 있습니다. 필드 기반 접근 허용 또는 세터/생성자 제공이 필요합니다.
  • 또한 type/category 문자열은 오타·불일치 위험이 있으니, enum 기반(예: NotificationType, 별도 Category enum)으로의 전환을 고려해 주세요.

적용 예시(필드 접근 허용 및 unknown 필드 무시):

 package com.wellmeet.notification.consumer.dto;

+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import lombok.Getter;
 import lombok.NoArgsConstructor;

 @Getter
 @NoArgsConstructor
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class NotificationInfo {

     private String type;
     private String category;
     private String recipient;
 }

다음 스크립트로 역직렬화 설정(필드 가시성/JsonDeserializer/신뢰 패키지) 존재 여부를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# Jackson 필드 가시성/AutoDetect/Deserializer 설정 탐색
rg -n -C2 -e '@JsonAutoDetect' -e 'setVisibility' -e 'JsonAutoDetect.Visibility' -e 'fieldVisibility' --type=java --type=yml --type=properties

# Spring Kafka JsonDeserializer 및 trusted.packages 설정 탐색
rg -n -C2 -e 'JsonDeserializer' -e 'spring.kafka.consumer' -e 'trusted.packages' --type=java --type=yml --type=properties

# 커스텀 ObjectMapper Bean 정의 탐색
rg -n -C2 -P 'ObjectMapper\s+\w+\s*\(' --type=java

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== File: NotificationInfo.java (first 200 lines) ==="
sed -n '1,200p' src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java || true
echo

echo "=== Search: Jackson annotations / ObjectMapper config / module registration ==="
rg -n -C2 --hidden -S '@JsonAutoDetect|@JsonIgnoreProperties|JsonAutoDetect.Visibility|setVisibility|fieldVisibility|Jackson2ObjectMapperBuilder|new ObjectMapper|@Bean\s+ObjectMapper|registerModule|SimpleModule|addDeserializer|@JsonComponent' || true
echo

echo "=== Search: Kafka JsonDeserializer / trusted.packages / spring.kafka.consumer ==="
rg -n -C2 --hidden -S 'JsonDeserializer|trusted.packages|spring.kafka.consumer|value-deserializer|key-deserializer|spring.kafka.properties' || true
echo

echo "=== Search: application.yml / application.yaml / application.properties for trusted.packages or spring.kafka.consumer ==="
rg -n -C2 --hidden -S 'trusted.packages|spring.kafka.consumer' -g 'application.*' -g '**/application.*' || true
echo

echo "=== Search: any mention of NotificationInfo usage (deserialization targets) ==="
rg -n -C2 --hidden -S 'NotificationInfo' || true

Length of output: 5962


조치 필요 — NotificationInfo 역직렬화 접근성 설정 및 Kafka JsonDeserializer trusted.packages 수정(치명적)

  • NotificationInfo (src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java): 현재 @NoArgsConstructor + private 필드(@Getter)만 있어 Jackson 기본 설정으로는 역직렬화 실패 가능. 클래스에 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 및 @JsonIgnoreProperties(ignoreUnknown = true)를 추가하거나, 각 필드에 세터/ @JsonProperty 또는 @JsonCreator 생성자를 제공해 역직렬화를 보장하세요.
  • KafkaConfig (src/main/java/com/wellmeet/config/KafkaConfig.java): props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto"); — 실제 DTO 패키지(com.wellmeet.notification.consumer.dto)와 불일치합니다. trusted.packages 값을 실제 패키지로 수정하거나 필요한 패키지를 포함시키지 않으면 JsonDeserializer가 역직렬화를 차단합니다.
  • 권장 변경: type/category 필드를 enum(NotificationType, Category)으로 전환해 타입 안정성 확보.
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java
around lines 6-12, the class only has @NoArgsConstructor and private fields with
@Getter which can prevent Jackson from deserializing; update the class to allow
Jackson deserialization by adding @JsonIgnoreProperties(ignoreUnknown = true)
and @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) to the
class or by adding setters/@JsonProperty annotations or a @JsonCreator
constructor for all fields; additionally, fix Kafka config in
src/main/java/com/wellmeet/config/KafkaConfig.java by setting
JsonDeserializer.TRUSTED_PACKAGES to the actual DTO package
"com.wellmeet.notification.consumer.dto" (or include it alongside existing
packages); optionally migrate type and category to enums (NotificationType,
Category) for stronger typing and update deserialization accordingly.

Comment on lines +6 to +13
@Getter
@NoArgsConstructor
public class NotificationMessage {

private MessageHeader header;
private NotificationInfo notification;
private NotificationPayload payload;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Kafka/Jackson 역직렬화 실패 가능성: setter 부재 + 다형성 payload 문제

  • 현재 DTO는 getter만 있어 Jackson이 바인딩에 실패할 수 있습니다.
  • 필드 payload가 추상 타입(NotificationPayload)이라 다형성 역직렬화 설정이 없으면 실패합니다.

다음 패치를 권장합니다(Setter + unknown 필드 허용):

 package com.wellmeet.notification.consumer.dto;

+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.NoArgsConstructor;

 @Getter
+@Setter
 @NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class NotificationMessage {

     private MessageHeader header;
     private NotificationInfo notification;
     private NotificationPayload payload;
 }

또한, 다형성 역직렬화를 위해 NotificationPayload에 타입 정보를 선언하세요(다른 파일 변경):

// src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java
package com.wellmeet.notification.consumer.dto;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "payloadType")
@JsonSubTypes({
    @JsonSubTypes.Type(value = ReservationCreatedPayload.class, name = "RESERVATION_CREATED")
})
public abstract class NotificationPayload {}

생성 측에서 JSON에 "payloadType": "RESERVATION_CREATED"를 포함시키세요.

🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java
around lines 6 to 13, Jackson deserialization can fail because the class only
exposes getters and the payload field is an abstract type without polymorphic
type info; add setters for all fields and annotate the class with
@JsonIgnoreProperties(ignoreUnknown = true) so unknown Kafka fields are allowed,
and update the abstract NotificationPayload class (separate file) to include
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
property = "payloadType") and @JsonSubTypes registrations for concrete payload
implementations; finally ensure producers include "payloadType" in the JSON
(e.g., "RESERVATION_CREATED") so Jackson can instantiate the correct subtype.

Comment on lines 11 to 27
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OwnerNotificationEnabled {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String ownerId;
private boolean reservationRequestWebPushEnabled;
private boolean reservationRequestEmailEnabled;
private boolean reservationCancelWebPushEnabled;
private boolean reservationCancelEmailEnabled;
private boolean promotionWebPushEnabled;
private boolean promotionEmailEnabled;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

엔티티를 애플리케이션 코드에서 생성/수정할 수 없습니다 — 생성자/빌더 등 도메인 메서드 추가 필요

현재 @Getter만 있고 모든 필드가 private이라 새 인스턴스 생성 시 값 설정이 불가합니다(특히 ownerId). 영속화/갱신을 위한 최소 생성자·빌더를 추가하세요.

아래와 같이 생성자(+빌더)를 추가하면 사용 가능합니다:

 @Getter
 @Entity
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class OwnerNotificationEnabled {
@@
     private boolean promotionWebPushEnabled;
     private boolean promotionEmailEnabled;
+
+    @lombok.Builder
+    public OwnerNotificationEnabled(
+            String ownerId,
+            boolean reservationRequestWebPushEnabled,
+            boolean reservationRequestEmailEnabled,
+            boolean reservationCancelWebPushEnabled,
+            boolean reservationCancelEmailEnabled,
+            boolean promotionWebPushEnabled,
+            boolean promotionEmailEnabled
+    ) {
+        this.ownerId = ownerId;
+        this.reservationRequestWebPushEnabled = reservationRequestWebPushEnabled;
+        this.reservationRequestEmailEnabled = reservationRequestEmailEnabled;
+        this.reservationCancelWebPushEnabled = reservationCancelWebPushEnabled;
+        this.reservationCancelEmailEnabled = reservationCancelEmailEnabled;
+        this.promotionWebPushEnabled = promotionWebPushEnabled;
+        this.promotionEmailEnabled = promotionEmailEnabled;
+    }
 }
📝 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
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OwnerNotificationEnabled {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String ownerId;
private boolean reservationRequestWebPushEnabled;
private boolean reservationRequestEmailEnabled;
private boolean reservationCancelWebPushEnabled;
private boolean reservationCancelEmailEnabled;
private boolean promotionWebPushEnabled;
private boolean promotionEmailEnabled;
}
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OwnerNotificationEnabled {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String ownerId;
private boolean reservationRequestWebPushEnabled;
private boolean reservationRequestEmailEnabled;
private boolean reservationCancelWebPushEnabled;
private boolean reservationCancelEmailEnabled;
private boolean promotionWebPushEnabled;
private boolean promotionEmailEnabled;
@lombok.Builder
public OwnerNotificationEnabled(
String ownerId,
boolean reservationRequestWebPushEnabled,
boolean reservationRequestEmailEnabled,
boolean reservationCancelWebPushEnabled,
boolean reservationCancelEmailEnabled,
boolean promotionWebPushEnabled,
boolean promotionEmailEnabled
) {
this.ownerId = ownerId;
this.reservationRequestWebPushEnabled = reservationRequestWebPushEnabled;
this.reservationRequestEmailEnabled = reservationRequestEmailEnabled;
this.reservationCancelWebPushEnabled = reservationCancelWebPushEnabled;
this.reservationCancelEmailEnabled = reservationCancelEmailEnabled;
this.promotionWebPushEnabled = promotionWebPushEnabled;
this.promotionEmailEnabled = promotionEmailEnabled;
}
}
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled
around lines 11 to 27, the entity only has @Getter and a protected no-args
constructor so callers cannot set required fields (e.g., ownerId) to create or
update instances; add a minimal constructor and/or a @Builder (or static
factory) that accepts ownerId and the notification flag fields and use the
existing protected no-args constructor for JPA; ensure the constructor/builder
sets all fields and keep the no-arg constructor protected for JPA compliance so
instances can be created and persisted/updated by application code.

Comment on lines +1 to +5
secret:
vapid:
public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs
private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU
subject: mailto:admin@wellmeet.com
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

하드코딩된 비밀키 포함(Gitleaks 검출) — 즉시 제거 및 키 회전 필요

비밀키가 저장소에 커밋되었습니다. 리포지토리에서 파일 제거 + 히스토리 정리(BFG/git filter-repo) + VAPID 키 즉시 재발급·교체가 필요합니다. 실제 비밀은 런타임(환경변수/외부 파일/Secret Manager)을 통해 주입하세요. 예시 파일만 커밋하세요.

다음과 같이 임시로 플레이스홀더로 치환하시고(그래도 키는 반드시 회전하세요), 실제 값은 .gitignore된 외부 파일로 관리하세요:

-secret:
-  vapid:
-    public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs
-    private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU
-    subject: mailto:admin@wellmeet.com
+secret:
+  vapid:
+    public-key: REPLACE_ME_PUBLIC_KEY
+    private-key: REPLACE_ME_PRIVATE_KEY
+    subject: mailto:REPLACE_ME@example.com

그리고 루트 .gitignore에 추가하세요(파일 외부 변경 제안):

# secrets
src/main/resources/*-secret.yml
src/main/resources/local-secret.yml

운영 절차:

  • 현재 커밋된 키 폐기 및 재발급(VAPID 키 페어).
  • 저장소 히스토리에서 해당 파일 완전 제거(BFG/git filter-repo).
  • 애플리케이션은 spring.config.import=optional:file:/path/to/dev-secret.yml 등 외부 경로에서 주입.
📝 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
secret:
vapid:
public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs
private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU
subject: mailto:admin@wellmeet.com
secret:
vapid:
public-key: REPLACE_ME_PUBLIC_KEY
private-key: REPLACE_ME_PRIVATE_KEY
subject: mailto:REPLACE_ME@example.com
🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 4-4: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In src/main/resources/local-secret.yml lines 1-5 there are hardcoded VAPID
private/public keys committed to the repo; remove these secrets immediately and
replace them with placeholders, add src/main/resources/local-secret.yml (and
pattern *-secret.yml) to .gitignore, and stop committing real secrets; rotate
the VAPID key pair now (revoke/issue new keys) and scrub the repository history
using BFG or git-filter-repo to remove the leaked file; change the app to load
VAPID values from a runtime secret source (environment variables, an external
ignored file like /path/to/dev-secret.yml loaded via
spring.config.import=optional:file:..., or a secret manager) and keep only an
example/template secret file in the repo.

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 (4)
src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (2)

18-23: 토픽/그룹 하드코딩 문제는 이전 리뷰에서 지적되었습니다

이전 리뷰 코멘트에서 토픽과 그룹 ID를 프로퍼티로 외부화하고 메타데이터 로깅을 추가하도록 제안했습니다.


18-23: 이전 리뷰에서 이미 지적된 사항입니다.

토픽명과 그룹 ID가 하드코딩되어 있는 문제는 이전 리뷰 코멘트에서 이미 제기되었습니다. 프로퍼티 기반 외부화를 통해 환경별 유연성을 확보하는 것이 권장됩니다.

src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (2)

9-22: 생성자/빌더 누락 문제는 이전 리뷰에서 지적되었습니다

이전 리뷰 코멘트에서 엔티티 인스턴스 생성 및 필드 설정을 위한 생성자와 @Builder 추가를 제안했습니다. 해당 제안사항을 참고하세요.


9-22: 이전 리뷰에서 이미 지적된 사항입니다.

엔티티 인스턴스를 애플리케이션 코드에서 생성할 수 있도록 빌더 또는 생성자를 추가해야 하는 문제는 이전 리뷰 코멘트에서 상세한 해결 방안과 함께 이미 제기되었습니다.

🧹 Nitpick comments (1)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)

9-22: UserNotificationEnabled와 코드 중복

OwnerNotificationEnabledUserNotificationEnabled(UserNotificationEnabled.java:8-23)가 거의 동일한 구조를 가지고 있습니다. 공통 필드와 동작을 추상화하여 @MappedSuperclass를 활용하거나, 공통 인터페이스를 도입하면 유지보수성이 향상됩니다.

예시: 공통 알림 설정 필드를 추상 클래스로 추출

@MappedSuperclass
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class NotificationEnabled {
    private boolean webPushEnabled;
    private boolean emailEnabled;
    // 공통 알림 채널 필드
}

그런 다음 각 엔티티에서 확장:

@Entity
public class OwnerNotificationEnabled extends NotificationEnabled {
    @Id
    private String ownerId;
    // Owner 전용 필드
}

단, 현재 Owner와 User의 알림 타입이 다르므로(예: reservationRequest vs reservationConfirm) 추상화 수준을 신중히 결정하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e162c4e and 743dc12.

📒 Files selected for processing (11)
  • src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/NotificationSender.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1 hunks)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java (1 hunks)
  • src/main/java/com/wellmeet/notification/domain/NotificationHistory.java (1 hunks)
  • src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1 hunks)
  • src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1 hunks)
  • src/main/java/com/wellmeet/notification/repository/NotificationHistoryRepository.java (1 hunks)
  • src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java (1 hunks)
  • src/main/java/com/wellmeet/notification/repository/UserNotificationEnabledRepository.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java
  • src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/wellmeet/notification/domain/NotificationHistory.java (3)
src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java (1)
  • Getter (6-14)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
  • Getter (9-22)
src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
  • Getter (9-24)
src/main/java/com/wellmeet/notification/consumer/NotificationSender.java (1)
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java (1)
  • Component (9-19)
src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
  • Getter (9-22)
src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java (1)
src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java (1)
  • Getter (9-24)
src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java (1)
src/main/java/com/wellmeet/notification/consumer/NotificationSender.java (1)
  • Component (10-20)
⏰ 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: build-and-push
  • GitHub Check: test / build-and-push
🔇 Additional comments (3)
src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java (2)

7-9: 표준 JPA 리포지토리 패턴이 올바르게 적용되었습니다

JpaRepository를 확장하여 기본 CRUD 작업을 상속받고, ID 타입을 String으로 올바르게 지정했습니다. 현재 단계에서 커스텀 쿼리 메서드가 필요 없다면 이 구현으로 충분합니다.


1-9: LGTM!

표준 Spring Data JPA 리포지토리 인터페이스가 올바르게 선언되었습니다. OwnerNotificationEnabled 엔티티의 기본 CRUD 작업을 지원합니다.

src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java (1)

10-16: 의존성 주입 및 서비스 구조가 적절합니다.

Lombok의 @RequiredArgsConstructor를 통한 생성자 주입 패턴이 올바르게 적용되었으며, 필요한 협력 객체들이 명확하게 선언되어 있습니다.

Comment on lines +16 to +18
public List<String> check(String recipient, String recipientType) {
return null;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

null 반환으로 인한 NPE 및 채널 확인 실패
Line 16: check가 언제나 null을 반환하면, 이를 사용하는 측에서 즉시 NullPointerException이 발생하거나 채널 판별이 아예 불가능해집니다. 실제 리포지토리 조회 후 활성화된 채널 목록을 돌려주는 로직을 구현해 주세요(비어 있으면 최소한 빈 리스트라도 반환해야 합니다).

🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java
around lines 16 to 18, the check method currently returns null which causes NPEs
and prevents channel determination; replace this stub with logic that queries
the notification/channel repository using recipient and recipientType, filters
for active/enabled channels, returns a List<String> of channel identifiers, and
never returns null (return Collections.emptyList() when none found); also catch
repository exceptions, log appropriately and return an empty list on error.

Comment on lines +19 to +23
public void consume(NotificationMessage message) {
List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(),
message.getNotification().getRecipientType());
notificationSender.send(message, enabled);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

에러 처리 로직 누락으로 메시지 처리 안정성 저하

notificationChecker.check() 또는 notificationSender.send()에서 예외 발생 시 Kafka 리스너의 기본 동작에 따라 메시지가 재처리되거나 DLQ로 이동할 수 있습니다. 명시적인 에러 핸들링과 로깅을 추가하여 장애 추적 및 복구 전략을 개선하세요.

다음 diff를 적용하여 에러 처리 및 로깅을 추가하세요:

     @KafkaListener(topics = "notification", groupId = "notification-group")
     public void consume(NotificationMessage message) {
+        try {
+            log.info("Processing notification message: recipient={}, type={}", 
+                     message.getNotification().getRecipient(),
+                     message.getNotification().getRecipientType());
+            
             List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(),
                     message.getNotification().getRecipientType());
             notificationSender.send(message, enabled);
+            
+            log.info("Successfully processed notification message for recipient={}",
+                     message.getNotification().getRecipient());
+        } catch (Exception e) {
+            log.error("Failed to process notification message: recipient={}, error={}",
+                      message.getNotification().getRecipient(), e.getMessage(), e);
+            throw e; // Re-throw to trigger Kafka retry/DLQ logic
+        }
     }
📝 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 consume(NotificationMessage message) {
List<String> enabled = notificationChecker.check(message.getNotification().getRecipient(),
message.getNotification().getRecipientType());
notificationSender.send(message, enabled);
}
@KafkaListener(topics = "notification", groupId = "notification-group")
public void consume(NotificationMessage message) {
try {
log.info("Processing notification message: recipient={}, type={}",
message.getNotification().getRecipient(),
message.getNotification().getRecipientType());
List<String> enabled = notificationChecker.check(
message.getNotification().getRecipient(),
message.getNotification().getRecipientType());
notificationSender.send(message, enabled);
log.info("Successfully processed notification message for recipient={}",
message.getNotification().getRecipient());
} catch (Exception e) {
log.error("Failed to process notification message: recipient={}, error={}",
message.getNotification().getRecipient(), e.getMessage(), e);
throw e; // Re-throw to trigger Kafka retry/DLQ logic
}
}
🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java
around lines 19 to 23, add explicit error handling and logging around the calls
to notificationChecker.check(...) and notificationSender.send(...) so exceptions
are not silently propagated: wrap the existing logic in a try-catch, log a
detailed error (include message id, recipient, recipientType and stacktrace)
when either call fails, and then decide to either rethrow a
RuntimeException/KafkaException to let the Kafka listener handle retries/DLQ or
call a recovery/dlqPublisher if you have one; ensure logging uses the class
logger and includes enough context to trace the message and failure.

Comment on lines +17 to +19
public void send(NotificationMessage payload, List<String> enabled) {

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

비어 있는 send 구현으로 기능이 수행되지 않습니다
Line 17: send 메서드가 비어 있어 NotificationConsumer가 호출해도 실제 알림 전송이나 이력 저장이 전혀 이뤄지지 않습니다. 이는 본 PR의 핵심 기능을 막는 치명적 문제입니다. 최소한 enabled 채널을 순회해 WebPushService 호출과 NotificationHistoryRepository 저장 로직을 구현해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/wellmeet/notification/consumer/NotificationSender.java
around lines 17 to 19, the send method is empty so notifications are never sent
or recorded; implement it to iterate over the provided enabled channel list, for
each channel call the appropriate service (e.g., WebPushService.send or other
channel-specific send methods), build and persist a NotificationHistory entity
via NotificationHistoryRepository.save (include payload, channel, status,
timestamp), and catch/log exceptions per-channel so one failure doesn't stop
others; ensure dependencies (WebPushService, NotificationHistoryRepository,
logger) are injected/used and update method signature behavior to
return/propagate errors if required by surrounding code.

@unifolio0 unifolio0 closed this Oct 2, 2025
@unifolio0 unifolio0 deleted the SCRUM-117 branch October 2, 2025 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants