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

Merged
merged 16 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
application-private.properties
application-test.properties

### STS ###
.apt_generated
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@
* import.sql id 명시적으로 작성
* 예외처리 메세지 추가 및 테스트 코드 적용
* Code Pruning

### 1단계
* 카카오 로그인 API 적용
* RestTemplateTest 클래스 추가

### 2단계
* 주문할 때 수령인에게 보낼 메시지를 작성
* 상품 옵션과 해당 수량을 선택하여 주문하면 해당 상품 옵션의 수량이 차감
* 해당 상품이 위시 리스트에 있는 경우 위시 리스트에서 삭제
* 주문 후 나에게 카카오 메세지 보내기
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.json:json:20210307'
}

tasks.named('test') {
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/gift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
@SpringBootApplication
public class Application {
public static void main(String[] args) {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/gift/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;


@Configuration
public class WebConfig {
Expand All @@ -20,6 +22,13 @@ public WebConfig(JwtTokenFilter jwtTokenFilter) {
}

@Bean

public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean

public FilterRegistrationBean<Filter> jwtFilter() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(jwtTokenFilter);
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/gift/controller/KakaoLoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package gift.controller;

import gift.entity.Member;
import gift.exception.MemberNotFoundException;
import gift.service.KakaoService;
import gift.util.KakaoProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class KakaoLoginController {

private final KakaoProperties kakaoProperties;
private final KakaoService kakaoService;

@Autowired
public KakaoLoginController(KakaoProperties kakaoProperties, KakaoService kakaoService) {
this.kakaoProperties = kakaoProperties;
this.kakaoService = kakaoService;
}
@GetMapping("/kakao/login")
public String login(Model model) {
model.addAttribute("kakaoClientId", kakaoProperties.clientId());
model.addAttribute("kakaoRedirectUrl", kakaoProperties.redirectUrl());
return "kakaoLogin";
}

@GetMapping("/kakao/oauth2/callback")
public String callbackKakao(@RequestParam String code, Model model) {

try {
String accessToken = kakaoService.login(code);
model.addAttribute("accessToken", accessToken);
return "home";
} catch (MemberNotFoundException e) {
model.addAttribute("member", new Member()); // Member 객체 추가
return "register";
}

}

@GetMapping("/kakao/loginSuccess")
public String loginSuccess() {
return "kakaoLoginSuccess";
}
Comment on lines +25 to +49

Choose a reason for hiding this comment

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

이렇게 전부 kakao를 prefix path로 사용한다면 컨트롤러 자체에 path 등록해두는게 좋지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

@RequestMapping("/kakao") 로 따로 뺐습니다!





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


import gift.exception.MemberNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.http.*;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;

@Service
public class KakaoService {

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

@Value("${kakao.redirect-url}")
private String redirectUri;

@Autowired
private RestTemplate restTemplate;

public String getKakaoAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();

String url = UriComponentsBuilder.fromHttpUrl("https://kauth.kakao.com/oauth/token")
.queryParam("grant_type", "authorization_code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.queryParam("code", code)
.toUriString();

Map<String, String> response = restTemplate.postForObject(url, null, HashMap.class);

return response.get("access_token");
Comment on lines +42 to +44

Choose a reason for hiding this comment

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

토큰을 이런식으로 빼셨군요 👍🏼

}

public JSONObject getUserInfo(String accessToken) {
String reqURL = "https://kapi.kakao.com/v2/user/me";

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(reqURL, HttpMethod.GET, request, String.class);

if (response.getStatusCode() == HttpStatus.OK) {
return new JSONObject(response.getBody());
}

throw new RuntimeException("Failed to get user info from Kakao");
}

public String login(String code) throws MemberNotFoundException {
String accessToken = getKakaoAccessToken(code);
JSONObject userInfo = getUserInfo(accessToken);

String email= null;
if (userInfo.has("kakao_account")) {
JSONObject kakaoAccount = userInfo.getJSONObject("kakao_account");
if (kakaoAccount.has("email")) {
email = kakaoAccount.getString("email");
}
}

if (email == null) {
throw new MemberNotFoundException("Email not found");
}
// 로그인 성공 처리
return accessToken;
}
Comment on lines +63 to +80

Choose a reason for hiding this comment

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

getKakaoAccessToken에서 리프레시토큰은 따로 가져오지 않아도 괜찮을까요?

Copy link
Author

Choose a reason for hiding this comment

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

토큰 두 개 모두 받아오는 객체를 새로 정의해서 받아오도록 수정했습니다!


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

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("kakao")
public record KakaoProperties(
String clientId,
String redirectUrl
) {}
3 changes: 3 additions & 0 deletions src/main/resources/application-secret.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kakao.client-id=7996810f652c133a85581783a17c69ff
kakao.redirect-url=http://localhost:8000/kakao/oauth2/callback
kakao.client-secret=aaaa
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
spring.application.name=spring-gift
spring.profiles.active=secret

# port
server.port=8000
Expand All @@ -17,6 +18,7 @@ spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console


# JWT Secret Key
jwt.secret=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=

Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/templates/kakaoLogin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Kakao Login</title>
</head>
<body>
<h1>Kakao Login</h1>
<a th:href="@{|https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${kakaoRedirectUrl}&response_type=code&scope=account_email|}">
Login with Kakao
</a>
</body>
</html>
10 changes: 10 additions & 0 deletions src/main/resources/templates/kakaoLoginSuccess.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Kakao Login Success</title>
</head>
<body>
<h1>Kakao Login Success</h1>
<p>Access Token: <span th:text="${accessToken}"></span></p>
</body>
</html>
10 changes: 10 additions & 0 deletions src/main/resources/templates/loginFailure.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login Failure</title>
</head>
<body>
<h1>Login Failure</h1>
<p>Something went wrong. Please try again.</p>
</body>
</html>
65 changes: 65 additions & 0 deletions src/test/java/gift/KakaoControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package gift;

import gift.service.KakaoService;
import gift.exception.MemberNotFoundException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import org.springframework.boot.test.mock.mockito.MockBean;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class KakaoControllerTest {

@LocalServerPort
private int port;

@Autowired
private TestRestTemplate restTemplate;

@MockBean
private KakaoService kakaoService;

// "/kakao/login" 엔드포인트에 GET 요청
@Test
public void testLogin() {
String url = "http://localhost:" + port + "/kakao/login";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}

// 유효한 코드로 카카오 로그인을 시도
@Test
public void testCallbackKakaoSuccess() {
String code = "valid_code";
String accessToken = "valid_access_token";

when(kakaoService.login(code)).thenReturn(accessToken);

String url = "http://localhost:" + port + "/kakao/oauth2/callback?code=" + code;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}

// 회원을 찾을 수 없는 경우
@Test
public void testCallbackKakaoMemberNotFound() {
String code = "valid_code";

when(kakaoService.login(code)).thenThrow(MemberNotFoundException.class); // 메시지 없이 예외 타입만 설정

String url = "http://localhost:" + port + "/kakao/oauth2/callback?code=" + code;
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("register");
}
Comment on lines +30 to +64

Choose a reason for hiding this comment

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

로그인 테스트 👍🏼

}
43 changes: 43 additions & 0 deletions src/test/java/gift/RestTemplateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package gift;

import gift.util.KakaoProperties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

@ActiveProfiles("secret")
@SpringBootTest
class RestTemplateTest {
private final RestTemplate client = new RestTemplateBuilder().build();

@Autowired
private KakaoProperties properties;

@ConfigurationProperties


@Test
void test1(){
var url = "https://kauth.kakao.com/oauth/token";
var code = "MZqnpa_Ch0moTrUURmm4PEi74CgD3sYdPdG1cJU4nnv4ilOioKXq5AAAAAQKPCQfAAABkPMfVqjNsk3jZ7dWzg";

Choose a reason for hiding this comment

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

이 코드가 여기 있어도 괜찮을까요?
최소한 properties로 빼서 관리되면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하고 서비스 내에서도 url 사용하도록 바꿨습니다!

var headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
var body = new LinkedMultiValueMap<String, String>();
body.add("grant_type", "authorization_code");
body.add("client_id", properties.clientId());
body.add("redirect_uri", properties.redirectUrl());
body.add("code", code);
var request = new RequestEntity<>(body, headers, HttpMethod.POST, URI.create(url));
}
}