diff --git a/build.gradle b/build.gradle index cab8cd0..dcebcfe 100644 --- a/build.gradle +++ b/build.gradle @@ -25,11 +25,30 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' + + //lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + //test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + + //mysql + runtimeOnly 'com.mysql:mysql-connector-j' + + //kafka + implementation 'org.springframework.kafka:spring-kafka' + + //Mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' } tasks.named('test') { diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..d56b91e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + + zookeeper: + image: wurstmeister/zookeeper:latest + ports: + - "2181:2181" + + kafka: + image: wurstmeister/kafka:latest + ports: + - "9092:9092" + expose: + - "9093" + environment: + KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + volumes: + - /var/run/docker.sock:/var/run/docker.sock diff --git a/src/main/java/com/server/kubacknotification/KubackNotificationApplication.java b/src/main/java/com/server/kubacknotification/KubackNotificationApplication.java index a845c10..be1a77e 100644 --- a/src/main/java/com/server/kubacknotification/KubackNotificationApplication.java +++ b/src/main/java/com/server/kubacknotification/KubackNotificationApplication.java @@ -2,8 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication +@EnableJpaRepositories("com.server.kubacknotification.infra.jpa.repositoryImpl") public class KubackNotificationApplication { public static void main(String[] args) { diff --git a/src/main/java/com/server/kubacknotification/api/AController.java b/src/main/java/com/server/kubacknotification/api/AController.java deleted file mode 100644 index 08ae1b9..0000000 --- a/src/main/java/com/server/kubacknotification/api/AController.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.server.kubacknotification.api; - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class AController { -} diff --git a/src/main/java/com/server/kubacknotification/api/NotificationController.java b/src/main/java/com/server/kubacknotification/api/NotificationController.java new file mode 100644 index 0000000..20ffb5b --- /dev/null +++ b/src/main/java/com/server/kubacknotification/api/NotificationController.java @@ -0,0 +1,19 @@ +package com.server.kubacknotification.api; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import com.server.kubacknotification.application.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class NotificationController { + private final NotificationService notificationService; + + @PostMapping("/api/notifications") + public void createTicketOpenNotifications(@RequestBody TicketOpenMessage ticketOpenMessage) { + notificationService.createTicketOpenNotification(ticketOpenMessage); + } +} \ No newline at end of file diff --git a/src/main/java/com/server/kubacknotification/application/dto/Request.java b/src/main/java/com/server/kubacknotification/application/dto/Request.java deleted file mode 100644 index bf5f64f..0000000 --- a/src/main/java/com/server/kubacknotification/application/dto/Request.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.server.kubacknotification.application.dto; - -public class Request { -} diff --git a/src/main/java/com/server/kubacknotification/application/dto/Response.java b/src/main/java/com/server/kubacknotification/application/dto/Response.java deleted file mode 100644 index eff51fa..0000000 --- a/src/main/java/com/server/kubacknotification/application/dto/Response.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.server.kubacknotification.application.dto; - -public class Response { -} diff --git a/src/main/java/com/server/kubacknotification/application/dto/request/TicketOpenMessage.java b/src/main/java/com/server/kubacknotification/application/dto/request/TicketOpenMessage.java new file mode 100644 index 0000000..396450f --- /dev/null +++ b/src/main/java/com/server/kubacknotification/application/dto/request/TicketOpenMessage.java @@ -0,0 +1,16 @@ +package com.server.kubacknotification.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class TicketOpenMessage { + private String title; // 공연 제목 + private String description; // 공연 설명 + private String email; + private Long userId; + private String userName; +} \ No newline at end of file diff --git a/src/main/java/com/server/kubacknotification/application/service/AService.java b/src/main/java/com/server/kubacknotification/application/service/AService.java deleted file mode 100644 index b9ef490..0000000 --- a/src/main/java/com/server/kubacknotification/application/service/AService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.server.kubacknotification.application.service; - -import org.springframework.stereotype.Service; - -@Service -public class AService { -} diff --git a/src/main/java/com/server/kubacknotification/application/service/EmailService.java b/src/main/java/com/server/kubacknotification/application/service/EmailService.java new file mode 100644 index 0000000..e04ba9d --- /dev/null +++ b/src/main/java/com/server/kubacknotification/application/service/EmailService.java @@ -0,0 +1,58 @@ +package com.server.kubacknotification.application.service; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import com.server.kubacknotification.global.common.PaymentMessage; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class EmailService { + + private final JavaMailSender javaMailSender; + private final SpringTemplateEngine templateEngine; + + // Kafka로부터 PaymentMessage를 수신하고 이메일 발송 + @KafkaListener(topics = "ticketOpen", groupId = "group2", containerFactory = "ticketOpenMessageKafkaListenerContainerFactory") + public void consumeNoticeMessage(TicketOpenMessage ticketOpenMessage) { + sendTicketOpenEmail(ticketOpenMessage); + } + + @Async + public void sendTicketOpenEmail(TicketOpenMessage ticketOpenMessage) { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); + mimeMessageHelper.setTo(ticketOpenMessage.getEmail()); // 수신자 이메일을 TicketOpenMessage 의 email 필드로 설정 + mimeMessageHelper.setSubject("티켓 오픈 안내"); // 이메일 제목 + + // HTML 콘텐츠 생성 + String htmlContent = setTicketOpenContext(ticketOpenMessage); + mimeMessageHelper.setText(htmlContent, true); // HTML 여부 true 설정 + + javaMailSender.send(mimeMessage); + log.info("Succeeded to send Email to: {}", ticketOpenMessage.getUserId()); + } catch (Exception e) { + log.error("Failed to send Email to: {}", ticketOpenMessage.getUserId(), e); + throw new RuntimeException(e); + } + } + + // HTML 템플릿에 TicketOpenMessage 데이터 주입 + public String setTicketOpenContext(TicketOpenMessage ticketOpenMessage) { + Context context = new Context(); + context.setVariable("ticketOpenMessage", ticketOpenMessage); + return templateEngine.process("ticketOpen", context); // "ticketOpen.html" 템플릿 사용 + } +} diff --git a/src/main/java/com/server/kubacknotification/application/service/NotificationService.java b/src/main/java/com/server/kubacknotification/application/service/NotificationService.java new file mode 100644 index 0000000..abca5d7 --- /dev/null +++ b/src/main/java/com/server/kubacknotification/application/service/NotificationService.java @@ -0,0 +1,17 @@ +package com.server.kubacknotification.application.service; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class NotificationService { + private final KafkaTemplate kafkaTemplate; + private static final String TOPIC = "ticketOpen"; + + public void createTicketOpenNotification(TicketOpenMessage ticketOpenMessage) { + kafkaTemplate.send(TOPIC, ticketOpenMessage); + } +} \ No newline at end of file diff --git a/src/main/java/com/server/kubacknotification/domain/entity/A.java b/src/main/java/com/server/kubacknotification/domain/entity/Notification.java similarity index 50% rename from src/main/java/com/server/kubacknotification/domain/entity/A.java rename to src/main/java/com/server/kubacknotification/domain/entity/Notification.java index 7ae2e73..1243261 100644 --- a/src/main/java/com/server/kubacknotification/domain/entity/A.java +++ b/src/main/java/com/server/kubacknotification/domain/entity/Notification.java @@ -8,13 +8,17 @@ @Getter @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class A { +public class Notification { private Long id; - private String name; + private Long userId; + private String title; + private String content; - public static A toDomain( + public static Notification toDomain( Long id, - String name) { - return new A(id, name); + Long userId, + String title, + String content) { + return new Notification(id, userId, title, content); } } diff --git a/src/main/java/com/server/kubacknotification/domain/repository/ARepository.java b/src/main/java/com/server/kubacknotification/domain/repository/ARepository.java deleted file mode 100644 index d4ddb0f..0000000 --- a/src/main/java/com/server/kubacknotification/domain/repository/ARepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.server.kubacknotification.domain.repository; - -import com.server.kubacknotification.domain.entity.A; -import org.springframework.stereotype.Repository; - -@Repository -public interface ARepository { - A findById(Long id); -} diff --git a/src/main/java/com/server/kubacknotification/domain/repository/NotificationRepository.java b/src/main/java/com/server/kubacknotification/domain/repository/NotificationRepository.java new file mode 100644 index 0000000..9345041 --- /dev/null +++ b/src/main/java/com/server/kubacknotification/domain/repository/NotificationRepository.java @@ -0,0 +1,9 @@ +package com.server.kubacknotification.domain.repository; + +import com.server.kubacknotification.domain.entity.Notification; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationRepository { + Notification findById(Long id); +} diff --git a/src/main/java/com/server/kubacknotification/global/common/PaymentMessage.java b/src/main/java/com/server/kubacknotification/global/common/PaymentMessage.java new file mode 100644 index 0000000..15783a9 --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/common/PaymentMessage.java @@ -0,0 +1,50 @@ +package com.server.kubacknotification.global.common; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "결제 메시지") +public class PaymentMessage { + + @Schema(description = "유저 ID", example = "1") + private Long userId; + + @Schema(description = "유저 이메일", example = "@gmail.com") + private String email; + + @Schema(description = "유저 이름", example = "문희상") + private String userName; + + @Schema(description = "좌석 번호", example = "A12") + private String seatNumber; + + @Schema(description = "좌석 등급", example = "VIP") + private String seatGrade; + + @Schema(description = "결제 금액", example = "15000") + private String payment; + + @Schema(description = "결제 날짜", example = "2024-10-06T14:00:00") + private LocalDateTime payDate; + + @Schema(description = "원래 가격", example = "20000") + private String originalPrice; + + @Schema(description = "할인 금액", example = "5000") + private String salePrice; + + @Schema(description = "최종 결제 금액", example = "15000") + private String finalPrice; +} + + + diff --git a/src/main/java/com/server/kubacknotification/global/config/EmailConfig.java b/src/main/java/com/server/kubacknotification/global/config/EmailConfig.java new file mode 100644 index 0000000..a3f5df7 --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/config/EmailConfig.java @@ -0,0 +1,69 @@ +package com.server.kubacknotification.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +@RequiredArgsConstructor +public class EmailConfig { + + private static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; + private static final String MAIL_DEBUG = "mail.smtp.debug"; + private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout"; + private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable"; + + // SMTP 서버 + @Value("${spring.mail.host}") + private String host; + + // 계정 + @Value("${spring.mail.username}") + private String username; + + // 비밀번호 + @Value("${spring.mail.password}") + private String password; + + // 포트번호 + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.debug}") + private boolean debug; + + @Value("${spring.mail.properties.mail.smtp.connectiontimeout}") + private int connectionTimeout; + + @Value("${spring.mail.properties.mail.starttls.enable}") + private boolean startTlsEnable; + + @Bean + public JavaMailSender javaMailService() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + javaMailSender.setHost(host); + javaMailSender.setUsername(username); + javaMailSender.setPassword(password); + javaMailSender.setPort(port); + + Properties properties = javaMailSender.getJavaMailProperties(); + properties.put(MAIL_SMTP_AUTH, auth); + properties.put(MAIL_DEBUG, debug); + properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout); + properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable); + + javaMailSender.setJavaMailProperties(properties); + javaMailSender.setDefaultEncoding("UTF-8"); + + return javaMailSender; + } + +} diff --git a/src/main/java/com/server/kubacknotification/global/config/KafkaConsumerConfig.java b/src/main/java/com/server/kubacknotification/global/config/KafkaConsumerConfig.java new file mode 100644 index 0000000..b0c2c4a --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/config/KafkaConsumerConfig.java @@ -0,0 +1,64 @@ +package com.server.kubacknotification.global.config; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import com.server.kubacknotification.global.common.PaymentMessage; +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.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConsumerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ConsumerFactory paymentMessageConsumerFactory() { + Map config = new HashMap<>(); + config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + config.put(ConsumerConfig.GROUP_ID_CONFIG, "payment"); + config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); + config.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); + config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, PaymentMessage.class.getName()); + + return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(), new JsonDeserializer<>(PaymentMessage.class)); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory paymentMessageKafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(paymentMessageConsumerFactory()); + return factory; + } + + @Bean + public ConsumerFactory ticketOpenMessageConsumerFactory() { + Map config = new HashMap<>(); + config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + config.put(ConsumerConfig.GROUP_ID_CONFIG, "ticketOpen"); + config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); + config.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); + config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, TicketOpenMessage.class.getName()); + + return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(), new JsonDeserializer<>(TicketOpenMessage.class)); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory ticketOpenMessageKafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(ticketOpenMessageConsumerFactory()); + return factory; + } +} + diff --git a/src/main/java/com/server/kubacknotification/global/config/KafkaProducerConfig.java b/src/main/java/com/server/kubacknotification/global/config/KafkaProducerConfig.java new file mode 100644 index 0000000..92e7cbf --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/config/KafkaProducerConfig.java @@ -0,0 +1,53 @@ +package com.server.kubacknotification.global.config; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import com.server.kubacknotification.global.common.PaymentMessage; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaProducerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory paymentMessageProducerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); // 메시지를 JSON으로 직렬화 + + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(paymentMessageProducerFactory()); + } + + @Bean + public ProducerFactory ticketOpenMessageProducerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); // 메시지를 JSON으로 직렬화 + + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate ticketOpenkafkaTemplate() { + return new KafkaTemplate<>(ticketOpenMessageProducerFactory()); + } +} diff --git a/src/main/java/com/server/kubacknotification/global/config/P6SpyFormatter.java b/src/main/java/com/server/kubacknotification/global/config/P6SpyFormatter.java new file mode 100644 index 0000000..2c60598 --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/config/P6SpyFormatter.java @@ -0,0 +1,38 @@ +package com.server.kubacknotification.global.config; + +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.spy.P6SpyOptions; +import com.p6spy.engine.spy.appender.MessageFormattingStrategy; +import jakarta.annotation.PostConstruct; +import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.springframework.context.annotation.Configuration; + +import java.util.Locale; + +@Configuration +public class P6SpyFormatter implements MessageFormattingStrategy { + + @PostConstruct + public void setLogMessageFormat() { + P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName()); + } + + @Override + public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) { + sql = formatSql(category, sql); + return String.format("[%s] | %d ms | %s", category, elapsed, formatSql(category, sql)); + } + + private String formatSql(String category, String sql) { + if (sql != null && !sql.trim().isEmpty() && Category.STATEMENT.getName().equals(category)) { + String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT); + if (trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment")) { + sql = FormatStyle.DDL.getFormatter().format(sql); + } else { + sql = FormatStyle.BASIC.getFormatter().format(sql); + } + return sql; + } + return sql; + } +} diff --git a/src/main/java/com/server/kubacknotification/global/config/WebConfig.java b/src/main/java/com/server/kubacknotification/global/config/WebConfig.java new file mode 100644 index 0000000..db4030d --- /dev/null +++ b/src/main/java/com/server/kubacknotification/global/config/WebConfig.java @@ -0,0 +1,17 @@ +package com.server.kubacknotification.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name()); + } +} \ No newline at end of file diff --git a/src/main/java/com/server/kubacknotification/infra/jpa/entity/AEntity.java b/src/main/java/com/server/kubacknotification/infra/jpa/entity/NotificationEntity.java similarity index 59% rename from src/main/java/com/server/kubacknotification/infra/jpa/entity/AEntity.java rename to src/main/java/com/server/kubacknotification/infra/jpa/entity/NotificationEntity.java index 7fb7182..bf5766c 100644 --- a/src/main/java/com/server/kubacknotification/infra/jpa/entity/AEntity.java +++ b/src/main/java/com/server/kubacknotification/infra/jpa/entity/NotificationEntity.java @@ -16,17 +16,23 @@ @Getter @AllArgsConstructor @NoArgsConstructor -public class AEntity { +public class NotificationEntity { @Id @GeneratedValue private Long id; @Column(nullable = false) - private String name; + private Long userId; + @Column(nullable = false) + private String title; + @Column(nullable = false) + private String content; - public static AEntity toEntity( + public static NotificationEntity toEntity( Long id, - String name + Long userId, + String title, + String content ) { - return new AEntity(id, name); + return new NotificationEntity(id, userId, title, content); } } diff --git a/src/main/java/com/server/kubacknotification/infra/jpa/repository/AJpaRepository.java b/src/main/java/com/server/kubacknotification/infra/jpa/repository/AJpaRepository.java deleted file mode 100644 index e0134e0..0000000 --- a/src/main/java/com/server/kubacknotification/infra/jpa/repository/AJpaRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.server.kubacknotification.infra.jpa.repository; - -import com.server.kubacknotification.infra.jpa.entity.AEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AJpaRepository extends JpaRepository { -} diff --git a/src/main/java/com/server/kubacknotification/infra/jpa/repository/NotificationJpaRepository.java b/src/main/java/com/server/kubacknotification/infra/jpa/repository/NotificationJpaRepository.java new file mode 100644 index 0000000..8eb009c --- /dev/null +++ b/src/main/java/com/server/kubacknotification/infra/jpa/repository/NotificationJpaRepository.java @@ -0,0 +1,10 @@ +package com.server.kubacknotification.infra.jpa.repository; + + +import com.server.kubacknotification.infra.jpa.entity.NotificationEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/AJpaRepositoryImpl.java b/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/AJpaRepositoryImpl.java deleted file mode 100644 index 3bfda67..0000000 --- a/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/AJpaRepositoryImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.server.kubacknotification.infra.jpa.repositoryImpl; - -import com.server.kubacknotification.domain.entity.A; -import com.server.kubacknotification.domain.repository.ARepository; -import com.server.kubacknotification.infra.jpa.repository.AJpaRepository; -import com.server.kubacknotification.infra.mapper.AMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class AJpaRepositoryImpl implements ARepository { - private final AJpaRepository aJpaRepository; - private final AMapper aMapper; - - - @Override - public A findById(Long id) { - return aMapper.toDomain(aJpaRepository.findById(id).orElseThrow()); - } -} diff --git a/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/NotificationRepositoryImpl.java b/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/NotificationRepositoryImpl.java new file mode 100644 index 0000000..2e2dfcf --- /dev/null +++ b/src/main/java/com/server/kubacknotification/infra/jpa/repositoryImpl/NotificationRepositoryImpl.java @@ -0,0 +1,19 @@ +package com.server.kubacknotification.infra.jpa.repositoryImpl; + +import com.server.kubacknotification.domain.entity.Notification; +import com.server.kubacknotification.domain.repository.NotificationRepository; +import com.server.kubacknotification.infra.jpa.repository.NotificationJpaRepository; +import com.server.kubacknotification.infra.mapper.NotificationMapper; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NotificationRepositoryImpl implements NotificationRepository { + private final NotificationJpaRepository notificationJpaRepository; + private final NotificationMapper notificationMapper; + + + @Override + public Notification findById(Long id) { + return notificationMapper.toDomain(notificationJpaRepository.findById(id).orElseThrow()); + } +} diff --git a/src/main/java/com/server/kubacknotification/infra/mapper/AMapper.java b/src/main/java/com/server/kubacknotification/infra/mapper/AMapper.java deleted file mode 100644 index f4bde71..0000000 --- a/src/main/java/com/server/kubacknotification/infra/mapper/AMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.server.kubacknotification.infra.mapper; - -import com.server.kubacknotification.domain.entity.A; -import com.server.kubacknotification.infra.jpa.entity.AEntity; -import org.springframework.stereotype.Component; - -@Component -public class AMapper { - public A toDomain(AEntity aEntity) { - return A.toDomain( - aEntity.getId(), - aEntity.getName() - ); - } - public AEntity toEntity(A a) { - return AEntity.toEntity( - a.getId(), - a.getName() - ); - } -} diff --git a/src/main/java/com/server/kubacknotification/infra/mapper/NotificationMapper.java b/src/main/java/com/server/kubacknotification/infra/mapper/NotificationMapper.java new file mode 100644 index 0000000..0a3fb6a --- /dev/null +++ b/src/main/java/com/server/kubacknotification/infra/mapper/NotificationMapper.java @@ -0,0 +1,24 @@ +package com.server.kubacknotification.infra.mapper; + +import com.server.kubacknotification.domain.entity.Notification; +import com.server.kubacknotification.infra.jpa.entity.NotificationEntity; +import org.springframework.stereotype.Component; + +@Component +public class NotificationMapper { + public Notification toDomain(NotificationEntity notification) { + return Notification.toDomain( + notification.getId(), + notification.getUserId(), + notification.getTitle(), + notification.getContent() + ); + } + public NotificationEntity toEntity(Notification notification) { + return NotificationEntity.toEntity( + notification.getId(), + notification.getUserId(), + notification.getTitle(), + notification.getContent()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 1364fb2..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=kuback-notification diff --git a/src/main/resources/templates/payment.html b/src/main/resources/templates/payment.html new file mode 100644 index 0000000..29cab92 --- /dev/null +++ b/src/main/resources/templates/payment.html @@ -0,0 +1,76 @@ + + + + + + + + +
+ + + + + + + + + + +
+ Company Logo +

+
+

안녕하세요, 님!

+

고객님이 선택하신 좌석 정보와 결제 정보는 아래와 같습니다:

+

좌석 번호:

+

좌석 등급:

+

결제 금액:

+

결제 일시:

+
+

결제 내역

+

원래 가격:

+

할인 금액:

+

최종 결제 금액:

+
+

고객님의 결제가 정상적으로 완료되었습니다. 추가적인 문의 사항이 있으시면 언제든지 연락해 주세요.

+
+
+ + diff --git a/src/main/resources/templates/ticketOpen.html b/src/main/resources/templates/ticketOpen.html new file mode 100644 index 0000000..ac5bac1 --- /dev/null +++ b/src/main/resources/templates/ticketOpen.html @@ -0,0 +1,68 @@ + + + + + + + + +
+ + + + + + + + + + +
+ Company Logo +

+
+

안녕하세요, 님!

+

고객님이 기다리시던 공연 의 티켓이 곧 오픈됩니다.

+

+
+

공연의 더 많은 정보와 좌석 정보를 확인하시려면 웹사이트를 방문해 주세요.

+
+
+ + diff --git a/src/test/java/com/server/kubacknotification/api/NotificationControllerTest.java b/src/test/java/com/server/kubacknotification/api/NotificationControllerTest.java new file mode 100644 index 0000000..22e7695 --- /dev/null +++ b/src/test/java/com/server/kubacknotification/api/NotificationControllerTest.java @@ -0,0 +1,44 @@ +package com.server.kubacknotification.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import com.server.kubacknotification.application.service.NotificationService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(NotificationController.class) +public class NotificationControllerTest { + + @Autowired + MockMvc mvc; + + @Test + @DisplayName("티켓팅 홍보 알림 테스트") + void createTicketOpenNotifications() throws Exception { + //given + TicketOpenMessage ticketOpenMessage = new TicketOpenMessage( + "뮤지컬 킹키부츠 티켓 오픈", + "뮤지컬 킹키부츠 2024.10.10.(목) 14:00에 티켓이 오픈됩니다!", + "somin455@gmail.com", + 1L, + "윤소민" + ); + + ObjectMapper objectMapper = new ObjectMapper(); + String requestJson = objectMapper.writeValueAsString(ticketOpenMessage); + + //when + mvc.perform(post("/api/notifications/ticket-open") + .contentType("application/json") + .content(requestJson)) + //then + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/server/kubacknotification/application/service/EmailServiceTest.java b/src/test/java/com/server/kubacknotification/application/service/EmailServiceTest.java new file mode 100644 index 0000000..3b5f82c --- /dev/null +++ b/src/test/java/com/server/kubacknotification/application/service/EmailServiceTest.java @@ -0,0 +1,76 @@ +package com.server.kubacknotification.application.service; + +import com.server.kubacknotification.application.dto.request.TicketOpenMessage; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mail.javamail.JavaMailSender; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.context.Context; + +import static org.mockito.Mockito.*; + +class EmailServiceTest { + + @Mock + private JavaMailSender javaMailSender; + + @Mock + private SpringTemplateEngine templateEngine; + + @InjectMocks + private EmailService emailService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); // 이 줄을 추가하여 목 객체를 초기화합니다. + } + + @Test + void testConsumeNoticeMessage() { + // given + TicketOpenMessage ticketOpenMessage = new TicketOpenMessage("뮤지컬 킹키부츠", "2024.10.10.(목) 14:00에 티켓이 오픈됩니다!", "somin455@gmail.com", 1L, "윤소민"); + + // when + emailService.consumeNoticeMessage(ticketOpenMessage); + + // then + // 이메일 발송 메서드가 호출되었는지 확인 + verify(javaMailSender, times(1)).createMimeMessage(); + } + + @Test + void testSendTicketOpenEmail() throws Exception { + // given + TicketOpenMessage ticketOpenMessage = new TicketOpenMessage("뮤지컬 킹키부츠", "2024.10.10.(목) 14:00에 티켓이 오픈됩니다!", "somin455@gmail.com", 1L, "윤소민"); + + MimeMessage mimeMessage = mock(MimeMessage.class); + when(javaMailSender.createMimeMessage()).thenReturn(mimeMessage); + + // HTML 템플릿 처리 결과를 설정 + when(templateEngine.process(anyString(), any(Context.class))).thenReturn("Test"); + + // when + emailService.sendTicketOpenEmail(ticketOpenMessage); + + // then + // 이메일 전송이 수행되었는지 검증 + verify(javaMailSender, times(1)).send(mimeMessage); + } + + @Test + void testSetTicketOpenContext() { + // given + TicketOpenMessage ticketOpenMessage = new TicketOpenMessage("뮤지컬 킹키부츠", "2024.10.10.(목) 14:00에 티켓이 오픈됩니다!", "somin455@gmail.com", 1L, "윤소민"); + + // when + String htmlContent = emailService.setTicketOpenContext(ticketOpenMessage); + + // then + // HTML 템플릿 생성 확인 + verify(templateEngine, times(1)).process("ticketOpen", any(Context.class)); + } +}