Skip to content

Commit 22176da

Browse files
authored
SCRUM-117 식당 주인은 사용자가 예약을 하면 해당 알림을 받을 수 있다 (#6)
1 parent 2cd6970 commit 22176da

39 files changed

Lines changed: 682 additions & 48 deletions

.github/workflows/Dev_CD.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: dev-cd
2+
3+
on:
4+
push:
5+
branches:
6+
- "develop"
7+
8+
permissions:
9+
contents: read
10+
checks: write
11+
actions: read
12+
pull-requests: write
13+
14+
jobs:
15+
test:
16+
uses: ./.github/workflows/Dev_CI.yml
17+
secrets: inherit
18+
19+
build:
20+
needs: test
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout Code
25+
uses: actions/checkout@v4
26+
27+
- name: Setting dev-secret.yml
28+
run: |
29+
echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
30+
31+
- name: Set up JDK 21
32+
uses: actions/setup-java@v4
33+
with:
34+
distribution: 'temurin'
35+
java-version: '21'
36+
37+
- name: Make gradlew executable
38+
run: chmod +x gradlew
39+
40+
- name: bootJar with Gradle
41+
run: ./gradlew bootJar --info
42+
43+
- name: Change artifact file name
44+
run: mv build/libs/*.jar build/libs/app.jar
45+
46+
- name: Upload artifact file
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: app-artifact
50+
path: ./build/libs/app.jar
51+
if-no-files-found: error
52+
53+
- name: Upload deploy scripts
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: deploy-scripts
57+
path: ./scripts/dev/
58+
if-no-files-found: error
59+
60+
deploy:
61+
needs: build
62+
runs-on: dev
63+
64+
steps:
65+
- name: Download artifact file
66+
uses: actions/download-artifact@v4
67+
with:
68+
name: app-artifact
69+
path: ~/app
70+
71+
- name: Download deploy scripts
72+
uses: actions/download-artifact@v4
73+
with:
74+
name: deploy-scripts
75+
path: ~/app/scripts
76+
77+
- name: Replace application to latest
78+
run: sudo sh ~/app/scripts/replace-new-version.sh

.github/workflows/Dev_CI.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: dev-ci
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- develop
7+
workflow_call:
8+
9+
permissions:
10+
contents: read
11+
checks: write
12+
pull-requests: write
13+
14+
jobs:
15+
build-and-push:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 10
18+
env:
19+
TEST_REPORT: true
20+
21+
services:
22+
mysql:
23+
image: mysql:8.0
24+
env:
25+
MYSQL_ROOT_PASSWORD: ""
26+
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
27+
MYSQL_DATABASE: wellmeet_noti_test
28+
ports:
29+
- 3306:3306
30+
options: >-
31+
--health-cmd="mysqladmin ping"
32+
--health-interval=10s
33+
--health-timeout=5s
34+
--health-retries=3
35+
36+
zookeeper:
37+
image: confluentinc/cp-zookeeper:7.0.1
38+
env:
39+
ZOOKEEPER_CLIENT_PORT: 2181
40+
ZOOKEEPER_TICK_TIME: 2000
41+
ports:
42+
- 2181:2181
43+
options: >-
44+
--health-cmd="curl -f http://localhost:8080/commands/stat || exit 1"
45+
--health-interval=10s
46+
--health-timeout=10s
47+
--health-retries=5
48+
49+
kafka:
50+
image: confluentinc/cp-kafka:7.0.1
51+
env:
52+
KAFKA_BROKER_ID: 1
53+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
54+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
55+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
56+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
57+
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
58+
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
59+
KAFKA_AUTO_CREATE_TOPICS_ENABLE: true
60+
ports:
61+
- 9092:9092
62+
options: >-
63+
--health-cmd="timeout 10s bash -c 'until printf \"\" 2>>/dev/null >>/dev/tcp/localhost/9092; do sleep 1; done'"
64+
--health-interval=10s
65+
--health-timeout=10s
66+
--health-retries=10
67+
68+
steps:
69+
- uses: actions/checkout@v4
70+
with:
71+
ref: ${{ github.head_ref }}
72+
73+
- name: Setting local-secret.yml
74+
run: |
75+
echo "${{ secrets.LOCAL_SECRET_YML }}" > ./src/main/resources/local-secret.yml
76+
77+
- name: Set up JDK 21
78+
uses: actions/setup-java@v4
79+
with:
80+
java-version: '21'
81+
distribution: 'temurin'
82+
83+
- name: Setup Gradle
84+
uses: gradle/actions/setup-gradle@v4
85+
86+
- name: Grant Permission
87+
run: chmod +x ./gradlew
88+
89+
- name: Build With Gradle
90+
run: ./gradlew clean build -x test
91+
92+
- name: Run Tests With Gradle
93+
run: ./gradlew test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ out/
3636
### VS Code ###
3737
.vscode/
3838
/src/main/resources/local-secret.yml
39+
.serena/

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ dependencies {
2727
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2828
implementation 'org.springframework.boot:spring-boot-starter-validation'
2929
implementation 'org.springframework.boot:spring-boot-starter-web'
30+
implementation 'org.springframework.boot:spring-boot-starter-mail'
31+
implementation 'org.springframework.kafka:spring-kafka'
3032
implementation('nl.martijndwars:web-push:5.1.1') {
3133
exclude group: 'org.asynchttpclient', module: 'async-http-client'
3234
}

scripts/dev/replace-new-version.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
3+
PID=$(lsof -t -i:8080)
4+
5+
# 프로세스 종료
6+
if [ -z "$PID" ]; then
7+
echo "No process is using port 8080."
8+
else
9+
echo "Killing process with PID: $PID"
10+
kill -15 "$PID"
11+
12+
# 직전 명령(프로세스 종료 명령)이 정상 동작했는지 확인
13+
if [ $? -eq 0 ]; then
14+
echo "Process $PID terminated successfully."
15+
else
16+
echo "Failed to terminate process $PID."
17+
fi
18+
fi
19+
20+
JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1)
21+
22+
echo "JAR 파일 실행: $JAR_FILE"
23+
24+
# 애플리케이션 로그 파일 설정
25+
APP_LOG_DIR="/home/ubuntu/app/logs"
26+
APP_LOG_FILE="$APP_LOG_DIR/application-$(date +%Y%m%d-%H%M%S).log"
27+
28+
echo "애플리케이션 로그 파일: $APP_LOG_FILE"
29+
30+
sudo nohup java \
31+
-Dspring.profiles.active=dev \
32+
-Duser.timezone=Asia/Seoul \
33+
-Dserver.port=8080 \
34+
-Ddd.service=wellmeet-notification \
35+
-Ddd.env=dev \
36+
-jar "$JAR_FILE" > "$APP_LOG_FILE" 2>&1 &
37+
38+
echo "애플리케이션이 백그라운드에서 실행되었습니다."
39+
echo "로그 확인: tail -f $APP_LOG_FILE"
40+
echo "=== 배포 완료 ==="
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.wellmeet.config;
2+
3+
import com.wellmeet.notification.consumer.dto.NotificationMessage;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import org.apache.kafka.clients.consumer.ConsumerConfig;
7+
import org.apache.kafka.common.serialization.StringDeserializer;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.kafka.annotation.EnableKafka;
12+
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
13+
import org.springframework.kafka.core.ConsumerFactory;
14+
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
15+
import org.springframework.kafka.support.serializer.JsonDeserializer;
16+
17+
@EnableKafka
18+
@Configuration
19+
public class KafkaConfig {
20+
21+
@Value("${spring.kafka.bootstrap-servers}")
22+
private String bootstrapServers;
23+
24+
@Value("${spring.kafka.consumer.group-id}")
25+
private String groupId;
26+
27+
@Bean
28+
public ConsumerFactory<String, NotificationMessage> consumerFactory() {
29+
Map<String, Object> props = new HashMap<>();
30+
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
31+
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
32+
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
33+
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
34+
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
35+
36+
props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
37+
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class);
38+
return new DefaultKafkaConsumerFactory<>(props);
39+
}
40+
41+
@Bean
42+
public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory() {
43+
ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory =
44+
new ConcurrentKafkaListenerContainerFactory<>();
45+
factory.setConsumerFactory(consumerFactory());
46+
return factory;
47+
}
48+
}

src/main/java/com/wellmeet/exception/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public enum ErrorCode {
1616
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
1717
CORS_ORIGIN_EMPTY(HttpStatus.INTERNAL_SERVER_ERROR, "CORS Origin 은 적어도 한 개 있어야 합니다"),
1818
WEB_PUSH_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "웹 푸시 전송에 실패했습니다."),
19-
;
19+
SENDER_NOT_FOUND(HttpStatus.BAD_REQUEST, "알림을 발송할 수 없습니다.");
2020

2121
private final HttpStatus status;
2222
private final String message;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.wellmeet.notification;
2+
3+
import com.wellmeet.notification.consumer.dto.NotificationMessage;
4+
import com.wellmeet.notification.domain.NotificationChannel;
5+
6+
public interface Sender {
7+
8+
boolean isEnabled(NotificationChannel channel);
9+
10+
void send(NotificationMessage message);
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.wellmeet.notification.consumer;
2+
3+
import com.wellmeet.notification.consumer.dto.NotificationMessage;
4+
import com.wellmeet.notification.domain.NotificationEnabled;
5+
import com.wellmeet.notification.repository.NotificationEnabledRepository;
6+
import java.util.List;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.kafka.annotation.KafkaListener;
10+
import org.springframework.stereotype.Service;
11+
12+
@Slf4j
13+
@Service
14+
@RequiredArgsConstructor
15+
public class NotificationConsumer {
16+
17+
private final NotificationEnabledRepository notificationEnabledRepository;
18+
private final NotificationSender notificationSender;
19+
20+
@KafkaListener(topics = "notification", groupId = "notification-group")
21+
public void consume(NotificationMessage message) {
22+
List<NotificationEnabled> enables = notificationEnabledRepository.findByUserIdAndType(
23+
message.getNotification().getRecipient(),
24+
message.getNotification().getType()
25+
);
26+
notificationSender.send(message, enables);
27+
}
28+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.wellmeet.notification.consumer;
2+
3+
import com.wellmeet.exception.ErrorCode;
4+
import com.wellmeet.exception.WellMeetNotificationException;
5+
import com.wellmeet.notification.Sender;
6+
import com.wellmeet.notification.consumer.dto.NotificationMessage;
7+
import com.wellmeet.notification.domain.NotificationEnabled;
8+
import com.wellmeet.notification.domain.NotificationHistory;
9+
import com.wellmeet.notification.repository.NotificationHistoryRepository;
10+
import java.util.List;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.stereotype.Component;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class NotificationSender {
17+
18+
private final List<Sender> senders;
19+
private final NotificationHistoryRepository notificationHistoryRepository;
20+
21+
public void send(NotificationMessage message, List<NotificationEnabled> enables) {
22+
NotificationHistory history = new NotificationHistory(message.getRecipient(),
23+
message.getPayload().toString());
24+
for (NotificationEnabled enabled : enables) {
25+
notificationHistoryRepository.save(history);
26+
Sender sender = senders.stream()
27+
.filter(low -> low.isEnabled(enabled.getChannel()))
28+
.findFirst()
29+
.orElseThrow(() -> new WellMeetNotificationException(ErrorCode.SENDER_NOT_FOUND));
30+
sender.send(message);
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)