Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
68da97f
build: .gitignore 업데이트
jiyun-im-dev May 21, 2025
b15daca
build: OAuth2 의존성 추가
jiyun-im-dev May 21, 2025
41c3467
style: DTO 클래스 이름 변경
jiyun-im-dev May 21, 2025
ff1cf7d
feat: 카카오 로그인 기능 추가
jiyun-im-dev May 21, 2025
1ce36d4
Merge branch 'dev' of https://github.com/pokerbearkr/nullnullTicket i…
jiyun-im-dev May 21, 2025
bb16949
Merge branch 'dev' of https://github.com/pokerbearkr/nullnullTicket i…
jiyun-im-dev May 22, 2025
20ef6ef
feat: 네이버 로그인 구조 잡기
jiyun-im-dev May 22, 2025
08ba4e0
Merge branch 'dev' of https://github.com/pokerbearkr/nullnullTicket i…
jiyun-im-dev May 23, 2025
184e1b4
Merge branch 'dev' of https://github.com/pokerbearkr/nullnullTicket i…
jiyun-im-dev May 23, 2025
e3fc7a0
refactor : 불필요한 애매 정보 생성 api 제거
crocusia May 23, 2025
1ceb711
feat : SeateSchedulerInfo 상태 업데이트 메서드, redisKey 생성 메서드 추가
crocusia May 23, 2025
1c32775
refactor : 회차별 좌석 상태 변경 메서드 적용
crocusia May 23, 2025
aec9f1f
feat: 카카오 로그인 기능 추가
jiyun-im-dev May 24, 2025
94f37d4
feat : TTL 기반 상태 변경을 위한 메서드 추가
crocusia May 24, 2025
7d9f416
feat : TTL 만료된 데이터 상태 변경을 위한 스케줄링 기능 추가
crocusia May 24, 2025
83475ee
feat : 예매 취소, 결제 완료 기능에 좌석 상태 변경 메서드 적용
crocusia May 24, 2025
d9c6e55
refactor(Seat, SeatScheduleInfo) : CustomExeption 예외로 수정
crocusia May 24, 2025
04e31da
Merge branch 'feat/oauth' of https://github.com/pokerbearkr/nullnullT…
crocusia May 25, 2025
03142ea
refactor : Redis Value 저장 형식 통일
crocusia May 26, 2025
2212991
refactor : 만료시간 설정 변경
crocusia May 26, 2025
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.example.siljeun.domain.auth.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.siljeun.domain.auth.dto.request.LoginRequest;
import org.example.siljeun.domain.auth.dto.request.SignUpRequest;
import org.example.siljeun.domain.auth.dto.response.LoginResponse;
Expand All @@ -17,6 +18,7 @@
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

private final AuthService authService;
Expand All @@ -33,6 +35,7 @@ public ResponseEntity<SignUpResponse> signUp(@RequestBody SignUpRequest request)
@PostMapping("/login")
public ResponseDto<LoginResponse> login(@RequestBody LoginRequest request) {
try {
log.debug("----- 로그인 메서드 실행 -----");
LoginResponse response = authService.login(request.username(), request.password());
return ResponseDto.success("로그인 성공", response);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public SignUpResponse signUp(SignUpRequest request) {
String password = passwordEncoder.encode(request.password());

// 회원 생성 및 저장
User user = new User(request.email(), request.username(), password, request.nickname(),
request.role(), request.provider());
User user = new User(request.email(), request.username(), password, request.name(),
request.nickname(), request.role(), request.provider());
User savedUser = userRepository.save(user);

return new SignUpResponse(savedUser.getId(), savedUser.getEmail(), savedUser.getUsername());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,71 @@
package org.example.siljeun.domain.oauth.client;

import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.siljeun.domain.oauth.dto.KakaoAccessToken;
import org.example.siljeun.domain.oauth.dto.KakaoUserInfo;
import org.example.siljeun.global.config.KakaoOAuthProperties;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

@Component
@RequiredArgsConstructor
public class KakaoApiClient {
@Slf4j
public class KakaoOAuthClient {

private final RestTemplate restTemplate;
private final KakaoOAuthProperties properties;

// 현재 카카오 API 서버에서 인가 코드를 제공한 상태이다
// 서비스 서버가 인가 코드를 이용해 카카오 API 서버로 액세스 토큰을 요청한다
public String getAccessToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
// 아래 4가지 값은 필수
params.add("grant_type", "authorization_code");
params.add("client_id", "eaee0e144aeb9afef54d5c449448baea");
params.add("redirect_uri", "http://localhost:8080/oauth/kakao/callback");
params.add("code", code);
params.add("client_id", "eaee0e144aeb9afef54d5c449448baea"); // 카카오 REST API 키
params.add("redirect_uri", "http://localhost:8080/oauth/kakao/callback"); // 여기서 문제 발생?
params.add("code", code); // 인가 코드

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
// 명시한 URL로 (인가 코드를 담은) POST 요청을 보내면 카카오 API 서버에서 액세스 토큰을 응답한다
KakaoAccessToken response = restTemplate.postForEntity(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
request,
new ParameterizedTypeReference<>() {
}
);
KakaoAccessToken.class
).getBody();

if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
throw new RuntimeException("카카오 Access Token 요청 실패");
}
log.debug("----- 액세스 토큰: {} -----", response.accessToken());

return response.getBody().get("access_token").toString();
return response.accessToken();
}

// 서비스 서버가 카카오 인증 서버에 저장된 회원 정보를 요청한다
public KakaoUserInfo getUserInfo(String accessToken) {
HttpHeaders headers = new HttpHeaders();
// HTTP 헤더 설정
final HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

HttpEntity<Void> request = new HttpEntity<>(headers);
// 설정한 HTTP 헤더를 이용해 요청 생성
final HttpEntity<Void> request = new HttpEntity<>(headers);

ResponseEntity<KakaoUserInfo> response = restTemplate.exchange(
// GET 메서드로 회원 정보를 요청한 후 KakaoUserInfo 객체에 담음
final KakaoUserInfo response = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
request,
KakaoUserInfo.class
);

if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
throw new RuntimeException("카카오 사용자 정보 요청 실패");
}
).getBody();

return response.getBody();
return response;
}

}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package org.example.siljeun.domain.oauth.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.siljeun.domain.auth.dto.response.LoginResponse;
import org.example.siljeun.domain.oauth.service.KakaoOAuthService;
import org.example.siljeun.global.dto.ResponseDto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -12,14 +13,21 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth")
@Slf4j
public class OAuthController {

private final KakaoOAuthService kakaoOAuthService;

/*
1. 클라이언트가 카카오 로그인을 요청한다
2. /oauth/kakao/callback?code={code}로 리다이렉트된다
3. 이때 카카오에서 쿼리 스트링으로 인가 코드를 넘겨준다
4. 넘어온 인가 코드를 이용해서 카카오 로그인 API를 호출한다
*/
@GetMapping("/kakao/callback")
public ResponseEntity<?> kakaoCallback(@RequestParam String code) {
String jwt = kakaoOAuthService.kakaoLogin(code);
return ResponseEntity.ok(jwt);
public ResponseEntity<LoginResponse> kakaoCallback(@RequestParam String code) {
log.debug("---------- METHOD: kakaoCallback ----------");
return ResponseEntity.ok(kakaoOAuthService.kakaoLogin(code));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example.siljeun.domain.oauth.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccessToken(String tokenType,
String accessToken,
Integer expiresIn,
String refreshToken,
Integer refreshTokenExpiresIn) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.example.siljeun.domain.oauth.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccount(
KakaoProfile profile, // 프로필 정보(닉네임, 프로필 사진)
String email
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.example.siljeun.domain.oauth.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoProfile(
String nickname,
String profileImageUrl
) {

}
Original file line number Diff line number Diff line change
@@ -1,41 +1,12 @@
package org.example.siljeun.domain.oauth.dto;

import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class KakaoUserInfo implements OAuth2UserInfo {

private Map<String, Object> attributes;
private Map<String, Object> attributesAccount;
private Map<String, Object> attributesProfile;

public KakaoUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
}

@Override
public String getProvider() {
return "Kakao";
}

@Override
public String getProviderId() {
return attributes.get("id").toString();
}

@Override
public String getEmail() {
return attributesAccount.get("email").toString();
}

@Override
public String getNickname() {
return attributesProfile.get("nickname").toString();
}
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserInfo(
Long id, // 회원 번호
KakaoAccount kakaoAccount // 카카오 계정 정보
) {

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,40 +1,73 @@
package org.example.siljeun.domain.oauth.service;

import lombok.RequiredArgsConstructor;
import org.example.siljeun.domain.oauth.client.KakaoApiClient;
import lombok.extern.slf4j.Slf4j;
import org.example.siljeun.domain.auth.dto.response.LoginResponse;
import org.example.siljeun.domain.oauth.client.KakaoOAuthClient;
import org.example.siljeun.domain.oauth.dto.KakaoUserInfo;
import org.example.siljeun.domain.user.entity.User;
import org.example.siljeun.domain.user.enums.Provider;
import org.example.siljeun.domain.user.enums.Role;
import org.example.siljeun.domain.user.repository.UserRepository;
import org.example.siljeun.global.security.JwtUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class KakaoOAuthService {

private final KakaoApiClient kakaoApiClient;
private final KakaoOAuthClient kakaoOAuthClient;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;

public String kakaoLogin(String code) {
// 1. 액세스 토큰 요청
String accessToken = kakaoApiClient.getAccessToken(code);
// 인가 코드를 이용해 카카오 로그인 API를 호출한다
public LoginResponse kakaoLogin(String code) {
// 1. 카카오에 인가 코드를 넘겨서 액세스 토큰을 획득한다
log.debug("----- 액세스 토큰 발급 -----");
final String accessToken = kakaoOAuthClient.getAccessToken(code);

// 2. 사용자 정보 요청
KakaoUserInfo userInfo = kakaoApiClient.getUserInfo(accessToken);
// 2. 카카오에 액세스 토큰을 넘겨서 카카오에 저장된 사용자 정보를 획득한다
log.debug("----- 사용자 정보 획득 -----");
final KakaoUserInfo userInfo = kakaoOAuthClient.getUserInfo(accessToken);

// 3. 회원 가입 또는 로그인 처리
User user = userRepository.findByEmail(userInfo.getEmail())
// 3. 해당 정보를 이용해 회원 가입 또는 로그인을 처리한다
log.debug("----- 회원 가입 또는 로그인 -----");
User user = userRepository.findByEmail(userInfo.kakaoAccount().email())
.orElseGet(() -> registerUser(userInfo));

// 4. JWT 토큰 발급
return jwtUtil.createToken(user.getUsername());
// 4. 서비스 서버에 저장된 회원 정보를 이용해 JWT 토큰을 발급받는다
log.debug("----- JWT 토큰 발급 -----");
String token = jwtUtil.createToken(user.getUsername());

return new LoginResponse(token);
}

private User registerUser(KakaoUserInfo userInfo) {
User user = new User(userInfo.getEmail(), userInfo.getNickname(), Provider.KAKAO,
userInfo.getProviderId());
String username = "kakao" + userInfo.id();
String password = passwordEncoder.encode(username);
User user = new User(
userInfo.kakaoAccount().email(),
username,
password,
userInfo.kakaoAccount().profile().nickname(),
userInfo.kakaoAccount().profile().nickname(),
Role.USER,
Provider.KAKAO,
userInfo.id()
);
log.debug("--------------------회원 가입 메서드 실행--------------------");
log.debug("email: {}, username: {}, password: {}, name: {}, nickname: {}, id: {}",
userInfo.kakaoAccount().email(),
username,
password,
userInfo.kakaoAccount().profile().nickname(),
userInfo.kakaoAccount().profile().nickname(),
userInfo.id()
);

return userRepository.save(user);
}

Expand Down

This file was deleted.

Loading