Skip to content

Commit

Permalink
feat: 알림톡 기능 구현 / 예약등록 알림톡 적용 (#284)
Browse files Browse the repository at this point in the history
sejineer authored Mar 5, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 157172d commit 90cb35c
Showing 13 changed files with 216 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.shallwe.domain.auth.dto.request;

import com.shallwe.domain.auth.dto.MessageMapping;
import com.shallwe.global.infrastructure.sms.dto.MessageMapping;
import lombok.Builder;
import lombok.Data;

Original file line number Diff line number Diff line change
@@ -28,8 +28,8 @@
import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ExperienceGiftServiceImpl implements ExperienceGiftService {

Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import com.shallwe.domain.common.Status;
import com.shallwe.domain.experiencegift.domain.ExperienceGift;
import com.shallwe.domain.shopowner.domain.ShopOwner;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@@ -13,7 +14,12 @@
public interface ExperienceGiftRepository extends JpaRepository<ExperienceGift, Long>, ExperienceGiftQuerydslRepository{

List<ExperienceGift> findByTitleContainsAndStatus(String title,Status status);

List<ExperienceGift> findByShopOwnerIdAndStatus(Long id, Status status);

Optional<ExperienceGift> findByIdAndShopOwner(Long experienceGiftId, ShopOwner shopOwner);

@EntityGraph(attributePaths = {"shopOwner"})
Optional<ExperienceGift> findExperienceGiftById(Long id);

}
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
public interface ReservationManipulationService {

List<ReservationResponse> addOwnerReservation(OwnerReservationCreate ownerReservationCreate, UserPrincipal userPrincipal);
ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal);
ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal) throws Exception;
ReservationResponse updateReservation(UpdateReservationReq updateReq, UserPrincipal userPrincipal);
DeleteReservationRes deleteReservation(Long id);

Original file line number Diff line number Diff line change
@@ -17,16 +17,14 @@
import com.shallwe.domain.reservation.exception.InvalidReservationException;
import com.shallwe.domain.reservation.exception.InvalidSenderException;
import com.shallwe.domain.reservation.exception.InvalidReceiverException;
import com.shallwe.domain.shopowner.domain.ShopOwner;
import com.shallwe.domain.shopowner.domain.repository.ShopOwnerRepository;
import com.shallwe.domain.shopowner.exception.InvalidShopOwnerException;
import com.shallwe.domain.user.domain.User;
import com.shallwe.domain.user.domain.repository.UserRepository;
import com.shallwe.global.config.security.token.UserPrincipal;

import java.util.List;
import java.util.stream.Collectors;

import com.shallwe.global.infrastructure.sms.NaverSmsClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -38,33 +36,30 @@ public class ReservationManipulationServiceImpl implements ReservationManipulati

private final ReservationRepository reservationRepository;
private final ExperienceGiftRepository experienceGiftRepository;
private final ShopOwnerRepository shopOwnerRepository;
private final UserRepository userRepository;
private final NaverSmsClient naverSmsClient;

@Transactional
public List<ReservationResponse> addOwnerReservation(OwnerReservationCreate ownerReservationCreate, UserPrincipal userPrincipal) {
ExperienceGift experienceGift = experienceGiftRepository.findById(ownerReservationCreate.getExperienceGiftId())
.orElseThrow(ExperienceGiftNotFoundException::new);

ShopOwner owner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);

List<Reservation> reservations = OwnerReservationCreate.toEntityForOwner(ownerReservationCreate, experienceGift, owner);
List<Reservation> reservations = OwnerReservationCreate.toEntityForOwner(ownerReservationCreate, experienceGift);
return reservations.stream()
.map(reservationRepository::save)
.map(ReservationResponse::toDtoOwner)
.collect(Collectors.toList());
}

@Transactional
public ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal) {
public ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal) throws Exception {
User sender = userRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidSenderException::new);

User receiver = userRepository.findByPhoneNumberAndStatus(reservationRequest.getPhoneNumber(), Status.ACTIVE)
.orElseThrow(InvalidReceiverException::new);

ExperienceGift experienceGift = experienceGiftRepository.findById(reservationRequest.getExperienceGiftId())
ExperienceGift experienceGift = experienceGiftRepository.findExperienceGiftById(reservationRequest.getExperienceGiftId())
.orElseThrow(ExperienceGiftNotFoundException::new);

Reservation reservation = reservationRepository.findByDateAndTimeAndExperienceGift(
@@ -74,6 +69,7 @@ public ReservationResponse addUserReservation(UserReservationCreate reservationR
if (reservation.getReservationStatus().equals(WAITING)) {
reservation.updateStatus(ReservationStatus.BOOKED);
reservation.updateUserReservationRequest(reservationRequest, sender, receiver);
naverSmsClient.sendReservationApply(sender, receiver, experienceGift, reservation);
experienceGift.addReservationCount();
} else {
throw new InvalidReservationException();
Original file line number Diff line number Diff line change
@@ -37,12 +37,6 @@ public class Reservation extends BaseEntity {
@Schema(description = "선물 ID")
private ExperienceGift experienceGift;

@ManyToOne
@JoinColumn(name = "owner_id", nullable = false)
@Schema(description = "사장ID")
private ShopOwner owner;


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id")
@Schema(description = "보내는이 ID")
@@ -82,9 +76,8 @@ public class Reservation extends BaseEntity {
private List<MemoryPhoto> memoryPhotos = new ArrayList<>();

@Builder
public Reservation(ExperienceGift experienceGift, ShopOwner owner, LocalDate date, LocalTime time, ReservationStatus reservationStatus) {
public Reservation(ExperienceGift experienceGift, LocalDate date, LocalTime time, ReservationStatus reservationStatus) {
this.experienceGift = experienceGift;
this.owner = owner;
this.date = date;
this.time = time;
this.reservationStatus = reservationStatus;
@@ -107,4 +100,5 @@ public void updateUserReservationRequest(UserReservationCreate reservationReques
public void updateStatus(ReservationStatus status) {
this.reservationStatus = status;
}

}
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ public class OwnerReservationCreate {
private Map<LocalDate, List<LocalTime>> dateTimeMap;

public static List<Reservation> toEntityForOwner(OwnerReservationCreate ownerReservationCreate,
ExperienceGift experienceGift, ShopOwner owner) {
ExperienceGift experienceGift) {
List<Reservation> reservations = new ArrayList<>();

for (Map.Entry<LocalDate, List<LocalTime>> entry : ownerReservationCreate.getDateTimeMap()
@@ -42,7 +42,6 @@ public static List<Reservation> toEntityForOwner(OwnerReservationCreate ownerRes
.experienceGift(experienceGift)
.date(date)
.time(time)
.owner(owner)
.reservationStatus(WAITING)
.build();
reservations.add(toEntity);
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
import com.shallwe.global.utils.AwsS3ImageUrlUtil;

import java.util.Optional;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -16,51 +17,48 @@
@Builder
public class ReservationResponse {

private Long id;
private Long senderId;
private Long ownerId;
private String sender;
private Long persons;
private String date;
private String time;
private Long experienceGiftId;
private String receiver;
private String phoneNumber;
private String invitationImageURL;
private String invitationComment;
private ReservationStatus reservationStatus;
private Long id;
private Long senderId;
private String sender;
private Long persons;
private String date;
private String time;
private Long experienceGiftId;
private String receiver;
private String phoneNumber;
private String invitationImageURL;
private String invitationComment;
private ReservationStatus reservationStatus;

public static ReservationResponse toDtoUser(Reservation reservation) {
ReservationResponseBuilder builder = ReservationResponse.builder()
.id(reservation.getId())
.experienceGiftId(reservation.getExperienceGift().getId())
.senderId(Optional.ofNullable(reservation.getSender()).map(User::getId).orElse(null))
.ownerId(reservation.getOwner().getId())
.sender(Optional.ofNullable(reservation.getSender()).map(User::getName).orElse(null))
.persons(Optional.ofNullable(reservation.getPersons()).orElse(null))
.date(reservation.getDate().toString())
.time(reservation.getTime().toString())
.phoneNumber(Optional.ofNullable(reservation.getPhoneNumber()).orElse(null))
.receiver(Optional.ofNullable(reservation.getReceiver()).map(User::getName).orElse(null))
.invitationImageURL(
Optional.ofNullable(reservation.getInvitationImg()).map(AwsS3ImageUrlUtil::toUrl)
.orElse(null))
.invitationComment(Optional.ofNullable(reservation.getInvitationComment()).orElse(null))
.reservationStatus(reservation.getReservationStatus());
public static ReservationResponse toDtoUser(Reservation reservation) {
ReservationResponseBuilder builder = ReservationResponse.builder()
.id(reservation.getId())
.experienceGiftId(reservation.getExperienceGift().getId())
.senderId(Optional.ofNullable(reservation.getSender()).map(User::getId).orElse(null))
.sender(Optional.ofNullable(reservation.getSender()).map(User::getName).orElse(null))
.persons(Optional.ofNullable(reservation.getPersons()).orElse(null))
.date(reservation.getDate().toString())
.time(reservation.getTime().toString())
.phoneNumber(Optional.ofNullable(reservation.getPhoneNumber()).orElse(null))
.receiver(Optional.ofNullable(reservation.getReceiver()).map(User::getName).orElse(null))
.invitationImageURL(
Optional.ofNullable(reservation.getInvitationImg()).map(AwsS3ImageUrlUtil::toUrl)
.orElse(null))
.invitationComment(Optional.ofNullable(reservation.getInvitationComment()).orElse(null))
.reservationStatus(reservation.getReservationStatus());

return builder.build();
}
return builder.build();
}

public static ReservationResponse toDtoOwner(Reservation reservation) {
ReservationResponseBuilder builder = ReservationResponse.builder()
.id(reservation.getId())
.experienceGiftId(reservation.getExperienceGift().getId())
.ownerId(reservation.getOwner().getId())
.date(reservation.getDate().toString())
.time(reservation.getTime().toString())
.reservationStatus(reservation.getReservationStatus());
public static ReservationResponse toDtoOwner(Reservation reservation) {
ReservationResponseBuilder builder = ReservationResponse.builder()
.id(reservation.getId())
.experienceGiftId(reservation.getExperienceGift().getId())
.date(reservation.getDate().toString())
.time(reservation.getTime().toString())
.reservationStatus(reservation.getReservationStatus());

return builder.build();
}
return builder.build();
}

}
}
Original file line number Diff line number Diff line change
@@ -126,7 +126,7 @@ public ResponseCustom<List<ReservationResponse>> getCurrentGiftReservation(
public ResponseCustom<ReservationResponse> createUserReservation(
@Parameter(description = "예약 요청을 확인해주세요.", required = true) @RequestBody UserReservationCreate reservationRequest,
@Parameter(description = "AccessToken 을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal
) {
) throws Exception {
return ResponseCustom.CREATED(
reservationManipulationService.addUserReservation(reservationRequest, userPrincipal));
}
Original file line number Diff line number Diff line change
@@ -40,9 +40,6 @@ public class ShopOwner extends BaseEntity {
@Column(name = "bankbook")
private String bankbook;

@OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
private List<Reservation> reservationList;

public void changePassword(String password) {
this.password = password;
}
@@ -60,15 +57,14 @@ public void updateBankbook(String bankbook) {
}

@Builder
public ShopOwner(String name, String phoneNumber, String password, Boolean marketingConsent, String identification, String businessRegistration, String bankbook, List<Reservation> reservationList) {
public ShopOwner(String name, String phoneNumber, String password, Boolean marketingConsent, String identification, String businessRegistration, String bankbook) {
this.name = name;
this.phoneNumber = phoneNumber;
this.password = password;
this.marketingConsent = marketingConsent;
this.identification = identification;
this.businessRegistration = businessRegistration;
this.bankbook = bankbook;
this.reservationList = reservationList;
}

}
}
143 changes: 131 additions & 12 deletions src/main/java/com/shallwe/global/infrastructure/sms/NaverSmsClient.java
Original file line number Diff line number Diff line change
@@ -3,9 +3,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.shallwe.domain.auth.domain.VerificationCode;
import com.shallwe.domain.auth.domain.repository.VerificationCodeRepository;
import com.shallwe.domain.auth.dto.MessageMapping;
import com.shallwe.domain.experiencegift.domain.ExperienceGift;
import com.shallwe.domain.reservation.domain.Reservation;
import com.shallwe.domain.user.domain.User;
import com.shallwe.global.infrastructure.sms.dto.MessageMapping;
import com.shallwe.domain.auth.dto.request.NaverCloudSmsReq;
import com.shallwe.domain.auth.dto.request.ValidVerificationCodeReq;
import com.shallwe.global.infrastructure.sms.dto.AlimTalkReq;
import com.shallwe.global.infrastructure.sms.exception.InvalidPhoneNumberException;
import com.shallwe.global.infrastructure.sms.exception.InvalidVerificationCodeException;
import com.shallwe.global.infrastructure.sms.exception.TimeOutException;
@@ -22,19 +26,20 @@
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Random;
import java.util.*;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NaverSmsClient implements SmsClient {

@Value("${sms.naver-cloud.service-id}")
private String SERVICE_ID;
@Value("${sms.naver-cloud.sms-service-id}")
private String SMS_SERVICE_ID;

@Value("${sms.naver-cloud.biztalk-service-id}")
private String BIZTALK_SERVICE_ID;

@Value("${sms.naver-cloud.access-key}")
private String ACCESS_KEY;
@@ -51,7 +56,8 @@ public class NaverSmsClient implements SmsClient {
@Transactional
public SmsResponseDto send(String receivePhoneNumber) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String signature = makeSignature(timestamp);
String url = "/sms/v2/services/" + SMS_SERVICE_ID + "/messages";
String signature = makeSignature(timestamp, url);

String code = generateRandomCode();

@@ -87,7 +93,7 @@ public SmsResponseDto send(String receivePhoneNumber) throws Exception {
.baseUrl("https://sens.apigw.ntruss.com/sms/v2/services")
.build();

return restClient.post().uri("/" + SERVICE_ID + "/messages")
return restClient.post().uri("/" + SMS_SERVICE_ID + "/messages")
.contentType(MediaType.APPLICATION_JSON)
.header("x-ncp-apigw-timestamp", timestamp)
.header("x-ncp-iam-access-key", ACCESS_KEY)
@@ -97,6 +103,120 @@ public SmsResponseDto send(String receivePhoneNumber) throws Exception {
.body(SmsResponseDto.class);
}

public SmsResponseDto sendReservationApply(User sender, User receiver, ExperienceGift experienceGift, Reservation reservation) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String url = "/alimtalk/v2/services/" + BIZTALK_SERVICE_ID + "/messages";
String signature = makeSignature(timestamp, url);

RestClient restClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.baseUrl("https://sens.apigw.ntruss.com/alimtalk/v2")
.defaultHeaders(header -> {
header.set("Content-Type", "application/json");
header.set("x-ncp-apigw-timestamp", timestamp);
header.set("x-ncp-iam-access-key", ACCESS_KEY);
header.set("x-ncp-apigw-signature-v2", signature);
})
.build();

NumberFormat format = NumberFormat.getNumberInstance();
String price = format.format(experienceGift.getPrice());
String account = experienceGift.getShopOwner().getBankbook();
String date = reservation.getDate().toString();
String time = reservation.getTime().toString();
String receiveUserName = receiver.getName();
String productName = experienceGift.getTitle();
String persons = reservation.getPersons().toString() + "명";

List<MessageMapping> messages = new ArrayList<>();
messages.add(MessageMapping.builder()
.to(receiver.getPhoneNumber())
.content("[셸위]\n" +
"예약이 접수되었습니다\n" +
"아래 계좌로 입금이 확인되면 예약확정과 함께 초대장이 발송됩니다\n" +
"\n" +
"\uD83D\uDCCC 금액: " + price + "원\n" +
"입금계좌: " + account + "\n" +
"\n" +
"예약날짜: " + date + "\n" +
"예약시간: " + time + "\n" +
"수취인: " + receiveUserName + "\n" +
"상품명: " + productName +"\n" +
"옵션: " + persons)
.build());

AlimTalkReq alimTalkReq = AlimTalkReq.builder()
.plusFriendId("@shallwee")
.templateCode("reservationApply")
.messages(messages)
.build();

ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(alimTalkReq);

return restClient.post()
.uri("/services/" + BIZTALK_SERVICE_ID + "/messages")
.body(body)
.retrieve()
.body(SmsResponseDto.class);
}

public SmsResponseDto sendInvitation(final User sender, final User receiver, final ExperienceGift experienceGift,
final Reservation reservation) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String url = "/alimtalk/v2/services/" + BIZTALK_SERVICE_ID + "/messages";
String signature = makeSignature(timestamp, url);

RestClient restClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.baseUrl("https://sens.apigw.ntruss.com/alimtalk/v2")
.defaultHeaders(header -> {
header.set("Content-Type", "application/json");
header.set("x-ncp-apigw-timestamp", timestamp);
header.set("x-ncp-iam-access-key", ACCESS_KEY);
header.set("x-ncp-apigw-signature-v2", signature);
})
.build();

String sendUserName = sender.getName();
String date = reservation.getDate().toString();
String time = reservation.getTime().toString();
String receiveUserName = receiver.getName();
String productName = experienceGift.getTitle();
String persons = reservation.getPersons().toString() + "명";

List<MessageMapping> messages = new ArrayList<>();
messages.add(MessageMapping.builder()
.to(receiver.getPhoneNumber())
.content("[셸위]\n" +
sendUserName + "님이 초대장을 보냈어요!\uD83C\uDF81\n" +
"\n" +
"예약날짜: " + date + "\n" +
"예약시간: " + time + "\n" +
"수취인: " + receiveUserName + "\n" +
"상품명: " + productName + "\n" +
"옵션: " + persons + "\n" +
"\n" +
"따뜻한 마음이 담긴 선물을\n" +
"지금 바로 셸위 어플에서 확인해보세요\uD83E\uDD70")
.build());

AlimTalkReq alimTalkReq = AlimTalkReq.builder()
.plusFriendId("@shallwee")
.templateCode("invitation")
.messages(messages)
.build();

ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(alimTalkReq);

return restClient.post()
.uri("/services/" + BIZTALK_SERVICE_ID + "/messages")
.body(body)
.retrieve()
.body(SmsResponseDto.class);
}

@Transactional
public Message validVerificationCode(ValidVerificationCodeReq validVerificationCodeReq) {
LocalDateTime time = LocalDateTime.now();
@@ -122,11 +242,10 @@ public Message validVerificationCode(ValidVerificationCodeReq validVerificationC
.build();
}

private String makeSignature(String timestamp) throws Exception {
public String makeSignature(String timestamp, String url) throws Exception {
String space = " ";
String newLine = "\n";
String method = "POST";
String url = "/sms/v2/services/" + SERVICE_ID + "/messages";

String message = new StringBuilder()
.append(method)
@@ -146,7 +265,7 @@ private String makeSignature(String timestamp) throws Exception {
return Base64.getEncoder().encodeToString(rawHmac);
}

private String generateRandomCode() {
public String generateRandomCode() {
return String.format("%06d", new Random().nextInt(999999));
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.shallwe.global.infrastructure.sms.dto;

import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
public class AlimTalkReq {

private String plusFriendId;
private String templateCode;
private List<MessageMapping> messages;

@Builder
public AlimTalkReq(String plusFriendId, String templateCode, List<MessageMapping> messages) {
this.plusFriendId = plusFriendId;
this.templateCode = templateCode;
this.messages = messages;
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shallwe.domain.auth.dto;
package com.shallwe.global.infrastructure.sms.dto;

import lombok.Builder;
import lombok.Data;

0 comments on commit 90cb35c

Please sign in to comment.