diff --git a/build.gradle b/build.gradle index f9c4143b..621905e8 100644 --- a/build.gradle +++ b/build.gradle @@ -85,6 +85,8 @@ dependencies { // poi-excel implementation 'org.apache.poi:poi-ooxml:5.4.0' + + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java b/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java index 2831ad9c..10a54dc7 100644 --- a/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java +++ b/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java @@ -11,6 +11,7 @@ import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; import life.mosu.mosuserver.domain.school.SchoolJpaEntity; import life.mosu.mosuserver.domain.school.SchoolJpaRepository; +import life.mosu.mosuserver.domain.school.SchoolQueryRepositoryImpl; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.global.util.FileRequest; @@ -31,6 +32,7 @@ public class ApplicationService { private final ApplicationJpaRepository applicationJpaRepository; private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository; private final AdmissionTicketImageJpaRepository admissionTicketImageJpaRepository; + private final SchoolQueryRepositoryImpl schoolQueryRepository; private final SchoolJpaRepository schoolJpaRepository; // 신청 @@ -42,10 +44,12 @@ public ApplicationResponse apply(Long userId, ApplicationRequest request) { ApplicationJpaEntity application = applicationJpaRepository.save(request.toEntity(userId)); Long applicationId = application.getId(); + //수험표 저장 admissionTicketImageJpaRepository.save( createAdmissionTicketImageIfPresent(request.admissionTicket(), applicationId)); for (ApplicationSchoolRequest schoolRequest : schoolRequests) { + //해당 학교가 존재하는 학교인지 Long schoolId = schoolJpaRepository.findBySchoolNameAndAreaAndExamDate( schoolRequest.schoolName(), schoolRequest.validatedArea(schoolRequest.area()), @@ -53,21 +57,72 @@ public ApplicationResponse apply(Long userId, ApplicationRequest request) { .orElseThrow(() -> new CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND)) .getId(); + //해당 학교를 이미 신청하였는지 if (applicationSchoolJpaRepository.existsByUserIdAndSchoolId(userId, schoolId)) { throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_ALREADY_APPLIED); } + //해당 학교를 찾을 수 있는지 SchoolJpaEntity school = schoolJpaRepository.findById(schoolId) .orElseThrow(() -> new CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND)); + //해당 학교를 신청했음을 저장하기 ApplicationSchoolJpaEntity applicationSchool = schoolRequest.toEntity(userId, applicationId, school); + + //반환용 리스트에 저장하기 savedEntities.add(applicationSchoolJpaRepository.save(applicationSchool)); } return ApplicationResponse.of(applicationId, savedEntities); } + //신청 - 리팩토링 +// @Transactional +// public ApplicationResponse apply(Long userId, ApplicationRequest request) { +// Set schoolRequests = request.schools(); +//// List savedEntities = new ArrayList<>(); +// +// ApplicationJpaEntity application = applicationJpaRepository.save(request.toEntity(userId)); +// Long applicationId = application.getId(); +// +// //수험표 저장 +// admissionTicketImageJpaRepository.save( +// createAdmissionTicketImageIfPresent(request.admissionTicket(), applicationId)); +// +// List> conditions = schoolRequests.stream() +// .map(school -> Triple.of( +// school.schoolName(), +// school.validatedArea(school.area()), +// school.examDate() +// )) +// .toList(); +// +// //이름, 지역, 날짜 조건에 맞는 학교가 있는지 +// List schoolIds = schoolQueryRepository.findSchoolsByConditions(conditions); +// if (schoolIds.size() != conditions.size()) { +// throw new CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND); +// } +// +// //해당 학교를 이미 신청하였는지 +// if (applicationSchoolJpaRepository.existsByUserIdAndSchoolIds(userId, schoolIds)) { +// throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_ALREADY_APPLIED); +// } +// + + /// / //해당 학교를 찾을 수 있는지 / SchoolJpaEntity school = + /// schoolJpaRepository.findById(schoolId) / .orElseThrow(() -> new + /// CustomRuntimeException(ErrorCode.SCHOOL_NOT_FOUND)); +// +// //해당 학교를 신청했음을 저장하기 +// ApplicationSchoolJpaEntity applicationSchool = schoolRequest.toEntity(userId, +// applicationId, school); +// +// //반환용 리스트에 저장하기 +// savedEntities.add(applicationSchoolJpaRepository.save(applicationSchool)); +// +// return ApplicationResponse.of(applicationId, savedEntities); +// } // 전체 신청 내역 조회 @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java index fd95e8f1..0c6497c8 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java @@ -4,8 +4,10 @@ import life.mosu.mosuserver.domain.inquiry.InquiryJpaRepository; import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaEntity; import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaRepository; +import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse; @@ -22,6 +24,9 @@ public class InquiryAnswerService { private final InquiryAnswerAttachmentService answerAttachmentService; private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; private final InquiryJpaRepository inquiryJpaRepository; + private final ProfileJpaRepository profileJpaRepository; + private final NotifyEventPublisher notifyEventPublisher; + @Transactional public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { @@ -36,6 +41,13 @@ public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { answerAttachmentService.createAttachment(request.attachments(), answerEntity); inquiryEntity.updateStatusToComplete(); + +// ProfileJpaEntity profile = profileJpaRepository.findByUserId(inquiryEntity.getUserId()) +// .orElseThrow(() -> new RuntimeException("")); +// NotifyEvent event = NotifyEvent.create(NotifyStatus.INQUIRY_ANSWER_SUCCESS, +// profile.getPhoneNumber(), postId); +// notifyEventPublisher.notify(event); + } @Transactional diff --git a/src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java b/src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java new file mode 100644 index 00000000..ee539d7a --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/notify/NotifyService.java @@ -0,0 +1,41 @@ +package life.mosu.mosuserver.application.notify; + +import life.mosu.mosuserver.domain.profile.ProfileJpaEntity; +import life.mosu.mosuserver.domain.profile.ProfileJpaRepository; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyClientAdapter; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.strategy.NotifyStrategy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class NotifyService { + + private final NotifyClientAdapter notifier; + private final ProfileJpaRepository profileJpaRepository; + private final ApplicationContext applicationContext; + + public void notify(NotifyEvent event) { + NotifyStrategy strategy = getNotifyStrategy(event.status().getStrategyName()); + + ProfileJpaEntity profile = profileJpaRepository.findByUserId(event.targetId()) + .orElseThrow(() -> new CustomRuntimeException(ErrorCode.PROFILE_NOT_FOUND)); + String parsedPhoneNumber = profile.getPhoneNumber().replaceAll("-", ""); + + NotifyEventRequest request = strategy.apply(parsedPhoneNumber, event); + + notifier.send(request); + } + + private NotifyStrategy getNotifyStrategy(String strategyName) { + return (NotifyStrategy) applicationContext.getBean(strategyName); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java index 0c4755ce..857a21d3 100644 --- a/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolJpaRepository.java @@ -8,6 +8,7 @@ public interface ApplicationSchoolJpaRepository extends JpaRepository { +// boolean existsByUserIdAndSchoolIds(Long userId, List schoolId); boolean existsByUserIdAndSchoolId(Long userId, Long schoolId); @@ -15,7 +16,25 @@ public interface ApplicationSchoolJpaRepository extends boolean existsByApplicationId(Long applicationId); + @Query("SELECT COUNT(a) = :size FROM ApplicationSchoolJpaEntity a WHERE a.id IN :applicationSchoolIds") boolean existsAllByIds(@Param("applicationSchoolIds") List applicationSchoolIds, @Param("size") long size); + + @Query(""" + SELECT p.paymentKey, a.examDate, a.schoolName, a.lunch + FROM ApplicationSchoolJpaEntity a + LEFT JOIN PaymentJpaEntity p ON a.id = p.applicationSchoolId + WHERE a.id = :applicationSchoolId + """) + ApplicationSchoolNotifyProjection findPaymentByApplicationSchoolId(Long applicationSchoolId); + + @Query(""" + SELECT p.paymentKey, a.examDate, a.schoolName + FROM ApplicationSchoolJpaEntity a + JOIN PaymentJpaEntity p ON a.id = p.applicationSchoolId + WHERE a.id = :applicationSchoolId + """) + OneWeekNotifyProjection findOneWeekNotifyByApplicationSchoolId(Long applicationSchoolId); + } diff --git a/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java b/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java new file mode 100644 index 00000000..00d64e07 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/applicationschool/ApplicationSchoolNotifyProjection.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.domain.applicationschool; + +import java.time.LocalDate; +import life.mosu.mosuserver.domain.application.Lunch; + +public record ApplicationSchoolNotifyProjection( + String paymentKey, + LocalDate examDate, + String schoolName, + Lunch lunch +) { + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java b/src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java new file mode 100644 index 00000000..cb63335b --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/applicationschool/OneWeekNotifyProjection.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.domain.applicationschool; + +import java.time.LocalDate; + +public record OneWeekNotifyProjection( + String paymentKey, + LocalDate examDate, + String schoolName +) { + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java index 3256da20..6fdc1ec8 100644 --- a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java @@ -1,6 +1,20 @@ package life.mosu.mosuserver.domain.refund; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface RefundJpaRepository extends JpaRepository { + + @Query(""" + SELECT + p.paymentKey AS paymentKey, + a.examDate AS examDate, + p.paymentMethod AS paymentMethod, + r.reason AS reason + FROM RefundJpaEntity r + LEFT JOIN PaymentJpaEntity p ON r.applicationSchoolId = p.applicationSchoolId + LEFT JOIN ApplicationSchoolJpaEntity a ON r.applicationSchoolId = a.id + WHERE r.applicationSchoolId = :applicationSchoolId + """) + RefundNotifyProjection findRefundByApplicationSchoolId(Long applicationSchoolId); } diff --git a/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java b/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java new file mode 100644 index 00000000..c7aa3ace --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java @@ -0,0 +1,15 @@ +package life.mosu.mosuserver.domain.refund; + +import java.time.LocalDate; +import life.mosu.mosuserver.domain.payment.PaymentMethod; + +public record RefundNotifyProjection( + String paymentKey, + LocalDate examDate, + String schoolName, + PaymentMethod paymentMethod, + String reason + // TODO: 환불 금액 추가 +) { + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java b/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java new file mode 100644 index 00000000..395d9973 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepository.java @@ -0,0 +1,12 @@ +package life.mosu.mosuserver.domain.school; + +import java.time.LocalDate; +import java.util.List; +import org.apache.commons.lang3.tuple.Triple; + +public interface SchoolQueryRepository { + + List findSchoolsByConditions( + List> conditions); + +} diff --git a/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java b/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java new file mode 100644 index 00000000..99d69be9 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/school/SchoolQueryRepositoryImpl.java @@ -0,0 +1,38 @@ +package life.mosu.mosuserver.domain.school; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Triple; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class SchoolQueryRepositoryImpl implements SchoolQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findSchoolsByConditions( + List> conditions) { + BooleanBuilder builder = new BooleanBuilder(); + + for (Triple cond : conditions) { + builder.or( + QSchoolJpaEntity.schoolJpaEntity.schoolName.eq(cond.getLeft()) + .and(QSchoolJpaEntity.schoolJpaEntity.area.eq(cond.getMiddle())) + .and(QSchoolJpaEntity.schoolJpaEntity.examDate.eq(cond.getRight())) + ); + } + + return queryFactory + .select(QSchoolJpaEntity.schoolJpaEntity.id) + .from(QSchoolJpaEntity.schoolJpaEntity) + .where(builder) + .fetch(); + } + + +} diff --git a/src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java b/src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java new file mode 100644 index 00000000..898ad70c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/annotation/ReactiveEventListener.java @@ -0,0 +1,16 @@ +package life.mosu.mosuserver.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Async +@EventListener +public @interface ReactiveEventListener { + +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java b/src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java new file mode 100644 index 00000000..6f9cb3ea --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/config/MessageSourceConfig.java @@ -0,0 +1,18 @@ +package life.mosu.mosuserver.global.config; + +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; + +@Configuration +public class MessageSourceConfig { + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.setBasename("classpath:messages"); + messageSource.setDefaultEncoding("UTF-8"); + return messageSource; + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/config/WebClientConfig.java b/src/main/java/life/mosu/mosuserver/global/config/WebClientConfig.java new file mode 100644 index 00000000..a166b5ee --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/config/WebClientConfig.java @@ -0,0 +1,17 @@ +package life.mosu.mosuserver.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder() + .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .build(); + } +} diff --git a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java index be7dd28b..4a0d5563 100644 --- a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java +++ b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java @@ -83,7 +83,11 @@ public enum ErrorCode { INQUIRY_ANSWER_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 문의 답변이 존재합니다."), // 공지 관련 에러 - NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "공지사항을 찾을 수 없습니다."); + NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "공지사항을 찾을 수 없습니다."), + + // 알림톡 관련 에러 + NOTIFY_STATUS_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."), + STRATEGY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전략을 찾을 수 없습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java index bcc45d4e..2cc3bf53 100644 --- a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java +++ b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java @@ -107,8 +107,7 @@ private List initializeUsersAndProfiles(Random random) { .userName(user.getName()) .gender(user.getGender()) .birth(user.getBirth()) - .phoneNumber("010-" + String.format("%04d", i) + "-" + String.format("%04d", - i + 1000)) + .phoneNumber("010-4870-5466") .email("user" + i + "@example.com") .education(Education.values()[random.nextInt(Education.values().length)]) .schoolInfo(new SchoolInfoJpaVO("모수대학교" + (i % 3 + 1), "123-23", "서울시 모수구 모수동")) diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java b/src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java new file mode 100644 index 00000000..8d8a2cfe --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/LunaSoftNotifier.java @@ -0,0 +1,65 @@ +package life.mosu.mosuserver.infra.notify; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.scheduler.Schedulers; + +@Component +@Slf4j +public class LunaSoftNotifier implements NotifyClientAdapter { + + private final WebClient webClient; + private final NotifyProperties properties; + + public LunaSoftNotifier( + WebClient webClient, + NotifyProperties properties + ) { + this.webClient = webClient; + this.properties = properties; + } + + @Override + public void send(NotifyEventRequest request) { + LunaNotifyRequest lunaRequest = createLunaNotifyRequest(request); + + webClient.post() + .uri(properties.alimTalkUrl) + .bodyValue(lunaRequest) + .retrieve() + .bodyToMono(String.class) + .publishOn(Schedulers.boundedElastic()) + .doOnSuccess(response -> log.info("알림톡 응답: {}", response)) + .doOnError(error -> log.error("알림톡 전송 실패", error)) + .doOnTerminate(() -> log.info("알림톡 전송 완료: {}", request)) + .subscribe(); + } + + private LunaNotifyRequest createLunaNotifyRequest(NotifyEventRequest request) { + return LunaNotifyRequest.of( + properties.userId, + properties.apiKey, + request.templateId(), + request.messages() + ); + } + + private record LunaNotifyRequest( + @JsonProperty("userId") String userId, + @JsonProperty("api_key") String apiKey, + @JsonProperty("template_id") Integer templateId, + @JsonProperty("messages") List> messages + ) { + + static LunaNotifyRequest of(String userId, String apiKey, Integer templateId, + List> messages) { + return new LunaNotifyRequest(userId, apiKey, templateId, messages); + } + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java new file mode 100644 index 00000000..ece759d4 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyClientAdapter.java @@ -0,0 +1,8 @@ +package life.mosu.mosuserver.infra.notify; + +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; + +public interface NotifyClientAdapter { + + void send(NotifyEventRequest request); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java new file mode 100644 index 00000000..f61f3147 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventPublisher.java @@ -0,0 +1,17 @@ +package life.mosu.mosuserver.infra.notify; + +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotifyEventPublisher { + + private final ApplicationEventPublisher publisher; + + public void notify(NotifyEvent event) { + publisher.publishEvent(event); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java new file mode 100644 index 00000000..5da898b1 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyEventTemplateGenerator.java @@ -0,0 +1,49 @@ +package life.mosu.mosuserver.infra.notify; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRemindMessageDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventSuccessMessageDto; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotifyEventTemplateGenerator { + + private final MessageSource messageSource; + + public String getProcessedMessage(String code, Map variables) { + String message = messageSource.getMessage(code, null, Locale.KOREA); + for (Map.Entry entry : variables.entrySet()) { + message = StringUtils.replace(message, "#{" + entry.getKey() + "}", entry.getValue()); + } + return message; + } + + public NotifyEventRequest getNotifyEventSuccessTemplate( + Integer templateId, + NotifyEventSuccessMessageDto message + ) { + + return new NotifyEventRequest( + templateId, + List.of(message.toMap()) + ); + } + + public NotifyEventRequest getNotifyEventRemindTemplate( + Integer templateId, + NotifyEventRemindMessageDto message + ) { + + return new NotifyEventRequest( + templateId, + List.of(message.toMap()) + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java new file mode 100644 index 00000000..78c76ee1 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/NotifyProperties.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.infra.notify; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class NotifyProperties { + @Value("${alimtalk.api.base-url}") String alimTalkUrl; + @Value("${alimtalk.user-id}") String userId; + @Value("${alimtalk.api-key}") String apiKey; +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java b/src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java new file mode 100644 index 00000000..fe186ad5 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/constant/NotifyConstants.java @@ -0,0 +1,21 @@ +package life.mosu.mosuserver.infra.notify.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class NotifyConstants { + + public static final String HOMEPAGE = "https://www.mosuedu.com"; + public static final String MYPAGE = "https://www.mosuedu.com/mypage"; + public static final String WARNING = "https://www.mosuedu.com/warning"; + public static final String INQUIRY = "https://www.mosuedu.com/notice"; + + public static final Integer APPLICATION_TEMPLATE_ID = 50037; + public static final Integer EXAM_1DAY_BEFORE_TEMPLATE_ID = 50041; + public static final Integer EXAM_3DAY_BEFORE_TEMPLATE_ID = 50040; + public static final Integer EXAM_1WEEK_BEFORE_TEMPLATE_ID = 50039; + public static final Integer INQUIRY_ANSWER_TEMPLATE_ID = 50038; + public static final Integer SIGN_UP_TEMPLATE_ID = 50042; + public static final Integer REFUND_TEMPLATE_ID = 50043; +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/ApplicationNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/ApplicationNotifyVariablesDto.java new file mode 100644 index 00000000..5a8b0f5c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/ApplicationNotifyVariablesDto.java @@ -0,0 +1,22 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDate; +import java.util.Map; + +public record ApplicationNotifyVariablesDto( + String paymentKey, + LocalDate examDate, + String schoolName, + String lunch +) { + + public Map toMap() { + return Map.of( + "paymentKey", paymentKey, + "examDate", examDate.toString(), + "schoolName", schoolName, + "lunch", lunch + ); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/EventStatus.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/EventStatus.java new file mode 100644 index 00000000..42ff0106 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/EventStatus.java @@ -0,0 +1,5 @@ +package life.mosu.mosuserver.infra.notify.dto; + +public enum EventStatus { + SUCCESS, FAILURE, INFO +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1DayBeforeNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1DayBeforeNotifyVariablesDto.java new file mode 100644 index 00000000..b4cb7736 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1DayBeforeNotifyVariablesDto.java @@ -0,0 +1,19 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDate; +import java.util.Map; + +public record Exam1DayBeforeNotifyVariablesDto( + LocalDate examDate, + String examinationNumber, + String schoolName +) { + + public Map toMap() { + return Map.of( + "examDate", examDate.toString(), + "examinationNumber", examinationNumber, + "schoolName", schoolName + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1WeekBeforeNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1WeekBeforeNotifyVariablesDto.java new file mode 100644 index 00000000..7653c59c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam1WeekBeforeNotifyVariablesDto.java @@ -0,0 +1,19 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDate; +import java.util.Map; + +public record Exam1WeekBeforeNotifyVariablesDto( + LocalDate examDate, + String paymentKey, + String schoolName +) { + + public Map toMap() { + return Map.of( + "examDate", examDate.toString(), + "paymentKey", paymentKey, + "schoolName", schoolName + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java new file mode 100644 index 00000000..a37c0cfa --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/Exam3DayBeforeNotifyVariablesDto.java @@ -0,0 +1,19 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDate; +import java.util.Map; + +public record Exam3DayBeforeNotifyVariablesDto( + LocalDate examDate, + String examinationNumber, + String schoolName +) { + + public Map toMap() { + return Map.of( + "examDate", examDate.toString(), + "examinationNumber", examinationNumber, + "schoolName", schoolName + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/InquiryAnswerNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/InquiryAnswerNotifyVariablesDto.java new file mode 100644 index 00000000..3b739c2d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/InquiryAnswerNotifyVariablesDto.java @@ -0,0 +1,14 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.util.Map; + +public record InquiryAnswerNotifyVariablesDto( + String title +) { + + public Map toMap() { + return Map.of( + "inquiryTitle", title + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrl.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrl.java new file mode 100644 index 00000000..b71d1291 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrl.java @@ -0,0 +1,24 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.util.Map; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class NotifyButtonUrl { + + private String urlPc; + private String urlMobile; + + public static NotifyButtonUrl of(String urlPc, String urlMobile) { + return new NotifyButtonUrl(urlPc, urlMobile); + } + + public Map toMap() { + return Map.of( + "url_pc", urlPc, + "url_mobile", urlMobile + ); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrls.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrls.java new file mode 100644 index 00000000..230d0928 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyButtonUrls.java @@ -0,0 +1,21 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NotifyButtonUrls { + + private final List btnUrls; + + public static NotifyButtonUrls of(NotifyButtonUrl... btns) { + return new NotifyButtonUrls(Arrays.asList(btns)); + } + + public List> toMapList() { + return btnUrls.stream().map(NotifyButtonUrl::toMap).collect(Collectors.toList()); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEvent.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEvent.java new file mode 100644 index 00000000..dcd9bf26 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEvent.java @@ -0,0 +1,12 @@ +package life.mosu.mosuserver.infra.notify.dto; + +public record NotifyEvent( + NotifyStatus status, + Long userId, + Long targetId +) { + + public static NotifyEvent create(NotifyStatus status, Long userId, Long targetId) { + return new NotifyEvent(status, userId, targetId); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java new file mode 100644 index 00000000..7336496b --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRemindMessageDto.java @@ -0,0 +1,46 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +public record NotifyEventRemindMessageDto( + Integer no, + String telNum, + LocalDateTime reserveTime, + String msgContent, + String smsContent, + NotifyButtonUrls btnUrls +) { + + public static NotifyEventRemindMessageDto create( + Integer no, + String telNum, + LocalDateTime reserveTime, + String msgContent, + String smsContent, + NotifyButtonUrls btnUrls + ) { + return new NotifyEventRemindMessageDto( + no, + telNum, + reserveTime, + msgContent, + smsContent, + btnUrls + ); + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("no", no); + map.put("tel_num", telNum); + map.put("reserve_time", reserveTime); + map.put("msg_content", msgContent); + map.put("sms_content", smsContent); + map.put("use_sms", "1"); + map.put("btn_url", btnUrls.toMapList()); + return map; + } + +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRequest.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRequest.java new file mode 100644 index 00000000..8cb94292 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventRequest.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.util.List; +import java.util.Map; + +public record NotifyEventRequest( + Integer templateId, + List> messages +) { + +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java new file mode 100644 index 00000000..ee5ffe31 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyEventSuccessMessageDto.java @@ -0,0 +1,39 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.util.HashMap; +import java.util.Map; + +public record NotifyEventSuccessMessageDto( + Integer no, + String telNum, + String msgContent, + String smsContent, + String useSms, + NotifyButtonUrls btnUrls +) { + + public static NotifyEventSuccessMessageDto create( + Integer no, + String telNum, + String msgContent, + String smsContent, + String useSms, + NotifyButtonUrls btnUrls + ) { + return new NotifyEventSuccessMessageDto( + no, telNum, msgContent, smsContent, useSms, btnUrls + ); + } + + + public Map toMap() { + Map map = new HashMap<>(); + map.put("no", no); + map.put("tel_num", telNum); + map.put("msg_content", msgContent); + map.put("sms_content", smsContent); + map.put("use_sms", useSms); + map.put("btn_url", btnUrls.toMapList()); + return map; + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyStatus.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyStatus.java new file mode 100644 index 00000000..ca7acc12 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotifyStatus.java @@ -0,0 +1,31 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum NotifyStatus { + + // success + SIGN_UP_SUCCESS(EventStatus.SUCCESS, "회원 가입 완료", + "SignUpNotifyStrategy"), + APPLICATION_SUCCESS(EventStatus.SUCCESS, "신청 완료", + "ApplicationNotifyStrategy"), + REFUND_SUCCESS(EventStatus.SUCCESS, "환불 완료", + "RefundNotifyStrategy"), + INQUIRY_ANSWER_SUCCESS(EventStatus.SUCCESS, "문의 답변 완료", + "InquiryAnswerNotifyStrategy"), + + // info + EXAM_1WEEK_BEFORE_REMINDER_INFO(EventStatus.INFO, "시험 1주일 전 리마인드 알림", + "Exam1DayBeforeNotifyStrategy"), + EXAM_3DAY_BEFORE_REMINDER_INFO(EventStatus.INFO, "시험 3일 전 리마인드 알림", + "Exam3DayBeforeNotifyStrategy"), + EXAM_1DAY_BEFORE_REMINDER_INFO(EventStatus.INFO, "시험 하루 전 리마인드 알림", + "Exam1WeekBeforeNotifyStrategy"); + + private final EventStatus status; + private final String message; + private final String strategyName; +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/RefundNotifyVariablesDto.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/RefundNotifyVariablesDto.java new file mode 100644 index 00000000..6adfd2a3 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/RefundNotifyVariablesDto.java @@ -0,0 +1,25 @@ +package life.mosu.mosuserver.infra.notify.dto; + +import java.time.LocalDate; +import java.util.Map; + +public record RefundNotifyVariablesDto( + String paymentKey, + LocalDate examDate, + String schoolName, +// String amount, + String paymentMethod, + String reason +) { + + public Map toMap() { + return Map.of( + "paymentKey", paymentKey, + "examDate", examDate.toString(), + "schoolName", schoolName, +// "amount", amount, + "paymentMethod", paymentMethod, + "reason", reason + ); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java new file mode 100644 index 00000000..39a8f1d9 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/ApplicationNotifyStrategy.java @@ -0,0 +1,53 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.APPLICATION_TEMPLATE_ID; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.MYPAGE; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.WARNING; + +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolNotifyProjection; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.ApplicationNotifyVariablesDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventSuccessMessageDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component("ApplicationNotifyStrategy") +@RequiredArgsConstructor +public class ApplicationNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + ApplicationSchoolNotifyProjection projection = applicationSchoolJpaRepository.findPaymentByApplicationSchoolId( + event.targetId()); + + ApplicationNotifyVariablesDto dto = new ApplicationNotifyVariablesDto( + projection.paymentKey(), projection.examDate(), projection.schoolName(), + projection.lunch().getLunchName()); + + String alimTalkContent = template.getProcessedMessage( + "notify.exam.application.complete.alimtalk", + dto.toMap()); + + String smsContent = template.getProcessedMessage("notify.exam.application.complete.sms", + dto.toMap()); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(WARNING, WARNING), + NotifyButtonUrl.of(MYPAGE, MYPAGE) + ); + + NotifyEventSuccessMessageDto eventMessage = NotifyEventSuccessMessageDto.create(1, + targetPhoneNumber, alimTalkContent, + smsContent, "1", btnUrls); + return template.getNotifyEventSuccessTemplate(APPLICATION_TEMPLATE_ID, eventMessage); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java new file mode 100644 index 00000000..fd9bc5b5 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1DayBeforeNotifyStrategy.java @@ -0,0 +1,62 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.EXAM_1DAY_BEFORE_TEMPLATE_ID; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.INQUIRY; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.MYPAGE; + +import java.time.LocalDateTime; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaEntity; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.Exam1DayBeforeNotifyVariablesDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRemindMessageDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component("Exam1DayBeforeNotifyStrategy") +@RequiredArgsConstructor +public class Exam1DayBeforeNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + ApplicationSchoolJpaEntity applicationSchool = applicationSchoolJpaRepository.findById( + event.targetId()).orElseThrow( + () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND)); + + Exam1DayBeforeNotifyVariablesDto dto = new Exam1DayBeforeNotifyVariablesDto( + applicationSchool.getExamDate(), applicationSchool.getExaminationNumber(), + applicationSchool.getSchoolName()); + + String alimTalkContent = template.getProcessedMessage( + "notify.exam.oneday.reminder.alimtalk", + dto.toMap()); + + String smsContent = template.getProcessedMessage("notify.exam.oneday.reminder.sms", + dto.toMap()); + + LocalDateTime reserveTime = applicationSchool.getExamDate() + .minusDays(1) + .atTime(8, 0); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(INQUIRY, INQUIRY), + NotifyButtonUrl.of(MYPAGE, MYPAGE) + ); + + NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1, + targetPhoneNumber, reserveTime, + alimTalkContent, + smsContent, btnUrls); + + return template.getNotifyEventRemindTemplate(EXAM_1DAY_BEFORE_TEMPLATE_ID, eventMessage); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java new file mode 100644 index 00000000..042cf220 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam1WeekBeforeNotifyStrategy.java @@ -0,0 +1,57 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.EXAM_1WEEK_BEFORE_TEMPLATE_ID; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.INQUIRY; + +import java.time.LocalDateTime; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; +import life.mosu.mosuserver.domain.applicationschool.OneWeekNotifyProjection; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.Exam1WeekBeforeNotifyVariablesDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRemindMessageDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + + +@Component("Exam1WeekBeforeNotifyStrategy") +@RequiredArgsConstructor +public class Exam1WeekBeforeNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + OneWeekNotifyProjection projection = applicationSchoolJpaRepository.findOneWeekNotifyByApplicationSchoolId( + event.targetId()); + + Exam1WeekBeforeNotifyVariablesDto dto = new Exam1WeekBeforeNotifyVariablesDto( + projection.examDate(), projection.paymentKey(), projection.schoolName()); + + String alimTalkContent = template.getProcessedMessage( + "notify.exam.oneweek.reminder.alimtalk", + dto.toMap()); + + String smsContent = template.getProcessedMessage("notify.exam.oneweek.reminder.sms", + dto.toMap()); + + LocalDateTime reserveTime = projection.examDate() + .minusDays(7) + .atTime(8, 0); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(INQUIRY, INQUIRY) + ); + + NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1, + targetPhoneNumber, reserveTime, + alimTalkContent, + smsContent, btnUrls); + + return template.getNotifyEventRemindTemplate(EXAM_1WEEK_BEFORE_TEMPLATE_ID, eventMessage); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java new file mode 100644 index 00000000..1bd6b444 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/Exam3DayBeforeNotifyStrategy.java @@ -0,0 +1,63 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.EXAM_3DAY_BEFORE_TEMPLATE_ID; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.INQUIRY; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.MYPAGE; + +import java.time.LocalDateTime; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaEntity; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaRepository; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.Exam3DayBeforeNotifyVariablesDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRemindMessageDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component("Exam3DayBeforeNotifyStrategy") +@RequiredArgsConstructor +public class Exam3DayBeforeNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final ApplicationSchoolJpaRepository applicationSchoolJpaRepository; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + ApplicationSchoolJpaEntity applicationSchool = applicationSchoolJpaRepository.findById( + event.targetId()).orElseThrow( + () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND)); + + Exam3DayBeforeNotifyVariablesDto dto = new Exam3DayBeforeNotifyVariablesDto( + applicationSchool.getExamDate(), applicationSchool.getExaminationNumber(), + applicationSchool.getSchoolName()); + + String alimTalkContent = template.getProcessedMessage( + "notify.exam.threeday.reminder.alimtalk", + dto.toMap()); + + String smsContent = template.getProcessedMessage("notify.exam.threeday.reminder.sms", + dto.toMap()); + + LocalDateTime reserveTime = applicationSchool.getExamDate() + .minusDays(3) + .atTime(8, 0); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(INQUIRY, INQUIRY), + NotifyButtonUrl.of(MYPAGE, MYPAGE) + ); + + NotifyEventRemindMessageDto eventMessage = NotifyEventRemindMessageDto.create(1, + targetPhoneNumber, reserveTime, + alimTalkContent, + smsContent, btnUrls); + + return template.getNotifyEventRemindTemplate(EXAM_3DAY_BEFORE_TEMPLATE_ID, eventMessage); + + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java new file mode 100644 index 00000000..16361a17 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/InquiryAnswerNotifyStrategy.java @@ -0,0 +1,55 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.INQUIRY; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.INQUIRY_ANSWER_TEMPLATE_ID; + +import life.mosu.mosuserver.domain.inquiry.InquiryJpaEntity; +import life.mosu.mosuserver.domain.inquiry.InquiryJpaRepository; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.InquiryAnswerNotifyVariablesDto; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventSuccessMessageDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("InquiryAnswerNotifyStrategy") +@RequiredArgsConstructor +public class InquiryAnswerNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final InquiryJpaRepository inquiryJpaRepository; + + //inquiry ID 가 없음 + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + log.info("log: {}", inquiryJpaRepository.findAll()); + InquiryJpaEntity inquiry = inquiryJpaRepository.findById(event.targetId()) + .orElseThrow(() -> new CustomRuntimeException( + ErrorCode.INQUIRY_NOT_FOUND)); + + InquiryAnswerNotifyVariablesDto dto = new InquiryAnswerNotifyVariablesDto( + inquiry.getTitle()); + + String alimTalkContent = template.getProcessedMessage("notify.inquiry.answered.alimtalk", + dto.toMap()); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(INQUIRY, INQUIRY) + ); + + NotifyEventSuccessMessageDto eventMessage = NotifyEventSuccessMessageDto.create(1, + targetPhoneNumber, alimTalkContent, + "", "0", btnUrls); + return template.getNotifyEventSuccessTemplate(INQUIRY_ANSWER_TEMPLATE_ID, eventMessage); + + } + + +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java new file mode 100644 index 00000000..08421d52 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/NotifyStrategy.java @@ -0,0 +1,9 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; + +public interface NotifyStrategy { + + NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event); +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java new file mode 100644 index 00000000..e91b9889 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/RefundNotifyStrategy.java @@ -0,0 +1,55 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.MYPAGE; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.REFUND_TEMPLATE_ID; + +import life.mosu.mosuserver.domain.refund.RefundJpaRepository; +import life.mosu.mosuserver.domain.refund.RefundNotifyProjection; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventSuccessMessageDto; +import life.mosu.mosuserver.infra.notify.dto.RefundNotifyVariablesDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component("RefundNotifyStrategy") +@RequiredArgsConstructor +public class RefundNotifyStrategy implements NotifyStrategy { + + private final NotifyEventTemplateGenerator template; + private final RefundJpaRepository refundJpaRepository; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId( + event.targetId()); + + //TODO: 가격 추가 + RefundNotifyVariablesDto dto = new RefundNotifyVariablesDto( + projection.paymentKey(), projection.examDate(), projection.schoolName(), + projection.paymentMethod().getName(), + projection.reason()); + + String alimTalkContent = template.getProcessedMessage( + "notify.refund.complete.alimtalk", + dto.toMap()); + + String smsContent = template.getProcessedMessage("notify.refund.complete.sms", + dto.toMap()); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(MYPAGE, MYPAGE) + ); + + NotifyEventSuccessMessageDto eventMessage = NotifyEventSuccessMessageDto.create(1, + targetPhoneNumber, alimTalkContent, + smsContent, "1", btnUrls); + return template.getNotifyEventSuccessTemplate(REFUND_TEMPLATE_ID + , eventMessage); + } + + +} diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java new file mode 100644 index 00000000..2696923a --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/notify/strategy/SignUpNotifyStrategy.java @@ -0,0 +1,40 @@ +package life.mosu.mosuserver.infra.notify.strategy; + +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.HOMEPAGE; +import static life.mosu.mosuserver.infra.notify.constant.NotifyConstants.SIGN_UP_TEMPLATE_ID; + +import java.util.Locale; +import life.mosu.mosuserver.infra.notify.NotifyEventTemplateGenerator; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrl; +import life.mosu.mosuserver.infra.notify.dto.NotifyButtonUrls; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventRequest; +import life.mosu.mosuserver.infra.notify.dto.NotifyEventSuccessMessageDto; +import lombok.RequiredArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +@Component("SignUpNotifyStrategy") +@RequiredArgsConstructor +public class SignUpNotifyStrategy implements NotifyStrategy { + + private final MessageSource messageSource; + private final NotifyEventTemplateGenerator template; + + @Override + public NotifyEventRequest apply(String targetPhoneNumber, NotifyEvent event) { + + String alimTalkContent = messageSource.getMessage("notify.signup.complete.alimtalk", null, + Locale.KOREA); + + NotifyButtonUrls btnUrls = NotifyButtonUrls.of( + NotifyButtonUrl.of(HOMEPAGE, HOMEPAGE) + ); + + NotifyEventSuccessMessageDto eventMessage = NotifyEventSuccessMessageDto.create( + 1, targetPhoneNumber, alimTalkContent, "", + "0", + btnUrls); + return template.getNotifyEventSuccessTemplate(SIGN_UP_TEMPLATE_ID, eventMessage); + } +} diff --git a/src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java b/src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java new file mode 100644 index 00000000..d495c486 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/infra/respository/BulkRepository.java @@ -0,0 +1,25 @@ +package life.mosu.mosuserver.infra.respository; + +import java.util.List; +import life.mosu.mosuserver.domain.applicationschool.ApplicationSchoolJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +public class BulkRepository { + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void saveAllApplicationSchools(List applicationSchools) { + String sql = """ + INSERT INTO application_school + (user_id, application_id, school_id, school_name, area, exam_date) + VALUES (?, ?, ?, ?, ?, ?) + """; + } + +} diff --git a/src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java b/src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java new file mode 100644 index 00000000..bc5c33d7 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/notify/NotifyEventListener.java @@ -0,0 +1,22 @@ +package life.mosu.mosuserver.presentation.notify; + +import life.mosu.mosuserver.application.notify.NotifyService; +import life.mosu.mosuserver.global.annotation.ReactiveEventListener; +import life.mosu.mosuserver.infra.notify.dto.NotifyEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NotifyEventListener { + + private final NotifyService notifyService; + + @ReactiveEventListener + public void notify(NotifyEvent event) { + notifyService.notify(event); + log.info("Notify event: {}", event); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dcf44621..33bfbe6b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,6 +46,9 @@ spring: redis: host: ${REDIS_HOST} port: ${VELKEY_PORT} + messages: + basename: messages + encoding: UTF-8 management: endpoints: @@ -70,3 +73,12 @@ toss: secret-key: test_sk_kYG57Eba3GYBMGeobgbLrpWDOxmA api: base-url: https://api.tosspayments.com/v1/payments + +alimtalk: + user-id: ${ALIMTALK_USER_ID} + api-key: ${ALIMTALK_API_KEY} + api: + base-url: ${ALIMTALK_URL} + +kakao: + channel-id: ${KAKAO_CHANNEL_ID} \ No newline at end of file diff --git a/src/main/resources/messages_ko.properties b/src/main/resources/messages_ko.properties new file mode 100644 index 00000000..87a51ce9 --- /dev/null +++ b/src/main/resources/messages_ko.properties @@ -0,0 +1,132 @@ +notify.exam.application.complete.alimtalk=\ +[\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5 \uC2E0\uCCAD\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!\n\n\ +\u25A0 \uACB0\uC81C\uBC88\uD638: #{paymentKey}\n\ +\u25A0 \uC751\uC2DC\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\ +\u25A0 \uB3C4\uC2DC\uB77D \uC2E0\uCCAD: #{lunch}\n\n\ +\uC2DC\uD5D8 1\uC8FC\uC77C \uC804, \uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4. +notify.exam.application.complete.sms=\ +[\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5 \uC2E0\uCCAD\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!\n\n\ +\u25A0 \uACB0\uC81C\uBC88\uD638: #{paymentKey}\n\ +\u25A0 \uC751\uC2DC\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\ +\u25A0 \uB3C4\uC2DC\uB77D \uC2E0\uCCAD: #{lunch}\n\n\ +\uC2DC\uD5D8 1\uC8FC\uC77C \uC804, \uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4. +notify.exam.oneweek.reminder.alimtalk=\ +[\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5\uC774 1\uC8FC\uC77C \uC55E\uC73C\uB85C \uB2E4\uAC00\uC654\uC2B5\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30* +notify.exam.oneweek.reminder.sms=\ +[\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5\uC774 1\uC8FC\uC77C \uC55E\uC73C\uB85C \uB2E4\uAC00\uC654\uC2B5\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30* +notify.exam.threeday.reminder.alimtalk=\ +[\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5 \uC751\uC2DC\uC77C 3\uC77C \uC804 \uC785\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30*\n\n\ + *\uC218\uD5D8\uD45C\uAC00 \uBC1C\uAE09\uB418\uC5C8\uC2B5\uB2C8\uB2E4.*\n\ +\uB9C8\uC774\uD398\uC774\uC9C0 > \uC2E0\uCCAD\uB0B4\uC5ED > \uC218\uD5D8\uD45C \uCD9C\uB825\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.exam.threeday.reminder.sms=\ + [\uBAA8\uC218] \uBAA8\uC758 \uC218\uB2A5 \uC751\uC2DC\uC77C 3\uC77C \uC804 \uC785\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30*\n\n\ + *\uC218\uD5D8\uD45C\uAC00 \uBC1C\uAE09\uB418\uC5C8\uC2B5\uB2C8\uB2E4.*\n\ +\uB9C8\uC774\uD398\uC774\uC9C0 > \uC2E0\uCCAD\uB0B4\uC5ED > \uC218\uD5D8\uD45C \uCD9C\uB825\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.exam.oneday.reminder.alimtalk=\ +[\uBAA8\uC218] \uB0B4\uC77C\uC740 \uBAA8\uC758 \uC218\uB2A5 \uC751\uC2DC\uC77C\uC785\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30*\n\n\ + *\uC218\uD5D8\uD45C\uAC00 \uBC1C\uAE09\uB418\uC5C8\uC2B5\uB2C8\uB2E4.*\n\ +\uB9C8\uC774\uD398\uC774\uC9C0 > \uC2E0\uCCAD\uB0B4\uC5ED > \uC218\uD5D8\uD45C \uCD9C\uB825\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.exam.oneday.reminder.sms=\ +[\uBAA8\uC218] \uB0B4\uC77C\uC740 \uBAA8\uC758 \uC218\uB2A5 \uC751\uC2DC\uC77C\uC785\uB2C8\uB2E4!\n\n\ +\u25A0 \uC2DC\uD5D8\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC218\uD5D8\uBC88\uD638: #{examinationNumber}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\n\ + *\uC2DC\uD5D8 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8*\n\ + - \uC624\uC804 08:00\uAE4C\uC9C0 \uC785\uC2E4 \uC644\uB8CC (\uC9C0\uAC01 \uC2DC \uBCC4\uB3C4\uB85C \uB9C8\uB828\uB41C \uB300\uAE30\uC2E4\uC5D0\uC11C \uB300\uAE30 \uD6C4 \uC785\uC7A5 \uAC00\uB2A5)\n\ + - \uC218\uD5D8\uD45C\uC640 \uC2E0\uBD84\uC99D \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uC9C0\uCC38\n\ + - \uC751\uC2DC\uD560 \uBAA8\uC758\uACE0\uC0AC \uD45C\uC9C0\uC5D0 \uC218\uD5D8\uBC88\uD638\uC640 \uC774\uB984 \uAE30\uC7AC\n\ + - \uB3C4\uC2DC\uB77D \uC2E0\uCCAD\uC790\uB294 \uD604\uC7A5 \uB3C4\uC2DC\uB77D \uC81C\uACF5\n\ + - \uAC10\uB3C5\uAD00 \uC9C0\uC2DC \uBBF8\uC774\uD589 \uC2DC \uD1F4\uC2E4 \uC870\uCE58 \uAC00\uB2A5\n\n\ + *\uC804\uCCB4 \uC720\uC758\uC0AC\uD56D \uAF2D \uC77D\uC5B4\uBCF4\uAE30*\n\n\ + *\uC218\uD5D8\uD45C\uAC00 \uBC1C\uAE09\uB418\uC5C8\uC2B5\uB2C8\uB2E4.*\n\ +\uB9C8\uC774\uD398\uC774\uC9C0 > \uC2E0\uCCAD\uB0B4\uC5ED > \uC218\uD5D8\uD45C \uCD9C\uB825\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.signup.complete.alimtalk=\ +[\uBAA8\uC218] \uD68C\uC6D0\uAC00\uC785\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\ +\uC9C0\uAE08\uBD80\uD130 \uBAA8\uC758\uC218\uB2A5 \uC2E0\uCCAD\uC744 \uC9C4\uD589\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n\ +\uC2E0\uCCAD \uB0B4\uC5ED\uC740 \uB9C8\uC774\uD398\uC774\uC9C0\uC5D0\uC11C \uD56D\uC2DC \uD655\uC778 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n\ +\uBAA8\uC218\uC640 \uD568\uAED8 \uC218\uB2A5\uC744 \uBBF8\uB9AC \uACBD\uD5D8\uD574\uBCF4\uC138\uC694!\n\ +\uCC44\uB110 \uCD94\uAC00\uD558\uACE0 \uC774 \uCC44\uB110\uC758 \uB9C8\uCF00\uD305 \uBA54\uC2DC\uC9C0 \uB4F1\uC744 \uCE74\uCE74\uC624\uD1A1\uC73C\uB85C \uBC1B\uAE30 +notify.inquiry.answered.alimtalk=\ +[\uBAA8\uC218] \uBB38\uC758\uD558\uC2E0 \uB0B4\uC6A9\uC5D0 \uB2F5\uBCC0\uC774 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\ +\uC81C\uBAA9: #{inquiryTitle}\n\n\ +\uB2F5\uBCC0\uC740 [\uBB38\uC758\uD558\uAE30 > \uB0B4 \uBB38\uC758\uAE00 \uC870\uD68C]\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.refund.complete.alimtalk=\ +[\uBAA8\uC218] \uD658\uBD88\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\ +\u25A0 \uACB0\uC81C\uBC88\uD638: #{paymentKey}\n\ +\u25A0 \uC751\uC2DC\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\ +\u25A0 \uD658\uBD88 \uAE08\uC561: #{refundAmount}\n\ +\u25A0 \uACB0\uC81C \uC218\uB2E8: #{paymentMethod}\n\ +\u25A0 \uCC98\uB9AC \uC0AC\uC720: #{reason}\n\n\ +\uC694\uCCAD\uD558\uC2E0 \uD658\uBD88\uC740 \uB0B4\uBD80 \uADDC\uC815\uC5D0 \uB530\uB77C \uCC98\uB9AC\uB418\uC5C8\uC73C\uBA70,\n\ +\uACB0\uC81C \uC218\uB2E8\uC744 \uD1B5\uD574 \uC601\uC5C5\uC77C \uAE30\uC900 3~7\uC77C \uC774\uB0B4 \uC785\uAE08\uB420 \uC608\uC815\uC785\uB2C8\uB2E4.\n\ +\uD658\uBD88 \uB0B4\uC5ED \uBC0F \uC2E0\uCCAD \uC815\uBCF4\uB294 \uB9C8\uC774\uD398\uC774\uC9C0\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. +notify.refund.complete.sms=\ +[\uBAA8\uC218] \uD658\uBD88\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\ +\u25A0 \uACB0\uC81C\uBC88\uD638: #{paymentKey}\n\ +\u25A0 \uC751\uC2DC\uC77C\uC790: #{examDate}\n\ +\u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\ +\u25A0 \uD658\uBD88 \uAE08\uC561: #{refundAmount}\n\ +\u25A0 \uACB0\uC81C \uC218\uB2E8: #{paymentMethod}\n\ +\u25A0 \uCC98\uB9AC \uC0AC\uC720: #{reason}\n\n\ +\uC694\uCCAD\uD558\uC2E0 \uD658\uBD88\uC740 \uB0B4\uBD80 \uADDC\uC815\uC5D0 \uB530\uB77C \uCC98\uB9AC\uB418\uC5C8\uC73C\uBA70,\n\ +\uACB0\uC81C \uC218\uB2E8\uC744 \uD1B5\uD574 \uC601\uC5C5\uC77C \uAE30\uC900 3~7\uC77C \uC774\uB0B4 \uC785\uAE08\uB420 \uC608\uC815\uC785\uB2C8\uB2E4.\n\ +\uD658\uBD88 \uB0B4\uC5ED \uBC0F \uC2E0\uCCAD \uC815\uBCF4\uB294 \uB9C8\uC774\uD398\uC774\uC9C0\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.