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

update main #413

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ff243c0
init: 이전 과제 복사 [spring-gift-order]
HaegyeongKim01 Jul 29, 2024
48282cb
refactor: 패키지 구조 변경
HaegyeongKim01 Jul 29, 2024
5c40490
refactor: 카카오 메시지에 message 추가
HaegyeongKim01 Jul 29, 2024
2af1c7b
refactor: 부정형으로 질의
HaegyeongKim01 Jul 29, 2024
2464cbd
feat: Swagger-@Tag 적용
HaegyeongKim01 Jul 29, 2024
f93279e
feat: Swagger-@Parameter 적용
HaegyeongKim01 Jul 29, 2024
5263dc3
feat: Swagger-@Schema 적용
HaegyeongKim01 Jul 30, 2024
d320cfb
refactor: import 추가
HaegyeongKim01 Jul 30, 2024
df86c8a
refactor: Member API 수정
HaegyeongKim01 Jul 30, 2024
711fabd
refactor: Controller return HttpStatus 방법 변경
HaegyeongKim01 Jul 30, 2024
8f28ef2
충남대 BE_김해경 6주차 과제 (0단계) (#60)
HaegyeongKim01 Jul 31, 2024
c249b4d
refactor: 팀 API 통일-Controller 파라미터명 변경
HaegyeongKim01 Jul 31, 2024
94b8b4f
refactor: 팀API 통일 위한 요청/반환 정보 변경
HaegyeongKim01 Jul 31, 2024
365cb58
Resolve conflict
HaegyeongKim01 Jul 31, 2024
b95a078
refactor: AuthorizationHeader 핸들러로 처리
HaegyeongKim01 Jul 31, 2024
286190c
docs: 기능요구사항 & 팀 API 명세서
HaegyeongKim01 Jul 31, 2024
a5ea704
init: 이전 과제 복사 [spring-gift-order]
HaegyeongKim01 Jul 29, 2024
93148c2
refactor: 패키지 구조 변경
HaegyeongKim01 Jul 29, 2024
5bd216f
refactor: Member API 수정
HaegyeongKim01 Jul 30, 2024
9adcd56
refactor: Controller return HttpStatus 방법 변경
HaegyeongKim01 Jul 30, 2024
633b605
refactor: 팀API 통일 위한 요청/반환 정보 변경
HaegyeongKim01 Jul 31, 2024
0f85ed4
refactor: AuthorizationHeader 핸들러로 처리
HaegyeongKim01 Jul 31, 2024
f1da78e
docs: 기능요구사항 & 팀 API 명세서
HaegyeongKim01 Jul 31, 2024
7e3c213
Merge branch 'step1' of https://github.com/HaegyeongKim01/spring-gift…
HaegyeongKim01 Jul 31, 2024
5f4ec85
style: 잘못올라간 text 삭제
HaegyeongKim01 Aug 2, 2024
d200ba0
feat: Setting CORS
HaegyeongKim01 Aug 2, 2024
d87400c
feat: 포인트 기능 구현
HaegyeongKim01 Aug 2, 2024
2c50f05
docs: [Step3] 기능 요구사항 작성
HaegyeongKim01 Aug 2, 2024
c9065fa
refactor: Controller 변경으로 인한 화면 수정
HaegyeongKim01 Aug 3, 2024
2ae9b3f
refactor: 포인트 누적 충전
HaegyeongKim01 Aug 3, 2024
1d89ddb
feat: 포인트 충전 화면 구현
HaegyeongKim01 Aug 3, 2024
6bc126c
충남대 BE_김해경 6주차 과제 (1단계) (#148)
HaegyeongKim01 Aug 4, 2024
98b581c
Merge branch 'haegyeongkim01' into step3
HaegyeongKim01 Aug 4, 2024
78f8bef
test: Point 등록 및 차감
HaegyeongKim01 Aug 5, 2024
50901f1
refactor: Swagger parameter 변경
HaegyeongKim01 Aug 5, 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
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# spring-gift-point
# spring-gift-point

https://www.notion.so/6-api-3d91cc23fb1e435eaf34ff1a8e51eaf1?pvs=4



**팀 API 명세서**
![img_5.png](img_5.png)
![img_6.png](img_6.png)
![img_3.png](img_3.png)
![img_4.png](img_4.png)
![img_7.png](img_7.png)
![img_8.png](img_8.png)
![img_9.png](img_9.png)
![img_10.png](img_10.png)
![img_11.png](img_11.png)

### 기능 요구사항 작성
- [X] 팀원과 의논한 팀 API 명세서를 바탕으로 코드 리팩토링

# 🚀포인트 구현

### 기능 요구 사항

---

상품 구매에 사용할 수 있는 포인트 기능을 구현한다.

- [X] 포인트는 사용자별로 보유
- [X] Member : Point = 1 : 1
- [X] 관리자 화면에서 포인트를 충전하여 사용하는 방식이다.
- [X] 포인트 차감
- [X] 상품을 주문하면 포인트에서 차감된다.
- [X] 관리자 화면에서 포인트 충전 가능
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ 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-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
Binary file added img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions src/main/java/gift/auth/JwtHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package gift.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtHelper {

private final static String SECRET_KEY = "mysecretmysecretmysecretmysecretmysecretmysecret";
private final static SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
private final static long TOKEN_TIME = 60 * 60 *1000L; //60분

public String generateToken(Long userId, String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId); // 사용자 ID를 클레임에 추가한다.
claims.put("email", email); // 사용자 이메일을 클레임에 추가한다.

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(new Date().getTime()))
.setExpiration(new Date(new Date().getTime() + TOKEN_TIME))
.signWith(KEY)
.compact();
}

/**
* Token에서 Claim 추출
* @param token JWT 토큰
* @return Claims
*/
public Claims getClaims(Token token) {
return Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token.token())
.getPayload();
}

public boolean isJwtToken(Token token) {
return token.token().split("\\.").length == 3;
}

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

import gift.service.KakaoApiService;
import gift.service.MemberService;
import gift.vo.Member;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;

@Component
public class JwtUtil {

private final KakaoApiService kakaoApiService;
private final MemberService memberService;

// Token 만료 시간
private final static long TOKEN_TIME = 60 * 60 *1000L; //60분
private final static String SECRET_KEY = "mysecretmysecretmysecretmysecretmysecretmysecret";
private final static SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
private final JwtHelper jwtHelper;

public JwtUtil(KakaoApiService kakaoApiService, MemberService memberService, JwtHelper jwtHelper) {
this.kakaoApiService = kakaoApiService;
this.memberService = memberService;
this.jwtHelper = jwtHelper;
}

private Long getMemberIdFromToken(Token token) {
return jwtHelper.getClaims(token).get("userId", Long.class);
}

/**
*
* @param authorizationHeader Authorization 헤더
* @return Bearer 토큰 추출
*/
public Token getBearerTokenFromAuthorizationHeader(String authorizationHeader) {
String bearerToken = authorizationHeader.replace("Bearer ", "");
return new Token(bearerToken);
}

private Long getMemberIdFromKakao(Token token) {
String memberEmail = kakaoApiService.getMemberEmailFromKakao(token);
return memberService.getMemberByEmail(memberEmail).getId();
}

public Member getMemberFromAuthorizationHeader(String authorizationHeader) {
Token fetchedToken = getBearerTokenFromAuthorizationHeader(authorizationHeader);

Long foundedMemberId;

if (jwtHelper.isJwtToken(fetchedToken)) {
foundedMemberId = getMemberIdFromToken(fetchedToken);
} else {
foundedMemberId = getMemberIdFromKakao(fetchedToken);
}

return memberService.getMemberById(foundedMemberId);
}

public boolean isNotJwtToken(Token token) {
return token.token().split("\\.").length != 3;
}

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

public record OAuthToken (
String access_token,
String token_type,
String refresh_token,
int expires_in,
String scope,
int refresh_token_expires_in
) {
}
11 changes: 11 additions & 0 deletions src/main/java/gift/auth/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gift.auth;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "Token")
public record Token(

@Schema(description = "Access Token")
String token
) {
}
38 changes: 38 additions & 0 deletions src/main/java/gift/component/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gift.component;

import gift.auth.JwtUtil;
import gift.service.MemberService;
import gift.vo.LoginMember;
import jakarta.servlet.http.HttpServletRequest;
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;

@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final MemberService memberService;
private final JwtUtil jwtUtil;

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

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginMember.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String authorizationHeader = request.getHeader("Authorization");

return jwtUtil.getMemberFromAuthorizationHeader(authorizationHeader);
}

}
86 changes: 86 additions & 0 deletions src/main/java/gift/component/kakao/KakaoApiProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package gift.component.kakao;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import gift.auth.OAuthToken;
import gift.dto.KakaoMessageRequestDto;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Component
public class KakaoApiProvider {

private final KakaoProperties kakaoProperties;
private final ObjectMapper objectMapper;

private static final String GRANT_TYPE = "authorization_code";
public static final String KAKAO_TOKEN_REQUEST_URI = "https://kauth.kakao.com/oauth/token";
public static final String KAKAO_USER_PROFILE_URI = "https://kapi.kakao.com/v2/user/me";
public static final String KAKAO_MESSAGE_API_URI = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
public static final String KAKAO_EMAIL = "kakao@kakao_";
public static final String KAKAO_PASSWORD = "KAKAO";
private static final String WEB_URL = "http://www.daum.net";
private static final String BUTTON_TITLE = "바로가기";

public KakaoApiProvider(KakaoProperties kakaoProperties, ObjectMapper objectMapper) {
this.kakaoProperties = kakaoProperties;
this.objectMapper = objectMapper;
}

public OAuthToken parseOAuthToken(String json) throws JsonProcessingException {
return objectMapper.readValue(json, OAuthToken.class);
}

public HttpHeaders makeHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return headers;
}

public MultiValueMap<String, String> makeGetAccessTokenBody(String code) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", GRANT_TYPE);
body.add("client_id", kakaoProperties.kakaoClientId());
body.add("redirect_uri", kakaoProperties.kakaoRedirectUrl());
body.add("code", code);
return body;
}

public MultiValueMap<String, String> makeTemplateObject(KakaoMessageRequestDto kakaoMessageRequestDto) {
String text = generateOrderText(kakaoMessageRequestDto);

ObjectNode templateJson = objectMapper.createObjectNode();
templateJson.put("object_type", "text");
templateJson.put("text", text);
templateJson.set("link", createLinkNode());
templateJson.put("button_title", BUTTON_TITLE);

String templateObject = templateJson.toString();
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("template_object", templateObject);

return body;
}

private String generateOrderText(KakaoMessageRequestDto kakaoMessageRequestDto) {
String productName = kakaoMessageRequestDto.productName();
String optionName = kakaoMessageRequestDto.optionName();
int num = kakaoMessageRequestDto.quantity();

String message = kakaoMessageRequestDto.message();

return productName + "[" + optionName + "]" + " 상품이 " + num + "개 주문되었습니다." + "\n메시지: " + message;
}

private ObjectNode createLinkNode() {
ObjectNode link = objectMapper.createObjectNode();
link.put("web_url", WEB_URL);

return link;
}

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public record KakaoProperties(

@Value("${kakao.clientId}") String kakaoClientId,

@Value("${kakao.redirectUrl}") String kakaoRedirectUrl
) {
}
23 changes: 23 additions & 0 deletions src/main/java/gift/config/RestClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package gift.config;

import gift.exception.CustomResponseErrorHandler;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Component
public class RestClientConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder, CustomResponseErrorHandler customResponseErrorHandler) {
return builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.errorHandler(customResponseErrorHandler)
.build();
};

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

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
String jwt = "JWT";
SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt);
Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme()
.name(jwt)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
);

return new OpenAPI()
.components(components)
.info(apiInfo())
.addSecurityItem(securityRequirement);
}

private Info apiInfo() {
return new Info()
.title("선물하기 Project Spring Boot API Test")
.description("Swagger UI")
.version("1.0.0");
}
}
Loading