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주차 과제 (3단계) #379

Open
wants to merge 47 commits into
base: rbm0524
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b8a0f64
Move from spring-gift-enhancement/step3
rbm0524 Jul 23, 2024
4d8103b
docs : 1단계 요구사항 작성
rbm0524 Jul 25, 2024
1c71190
image : 로그인 화면에 사용할 이미지 추가
rbm0524 Jul 25, 2024
a84b2ab
feat : 카카오 api 사용에 필요한 Dto 작성
rbm0524 Jul 25, 2024
9e6a03d
feat : 카카오 api 사용에 필요한 Dto 작성
rbm0524 Jul 25, 2024
e923af2
feat : 카카오 api를 통해 로그인 기능을 하는 Controller 작성
rbm0524 Jul 25, 2024
3578bd9
feat : 카카오 서버로부터 받은 토큰 검증하는 클래스 작성
rbm0524 Jul 25, 2024
e8c36ee
refactor : clientId 추가
rbm0524 Jul 25, 2024
684b2ea
refactor : 카카오 로그인 버튼 추가
rbm0524 Jul 25, 2024
c7fd8e0
refactor : 카카오 토큰 관련 로직 추가
rbm0524 Jul 25, 2024
265c315
refactor : 카카오id 저장할 attribute 추가
rbm0524 Jul 25, 2024
7f644c1
refactor : 카카오id 저장할 필드 추가
rbm0524 Jul 25, 2024
603d993
refactor : 카카오id로 Member 찾는 메서드 추가
rbm0524 Jul 25, 2024
8a36eae
refactor : 카카오id 관련 메서드 추가, Member에 kakaoId set 추가
rbm0524 Jul 25, 2024
fc6e7cd
refactor : Optional로 받도록 수정
rbm0524 Jul 25, 2024
1c96884
refactor : Qualifier 제거
rbm0524 Jul 25, 2024
1d483c9
docs : README.md 수정
rbm0524 Jul 25, 2024
d5c2937
feat : KakaoOAuthController 테스트 작성
rbm0524 Jul 25, 2024
f3789e5
refactor : OptionService 추가
rbm0524 Jul 26, 2024
ef7572f
refactor : OptionService 추가
rbm0524 Jul 26, 2024
e71c19d
feat : purchaseWishlist 추가
rbm0524 Jul 26, 2024
b260eae
refactor : subtractOption이 List를 받도록 수정
rbm0524 Jul 26, 2024
3e730a1
refactor : 한 줄 띄우기
rbm0524 Jul 26, 2024
a59b5a0
refactor : Option 보여주는 부분 추가
rbm0524 Jul 26, 2024
8bca880
refactor : 구매하기 기능 추가
rbm0524 Jul 26, 2024
c3f1bf8
refactor : Option 외래키 추가
rbm0524 Jul 26, 2024
2d345fa
refactor : 안쓰는 import 삭제
rbm0524 Jul 26, 2024
9da2bfa
refactor : optionId 추가
rbm0524 Jul 26, 2024
8901763
refactor : optionJpaRepository 추가
rbm0524 Jul 26, 2024
a369e3a
refactor : 테스트에 사용할 Option 추가
rbm0524 Jul 26, 2024
592c130
docs : README.md 2단계 요구사항 추가
rbm0524 Jul 26, 2024
0e4ac1d
feat : 카카오 나에게 메시지 보내 기능을 위한 클래스 작성
rbm0524 Jul 26, 2024
9bcad34
fix : 테스트 수정
rbm0524 Jul 27, 2024
73373cc
fix : 타입 수정
rbm0524 Jul 27, 2024
2db30a0
feat: getter, setter 추가
rbm0524 Jul 27, 2024
6678a21
refactor : userId로 되어있던 부분 memberId로 수정
rbm0524 Jul 27, 2024
7a315e1
feat : getter, setter 추가
rbm0524 Jul 27, 2024
eeded51
refactor : import 수정
rbm0524 Jul 27, 2024
3671d08
feat : 주문 이후 위시리스트 비우는 함수 추가
rbm0524 Jul 27, 2024
1f1b290
reafactor : memberId로 수정
rbm0524 Jul 27, 2024
4bb18a3
feat : memberId에 따라 위시리스트 비우는 기능 추가
rbm0524 Jul 27, 2024
8087c6e
feat : memberId로 수정
rbm0524 Jul 27, 2024
c134f70
refactor : 마지막 줄 추가
rbm0524 Jul 28, 2024
95d00e3
refactor : category, product 저장 추가
rbm0524 Jul 28, 2024
acd5ae6
refactor : Swagger UI를 사용하기 위한 의존성 추가
rbm0524 Jul 28, 2024
3e73fe3
refactor : @Tag와 Operation을 통해 API에 대한 설명 추가
rbm0524 Jul 28, 2024
3d1243b
dcos : README.md 수정
rbm0524 Jul 28, 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
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# spring-gift-order
# spring-gift-order
# 1단계 - 카카오 로그인

## 요구사항
### 기능 요구사항
1. 카카오 로그인을 통해 인가 코드를 받는다.
2. 인가 코드를 사용해 토큰을 받은 후 향후 카카오 API 사용을 준비한다.
3. 카카오계정 로그인을 통해 인증 코드를 받는다.
4. 토큰 받기를 읽고 액세스 토큰을 추출한다.
5. 앱 키, 인가 코드가 절대 유출되지 않도록 한다.
- 특히 시크릿 키는 GitHub나 클라이언트 코드 등 외부에서 볼 수 있는 곳에 추가하지 않는다.
6. 오류 처리
레퍼런스와 문제 해결을 참고하여 발생할 수 있는 다양한 오류를 처리한다.

# 2단계 - 주문하기

## 요구사항
### 기능 요구사항
1. 카카오톡 메시지 API를 사용하여 주문하기 기능을 구현한다.
2. 주문할 때 수령인에게 보낼 메시지를 작성할 수 있다.
3. 상품 옵션과 해당 수량을 선택하여 주문하면 해당 상품 옵션의 수량이 차감된다.
4. 해당 상품이 위시 리스트에 있는 경우 위시 리스트에서 삭제한다.
5. 나에게 보내기를 읽고 주문 내역을 카카오톡 메시지로 전송한다.
6. 메시지는 메시지 템플릿의 기본 템플릿이나 사용자 정의 템플릿을 사용하여 자유롭게 작성한다.

# 3단계 - API 문서 만들기

## 요구사항
### 기능 요구사항
API 사양에 관해 클라이언트와 어떻게 소통할 수 있을까? 어떻게 하면 편하게 소통할 수 있을지 고민해 보고 그 방법을 구현한다.
1. API 문서를 만들어 클라이언트에게 제공한다.
2. API 문서는 Swagger 또는 restDoc을 사용하여 만든다.
3. API 문서에는 API 명세, 요청, 응답, 예외 처리 등이 포함되어야 한다.
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'org.springframework.plugin:spring-plugin-core:3.0.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
runtimeOnly 'com.h2database:h2'
compileOnly 'io.jsonwebtoken:jjwt-api:0.11.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-inline:4.9.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

}

tasks.named('test') {
Expand Down
80 changes: 80 additions & 0 deletions src/main/java/gift/Annotation/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package gift.Annotation;

import gift.Model.MemberDto;
import gift.Service.MemberService;
import gift.Utils.JwtUtil;
import gift.Validation.KakaoTokenValidator;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.util.Optional;

@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
private final MemberService memberService;
private final JwtUtil jwtUtil;
private boolean isKakaoToken = false;

@Autowired
public LoginMemberArgumentResolver(MemberService memberService, JwtUtil jwtUtil) {
this.memberService = memberService;
this.jwtUtil = jwtUtil;
}

@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(MemberDto.class);
}

@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();

// 쿠키에서 JWT 토큰 추출
String token = null;
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("token".equals(cookie.getName())) {
token = cookie.getValue();
break;
}

if ("accessToken".equals(cookie.getName())) {
token = cookie.getValue();
isKakaoToken = true;
break;
}
}
}

if (token == null) {
throw new IllegalArgumentException("JWT token not found in cookies");
}

if (isKakaoToken) {
//카카오 토큰 검증
long kakaoId = KakaoTokenValidator.validateToken(token);
Optional<MemberDto> memberDto = memberService.findByKakaoId(kakaoId);

return memberDto.orElseThrow(() -> new IllegalArgumentException("User not found"));
}

Claims claims = jwtUtil.decodeToken(token); //decode
String memberEmail = claims.getSubject(); // subject를 email로 설정했기 때문에 userEmail로 사용
Optional<MemberDto> memberDto = memberService.findByEmail(memberEmail); //null이라면 인증된 것이 아닐 것이고 null이 아니라면 인증된 것
if (memberDto.isEmpty()) {
throw new IllegalArgumentException("User not found");
}
return memberDto;

}

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginMemberResolver {
}
22 changes: 22 additions & 0 deletions src/main/java/gift/Config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gift.Config;

import gift.Annotation.LoginMemberArgumentResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private LoginMemberArgumentResolver loginMemberResolverHandlerMethodArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginMemberResolverHandlerMethodArgumentResolver);
}

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

import gift.Model.CategoryDto;
import gift.Service.CategoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
@Tag(name = "Category", description = "카테고리 관련 api")
public class CategoryController {

private final CategoryService categoryService;

public CategoryController(CategoryService categoryService) {
this.categoryService = categoryService;
}

@Operation(summary = "모든 카테고리 조회", description = "모든 카테고리를 조회합니다.")
@GetMapping("/api/categories")
public ResponseEntity<List<CategoryDto>> getAllCategories() {
List<CategoryDto> categoryDtoList = categoryService.getAllCategories();
return ResponseEntity.ok(categoryDtoList);
}
}
45 changes: 45 additions & 0 deletions src/main/java/gift/Controller/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package gift.Controller;

import gift.Exception.ForbiddenException;
import gift.Exception.UnauthorizedException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.ui.Model;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex, Model model, HttpServletRequest request) {
FieldError fieldError = ex.getBindingResult().getFieldError();
String errorMessage = fieldError.getDefaultMessage();

model.addAttribute("error", errorMessage);
model.addAttribute("product", ex.getBindingResult().getTarget());

// 에러가 발생한 URL에 따라 다시 해당 페이지로 돌아가기
String requestUrl = request.getRequestURI();
if (requestUrl.equals("/api/products/create") || requestUrl.startsWith("/api/products/update")) {
return "product_form"; // product_form 뷰로 포워딩
}

return "redirect:/api/products";
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<String> handleUnauthorizedException(UnauthorizedException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<String> handleForbiddenException(ForbiddenException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN);
}

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

import com.fasterxml.jackson.databind.ObjectMapper;
import gift.Model.KakaoAccessTokenDto;
import gift.Model.KakaoMemberDto;
import gift.Model.MemberDto;
import gift.Service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;

@RestController
@Tag(name = "Kakao Login", description = "카카오 로그인 관련 api")
public class KakaoOAuthController {

@Value("${kakao.clientId}")
private String clientId;

@Autowired
private MemberService memberService;

@GetMapping("/oauth/authorize")
@Operation(summary = "카카오 로그인 화면", description = "카카오 로그인 화면을 보여줍니다.")
public void authorize(HttpServletResponse response) throws IOException {
var url = "https://kauth.kakao.com/oauth/authorize";
var redirectUri = "http://localhost:8080/auth/kakao/callback";
var responseType = "code";

String redirectUrl = url + "?response_type=" + responseType + "&client_id=" + clientId + "&redirect_uri=" + redirectUri;
response.sendRedirect(redirectUrl);
}


@GetMapping("/auth/kakao/callback")
@Operation(summary = "카카오 로그인 수행", description = "인가 코드를 통해 로그인을 수행합니다.")
public ResponseEntity<?> callBack(@RequestParam("code") String code, HttpServletResponse response) throws Exception {
//인가 코드로 토큰 받아오기
String accessToken = getAccessToken(code);

//토큰을 쿠키에 저장하기
Cookie cookie = new Cookie("accessToken", accessToken);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);

//토큰을 통해 사용자 정보 받아오기
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<String> responseEntity = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
entity,
String.class
);

ObjectMapper objectMapper = new ObjectMapper();
//id는 가능한데 email을 가져오려고 하면 null이 되는 이유??????????
KakaoMemberDto kakaoMemberDto = objectMapper.readValue(responseEntity.getBody(), KakaoMemberDto.class);
long id = kakaoMemberDto.getId();

//이미 가입된 회원인지 확인, 가입되지 않은 회원이라면 회원가입 진행
if (memberService.findByKakaoId(id).isEmpty()) {
MemberDto memberDto = new MemberDto();
memberDto.setKakaoId(id);
memberService.register(memberDto);
}

//로그인 처리
headers = new HttpHeaders();
headers.add("Location", "/products");

return new ResponseEntity<>("Successfully logged in", headers, HttpStatus.SEE_OTHER);

}

@Operation(summary = "토큰 발급", description = "토큰을 발급받습니다.")
public String getAccessToken(String code) throws Exception {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", "http://localhost:8080/auth/kakao/callback");
body.add("code", code);

HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers); //순서가 body가 먼저

ResponseEntity<String> response = restTemplate.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
requestEntity,
String.class
); //Http 요청 보내고 받기

ObjectMapper objectMapper = new ObjectMapper();
KakaoAccessTokenDto kakaoAccessTokenDto = objectMapper.readValue(response.getBody(), KakaoAccessTokenDto.class);
return kakaoAccessTokenDto.getAccess_token();
}
}
Loading