diff --git a/.github/workflows/Dev_CD.yml b/.github/workflows/Dev_CD.yml new file mode 100644 index 0000000..774b713 --- /dev/null +++ b/.github/workflows/Dev_CD.yml @@ -0,0 +1,79 @@ +name: dev-cd + +on: + push: + branches: + - "develop" + - "SCRUM-117" + +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 diff --git a/.github/workflows/Dev_CI.yml b/.github/workflows/Dev_CI.yml new file mode 100644 index 0000000..d9d5192 --- /dev/null +++ b/.github/workflows/Dev_CI.yml @@ -0,0 +1,91 @@ +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 + + - name: Setting local-secret.yml + run: | + echo "${{ secrets.LOCAL_SECRET_YML }}" > ./src/main/resources/local-secret.yml + + - 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 diff --git a/.gitignore b/.gitignore index 5d7bd8d..8586c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out/ ### VS Code ### .vscode/ /src/main/resources/local-secret.yml +.serena/ diff --git a/build.gradle b/build.gradle index a4c59f0..28f67a6 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } diff --git a/scripts/dev/replace-new-version.sh b/scripts/dev/replace-new-version.sh new file mode 100644 index 0000000..8f3900c --- /dev/null +++ b/scripts/dev/replace-new-version.sh @@ -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" + +# 애플리케이션 로그 파일 설정 +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 & + +echo "애플리케이션이 백그라운드에서 실행되었습니다." +echo "로그 확인: tail -f $APP_LOG_FILE" +echo "=== 배포 완료 ===" diff --git a/src/main/java/com/wellmeet/config/KafkaConfig.java b/src/main/java/com/wellmeet/config/KafkaConfig.java new file mode 100644 index 0000000..ddc96ae --- /dev/null +++ b/src/main/java/com/wellmeet/config/KafkaConfig.java @@ -0,0 +1,49 @@ +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 consumerFactory() { + Map 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.TRUSTED_PACKAGES, "com.wellmeet.consumer.dto"); + props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, NotificationMessage.class); + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } +} diff --git a/src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java b/src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java new file mode 100644 index 0000000..9184440 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/NotificationChecker.java @@ -0,0 +1,19 @@ +package com.wellmeet.notification.consumer; + +import com.wellmeet.notification.repository.OwnerNotificationEnabledRepository; +import com.wellmeet.notification.repository.UserNotificationEnabledRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotificationChecker { + + private final OwnerNotificationEnabledRepository ownerNotificationEnabledRepository; + private final UserNotificationEnabledRepository userNotificationEnabledRepository; + + public List check(String recipient, String recipientType) { + return null; + } +} diff --git a/src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java b/src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java new file mode 100644 index 0000000..3cf1f67 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/NotificationConsumer.java @@ -0,0 +1,24 @@ +package com.wellmeet.notification.consumer; + +import com.wellmeet.notification.consumer.dto.NotificationMessage; +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 NotificationChecker notificationChecker; + private final NotificationSender notificationSender; + + @KafkaListener(topics = "notification", groupId = "notification-group") + public void consume(NotificationMessage message) { + List enabled = notificationChecker.check(message.getNotification().getRecipient(), + message.getNotification().getRecipientType()); + notificationSender.send(message, enabled); + } +} diff --git a/src/main/java/com/wellmeet/notification/consumer/NotificationSender.java b/src/main/java/com/wellmeet/notification/consumer/NotificationSender.java new file mode 100644 index 0000000..98b22e9 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/NotificationSender.java @@ -0,0 +1,20 @@ +package com.wellmeet.notification.consumer; + +import com.wellmeet.notification.consumer.dto.NotificationMessage; +import com.wellmeet.notification.repository.NotificationHistoryRepository; +import com.wellmeet.notification.webpush.WebPushService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotificationSender { + + private final WebPushService webPushService; + private final NotificationHistoryRepository notificationHistoryRepository; + + public void send(NotificationMessage payload, List enabled) { + + } +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java b/src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java new file mode 100644 index 0000000..2ac3a81 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/MessageHeader.java @@ -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; +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java new file mode 100644 index 0000000..559ba94 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationInfo.java @@ -0,0 +1,14 @@ +package com.wellmeet.notification.consumer.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class NotificationInfo { + + private String type; + private String category; + private String recipient; + private String recipientType; +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java new file mode 100644 index 0000000..acec9f3 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationMessage.java @@ -0,0 +1,13 @@ +package com.wellmeet.notification.consumer.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class NotificationMessage { + + private MessageHeader header; + private NotificationInfo notification; + private NotificationPayload payload; +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java new file mode 100644 index 0000000..54ea0b3 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationPayload.java @@ -0,0 +1,4 @@ +package com.wellmeet.notification.consumer.dto; + +public abstract class NotificationPayload { +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java new file mode 100644 index 0000000..0584827 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/NotificationType.java @@ -0,0 +1,15 @@ +package com.wellmeet.notification.consumer.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationType { + + RESERVATION_CREATED("reservation.created", "HIGH", "wellmeet-user-server"); + + private final String name; + private final String category; + private final String source; +} diff --git a/src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java b/src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java new file mode 100644 index 0000000..d4dc70e --- /dev/null +++ b/src/main/java/com/wellmeet/notification/consumer/dto/ReservationCreatedPayload.java @@ -0,0 +1,15 @@ +package com.wellmeet.notification.consumer.dto; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReservationCreatedPayload extends NotificationPayload { + + private Long reservationId; + private String customerName; + private LocalDateTime reservationTime; + private int partySize; +} diff --git a/src/main/java/com/wellmeet/notification/domain/NotificationHistory.java b/src/main/java/com/wellmeet/notification/domain/NotificationHistory.java new file mode 100644 index 0000000..4517219 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/domain/NotificationHistory.java @@ -0,0 +1,19 @@ +package com.wellmeet.notification.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NotificationHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; +} diff --git a/src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java b/src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java new file mode 100644 index 0000000..dfbe60f --- /dev/null +++ b/src/main/java/com/wellmeet/notification/domain/OwnerNotificationEnabled.java @@ -0,0 +1,22 @@ +package com.wellmeet.notification.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OwnerNotificationEnabled { + + @Id + private String ownerId; + private boolean reservationRequestWebPushEnabled; + private boolean reservationRequestEmailEnabled; + private boolean reservationCancelWebPushEnabled; + private boolean reservationCancelEmailEnabled; + private boolean promotionWebPushEnabled; + private boolean promotionEmailEnabled; +} diff --git a/src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java b/src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java new file mode 100644 index 0000000..797e4c1 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/domain/UserNotificationEnabled.java @@ -0,0 +1,24 @@ +package com.wellmeet.notification.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserNotificationEnabled { + + @Id + private String userId; + private boolean reservationConfirmWebPushEnabled; + private boolean reservationConfirmEmailEnabled; + private boolean reservationCancelWebPushEnabled; + private boolean reservationCancelEmailEnabled; + private boolean reminderWebPushEnabled; + private boolean reminderEmailEnabled; + private boolean promotionWebPushEnabled; + private boolean promotionEmailEnabled; +} diff --git a/src/main/java/com/wellmeet/notification/repository/NotificationHistoryRepository.java b/src/main/java/com/wellmeet/notification/repository/NotificationHistoryRepository.java new file mode 100644 index 0000000..d7681b2 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/repository/NotificationHistoryRepository.java @@ -0,0 +1,9 @@ +package com.wellmeet.notification.repository; + +import com.wellmeet.notification.domain.NotificationHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationHistoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java b/src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java new file mode 100644 index 0000000..4dd2c02 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/repository/OwnerNotificationEnabledRepository.java @@ -0,0 +1,9 @@ +package com.wellmeet.notification.repository; + +import com.wellmeet.notification.domain.OwnerNotificationEnabled; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OwnerNotificationEnabledRepository extends JpaRepository { +} diff --git a/src/main/java/com/wellmeet/notification/repository/UserNotificationEnabledRepository.java b/src/main/java/com/wellmeet/notification/repository/UserNotificationEnabledRepository.java new file mode 100644 index 0000000..6271ac5 --- /dev/null +++ b/src/main/java/com/wellmeet/notification/repository/UserNotificationEnabledRepository.java @@ -0,0 +1,9 @@ +package com.wellmeet.notification.repository; + +import com.wellmeet.notification.domain.UserNotificationEnabled; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserNotificationEnabledRepository extends JpaRepository { +} diff --git a/src/main/java/com/wellmeet/webpush/WebPushController.java b/src/main/java/com/wellmeet/notification/webpush/WebPushController.java similarity index 83% rename from src/main/java/com/wellmeet/webpush/WebPushController.java rename to src/main/java/com/wellmeet/notification/webpush/WebPushController.java index 17b2fd4..e2678be 100644 --- a/src/main/java/com/wellmeet/webpush/WebPushController.java +++ b/src/main/java/com/wellmeet/notification/webpush/WebPushController.java @@ -1,9 +1,9 @@ -package com.wellmeet.webpush; +package com.wellmeet.notification.webpush; -import com.wellmeet.webpush.dto.SubscribeRequest; -import com.wellmeet.webpush.dto.SubscribeResponse; -import com.wellmeet.webpush.dto.TestPushRequest; -import com.wellmeet.webpush.dto.UnsubscribeRequest; +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 lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/wellmeet/webpush/WebPushService.java b/src/main/java/com/wellmeet/notification/webpush/WebPushService.java similarity index 80% rename from src/main/java/com/wellmeet/webpush/WebPushService.java rename to src/main/java/com/wellmeet/notification/webpush/WebPushService.java index 8784251..616a2c2 100644 --- a/src/main/java/com/wellmeet/webpush/WebPushService.java +++ b/src/main/java/com/wellmeet/notification/webpush/WebPushService.java @@ -1,14 +1,14 @@ -package com.wellmeet.webpush; +package com.wellmeet.notification.webpush; import com.wellmeet.exception.ErrorCode; import com.wellmeet.exception.WellMeetNotificationException; -import com.wellmeet.webpush.domain.PushSubscription; -import com.wellmeet.webpush.dto.SubscribeRequest; -import com.wellmeet.webpush.dto.SubscribeResponse; -import com.wellmeet.webpush.dto.TestPushRequest; -import com.wellmeet.webpush.dto.UnsubscribeRequest; -import com.wellmeet.webpush.infrastructure.WebPushSender; -import com.wellmeet.webpush.repository.PushSubscriptionRepository; +import com.wellmeet.notification.webpush.domain.PushSubscription; +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 com.wellmeet.notification.webpush.infrastructure.WebPushSender; +import com.wellmeet.notification.webpush.repository.PushSubscriptionRepository; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -28,11 +28,13 @@ public SubscribeResponse subscribe(String userId, SubscribeRequest request) { Optional pushSubscription = existingSubscriptions.stream() .filter(subscription -> subscription.isSameEndpoint(request.endpoint())) .findAny(); + if (pushSubscription.isPresent()) { PushSubscription subscription = pushSubscription.get(); subscription.update(request.toDomain(userId)); return new SubscribeResponse(subscription); } + PushSubscription subscription = request.toDomain(userId); PushSubscription savedSubscription = pushSubscriptionRepository.save(subscription); return new SubscribeResponse(savedSubscription); diff --git a/src/main/java/com/wellmeet/webpush/domain/PushSubscription.java b/src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java similarity index 96% rename from src/main/java/com/wellmeet/webpush/domain/PushSubscription.java rename to src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java index 488a428..9520142 100644 --- a/src/main/java/com/wellmeet/webpush/domain/PushSubscription.java +++ b/src/main/java/com/wellmeet/notification/webpush/domain/PushSubscription.java @@ -1,4 +1,4 @@ -package com.wellmeet.webpush.domain; +package com.wellmeet.notification.webpush.domain; import com.wellmeet.common.domain.BaseEntity; import jakarta.persistence.Entity; diff --git a/src/main/java/com/wellmeet/webpush/dto/SubscribeRequest.java b/src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java similarity index 87% rename from src/main/java/com/wellmeet/webpush/dto/SubscribeRequest.java rename to src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java index b1fb8fa..3281cc9 100644 --- a/src/main/java/com/wellmeet/webpush/dto/SubscribeRequest.java +++ b/src/main/java/com/wellmeet/notification/webpush/dto/SubscribeRequest.java @@ -1,6 +1,6 @@ -package com.wellmeet.webpush.dto; +package com.wellmeet.notification.webpush.dto; -import com.wellmeet.webpush.domain.PushSubscription; +import com.wellmeet.notification.webpush.domain.PushSubscription; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/wellmeet/webpush/dto/SubscribeResponse.java b/src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java similarity index 80% rename from src/main/java/com/wellmeet/webpush/dto/SubscribeResponse.java rename to src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java index 832cc1d..0202344 100644 --- a/src/main/java/com/wellmeet/webpush/dto/SubscribeResponse.java +++ b/src/main/java/com/wellmeet/notification/webpush/dto/SubscribeResponse.java @@ -1,6 +1,6 @@ -package com.wellmeet.webpush.dto; +package com.wellmeet.notification.webpush.dto; -import com.wellmeet.webpush.domain.PushSubscription; +import com.wellmeet.notification.webpush.domain.PushSubscription; public record SubscribeResponse( Long subscriptionId, diff --git a/src/main/java/com/wellmeet/webpush/dto/TestPushRequest.java b/src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java similarity index 85% rename from src/main/java/com/wellmeet/webpush/dto/TestPushRequest.java rename to src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java index 133edf1..d4bc6fa 100644 --- a/src/main/java/com/wellmeet/webpush/dto/TestPushRequest.java +++ b/src/main/java/com/wellmeet/notification/webpush/dto/TestPushRequest.java @@ -1,4 +1,4 @@ -package com.wellmeet.webpush.dto; +package com.wellmeet.notification.webpush.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/wellmeet/webpush/dto/UnsubscribeRequest.java b/src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java similarity index 73% rename from src/main/java/com/wellmeet/webpush/dto/UnsubscribeRequest.java rename to src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java index eb8833e..a8cbbec 100644 --- a/src/main/java/com/wellmeet/webpush/dto/UnsubscribeRequest.java +++ b/src/main/java/com/wellmeet/notification/webpush/dto/UnsubscribeRequest.java @@ -1,4 +1,4 @@ -package com.wellmeet.webpush.dto; +package com.wellmeet.notification.webpush.dto; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/wellmeet/webpush/infrastructure/WebPushSender.java b/src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java similarity index 94% rename from src/main/java/com/wellmeet/webpush/infrastructure/WebPushSender.java rename to src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java index 2bee5e4..c95956f 100644 --- a/src/main/java/com/wellmeet/webpush/infrastructure/WebPushSender.java +++ b/src/main/java/com/wellmeet/notification/webpush/infrastructure/WebPushSender.java @@ -1,11 +1,11 @@ -package com.wellmeet.webpush.infrastructure; +package com.wellmeet.notification.webpush.infrastructure; import com.fasterxml.jackson.databind.ObjectMapper; import com.wellmeet.config.VapidConfig; import com.wellmeet.exception.ErrorCode; import com.wellmeet.exception.WellMeetNotificationException; -import com.wellmeet.webpush.domain.PushSubscription; -import com.wellmeet.webpush.dto.TestPushRequest; +import com.wellmeet.notification.webpush.domain.PushSubscription; +import com.wellmeet.notification.webpush.dto.TestPushRequest; import jakarta.annotation.PostConstruct; import java.io.IOException; import java.security.GeneralSecurityException; diff --git a/src/main/java/com/wellmeet/webpush/repository/PushSubscriptionRepository.java b/src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java similarity index 78% rename from src/main/java/com/wellmeet/webpush/repository/PushSubscriptionRepository.java rename to src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java index 5b60aa9..51e31bb 100644 --- a/src/main/java/com/wellmeet/webpush/repository/PushSubscriptionRepository.java +++ b/src/main/java/com/wellmeet/notification/webpush/repository/PushSubscriptionRepository.java @@ -1,6 +1,6 @@ -package com.wellmeet.webpush.repository; +package com.wellmeet.notification.webpush.repository; -import com.wellmeet.webpush.domain.PushSubscription; +import com.wellmeet.notification.webpush.domain.PushSubscription; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..a723c3c --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,52 @@ +spring: + application: + name: wellmeet-notification + config: + import: classpath:dev-secret.yml + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + username: ${secret.datasource.username} + password: ${secret.datasource.password} + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + sql: + init: + mode: always + schema-locations: classpath:schema.sql + platform: mysql + encoding: UTF-8 + mail: + host: smtp.gmail.com + port: 587 + username: ${secret.mail.username} + password: ${secret.mail.password} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + timeout: 5000 + connectiontimeout: 5000 + writetimeout: 5000 + kafka: + bootstrap-servers: ${secret.kafka.bootstrap-servers} + consumer: + group-id: ${secret.kafka.consumer.group-id} + auto-offset-reset: ${secret.kafka.consumer.auto-offset-reset} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + +cors: + origin: http://localhost:5173 + +vapid: + public-key: ${secret.vapid.public-key} + private-key: ${secret.vapid.private-key} + subject: ${secret.vapid.subject} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7a9b59c..6df18b7 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -20,6 +20,28 @@ spring: schema-locations: classpath:schema.sql platform: mysql encoding: UTF-8 + 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 + 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 cors: origin: http://localhost:5173 @@ -28,3 +50,5 @@ vapid: public-key: ${secret.vapid.public-key} private-key: ${secret.vapid.private-key} subject: ${secret.vapid.subject} +server: + port: 8082 diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 4b6fd43..7a70e64 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -20,6 +20,13 @@ spring: schema-locations: classpath:schema.sql platform: mysql encoding: UTF-8 + 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 cors: origin: http://localhost:5173 diff --git a/src/main/resources/dev-secret.yml b/src/main/resources/dev-secret.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/local-secret.yml b/src/main/resources/local-secret.yml new file mode 100644 index 0000000..b0ab58e --- /dev/null +++ b/src/main/resources/local-secret.yml @@ -0,0 +1,5 @@ +secret: + vapid: + public-key: BCjLRdYi3EapfKAjZlIONNWb7PgUGnSo9-HDedbcd02o0zwriW-93jZ35Ufqu_C4jFtcKuHCdsGA_3TYyAHXqxs + private-key: LjC3sekYvWtxxtN6R4qEEUunAI592EcpK8bc1Ggy8tU + subject: mailto:admin@wellmeet.com diff --git a/src/test/java/com/wellmeet/BaseControllerTest.java b/src/test/java/com/wellmeet/BaseControllerTest.java index caa2ebd..e669f4d 100644 --- a/src/test/java/com/wellmeet/BaseControllerTest.java +++ b/src/test/java/com/wellmeet/BaseControllerTest.java @@ -1,6 +1,6 @@ package com.wellmeet; -import com.wellmeet.webpush.repository.PushSubscriptionRepository; +import com.wellmeet.notification.webpush.repository.PushSubscriptionRepository; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; diff --git a/src/test/java/com/wellmeet/BaseServiceTest.java b/src/test/java/com/wellmeet/BaseServiceTest.java index d1813aa..dbe7792 100644 --- a/src/test/java/com/wellmeet/BaseServiceTest.java +++ b/src/test/java/com/wellmeet/BaseServiceTest.java @@ -1,6 +1,6 @@ package com.wellmeet; -import com.wellmeet.webpush.repository.PushSubscriptionRepository; +import com.wellmeet.notification.webpush.repository.PushSubscriptionRepository; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; diff --git a/src/test/java/com/wellmeet/webpush/WebPushControllerTest.java b/src/test/java/com/wellmeet/webpush/WebPushControllerTest.java index 8091f6c..69fbfef 100644 --- a/src/test/java/com/wellmeet/webpush/WebPushControllerTest.java +++ b/src/test/java/com/wellmeet/webpush/WebPushControllerTest.java @@ -5,11 +5,11 @@ import com.wellmeet.BaseControllerTest; import com.wellmeet.fixture.NullAndEmptyAndBlankSource; -import com.wellmeet.webpush.domain.PushSubscription; -import com.wellmeet.webpush.dto.SubscribeRequest; -import com.wellmeet.webpush.dto.SubscribeResponse; -import com.wellmeet.webpush.dto.TestPushRequest; -import com.wellmeet.webpush.dto.UnsubscribeRequest; +import com.wellmeet.notification.webpush.domain.PushSubscription; +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 io.restassured.http.ContentType; import java.util.Map; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/wellmeet/webpush/WebPushServiceTest.java b/src/test/java/com/wellmeet/webpush/WebPushServiceTest.java index 5f181d4..8a94b96 100644 --- a/src/test/java/com/wellmeet/webpush/WebPushServiceTest.java +++ b/src/test/java/com/wellmeet/webpush/WebPushServiceTest.java @@ -7,10 +7,11 @@ import com.wellmeet.BaseServiceTest; import com.wellmeet.exception.ErrorCode; import com.wellmeet.exception.WellMeetNotificationException; -import com.wellmeet.webpush.domain.PushSubscription; -import com.wellmeet.webpush.dto.SubscribeRequest; -import com.wellmeet.webpush.dto.SubscribeResponse; -import com.wellmeet.webpush.dto.UnsubscribeRequest; +import com.wellmeet.notification.webpush.WebPushService; +import com.wellmeet.notification.webpush.domain.PushSubscription; +import com.wellmeet.notification.webpush.dto.SubscribeRequest; +import com.wellmeet.notification.webpush.dto.SubscribeResponse; +import com.wellmeet.notification.webpush.dto.UnsubscribeRequest; import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Nested; @@ -42,7 +43,7 @@ class Subscribe { } @Test - void 동일한_유저_아이디와_엔드포인트로_구독하면_기존_구독을_반환한다() { + void 동일한_유저_아이디와_엔드포인트로_구독하면_기존_구독을_업데이트한다() { String userId = UUID.randomUUID().toString(); String endpoint = "endpoint"; PushSubscription pushSubscription = new PushSubscription(userId, endpoint, "p256dh", "auth"); @@ -55,8 +56,8 @@ class Subscribe { assertAll( () -> assertThat(subscription.getUserId()).isEqualTo(userId), () -> assertThat(subscription.getEndpoint()).isEqualTo(endpoint), - () -> assertThat(subscription.getP256dh()).isEqualTo(pushSubscription.getP256dh()), - () -> assertThat(subscription.getAuth()).isEqualTo(pushSubscription.getAuth()) + () -> assertThat(subscription.getP256dh()).isEqualTo(request.p256dh()), + () -> assertThat(subscription.getAuth()).isEqualTo(request.auth()) ); } }