Skip to content
Open
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ dependencies {

// OAuth2 login
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// WebFlux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
import com.example.umc7th.domain.member.dto.MemberRequestDTO;
import com.example.umc7th.domain.member.dto.MemberResponseDTO;
import com.example.umc7th.domain.member.service.command.MemberCommandService;
import com.example.umc7th.domain.member.service.command.OAuth2Service;
import com.example.umc7th.global.apiPayload.CustomResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class MemberController {

private final MemberCommandService memberCommandService;
private final OAuth2Service oAuth2Service;

@PostMapping("/login")
public CustomResponse<MemberResponseDTO.MemberTokenDTO> login(@RequestBody MemberRequestDTO.MemberLoginDTO dto) {
Expand All @@ -26,5 +26,10 @@ public CustomResponse<MemberResponseDTO.MemberTokenDTO> signup(@RequestBody Memb
return CustomResponse.onSuccess(memberCommandService.signUp(dto));
}

// 이건 카카였 μ„œλ²„μ—μ„œ ν˜ΈμΆœν•¨
@GetMapping("/oauth2/callback/kakao")
public CustomResponse<MemberResponseDTO.MemberTokenDTO> loginWithKakao(@RequestParam("code") String code) {
return CustomResponse.onSuccess(oAuth2Service.login("kakao", code));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ public class MemberResponseDTO {
@AllArgsConstructor
@NoArgsConstructor
public static class MemberTokenDTO {
String accessToken;
String refreshToken;
//ν΄λΌμ΄μ–ΈνŠΈκ°€ 두 토큰을 κ°€μ§€κ³  μžˆλ‹€κ°€ 전솑 μ‹œμ—λŠ” μœ νš¨μ‹œκ°„μ΄ 짧은 accessToken만 μ „μ†‘ν•˜μ—¬ νƒˆμ·¨μ‹œ ν”Όν•΄λ²”μœ„ μ΅œμ†Œν™”
//토큰은 ν•œλ²ˆ λ°œκΈ‰ν•˜λ©΄ μœ νš¨μ‹œκ°„μ„ μˆ˜μ •ν•  수 μ—†κΈ° λ•Œλ¬Έ
//보톡 토큰은 μ•ˆμ „ν•œ 쿠킀에 λ‹΄μ•„ 전솑
String accessToken; // μ‹€μ œ 인증할 λ•Œλ§ˆλ‹€ μ‚¬μš©ν•˜λŠ” 토큰, μœ νš¨μ‹œκ°„μ„ μž‘κ²Œ ν•˜μ—¬ refreshToken으둜 μž¬λ°œκΈ‰
String refreshToken;// accessToken이 만료되면 μž¬λ°œκΈ‰ν•˜λŠ” 토큰, (λ‘œκ·Έμ•„μ›ƒ μ‹œ μ‚­μ œ)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.umc7th.global.oauth.dto;
package com.example.umc7th.domain.member.dto;

import lombok.Getter;

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.umc7th.domain.member.enums;

public enum SocialType {
KAKAO, GOOGLE, NAVER;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ public enum MemberErrorCode implements BaseErrorCode {
NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER400", "이미 μ‘΄μž¬ν•˜λŠ” μ‚¬μš©μžμž…λ‹ˆλ‹€."),
INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401", "λΉ„λ°€λ²ˆν˜Έκ°€ ν‹€λ¦½λ‹ˆλ‹€."),
OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "λΉ„λ°€λ²ˆν˜Έκ°€ ν‹€λ¦½λ‹ˆλ‹€."),
OAUTH_USER_INFO_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "λΉ„λ°€λ²ˆν˜Έκ°€ ν‹€λ¦½λ‹ˆλ‹€."),

OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "μΈκ°€μ½”λ“œλ‘œ 토큰을 κ°€μ Έμ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."),
OAUTH_USER_INFO_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "ν† ν°μœΌλ‘œ μ‚¬μš©μž 정보λ₯Ό κ°€μ Έμ˜€λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."),
UNSUPPORTED_OAUTH_TYPE(HttpStatus.BAD_REQUEST, "MEMBER400", "μ§€μ›ν•˜μ§€ μ•ŠλŠ” μ†Œμ…œ λ‘œκ·ΈμΈμž…λ‹ˆλ‹€."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public class PrincipalDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(username).orElseThrow(() ->
new MemberException(MemberErrorCode.NOT_FOUND));
// MemberExceptionκ³Ό MemberErrorCode도 ν•œ 번 μž‘μ„±ν•΄μ£Όμ„Έμš”.
return new PrincipalDetails(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.umc7th.domain.member.service.command;

import com.example.umc7th.domain.member.dto.MemberResponseDTO;

public interface OAuth2Service {
MemberResponseDTO.MemberTokenDTO login(String provider, String code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.example.umc7th.domain.member.service.command;

import com.example.umc7th.domain.member.dto.MemberResponseDTO;
import com.example.umc7th.domain.member.dto.OAuth2DTO;
import com.example.umc7th.domain.member.entity.Member;
import com.example.umc7th.domain.member.enums.SocialType;
import com.example.umc7th.domain.member.exception.MemberErrorCode;
import com.example.umc7th.domain.member.exception.MemberException;
import com.example.umc7th.domain.member.repository.MemberRepository;
import com.example.umc7th.global.jwt.JwtProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class OAuth2ServiceImpl implements OAuth2Service {

@Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
private String tokenURI; // Resource Server에 토큰 μš”μ²­μ‹œ μ‚¬μš©ν•  URI

@Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}")
private String userInfoURI;// μ‚¬μš©μž 정보 κ°€μ Έμ˜¬ λ•Œ μ‚¬μš©ν•  URI

@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String clientId;// API KEY

@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String redirectURI;// μ„€μ •ν•œ Redirect uri

private final MemberRepository memberRepository;
private final JwtProvider jwtProvider;

@Override
public MemberResponseDTO.MemberTokenDTO login(String provider, String code) {
if (provider.equalsIgnoreCase(SocialType.KAKAO.name())) {
return loginWithKakao(code);
} else {
throw new MemberException(MemberErrorCode.UNSUPPORTED_OAUTH_TYPE);
}
}

private MemberResponseDTO.MemberTokenDTO loginWithKakao(String code) {
String token = getAccessTokenFromKakao(code);
OAuth2DTO.KakaoProfile profile = getProfileFromKakao(token);
String email = profile.getKakao_account().getEmail();
return loginOrSignup(SocialType.KAKAO, email);
}

private MemberResponseDTO.MemberTokenDTO loginOrSignup(SocialType socialType, String email) {
// SocialType을 Member에 providerλΌλŠ” ν•„λ“œλ‘œ μΆ”κ°€ν•΄μ„œ μ €μž₯해도 μ’‹μŒ
Member member;
Optional<Member> optional = memberRepository.findByEmail(email);

// orElse() λŠ” μ¦‰μ‹œ 평가 이기 λ•Œλ¬Έμ— 값이 있던 μ—†λ˜ λ‚΄λΆ€ 둜직이 μ‹€ν–‰ λ˜μ–΄ λΉ„ 효율적 μž„
// μ•„λž˜ 처럼 λ‘œμ§μ„ 뢄리 ν•˜λ©΄ μ„±λŠ₯을 높일 수 있음
member = optional.orElseGet(() -> memberRepository.save(Member.builder()
.email(email)
.role("ROLE_USER")
.build()));


return MemberResponseDTO.MemberTokenDTO.builder()
.accessToken(jwtProvider.createAccessToken(member))
.refreshToken(jwtProvider.createRefreshToken(member))
.build();
}

//μΈκ°€μ½”λ“œλ‘œ 토큰 κ°€μ Έμ˜€κΈ°
private String getAccessTokenFromKakao(String accessCode) {
// μΈκ°€μ½”λ“œ 토큰 κ°€μ Έμ˜€κΈ°
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();

//Content-Type == HTTP μš”μ²­ 본문의 데이터 ν˜•μ‹μ„ λͺ…μ‹œ
//application/x-www-form-urlencoded == 데이터가 URL 인코딩 λ°©μ‹μœΌλ‘œ 전솑됨
httpHeaders.add("Content-Type", "application/x-www-form-urlencoded");

MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "authorization_code");
map.add("client_id", clientId);
map.add("redirect_uri", redirectURI);
map.add("code", accessCode);
HttpEntity<MultiValueMap> request = new HttpEntity<>(map, httpHeaders);

ResponseEntity<String> response1 = restTemplate.exchange(//이게 httpμš”μ²­μ„ μ„œλ²„λ‘œ 보내고 μ„œλ²„λ‘œ 받은 응닡을 λ°˜ν™˜ν•˜λŠ” λ©”μ„œλ“œμž„
tokenURI, // URI
HttpMethod.POST, // Method
request, // Request λ‚΄μš©
String.class); // 받을 응닡 μžλ£Œν˜•

ObjectMapper objectMapper = new ObjectMapper();
OAuth2DTO.OAuth2TokenDTO oAuth2TokenDTO = null;

try {//objectMapper.readValue(λ³€ν™˜ν•  json, λ°”κΏ€ 클래슀.class) -> json 데이터λ₯Ό java 객체둜 λ³€κ²½
oAuth2TokenDTO = objectMapper.readValue(response1.getBody(), OAuth2DTO.OAuth2TokenDTO.class);
return oAuth2TokenDTO.getAccess_token();
} catch (Exception e) {
throw new MemberException(MemberErrorCode.OAUTH_TOKEN_FAIL);
}
}

//ν† ν°μœΌλ‘œ μΉ΄μΉ΄μ˜€μ„œλ²„μ—μ„œ μœ μ €μ •λ³΄ κ°€μ Έμ˜€κΈ°
private OAuth2DTO.KakaoProfile getProfileFromKakao(String accessToken) {
// ν† ν°μœΌλ‘œ 정보 κ°€μ Έμ˜€κΈ°
RestTemplate restTemplate = new RestTemplate();// μš”μ²­μ„ 보내기 μœ„ν•œ RestTemplate
HttpHeaders httpHeaders = new HttpHeaders();//헀더 μ„ μ–Έ

//Content-Type == HTTP μš”μ²­ 본문의 데이터 ν˜•μ‹μ„ λͺ…μ‹œ
//application/x-www-form-urlencoded == 데이터가 URL 인코딩 λ°©μ‹μœΌλ‘œ 전솑됨

//Authorization == 인증 κ΄€λ ¨ 정보λ₯Ό μ„œλ²„μ— μ „λ‹¬ν•˜κΈ° μœ„ν•œ 헀더
httpHeaders.add("Authorization", "Bearer " + accessToken);//Bearer 뒀에 토큰을 μΆ”κ°€ν•˜λ©΄ μ„œλ²„κ°€ μš”μ²­μžμ˜ 인증 μƒνƒœλ₯Ό 확인 κ°€λŠ₯
httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

HttpEntity<MultiValueMap> request1 = new HttpEntity<>(httpHeaders);

ResponseEntity<String> response2 = restTemplate.exchange(
userInfoURI,// URI
HttpMethod.GET, // Method
request1, // Request λ‚΄μš©
String.class // 받을 응닡 μžλ£Œν˜•
);


ObjectMapper om = new ObjectMapper();

try {
return om.readValue(response2.getBody(), OAuth2DTO.KakaoProfile.class);
} catch (Exception e) {
throw new MemberException(MemberErrorCode.OAUTH_USER_INFO_FAIL);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.umc7th.domain.openAPI.component;

import org.springframework.web.reactive.function.client.WebClient;

public interface OpenApiWebClient {
// ν•œκ΅­ 관광정보λ₯Ό κ°€μ Έμ˜¬ 수 μžˆλŠ” WebClientλ₯Ό λ°˜ν™˜ν•˜λŠ” λ©”μ†Œλ“œ μ •μ˜
public WebClient getTourWebClient(String language);
// μ•„λž˜μ— λ©”μ†Œλ“œλ₯Ό μΆ”κ°€ν•˜λ©΄μ„œ ν™•μž₯ν•΄λ‚˜κ°ˆ 수 있겠죠?

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.umc7th.domain.openAPI.component;

import com.example.umc7th.domain.openAPI.exception.OpenAPIErrorCode;
import com.example.umc7th.domain.openAPI.exception.OpenAPIException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;

@Component
@Slf4j
public class OpenApiWebClientImpl implements OpenApiWebClient {

@Override
public WebClient getTourWebClient(String language) {
if (language.equals("korean")) {
return getWebClient("https://apis.data.go.kr/B551011/KorService1");
} else if (language.equals("english")) {
return getWebClient("https://apis.data.go.kr/B551011/EngService1");
} else {
throw new OpenAPIException(OpenAPIErrorCode.UNSUPPORTED_LANGUAGE);
}
}

// 영문 APIλ₯Ό μΆ”κ°€ν•œ 경우
// @Override
// public WebClient getEnglishTourWebClient() {
// return getWebClient("https://apis.data.go.kr/B551011/EngService1");
// }

private WebClient getWebClient(String baseUrl) {
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(20000));

// Uriλ₯Ό buildν•˜λŠ” factory 생성 (baseUrl을 WebClient λŒ€μ‹  여기에 ν¬ν•¨ν•˜λ„λ‘)
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
// Uri factory에 인코딩 λͺ¨λ“œλ₯Ό NONE으둜 λ°”κΎΈμ–΄ μΈμ½”λ”©ν•˜μ§€ μ•Šλ„λ‘ν•΄μ€λ‹ˆλ‹€.
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);

return WebClient.builder()
.uriBuilderFactory(factory)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.filter((request, next) -> {
log.info("Web Client Request: " + request.url());
return next.exchange(request);
})
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.umc7th.domain.openAPI.controller;

import com.example.umc7th.domain.openAPI.dto.OpenApiResponseDTO;
import com.example.umc7th.domain.openAPI.service.OpenApiQueryService;
import com.example.umc7th.global.apiPayload.CustomResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class OpenApiController {

private final OpenApiQueryService openApiQueryService;

@GetMapping("/searchStay")
public CustomResponse<OpenApiResponseDTO.SearchStayResponseListDTO> controller(@RequestParam(name = "arrange", defaultValue = "A") String arrange,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "offset", defaultValue = "10") int offset) {
return CustomResponse.onSuccess(openApiQueryService.searchStay(arrange, page, offset));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.umc7th.domain.openAPI.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

public class OpenApiResponseDTO {

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
// ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ json 값을 Parsingν•  λ•Œ ν•„λ“œκ°€ μ—†λŠ” 경우 λ¬΄μ‹œν•˜μ—¬ μ—λŸ¬κ°€ ν„°μ§€λŠ” 것을 λ°©μ§€, ν•œλ²ˆ 없이 λŒλ €λ³΄μ‹œλ©΄ 이해가 더 잘 λ˜μ‹€κ²λ‹ˆλ‹€.
@JsonIgnoreProperties(ignoreUnknown = true)
public static class SearchStayResponseDTO {
// μ•„λž˜ λ³€μˆ˜λŠ” Api λͺ…μ„Έμ„œμ˜ 응닡을 보고 κ·ΈλŒ€λ‘œ λ°›κ³  싢은 κ°’λ“€λ§Œ λ˜‘κ°™μ€ μ΄λ¦„μœΌλ‘œ λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.
private String addr1;
private String title;
private String tel;
private String contentid;
private String contenttypeid;
private String createdtime;
private String firstimage;
private String firstimage2;
private String mapx;
private String mapy;
}

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public static class SearchStayResponseListDTO {
private List<SearchStayResponseDTO> item;

public static SearchStayResponseListDTO from(List<SearchStayResponseDTO> list) {
return SearchStayResponseListDTO.builder()
.item(list)
.build();
}
}
}
Loading