Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

전남대 BE_김보민 5주차 과제 (2단계) #373

Open
wants to merge 33 commits into
base: kbm05haruin30
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
60fe159
docs : 2단계 주문하기 구현할 기능 목록 정리
KBM05haruin30 Jul 28, 2024
6997d12
feat : 액세스 토큰으로부터 정보 받기 위한 DTO 생성
KBM05haruin30 Jul 28, 2024
0640ac4
feat : 액세스 토큰으로부터 이메일 추출
KBM05haruin30 Jul 28, 2024
b23b8ae
feat : 이메일 사용하여 멤버에 저장
KBM05haruin30 Jul 28, 2024
cff38b6
feat : jwtToken 만들기
KBM05haruin30 Jul 28, 2024
fef7ea6
feat : KakaoProperties에서 url 관리
KBM05haruin30 Jul 28, 2024
e6730f2
feat : KakaoLoginController 수정
KBM05haruin30 Jul 28, 2024
2b9840f
feat : kakao_access_token.html 수정
KBM05haruin30 Jul 28, 2024
0795b1d
feat : authToken에서 jwtToken으로 이름 변경으로 인한 수정
KBM05haruin30 Jul 28, 2024
5ae4719
feat : Order 엔티티 생성
KBM05haruin30 Jul 28, 2024
c16fa52
feat : orders 테이블 생성
KBM05haruin30 Jul 28, 2024
c02d103
feat : Order 관련 DTO 생성
KBM05haruin30 Jul 28, 2024
01c1d33
feat : OrderRepository 생성
KBM05haruin30 Jul 28, 2024
29f1c77
feat : OptionSubtractQuantityDTO 제거
KBM05haruin30 Jul 28, 2024
16dd27f
feat : OrderService 생성
KBM05haruin30 Jul 28, 2024
96645ac
feat : OrderController 생성
KBM05haruin30 Jul 28, 2024
493847c
feat : 주문하기 버튼 생성
KBM05haruin30 Jul 28, 2024
00c15b8
feat : order_form 만들기
KBM05haruin30 Jul 28, 2024
ea2c247
feat : 템플릿 만들기
KBM05haruin30 Jul 28, 2024
15f57aa
feat : 카카오톡 메시지 보내는 메서드 추가
KBM05haruin30 Jul 28, 2024
1312eff
feat : url은 KakaoProperties에서 관리
KBM05haruin30 Jul 28, 2024
60c8919
feat : OrderController 수정
KBM05haruin30 Jul 28, 2024
9c13918
feat : 잘못된 코드 수정
KBM05haruin30 Jul 28, 2024
c3ad5d1
refactor : 코드 수정
KBM05haruin30 Jul 28, 2024
5408450
feat : password 랜덤하게 생성
KBM05haruin30 Jul 28, 2024
d8bb7dd
feat : TemplateObjectDTO 생성
KBM05haruin30 Jul 30, 2024
c656659
feat : TemplateObjectDTO 사용
KBM05haruin30 Jul 30, 2024
27a49dd
refactor : token 이름 수정
KBM05haruin30 Jul 30, 2024
c0f87b3
feat : 위시리스트에 해당 상품 있을 때만 위시리스트에서 상품 삭제
KBM05haruin30 Jul 30, 2024
f5d5d15
feat : 이전 상태로 되돌리기
KBM05haruin30 Jul 30, 2024
09f1468
feat : 로그 남기기 위해 추가
KBM05haruin30 Jul 30, 2024
0527adc
feat : 주문하기 트랜잭션 분리
KBM05haruin30 Jul 30, 2024
3f27c40
feat : 메시지 전송 수정
KBM05haruin30 Jul 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,41 @@
- 카카오 로그인 화면 만들기
- 버튼 클릭 시 로그인 화면으로 이동
- 액세스 토큰 얻기 성공 화면 만들기
</details>

<details>
<summary><strong>2단계 - 주문하기</strong></summary>

- 액세스 토큰으로부터 정보 추출하기
- 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 수정
- 주문 버튼을 눌렀을 때 메시지 보내도록 수정
</details>
5 changes: 4 additions & 1 deletion src/main/java/gift/config/KakaoProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/gift/controller/KakaoLoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/gift/controller/OrderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.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 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();
kakaoService.sendKakaoMessage(accessToken, orderResponseDTO);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카카오톡 메시지 전송 로직에서 에러가 발생하게 된다면 정상 응답이 안내려갈 것 같아요. 주문은 정상적으로 완료되었으나, 카카오톡 전송 로직이 실패했다고 해서 사용자가 실패응답을 받는게 맞을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞지 않은 것 같습니다. 이것도 마찬가지로 예외가 발생하면 로그를 남기는 형식으로 처리해보도록 하겠습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에는 model.addAttribute를 사용하여 "message"를 만들고 에러를 처리하려고 했었습니다. 하지만 생각대로 출력이 되지 않아서 로그를 남기는 형식으로 처리했습니다.

return "redirect:/admin/products";
}

}
10 changes: 10 additions & 0 deletions src/main/java/gift/dto/KakaoAccountDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gift.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoAccountDTO(
@JsonProperty("email")
String email
) {

}
12 changes: 12 additions & 0 deletions src/main/java/gift/dto/KakaoUserInfoDTO.java
Original file line number Diff line number Diff line change
@@ -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
) {

}
12 changes: 0 additions & 12 deletions src/main/java/gift/dto/OptionSubtractQuantityDTO.java

This file was deleted.

10 changes: 10 additions & 0 deletions src/main/java/gift/dto/OrderRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gift.dto;

public record OrderRequestDTO(
Long optionId,
Long quantity,
String message,
String accessToken
) {

}
13 changes: 13 additions & 0 deletions src/main/java/gift/dto/OrderResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gift.dto;

import java.time.LocalDateTime;

public record OrderResponseDTO(
Long id,
Long optionId,
Long quantity,
LocalDateTime orderDateTime,
String message
) {

}
75 changes: 75 additions & 0 deletions src/main/java/gift/model/Order.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
8 changes: 8 additions & 0 deletions src/main/java/gift/repository/OrderRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gift.repository;

import gift.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {

}
84 changes: 83 additions & 1 deletion src/main/java/gift/service/KakaoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

import gift.config.KakaoProperties;
import gift.dto.KakaoAccessTokenDTO;
import gift.dto.KakaoUserInfoDTO;
import gift.dto.MemberDTO;
import gift.dto.OrderResponseDTO;
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;

Expand All @@ -14,9 +23,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() {
Expand Down Expand Up @@ -44,4 +58,72 @@ private LinkedMultiValueMap<String, String> createBody(String authorizationCode)
body.add("code", authorizationCode);
return body;
}

public String getUserEmail(String accessToken) {
String url = kakaoProperties.userInfoUrl();
ResponseEntity<KakaoUserInfoDTO> 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();
final LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("template_object", TemplateObject(orderResponseDTO.id(),
orderResponseDTO.optionId(), orderResponseDTO.quantity(),
orderResponseDTO.orderDateTime().toString(), orderResponseDTO.message()));
ResponseEntity<String> response = restClient.post()
.uri(URI.create(url))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.toEntity(String.class);
}

private String TemplateObject(Long id, Long optionId, Long quantity, String orderDateTime,
String message) {
return "{"
+ "\"object_type\":\"text\","
+ "\"text\":\"주문 정보:\\n주문 ID: " + id + "\\n옵션 ID: " + optionId + "\\n수량: " + quantity
+ "\\n주문 시간: " + orderDateTime + "\\n메시지: " + message + "\","
+ "\"link\":{\"web_url\":\"http://localhost:8080/admin/products\",\"mobile_web_url\":\"http://localhost:8080/admin/products\"}"
+ "}";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 문자열로 다 입력하게 된다면, 관리하기 힘들지 않을까요? 전송할 내용이 많아진다면 문자열을 일일히 수정해야 될 것 같아요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확실히 전송할 내용이 많아지게 되면 문자열을 일일히 수정해야 한다는 불편한 점이 생길 것 같습니다. 하나의 TemplateObject DTO 클래스를 만들어 관리할 수 있도록 수정하겠습니다!

}

private String generateRandomPassword() {
int leftLimit = 48;
int rightLimit = 122;
int targetStringLength = 20;
Random random = new Random();
String generatedPassword = random.ints(leftLimit, rightLimit + 1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

범위를 이렇게 잡은 이유가 어떻게 될까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비밀 번호를 생성할 때 랜덤하게 만들고 싶었습니다. 그 중 영어 대문자, 소문자, 숫자를 사용해서 만들고 싶었습니다. 숫자 0은 아스키 코드에서 48이고 소문자 z는 아스키 코드에서 122이므로 범위를 위와 같이 잡았습니다. 비밀 번호의 길이는 몇 자가 적당할까 하다가 20자로 정했습니다.

.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
return generatedPassword;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작성하신 알고리즘의 원리가 궁금합니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random을 사용하여 48에서 122까지의 난수를 생성합니다.
filter를 통해 숫자와 알파벳만 포함하도록 합니다.
limit를 통해 제가 원하는 길이만큼만 생성되도록 제한합니다.
collect를 통해 필터링된 난수를 문자열로 반환하여 StringBuilder에 저장합니다.
toString을 통해 StringBuilder를 문자열로 반환하여 generatedPasseord에 저장합니다.

StringBuilder에 관한 설명
StringBuilder::new는 새로운 StringBuilder 객체를 생성합니다.
StringBuilder::appendCodePoint는 주어진 숫자에 해당하는 아스키 코드를 StringBuilder에 추가합니다.
StringBuilder::append는 두 StringBuilder 객체를 병합합니다. 결과를 하나의 StringBuilder로 결합하는 역할을 합니다.

}
}
Loading