Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e5dadde
feat: Kafka 세팅
unifolio0 Aug 28, 2025
c9e5ccd
feat: CI 세팅
unifolio0 Aug 28, 2025
59fd16e
feat: CI 세팅
unifolio0 Aug 28, 2025
16612bf
feat: CI 세팅
unifolio0 Aug 28, 2025
6844014
feat: CI 세팅
unifolio0 Aug 28, 2025
686a9a0
feat: CI 세팅
unifolio0 Aug 28, 2025
0d3eb61
feat: CI 세팅
unifolio0 Aug 28, 2025
ba9b9c3
feat: CI 세팅
unifolio0 Aug 28, 2025
18b83b3
feat: CI 세팅
unifolio0 Aug 28, 2025
f033261
feat: CD 세팅
unifolio0 Aug 28, 2025
6c0b07c
feat: CD 세팅
unifolio0 Aug 28, 2025
4d271b4
feat: CD 세팅
unifolio0 Aug 28, 2025
ddbcc1b
feat: CD 세팅
unifolio0 Aug 28, 2025
e162c4e
chore: 패키지 이동
unifolio0 Sep 23, 2025
743dc12
refactor: 컨슈머 로직 재구성
unifolio0 Oct 2, 2025
818e796
refactor: NotificationEnabled 테이블 수정
unifolio0 Oct 2, 2025
bab4543
refactor: 알림 발송 로직 수정
unifolio0 Oct 2, 2025
a47767e
refactor: 알림 발송 로직 구현
unifolio0 Oct 2, 2025
95c2db1
chore: cd 스크립트 수정
unifolio0 Oct 4, 2025
0bc8e9e
refactor: 변수명 수정
unifolio0 Oct 4, 2025
c33a28e
refactor: sql 수정
unifolio0 Oct 4, 2025
8e7b480
refactor: 알림 발송 로직 순서 변경
unifolio0 Oct 4, 2025
a3f42c1
refactor: 접근 제한자 제거
unifolio0 Oct 4, 2025
21282c0
refactor: 데이터베이스 수정
unifolio0 Oct 4, 2025
8c23522
refactor: 스키마 수정
unifolio0 Oct 4, 2025
8a63bf9
refactor: 로깅 제거
unifolio0 Oct 4, 2025
6ad2375
refactor: 알림 내역 저장
unifolio0 Oct 4, 2025
ef393b7
refactor: 알림 발송 예외 추가
unifolio0 Oct 4, 2025
dc5a993
refactor: 불필요한 설정 제거
unifolio0 Oct 4, 2025
785b672
refactor: 설정 수정
unifolio0 Oct 4, 2025
4a924e9
refactor: 설정 제거
unifolio0 Oct 4, 2025
bd3cae6
refactor: deserializer 설정 수정
unifolio0 Oct 4, 2025
7ec23bc
refactor: ci 수정
unifolio0 Oct 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/Dev_CD.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: dev-cd

on:
push:
branches:
- "develop"

permissions:
contents: read
checks: write
actions: read
pull-requests: write

jobs:
test:
uses: ./.github/workflows/Dev_CI.yml
secrets: inherit

build:
needs: test
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

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

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

- name: Make gradlew executable
run: chmod +x gradlew

- name: bootJar with Gradle
run: ./gradlew bootJar --info

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

- 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 deploy scripts
uses: actions/upload-artifact@v4
with:
name: deploy-scripts
path: ./scripts/dev/
if-no-files-found: error

deploy:
needs: build
runs-on: dev

steps:
- name: Download artifact file
uses: actions/download-artifact@v4
with:
name: app-artifact
path: ~/app

- name: Download deploy scripts
uses: actions/download-artifact@v4
with:
name: deploy-scripts
path: ~/app/scripts

- name: Replace application to latest
run: sudo sh ~/app/scripts/replace-new-version.sh
93 changes: 93 additions & 0 deletions .github/workflows/Dev_CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: dev-ci

on:
pull_request:
branches:
- develop
workflow_call:

permissions:
contents: read
checks: write
pull-requests: write

jobs:
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
TEST_REPORT: true

services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: wellmeet_noti_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3

zookeeper:
image: confluentinc/cp-zookeeper:7.0.1
env:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- 2181:2181
options: >-
--health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
--health-interval=10s
--health-timeout=10s
--health-retries=5

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_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: true
ports:
- 9092:9092
options: >-
--health-cmd="timeout 10s bash -c 'until printf \"\" 2>>/dev/null >>/dev/tcp/localhost/9092; do sleep 1; done'"
--health-interval=10s
--health-timeout=10s
--health-retries=10

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}

- name: Setting local-secret.yml
run: |
echo "${{ secrets.LOCAL_SECRET_YML }}" > ./src/main/resources/local-secret.yml

Comment on lines +73 to +76
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

포크 PR에서 시크릿 미전달로 인한 CI 실패 가능성

pull_request 이벤트로 실행될 때 외부 포크에서 올라온 PR에는 GitHub 시크릿이 전달되지 않습니다. 이 단계가 그대로 실행되면 빈 local-secret.yml만 생성되고 이후 빌드/테스트가 비정상 종료되어 외부 기여자의 CI가 항상 실패하게 됩니다. 포크 PR을 고려해 시크릿이 비어 있을 때는 단계를 건너뛰거나, 대체 설정(예: 더미 설정·if 조건으로 전체 잡 스킵)을 두는 식의 방어 로직을 추가해 주세요.

🤖 Prompt for AI Agents
.github/workflows/Dev_CI.yml lines 71-74: the workflow writes
secrets.LOCAL_SECRET_YML unconditionally which causes empty local-secret.yml and
CI failures for forked PRs (secrets are not available). Fix by guarding the step
with an if conditional that only runs when the secret is present (e.g. if: ${{
secrets.LOCAL_SECRET_YML != '' }}) or replace the step with a small script that
checks if the secret is set and either writes the real secret or writes a
harmless dummy/local-default file and logs that a fallback was used; ensure the
job downstream expects the dummy or skip downstream jobs with an appropriate if
if you choose to skip generation.

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

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Grant Permission
run: chmod +x ./gradlew

- name: Build With Gradle
run: ./gradlew clean build -x test

- name: Run Tests With Gradle
run: ./gradlew test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ out/
### VS Code ###
.vscode/
/src/main/resources/local-secret.yml
.serena/
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.kafka:spring-kafka'
implementation('nl.martijndwars:web-push:5.1.1') {
exclude group: 'org.asynchttpclient', module: 'async-http-client'
}
Expand Down
40 changes: 40 additions & 0 deletions scripts/dev/replace-new-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

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

JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)

echo "JAR 파일 실행: $JAR_FILE"

Comment on lines +20 to +23
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

JAR 파일 존재 여부를 먼저 확인해야 합니다.

ls /home/ubuntu/app/*.jar가 빈 결과를 반환하면 JAR_FILE이 비어 있는 상태로 java -jar가 실행되어 배포가 실패합니다. JAR 미존재 시 명확히 종료하도록 가드가 필요합니다.

다음과 같이 방어 로직을 추가해 주세요:

 JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)

 echo "JAR 파일 실행: $JAR_FILE"
+
+if [ -z "$JAR_FILE" ]; then
+  echo "실행 가능한 JAR 파일을 찾을 수 없습니다."
+  exit 1
+fi
🤖 Prompt for AI Agents
In scripts/dev/replace-new-version.sh around lines 20 to 23, the script assigns
JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) but does not verify the
result; add a guard that checks whether JAR_FILE is non-empty and points to an
existing file, and if not, print a clear error message to stderr and exit with a
non-zero status; implement this by testing [ -z "$JAR_FILE" ] || [ ! -f
"$JAR_FILE" ] and then echo the error and exit 1 before attempting to run java
-jar.

# 애플리케이션 로그 파일 설정
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"

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 &
Comment on lines +30 to +36

Choose a reason for hiding this comment

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

high

Java 애플리케이션을 실행하기 위해 sudo를 사용하는 것은 보안상 위험합니다. 애플리케이션은 최소한의 권한으로 실행되어야 합니다. 8080 포트는 1024보다 크므로 루트 권한이 필요하지 않습니다. 특별한 이유가 없다면 sudo 없이 ubuntu와 같은 일반 사용자 권한으로 애플리케이션을 실행하는 것이 좋습니다.

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 &
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 &

Comment on lines +24 to +36
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

로그 디렉터리를 먼저 생성하지 않으면 재실행이 실패합니다.

APP_LOG_DIR가 존재하지 않는 상태에서 리디렉션이 수행되면 nohup 실행이 바로 실패합니다. 로그 디렉터리를 선행 생성해 주세요.

아래와 같이 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"

+sudo mkdir -p "$APP_LOG_DIR"
+
 sudo nohup java \
📝 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"
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 &
# 애플리케이션 로그 파일 설정
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"
sudo mkdir -p "$APP_LOG_DIR"
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 &
🧰 Tools
🪛 Shellcheck (0.11.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 24 to 36, the script
redirects Java output to a file inside APP_LOG_DIR but never ensures that
APP_LOG_DIR exists, causing nohup to fail if the directory is missing; before
starting the Java process, create the log directory (e.g., run mkdir -p
"$APP_LOG_DIR") and optionally set ownership/permissions as needed so the
redirection can succeed, then proceed with the nohup java command to write to
the log file.


echo "애플리케이션이 백그라운드에서 실행되었습니다."
echo "로그 확인: tail -f $APP_LOG_FILE"
echo "=== 배포 완료 ==="
48 changes: 48 additions & 0 deletions src/main/java/com/wellmeet/config/KafkaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.wellmeet.config;

import com.wellmeet.notification.consumer.dto.NotificationMessage;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;

@EnableKafka
@Configuration
public class KafkaConfig {

@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;

@Value("${spring.kafka.consumer.group-id}")
private String groupId;

@Bean
public ConsumerFactory<String, NotificationMessage> 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, JsonDeserializer.class);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
return new DefaultKafkaConsumerFactory<>(props);
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/wellmeet/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public enum ErrorCode {
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
CORS_ORIGIN_EMPTY(HttpStatus.INTERNAL_SERVER_ERROR, "CORS Origin 은 적어도 한 개 있어야 합니다"),
WEB_PUSH_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "웹 푸시 전송에 실패했습니다."),
;
SENDER_NOT_FOUND(HttpStatus.BAD_REQUEST, "알림을 발송할 수 없습니다.");

private final HttpStatus status;
private final String message;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/wellmeet/notification/Sender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wellmeet.notification;

import com.wellmeet.notification.consumer.dto.NotificationMessage;
import com.wellmeet.notification.domain.NotificationChannel;

public interface Sender {

boolean isEnabled(NotificationChannel channel);

void send(NotificationMessage message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.wellmeet.notification.consumer;

import com.wellmeet.notification.consumer.dto.NotificationMessage;
import com.wellmeet.notification.domain.NotificationEnabled;
import com.wellmeet.notification.repository.NotificationEnabledRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationConsumer {

private final NotificationEnabledRepository notificationEnabledRepository;
private final NotificationSender notificationSender;

@KafkaListener(topics = "notification", groupId = "notification-group")

Choose a reason for hiding this comment

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

medium

Kafka groupId가 하드코딩되어 있습니다. 설정 관리를 중앙화하기 위해 application.yml 파일의 값을 프로퍼티 플레이스홀더(${spring.kafka.consumer.group-id})를 통해 주입받는 것이 좋습니다.

Suggested change
@KafkaListener(topics = "notification", groupId = "notification-group")
@KafkaListener(topics = "notification", groupId = "${spring.kafka.consumer.group-id}")

public void consume(NotificationMessage message) {
List<NotificationEnabled> enables = notificationEnabledRepository.findByUserIdAndType(
message.getNotification().getRecipient(),
message.getNotification().getType()
);
notificationSender.send(message, enables);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.wellmeet.notification.consumer;

import com.wellmeet.exception.ErrorCode;
import com.wellmeet.exception.WellMeetNotificationException;
import com.wellmeet.notification.Sender;
import com.wellmeet.notification.consumer.dto.NotificationMessage;
import com.wellmeet.notification.domain.NotificationEnabled;
import com.wellmeet.notification.domain.NotificationHistory;
import com.wellmeet.notification.repository.NotificationHistoryRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class NotificationSender {

private final List<Sender> senders;
private final NotificationHistoryRepository notificationHistoryRepository;

public void send(NotificationMessage message, List<NotificationEnabled> enables) {
NotificationHistory history = new NotificationHistory(message.getRecipient(),
message.getPayload().toString());
for (NotificationEnabled enabled : enables) {
notificationHistoryRepository.save(history);
Sender sender = senders.stream()
.filter(low -> low.isEnabled(enabled.getChannel()))
.findFirst()
.orElseThrow(() -> new WellMeetNotificationException(ErrorCode.SENDER_NOT_FOUND));
sender.send(message);
}
}
Comment on lines 21 to 32

Choose a reason for hiding this comment

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

high

send 메서드에 몇 가지 개선이 필요합니다:

  1. notificationHistoryRepository.save()가 루프 내에 있어 사용자의 각 활성화된 알림 채널에 대해 중복된 히스토리 레코드를 생성합니다. 알림 메시지당 하나의 히스토리만 기록하는 것이 의도라면 이 코드는 루프 밖으로 이동해야 합니다.
  2. .orElseThrow()를 인수 없이 사용하면 NoSuchElementException이 발생하여 Kafka 컨슈머가 중단되고 동일한 메시지를 계속 재시도할 수 있습니다. 해당 채널에 대한 Sender가 없는 경우를 우아하게 처리하는 것이 좋습니다(예: 경고 로깅).
  3. filter의 람다 파라미터 이름 low는 의미가 명확하지 않습니다. sender와 같이 더 설명적인 이름을 사용하는 것이 좋습니다.
    public void send(NotificationMessage message, List<NotificationEnabled> enables) {
        if (enables.isEmpty()) {
            return;
        }
        notificationHistoryRepository.save(new NotificationHistory(message.getNotification().getRecipient()));
        for (NotificationEnabled enabled : enables) {
            senders.stream()
                    .filter(sender -> sender.isEnabled(enabled.getChannel()))
                    .findFirst()
                    .ifPresent(sender -> sender.send(message));
        }
    }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.wellmeet.notification.consumer.dto;

import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MessageHeader {

private String messageId;
private LocalDateTime timestamp;
private String source;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.wellmeet.notification.consumer.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class NotificationInfo {

private NotificationType type;
private String recipient;
}
Loading
Loading