diff --git a/README.md b/README.md
index 165211360..97f5c2c7d 100644
--- a/README.md
+++ b/README.md
@@ -22,4 +22,41 @@
- 카카오 로그인 화면 만들기
- 버튼 클릭 시 로그인 화면으로 이동
- 액세스 토큰 얻기 성공 화면 만들기
+
+
+
+2단계 - 주문하기
+
+- 액세스 토큰으로부터 정보 추출하기
+ - KakaoAccountDTO 생성
+ - KakaoUserInfoDTO 생성
+ - KakaoService에 메서드 추가
+ - KakaoProperties에서 url 관리하기
+ - 액세스 토큰으로부터 이메일 뽑아와서 멤버로 저장하기
+ - JwtToken 생성하기
+ - KakaoLoginController 수정
+ - kakao_access_token.html 수정
+
+- 주문하기
+ - Order 엔티티 만들기
+ - OrderDTO 만들기
+ - OrderRequestDTO
+ - 주문 버튼 눌렀을 떄 넘겨줄 정보
+ - OrderResponseDTO
+ - 카카오톡 메시지 보낼 때 필요한 정보
+ - OrderRepository 만들기
+ - OrderService 만들기
+ - OrderController 만들기
+ - order_form 이동 시 예시 데이터 넣기
+ - Order가 생성되면 주문이 된 것이므로 수량 차감하기
+ - option_list.html 수정
+ - order_form으로 이동하는 버튼 만들기
+ - order_form 생성
+
+- 카카오톡 메시지 보내기
+ - KakaoService에 메서드 추가
+ - 카카오톡 메시지 보내는 메서드 추가
+ - 메시지 템플릿 생성
+ - OrderController 수정
+ - 주문 버튼을 눌렀을 때 메시지 보내도록 수정
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index e0aeaa97d..70e129b18 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.slf4j:slf4j-api:1.7.30'
compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
diff --git a/src/main/java/gift/config/KakaoProperties.java b/src/main/java/gift/config/KakaoProperties.java
index ec5ef6543..89caab49c 100644
--- a/src/main/java/gift/config/KakaoProperties.java
+++ b/src/main/java/gift/config/KakaoProperties.java
@@ -9,8 +9,11 @@ public record KakaoProperties(
String clientId,
String redirectUrl,
String authUrl,
- String tokenUrl
+ String tokenUrl,
+ String userInfoUrl,
+ String sendMessageUrl
) {
+
public String generateLoginUrl() {
return String.format("%s?response_type=code&client_id=%s&redirect_uri=%s",
authUrl, clientId, redirectUrl);
diff --git a/src/main/java/gift/controller/KakaoLoginController.java b/src/main/java/gift/controller/KakaoLoginController.java
index 8714e65b0..03534c74b 100644
--- a/src/main/java/gift/controller/KakaoLoginController.java
+++ b/src/main/java/gift/controller/KakaoLoginController.java
@@ -2,6 +2,7 @@
import gift.config.KakaoProperties;
+import gift.model.Member;
import gift.service.KakaoService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -33,7 +34,12 @@ public String kakaoAccessToken(@RequestParam(value = "code") String authorizatio
RedirectAttributes redirectAttributes) {
if (authorizationCode != null) {
String accessToken = kakaoService.getAccessToken(authorizationCode);
+ String email = kakaoService.getUserEmail(accessToken);
+ Member member = kakaoService.saveKakaoUser(email);
+ String jwtToken = kakaoService.generateToken(member.getEmail(), member.getRole());
redirectAttributes.addFlashAttribute("accessToken", accessToken);
+ redirectAttributes.addFlashAttribute("email", email);
+ redirectAttributes.addFlashAttribute("jwtToken", jwtToken);
return "redirect:/kakao/success";
}
String loginUrl = kakaoService.generateKakaoLoginUrl();
diff --git a/src/main/java/gift/controller/OrderController.java b/src/main/java/gift/controller/OrderController.java
new file mode 100644
index 000000000..ebd74918c
--- /dev/null
+++ b/src/main/java/gift/controller/OrderController.java
@@ -0,0 +1,58 @@
+package gift.controller;
+
+import gift.annotation.LoginMember;
+import gift.dto.OrderRequestDTO;
+import gift.dto.OrderResponseDTO;
+import gift.model.Member;
+import gift.service.KakaoService;
+import gift.service.OrderService;
+import jakarta.validation.Valid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@RequestMapping("/orders/{optionId}")
+public class OrderController {
+
+ private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
+
+ private final OrderService orderService;
+ private final KakaoService kakaoService;
+
+ public OrderController(OrderService orderService, KakaoService kakaoService) {
+ this.orderService = orderService;
+ this.kakaoService = kakaoService;
+ }
+
+ @GetMapping
+ public String showOrderForm(@PathVariable("optionId") Long optionId, Model model) {
+ OrderRequestDTO orderRequestDTO = new OrderRequestDTO(optionId, 1L, "임시 메시지", null);
+ model.addAttribute("orderRequestDTO", orderRequestDTO);
+ return "order_form";
+ }
+
+ @PostMapping
+ public String addOrder(@PathVariable("optionId") Long optionId,
+ @RequestBody @Valid OrderRequestDTO orderRequestDTO, @LoginMember Member member) {
+ if (member == null) {
+ return "redirect:/members/login";
+ }
+ OrderResponseDTO orderResponseDTO = orderService.createOrder(orderRequestDTO,
+ member.getEmail());
+ String accessToken = orderRequestDTO.accessToken();
+ try {
+ kakaoService.sendKakaoMessage(accessToken, orderResponseDTO);
+ } catch (Exception e) {
+ logger.error("카카오톡 메시지 전송 실패");
+ }
+ return "redirect:/admin/products";
+ }
+
+}
diff --git a/src/main/java/gift/dto/KakaoAccountDTO.java b/src/main/java/gift/dto/KakaoAccountDTO.java
new file mode 100644
index 000000000..485cd6d3d
--- /dev/null
+++ b/src/main/java/gift/dto/KakaoAccountDTO.java
@@ -0,0 +1,10 @@
+package gift.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record KakaoAccountDTO(
+ @JsonProperty("email")
+ String email
+) {
+
+}
diff --git a/src/main/java/gift/dto/KakaoUserInfoDTO.java b/src/main/java/gift/dto/KakaoUserInfoDTO.java
new file mode 100644
index 000000000..7398ea492
--- /dev/null
+++ b/src/main/java/gift/dto/KakaoUserInfoDTO.java
@@ -0,0 +1,12 @@
+package gift.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record KakaoUserInfoDTO(
+ @JsonProperty("id")
+ Long id,
+ @JsonProperty("kakao_account")
+ KakaoAccountDTO kakaoAccountDTO
+) {
+
+}
diff --git a/src/main/java/gift/dto/OptionSubtractQuantityDTO.java b/src/main/java/gift/dto/OptionSubtractQuantityDTO.java
deleted file mode 100644
index 45bf904c1..000000000
--- a/src/main/java/gift/dto/OptionSubtractQuantityDTO.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package gift.dto;
-
-import jakarta.validation.constraints.Min;
-import jakarta.validation.constraints.NotNull;
-
-public record OptionSubtractQuantityDTO(
- @NotNull(message = "차감할 수량을 입력해야 합니다.")
- @Min(value = 1, message = "차감할 수량은 0보다 커야 합니다.")
- Long subtractQuantity
-) {
-
-}
diff --git a/src/main/java/gift/dto/OrderRequestDTO.java b/src/main/java/gift/dto/OrderRequestDTO.java
new file mode 100644
index 000000000..2e60abb9c
--- /dev/null
+++ b/src/main/java/gift/dto/OrderRequestDTO.java
@@ -0,0 +1,10 @@
+package gift.dto;
+
+public record OrderRequestDTO(
+ Long optionId,
+ Long quantity,
+ String message,
+ String accessToken
+) {
+
+}
diff --git a/src/main/java/gift/dto/OrderResponseDTO.java b/src/main/java/gift/dto/OrderResponseDTO.java
new file mode 100644
index 000000000..91ca541c5
--- /dev/null
+++ b/src/main/java/gift/dto/OrderResponseDTO.java
@@ -0,0 +1,13 @@
+package gift.dto;
+
+import java.time.LocalDateTime;
+
+public record OrderResponseDTO(
+ Long id,
+ Long optionId,
+ Long quantity,
+ LocalDateTime orderDateTime,
+ String message
+) {
+
+}
diff --git a/src/main/java/gift/dto/TemplateObjectDTO.java b/src/main/java/gift/dto/TemplateObjectDTO.java
new file mode 100644
index 000000000..1df010281
--- /dev/null
+++ b/src/main/java/gift/dto/TemplateObjectDTO.java
@@ -0,0 +1,26 @@
+package gift.dto;
+
+import java.util.Map;
+import java.util.HashMap;
+
+public record TemplateObjectDTO(
+ String object_type,
+ String text,
+ Map link
+) {
+ public TemplateObjectDTO(Long id, Long optionId, Long quantity, String orderDateTime, String message) {
+ this(
+ "text",
+ String.format("주문 정보:\n주문 ID: %d\n옵션 ID: %d\n수량: %d\n주문 시간: %s\n메시지: %s",
+ id, optionId, quantity, orderDateTime, message),
+ createLink()
+ );
+ }
+
+ private static Map createLink() {
+ Map link = new HashMap<>();
+ link.put("web_url", "http://localhost:8080/admin/products");
+ link.put("mobile_web_url", "http://localhost:8080/admin/products");
+ return link;
+ }
+}
diff --git a/src/main/java/gift/model/Order.java b/src/main/java/gift/model/Order.java
new file mode 100644
index 000000000..cb417c981
--- /dev/null
+++ b/src/main/java/gift/model/Order.java
@@ -0,0 +1,75 @@
+package gift.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "orders")
+public class Order {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne
+ @JoinColumn(name = "option_id", nullable = false)
+ private Option option;
+
+ @Column(name = "quantity", nullable = false)
+ private Long quantity;
+
+ @Column(name = "order_date_time", nullable = false)
+ private LocalDateTime orderDateTime;
+
+ @Column(name = "message")
+ private String message;
+
+ @ManyToOne
+ @JoinColumn(name = "member_id", nullable = false)
+ private Member member;
+
+ protected Order() {
+ }
+
+ public Order(Long id, Option option, Long quantity, LocalDateTime orderDateTime, String message,
+ Member member) {
+ this.id = id;
+ this.option = option;
+ this.quantity = quantity;
+ this.orderDateTime = orderDateTime;
+ this.message = message;
+ this.member = member;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public Option getOption() {
+ return option;
+ }
+
+ public Long getQuantity() {
+ return quantity;
+ }
+
+ public LocalDateTime getOrderDateTime() {
+ return orderDateTime;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Member getMember() {
+ return member;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/gift/repository/OrderRepository.java b/src/main/java/gift/repository/OrderRepository.java
new file mode 100644
index 000000000..28c6ddc30
--- /dev/null
+++ b/src/main/java/gift/repository/OrderRepository.java
@@ -0,0 +1,8 @@
+package gift.repository;
+
+import gift.model.Order;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface OrderRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/gift/service/KakaoService.java b/src/main/java/gift/service/KakaoService.java
index 17ec05a90..57db9a887 100644
--- a/src/main/java/gift/service/KakaoService.java
+++ b/src/main/java/gift/service/KakaoService.java
@@ -1,11 +1,23 @@
package gift.service;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import gift.config.KakaoProperties;
import gift.dto.KakaoAccessTokenDTO;
+import gift.dto.KakaoUserInfoDTO;
+import gift.dto.MemberDTO;
+import gift.dto.OrderResponseDTO;
+import gift.dto.TemplateObjectDTO;
+import gift.model.Member;
+import gift.repository.MemberRepository;
+import gift.util.JwtUtil;
import java.net.URI;
+import java.util.Random;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestClient;
@@ -14,9 +26,14 @@ public class KakaoService {
private final KakaoProperties kakaoProperties;
private final RestClient restClient = RestClient.builder().build();
+ private final MemberRepository memberRepository;
+ private final JwtUtil jwtUtil;
- public KakaoService(KakaoProperties kakaoProperties) {
+ public KakaoService(KakaoProperties kakaoProperties, MemberRepository memberRepository,
+ JwtUtil jwtUtil) {
this.kakaoProperties = kakaoProperties;
+ this.memberRepository = memberRepository;
+ this.jwtUtil = jwtUtil;
}
public String generateKakaoLoginUrl() {
@@ -44,4 +61,79 @@ private LinkedMultiValueMap createBody(String authorizationCode)
body.add("code", authorizationCode);
return body;
}
+
+ public String getUserEmail(String accessToken) {
+ String url = kakaoProperties.userInfoUrl();
+ ResponseEntity response = restClient.get()
+ .uri(URI.create(url))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
+ .retrieve()
+ .toEntity(KakaoUserInfoDTO.class);
+ KakaoUserInfoDTO kakaoUserInfoDTO = response.getBody();
+ return kakaoUserInfoDTO.kakaoAccountDTO().email();
+ }
+
+ @Transactional
+ public Member saveKakaoUser(String email) {
+ Member member = memberRepository.findByEmail(email);
+ if (member == null) {
+ String name = email.split("@")[0];
+ String password = generateRandomPassword();
+ MemberDTO memberDTO = new MemberDTO(name, email, password);
+ member = new Member(null, memberDTO.name(), memberDTO.email(), memberDTO.password(),
+ "user");
+ memberRepository.save(member);
+ }
+ return member;
+ }
+
+ public String generateToken(String email, String role) {
+ String jwtToken = jwtUtil.generateToken(email, role);
+ return jwtToken;
+ }
+
+ public void sendKakaoMessage(String accessToken, OrderResponseDTO orderResponseDTO) {
+ String url = kakaoProperties.sendMessageUrl();
+ String templateObjectJson = getTemplateObjectJson(orderResponseDTO);
+ final LinkedMultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("template_object", templateObjectJson);
+ ResponseEntity response = restClient.post()
+ .uri(URI.create(url))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .body(body)
+ .retrieve()
+ .toEntity(String.class);
+ }
+
+ private static String getTemplateObjectJson(OrderResponseDTO orderResponseDTO) {
+ TemplateObjectDTO templateObjectDTO = new TemplateObjectDTO(
+ orderResponseDTO.id(),
+ orderResponseDTO.optionId(),
+ orderResponseDTO.quantity(),
+ orderResponseDTO.orderDateTime().toString(),
+ orderResponseDTO.message()
+ );
+ ObjectMapper objectMapper = new ObjectMapper();
+ String templateObjectJson;
+ try {
+ templateObjectJson = objectMapper.writeValueAsString(templateObjectDTO);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("JSON으로 변환에 실패했습니다.");
+ }
+ return templateObjectJson;
+ }
+
+ private String generateRandomPassword() {
+ int leftLimit = 48;
+ int rightLimit = 122;
+ int targetStringLength = 20;
+ Random random = new Random();
+ String generatedPassword = random.ints(leftLimit, rightLimit + 1)
+ .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
+ .limit(targetStringLength)
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ return generatedPassword;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/gift/service/OptionService.java b/src/main/java/gift/service/OptionService.java
index d0cd6f8c0..40c775b3e 100644
--- a/src/main/java/gift/service/OptionService.java
+++ b/src/main/java/gift/service/OptionService.java
@@ -1,7 +1,6 @@
package gift.service;
import gift.dto.OptionDTO;
-import gift.dto.OptionSubtractQuantityDTO;
import gift.model.Option;
import gift.model.Product;
import gift.repository.OptionRepository;
@@ -61,9 +60,8 @@ public void deleteOption(Long optionId, Long productId) {
}
@Transactional
- public void subtractQuantity(Long optionId, OptionSubtractQuantityDTO optionSubtractQuantityDTO) {
+ public void subtractQuantity(Long optionId, Long subtractQuantity) {
Option option = optionRepository.findById(optionId).orElse(null);
- Long subtractQuantity = optionSubtractQuantityDTO.subtractQuantity();
option.subtractQuantity(subtractQuantity);
optionRepository.save(option);
}
diff --git a/src/main/java/gift/service/OrderService.java b/src/main/java/gift/service/OrderService.java
new file mode 100644
index 000000000..26d2e3066
--- /dev/null
+++ b/src/main/java/gift/service/OrderService.java
@@ -0,0 +1,62 @@
+package gift.service;
+
+
+import gift.dto.OrderRequestDTO;
+import gift.dto.OrderResponseDTO;
+import gift.model.Member;
+import gift.model.Option;
+import gift.model.Order;
+import gift.repository.OrderRepository;
+import java.time.LocalDateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+public class OrderService {
+
+ private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
+
+ private final OrderRepository orderRepository;
+ private final OptionService optionService;
+ private final MemberService memberService;
+ private final WishlistService wishlistService;
+
+ public OrderService(OrderRepository orderRepository, OptionService optionService,
+ MemberService memberService,
+ WishlistService wishlistService) {
+ this.orderRepository = orderRepository;
+ this.optionService = optionService;
+ this.memberService = memberService;
+ this.wishlistService = wishlistService;
+ }
+
+ @Transactional
+ public OrderResponseDTO createOrder(OrderRequestDTO orderRequestDTO, String email) {
+ Option option = optionService.findOptionById(orderRequestDTO.optionId());
+ Member member = memberService.findMemberByEmail(email);
+ Long quantity = orderRequestDTO.quantity();
+ optionService.subtractQuantity(option.getId(), quantity);
+ Order order = new Order(null, option, quantity, LocalDateTime.now(),
+ orderRequestDTO.message(), member);
+ orderRepository.save(order);
+ removeFromWishlist(member.getEmail(), option.getProduct().getId());
+ OrderResponseDTO orderResponseDTO = new OrderResponseDTO(order.getId(),
+ order.getOption().getId(), order.getQuantity(), order.getOrderDateTime(),
+ order.getMessage());
+ return orderResponseDTO;
+ }
+
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public void removeFromWishlist(String email, Long productId) {
+ try {
+ wishlistService.removeWishlist(email, productId);
+ } catch (Exception e) {
+ logger.error("주문은 성공했지만 위시리스트에서의 삭제는 실패했습니다.");
+ }
+ }
+
+}
diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java
index 1314ce4ba..312ed2f79 100644
--- a/src/main/java/gift/service/ProductService.java
+++ b/src/main/java/gift/service/ProductService.java
@@ -53,7 +53,8 @@ public void updateProduct(ProductDTO productDTO, Long id) {
Product existingProduct = productRepository.findById(id).orElse(null);
if (existingProduct != null) {
Category category = categoryService.findCategoryById(productDTO.categoryId());
- existingProduct.updateProduct(productDTO.name(),productDTO.price(),category,productDTO.imageUrl());
+ existingProduct.updateProduct(productDTO.name(), productDTO.price(), category,
+ productDTO.imageUrl());
productRepository.save(existingProduct);
}
}
diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql
index c4370a176..ee50664d6 100644
--- a/src/main/resources/db/schema.sql
+++ b/src/main/resources/db/schema.sql
@@ -40,4 +40,16 @@ CREATE TABLE options
product_id BIGINT NOT NULL,
FOREIGN KEY (product_id) REFERENCES product (id),
UNIQUE (product_id, name)
+);
+
+CREATE TABLE orders
+(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ option_id BIGINT NOT NULL,
+ quantity BIGINT NOT NULL,
+ order_date_time TIMESTAMP NOT NULL,
+ message VARCHAR(255),
+ member_id BIGINT NOT NULL,
+ FOREIGN KEY (option_id) REFERENCES options (id),
+ FOREIGN KEY (member_id) REFERENCES member (id)
);
\ No newline at end of file
diff --git a/src/main/resources/templates/kakao_access_token.html b/src/main/resources/templates/kakao_access_token.html
index 4df82dcd2..e3847f19c 100644
--- a/src/main/resources/templates/kakao_access_token.html
+++ b/src/main/resources/templates/kakao_access_token.html
@@ -2,29 +2,49 @@
- 카카오 액세스 토큰 얻기
+ 카카오 로그인 성공
-카카오 액세스 토큰 얻기
-액세스 토큰:
-
+카카오 로그인 성공
+ 카카오 로그인 성공
+
+
+
+상품 목록으로 이동
\ No newline at end of file
diff --git a/src/main/resources/templates/login_success.html b/src/main/resources/templates/login_success.html
index a63d60f67..d73a7a1df 100644
--- a/src/main/resources/templates/login_success.html
+++ b/src/main/resources/templates/login_success.html
@@ -6,7 +6,7 @@
+
+
+주문하기
+
+
+