From b0cab9808d6eed69b80384fe6bf45f9173c7363e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Thu, 10 Jul 2025 16:53:43 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20Kakao=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20KakaoProperties=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20JsonProperty=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 +- .../moddo/domain/auth/dto/KakaoProfile.java | 30 ++++++++++--------- .../domain/auth/service/AuthService.java | 2 +- .../domain/auth/service/KakaoClient.java | 16 ++++------ .../CookieProperties.java | 2 +- .../moddo/global/config/KakaoProperties.java | 13 ++++++++ .../domain/auth/service/AuthServiceTest.java | 4 +-- .../domain/auth/service/KakaoClientTest.java | 2 +- 8 files changed, 41 insertions(+), 30 deletions(-) rename src/main/java/com/dnd/moddo/global/{jwt/properties => config}/CookieProperties.java (86%) create mode 100644 src/main/java/com/dnd/moddo/global/config/KakaoProperties.java diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index 3486865..d67c411 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -15,9 +15,9 @@ import com.dnd.moddo.domain.auth.service.AuthService; import com.dnd.moddo.domain.auth.service.KakaoClient; import com.dnd.moddo.domain.auth.service.RefreshTokenService; +import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.dto.RefreshResponse; import com.dnd.moddo.global.jwt.dto.TokenResponse; -import com.dnd.moddo.global.jwt.properties.CookieProperties; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java index 0f61a89..cc9f71a 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java @@ -1,36 +1,38 @@ package com.dnd.moddo.domain.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + public record KakaoProfile( Long id, - String connected_at, + @JsonProperty("connected_at") String connectedAt, Properties properties, - KakaoAccount kakao_account + @JsonProperty("kakao_account") KakaoAccount kakaoAccount ) { public record Properties( String nickname, - String profile_image, - String thumbnail_image + @JsonProperty("profile_image") String profileImage, + @JsonProperty("thumbnail_image") String thumbnailImage ) { } public record KakaoAccount( - Boolean profile_nickname_needs_agreement, - Boolean profile_image_needs_agreement, + @JsonProperty("profile_nickname_needs_agreement") Boolean profileNicknameNeedsAgreement, + @JsonProperty("profile_image_needs_agreement") Boolean profileImageNeedsAgreement, Profile profile, - Boolean has_email, - Boolean email_needs_agreement, - Boolean is_email_valid, - Boolean is_email_verified, + @JsonProperty("has_email") Boolean hasEmail, + @JsonProperty("email_needs_agreement") Boolean emailNeedsAgreement, + @JsonProperty("is_email_valid") Boolean isEmailValid, + @JsonProperty("is_email_verified") Boolean isEmailVerified, String email ) { } public record Profile( String nickname, - String thumbnail_image_url, - String profile_image_url, - Boolean is_default_image, - Boolean is_default_nickname + @JsonProperty("thumbnail_image_url") String thumbnailImageUrl, + @JsonProperty("profile_image_url") String profileImageUrl, + @JsonProperty("is_default_image") Boolean isDefaultImage, + @JsonProperty("is_default_nickname") Boolean isDefaultNickname ) { } } diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index 82cbf1e..b48ff43 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -60,7 +60,7 @@ private User createUser(String email, String name, boolean isMember) { public TokenResponse getOrCreateKakaoUserToken(String token) { KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(token); - String email = kakaoProfile.kakao_account().email(); + String email = kakaoProfile.kakaoAccount().email(); String nickname = kakaoProfile.properties().nickname(); User kakaoUser = userRepository.findByEmail(email) diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java index aa845b9..d50fccd 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java @@ -1,6 +1,6 @@ package com.dnd.moddo.domain.auth.service; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -12,6 +12,7 @@ import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.global.config.KakaoProperties; import com.dnd.moddo.global.exception.ModdoException; import lombok.RequiredArgsConstructor; @@ -20,14 +21,9 @@ @RequiredArgsConstructor @Slf4j @Component +@EnableConfigurationProperties(KakaoProfile.class) public class KakaoClient { - - @Value("${kakao.auth.client_id}") - String client_id; - - @Value("${kakao.auth.redirect_uri}") - String redirect_uri; - + private final KakaoProperties kakaoProperties; private final RestClient.Builder builder; public KakaoTokenResponse join(String code) { @@ -37,8 +33,8 @@ public KakaoTokenResponse join(String code) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); - params.add("client_id", client_id); - params.add("redirect_uri", redirect_uri); + params.add("client_id", kakaoProperties.client_id()); + params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", code); try { diff --git a/src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java similarity index 86% rename from src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java rename to src/main/java/com/dnd/moddo/global/config/CookieProperties.java index d511368..e274108 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/properties/CookieProperties.java +++ b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java @@ -1,4 +1,4 @@ -package com.dnd.moddo.global.jwt.properties; +package com.dnd.moddo.global.config; import java.time.Duration; diff --git a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java new file mode 100644 index 0000000..a90faf8 --- /dev/null +++ b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java @@ -0,0 +1,13 @@ +package com.dnd.moddo.global.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@ConfigurationProperties(prefix = "kakao") +public record KakaoProperties( + @JsonProperty("redirect_uri") String redirectUri, + @JsonProperty("client_id") String client_id +) { + +} diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index e574fa0..02b45d9 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -71,7 +71,7 @@ void whenKakaoUserExists_thenTokenIsIssued() { "test@example.com" ) ); - String email = kakaoProfile.kakao_account().email(); + String email = kakaoProfile.kakaoAccount().email(); User user = createWithEmail(email); when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); @@ -113,7 +113,7 @@ void whenNewKakaoUser_thenRegisterAndIssueToken() { "test@example.com" ) ); - String email = kakaoProfile.kakao_account().email(); + String email = kakaoProfile.kakaoAccount().email(); User user = createWithEmail(email); when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java index ed85bf1..a025648 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java @@ -149,7 +149,7 @@ void whenGetKakaoProfile_thenReturnKakaoProfile() { // then assertThat(profile).isNotNull(); assertThat(profile.id()).isEqualTo(12345L); - assertThat(profile.kakao_account().email()).isEqualTo("test@example.com"); + assertThat(profile.kakaoAccount().email()).isEqualTo("test@example.com"); assertThat(profile.properties().nickname()).isEqualTo("테스트유저"); } From b02294bd9e094bdb1e4fdc31e03bf97c167f7114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 00:18:32 +0900 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=AA=85=ED=99=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게스트/카카오 로그인 플로우를 분리 - loginWithGuest, loginOrRegisterWithKakao 메서드로 역할 명확히 구분 - 인증 로직 구조 개선 및 가독성 향상 - 카카오 프로필 데이터 구조를 단순화하여 필요한 정보만 사용하도록 변경 --- .../java/com/dnd/moddo/ModdoApplication.java | 2 + .../auth/controller/AuthController.java | 13 ++-- .../moddo/domain/auth/dto/KakaoProfile.java | 27 ++------ .../domain/auth/dto/KakaoTokenResponse.java | 10 ++- .../domain/auth/service/AuthService.java | 54 +++++---------- .../domain/auth/service/KakaoClient.java | 9 +-- .../user/dto/request/UserCreateRequest.java | 26 ++++++++ .../dnd/moddo/domain/user/entity/User.java | 65 ++++++++++++------- .../user/repository/UserRepository.java | 25 +++---- .../domain/user/service/UserService.java | 32 +++++++++ .../moddo/global/config/KakaoProperties.java | 8 +-- src/main/resources/application.yml | 6 ++ .../auth/controller/AuthControllerTest.java | 8 +-- .../domain/auth/service/AuthServiceTest.java | 56 +++++----------- .../domain/auth/service/KakaoClientTest.java | 30 +++------ .../integration/CacheIntegrationTest.java | 6 +- src/test/resources/application.yml | 6 +- 17 files changed, 194 insertions(+), 189 deletions(-) create mode 100644 src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java create mode 100644 src/main/java/com/dnd/moddo/domain/user/service/UserService.java diff --git a/src/main/java/com/dnd/moddo/ModdoApplication.java b/src/main/java/com/dnd/moddo/ModdoApplication.java index 25ebbc2..3923d8d 100644 --- a/src/main/java/com/dnd/moddo/ModdoApplication.java +++ b/src/main/java/com/dnd/moddo/ModdoApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@ConfigurationPropertiesScan public class ModdoApplication { public static void main(String[] args) { diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index d67c411..b36c268 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -1,9 +1,9 @@ package com.dnd.moddo.domain.auth.controller; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.auth.service.AuthService; import com.dnd.moddo.domain.auth.service.KakaoClient; import com.dnd.moddo.domain.auth.service.RefreshTokenService; @@ -24,7 +23,7 @@ @RequiredArgsConstructor @RestController -@EnableConfigurationProperties(CookieProperties.class) +@Validated @RequestMapping("/api/v1") public class AuthController { @@ -35,7 +34,7 @@ public class AuthController { @GetMapping("/user/guest/token") public ResponseEntity getGuestToken() { - TokenResponse tokenResponse = authService.createGuestUser(); + TokenResponse tokenResponse = authService.loginWithGuest(); String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString(); @@ -50,10 +49,9 @@ public RefreshResponse reissueAccessToken(@RequestHeader(value = "Authorization" } @GetMapping("/login/oauth2/callback") - public ResponseEntity kakaoLoginCallback(@RequestParam String code) { - KakaoTokenResponse kakaoTokenResponse = kakaoClient.join(code); + public ResponseEntity kakaoLoginCallback(@RequestParam @NotBlank String code) { - TokenResponse tokenResponse = authService.getOrCreateKakaoUserToken(kakaoTokenResponse.access_token()); + TokenResponse tokenResponse = authService.loginOrRegisterWithKakao(code); String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString(); @@ -93,4 +91,5 @@ private ResponseCookie expireCookie(String name) { .maxAge(0L) .build(); } + } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java index cc9f71a..2cf1866 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java @@ -4,35 +4,22 @@ public record KakaoProfile( Long id, - @JsonProperty("connected_at") String connectedAt, - Properties properties, - @JsonProperty("kakao_account") KakaoAccount kakaoAccount + @JsonProperty("kakao_account") KakaoAccount kakaoAccount, + Properties properties ) { public record Properties( - String nickname, - @JsonProperty("profile_image") String profileImage, - @JsonProperty("thumbnail_image") String thumbnailImage + String nickname ) { } public record KakaoAccount( - @JsonProperty("profile_nickname_needs_agreement") Boolean profileNicknameNeedsAgreement, - @JsonProperty("profile_image_needs_agreement") Boolean profileImageNeedsAgreement, - Profile profile, - @JsonProperty("has_email") Boolean hasEmail, - @JsonProperty("email_needs_agreement") Boolean emailNeedsAgreement, - @JsonProperty("is_email_valid") Boolean isEmailValid, - @JsonProperty("is_email_verified") Boolean isEmailVerified, - String email + String email, + Profile profile ) { } public record Profile( - String nickname, - @JsonProperty("thumbnail_image_url") String thumbnailImageUrl, - @JsonProperty("profile_image_url") String profileImageUrl, - @JsonProperty("is_default_image") Boolean isDefaultImage, - @JsonProperty("is_default_nickname") Boolean isDefaultNickname + String nickname ) { } -} +} \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java index c1d45b5..7e19764 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoTokenResponse.java @@ -1,11 +1,9 @@ package com.dnd.moddo.domain.auth.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + public record KakaoTokenResponse( - String access_token, - String token_type, - String refresh_token, - int expires_in, - String scope, - int refresh_token_expires_in + @JsonProperty("access_token") String accessToken, + @JsonProperty("expires_in") int expiresIn ) { } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index b48ff43..611b6b9 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -1,16 +1,15 @@ package com.dnd.moddo.domain.auth.service; -import java.time.LocalDateTime; import java.util.UUID; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.dnd.moddo.domain.auth.dto.KakaoProfile; +import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.domain.user.dto.request.UserCreateRequest; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.UserService; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @@ -22,54 +21,37 @@ @Slf4j public class AuthService { - private final UserRepository userRepository; + private final UserService userService; private final JwtProvider jwtProvider; private final KakaoClient kakaoClient; - @Value("${kakao.auth.client_id}") - String client_id; - - @Value("${kakao.auth.redirect_uri}") - String redirect_uri; - @Transactional - public TokenResponse createGuestUser() { + public TokenResponse loginWithGuest() { String guestEmail = "guest-" + UUID.randomUUID() + "@guest.com"; + UserCreateRequest request = new UserCreateRequest(guestEmail, "Guest", null, false); - User guestUser = createUser(guestEmail, "Guest", false); - - return jwtProvider.generateToken(guestUser.getId(), guestUser.getEmail(), guestUser.getAuthority().toString(), - guestUser.getIsMember()); - } - - private User createUser(String email, String name, boolean isMember) { - User user = User.builder() - .email(email) - .name(name) - .profile(null) - .createdAt(LocalDateTime.now()) - .expiredAt(LocalDateTime.now().plusMonths(1)) - .authority(Authority.USER) - .isMember(isMember) - .build(); + User user = userService.createGuestUser(request); - return userRepository.save(user); + return jwtProvider.generateToken(user.getId(), user.getEmail(), + user.getAuthority().toString(), user.getIsMember()); } @Transactional - public TokenResponse getOrCreateKakaoUserToken(String token) { - KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(token); + public TokenResponse loginOrRegisterWithKakao(String code) { + KakaoTokenResponse tokenResponse = kakaoClient.join(code); + KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(tokenResponse.accessToken()); String email = kakaoProfile.kakaoAccount().email(); String nickname = kakaoProfile.properties().nickname(); + Long kakaoId = kakaoProfile.id(); - User kakaoUser = userRepository.findByEmail(email) - .orElseGet(() -> createUser(email, nickname, true)); + UserCreateRequest request = new UserCreateRequest(email, nickname, kakaoId, true); + User user = userService.getOrCreateUser(request); - log.info("[USER_LOGIN] 로그인 성공 : email={}, name={}", kakaoUser.getEmail(), kakaoUser.getName()); + log.info("[USER_LOGIN] 로그인 성공 : code = {}, kakaoId = {}, nickname = {}", code, kakaoId, nickname); - return jwtProvider.generateToken(kakaoUser.getId(), kakaoUser.getEmail(), kakaoUser.getAuthority().toString(), - kakaoUser.getIsMember()); + return jwtProvider.generateToken(user.getId(), user.getEmail(), user.getAuthority().toString(), + user.getIsMember()); } } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java index d50fccd..366a7dd 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java @@ -1,6 +1,5 @@ package com.dnd.moddo.domain.auth.service; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -21,7 +20,6 @@ @RequiredArgsConstructor @Slf4j @Component -@EnableConfigurationProperties(KakaoProfile.class) public class KakaoClient { private final KakaoProperties kakaoProperties; private final RestClient.Builder builder; @@ -29,11 +27,11 @@ public class KakaoClient { public KakaoTokenResponse join(String code) { RestClient restClient = builder.build(); - String uri = "https://kauth.kakao.com/oauth/token"; + String uri = kakaoProperties.tokenRequestUri(); MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); - params.add("client_id", kakaoProperties.client_id()); + params.add("client_id", kakaoProperties.clientId()); params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", code); @@ -58,13 +56,12 @@ public KakaoTokenResponse join(String code) { public KakaoProfile getKakaoProfile(String token) { RestClient restClient = builder.build(); - String uri = "https://kapi.kakao.com/v2/user/me"; + String uri = kakaoProperties.profileRequestUri(); try { return restClient.get() .uri(uri) .header("Authorization", "Bearer " + token) - .header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") .retrieve() .body(KakaoProfile.class); diff --git a/src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java new file mode 100644 index 0000000..8f599fe --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java @@ -0,0 +1,26 @@ +package com.dnd.moddo.domain.user.dto.request; + +import java.time.LocalDateTime; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.entity.type.Authority; + +public record UserCreateRequest( + String email, + String name, + Long kakaoId, + boolean isMember +) { + public User toEntity() { + return User.builder() + .email(email) + .name(name) + .kakaoId(kakaoId) + .isMember(isMember) + .authority(Authority.USER) + .profile(null) + .createdAt(LocalDateTime.now()) + .expiredAt(LocalDateTime.now().plusMonths(1)) + .build(); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/entity/User.java b/src/main/java/com/dnd/moddo/domain/user/entity/User.java index 8005d6d..88aa99c 100644 --- a/src/main/java/com/dnd/moddo/domain/user/entity/User.java +++ b/src/main/java/com/dnd/moddo/domain/user/entity/User.java @@ -1,45 +1,60 @@ package com.dnd.moddo.domain.user.entity; +import java.time.LocalDateTime; + import com.dnd.moddo.domain.user.entity.type.Authority; -import jakarta.persistence.*; -import lombok.*; -import java.time.LocalDateTime; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "users") -public class User{ +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private String name; - private String name; + private String email; - private String email; + private String profile; - private String profile; + private Boolean isMember; - private Boolean isMember; + private Long kakaoId; - private LocalDateTime createdAt; + private LocalDateTime createdAt; - private LocalDateTime expiredAt; + private LocalDateTime expiredAt; - @Enumerated(EnumType.STRING) - private Authority authority; + @Enumerated(EnumType.STRING) + private Authority authority; - @Builder - public User(String name, String email, String profile, Boolean isMember, Authority authority, LocalDateTime createdAt, LocalDateTime expiredAt) { - this.name = name; - this.email = email; - this.profile = profile; - this.isMember = isMember; - this.createdAt = createdAt; - this.expiredAt = expiredAt; - this.authority = authority; - } + @Builder + public User(String name, String email, String profile, Boolean isMember, Authority authority, Long kakaoId, + LocalDateTime createdAt, LocalDateTime expiredAt) { + this.name = name; + this.email = email; + this.profile = profile; + this.isMember = isMember; + this.kakaoId = kakaoId; + this.createdAt = createdAt; + this.expiredAt = expiredAt; + this.authority = authority; + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java index d2788cb..2c69fec 100644 --- a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java +++ b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java @@ -1,26 +1,27 @@ package com.dnd.moddo.domain.user.repository; +import java.util.Optional; -import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.exception.UserNotFoundException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.Optional; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.exception.UserNotFoundException; @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmail(String email); - default User getByEmail(String email) { - return findByEmail(email) - .orElseThrow(() -> new UserNotFoundException(email)); - } + Optional findByKakaoId(Long kakaoId); + default User getByEmail(String email) { + return findByEmail(email) + .orElseThrow(() -> new UserNotFoundException(email)); + } - default User getById(Long id) { - return findById(id) - .orElseThrow(() -> new UserNotFoundException(id)); - } + default User getById(Long id) { + return findById(id) + .orElseThrow(() -> new UserNotFoundException(id)); + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/service/UserService.java b/src/main/java/com/dnd/moddo/domain/user/service/UserService.java new file mode 100644 index 0000000..079a85d --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/UserService.java @@ -0,0 +1,32 @@ +package com.dnd.moddo.domain.user.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.dto.request.UserCreateRequest; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class UserService { + private final UserRepository userRepository; + + @Transactional + public User createGuestUser(UserCreateRequest request) { + return userRepository.save(request.toEntity()); + } + + @Transactional + public User createKakaoUser(UserCreateRequest request) { + return userRepository.save(request.toEntity()); + } + + @Transactional + public User getOrCreateUser(UserCreateRequest request) { + return userRepository.findByKakaoId(request.kakaoId()) + .orElseGet(() -> createKakaoUser(request)); + } +} diff --git a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java index a90faf8..409a9b4 100644 --- a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java +++ b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java @@ -2,12 +2,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - @ConfigurationProperties(prefix = "kakao") public record KakaoProperties( - @JsonProperty("redirect_uri") String redirectUri, - @JsonProperty("client_id") String client_id + String redirectUri, + String clientId, + String tokenRequestUri, + String profileRequestUri ) { } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1560799..74d9a77 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,7 +25,13 @@ management: exposure: include: health,info,prometheus +kakao: + token-request-uri: https://kauth.kakao.com/oauth/token + profile-request-uri: https://kapi.kakao.com/v2/user/me + logout-request-uri: https://kapi.kakao.com/v1/user/logout + cookie: + secure: true http-only: false path: / same-site: none diff --git a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java index ad55b20..995176f 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java @@ -30,7 +30,7 @@ void getGuestToken() throws Exception { ZonedDateTime.now().plusDays(30), false ); - given(authService.createGuestUser()).willReturn(response); + given(authService.loginWithGuest()).willReturn(response); // when & then mockMvc.perform(get("/api/v1/user/guest/token")) @@ -79,14 +79,12 @@ void reissueAccessToken() throws Exception { @DisplayName("카카오에서 인가코드를 통해 토큰을 발급받아 사용자 정보를 가져와 등록시킨 뒤 엑세스 토큰을 발급하여 쿠키로 전달한다.") void kakaoLoginCallback() throws Exception { //given - KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("kakao-access-token", "bearer", - "kakao-refresh-token", - 3600, "profile", 7200); + KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("kakao-access-token", 3600); given(kakaoClient.join(anyString())).willReturn(kakaoTokenResponse); TokenResponse tokenResponse = new TokenResponse("access-token", "refresh-token", ZonedDateTime.now().plusMonths(1), true); - given(authService.getOrCreateKakaoUserToken(anyString())).willReturn(tokenResponse); + given(authService.loginOrRegisterWithKakao(anyString())).willReturn(tokenResponse); //when & then mockMvc.perform(get("/api/v1/login/oauth2/callback") diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index 02b45d9..e6ef58e 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -36,7 +36,7 @@ void whenCreateGuestUser_thenSaveAndIssueToken() { User user = createGuestDefault(); when(userRepository.save(any(User.class))).thenReturn(user); //when - TokenResponse response = authService.createGuestUser(); + TokenResponse response = authService.loginWithGuest(); //then verify(userRepository, times(1)).save(any(User.class)); } @@ -48,27 +48,14 @@ void whenKakaoUserExists_thenTokenIsIssued() { String token = "test_token"; KakaoProfile kakaoProfile = new KakaoProfile( 12345L, - "2025.06.29T00:00:00", - new KakaoProfile.Properties( - "테스트유저", - "profile_image", - "thumbnail_image" - ), new KakaoProfile.KakaoAccount( - true, - true, + "test@example.com", new KakaoProfile.Profile( - "테스트 유저", - "thumbnail_image_url", - "profile_image_url", - true, - true - ), - true, - true, - true, - true, - "test@example.com" + "테스트 유저" + ) + ), + new KakaoProfile.Properties( + "테스트유저" ) ); String email = kakaoProfile.kakaoAccount().email(); @@ -77,7 +64,7 @@ void whenKakaoUserExists_thenTokenIsIssued() { when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); //when - TokenResponse response = authService.getOrCreateKakaoUserToken(token); + TokenResponse response = authService.loginOrRegisterWithKakao(token); //then verify(jwtProvider, times(1)).generateToken(any(), anyString(), anyString(), anyBoolean()); @@ -90,27 +77,14 @@ void whenNewKakaoUser_thenRegisterAndIssueToken() { String token = "test_token"; KakaoProfile kakaoProfile = new KakaoProfile( 12345L, - "2025.06.29T00:00:00", - new KakaoProfile.Properties( - "테스트유저", - "profile_image", - "thumbnail_image" - ), new KakaoProfile.KakaoAccount( - true, - true, + "test@example.com", new KakaoProfile.Profile( - "테스트 유저", - "thumbnail_image_url", - "profile_image_url", - true, - true - ), - true, - true, - true, - true, - "test@example.com" + "테스트 유저" + ) + ), + new KakaoProfile.Properties( + "테스트유저" ) ); String email = kakaoProfile.kakaoAccount().email(); @@ -121,7 +95,7 @@ void whenNewKakaoUser_thenRegisterAndIssueToken() { when(userRepository.save(any(User.class))).thenReturn(user); //when - TokenResponse response = authService.getOrCreateKakaoUserToken(token); + TokenResponse response = authService.loginOrRegisterWithKakao(token); //then verify(userRepository, times(1)).save(any(User.class)); diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java index a025648..55b906b 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java @@ -4,12 +4,10 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -21,6 +19,7 @@ import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; +import com.dnd.moddo.global.config.KakaoProperties; import com.dnd.moddo.global.exception.ModdoException; @ExtendWith(SpringExtension.class) @@ -32,16 +31,7 @@ public class KakaoClientTest { @Autowired private MockRestServiceServer mockServer; - @Value("${kakao.auth.client_id}") - private String clientId; - - @Value("${kakao.auth.redirect_uri}") - private String redirectUri; - - @AfterEach - void tearDown() { - mockServer.reset(); - } + private KakaoProperties kakaoProperties; @DisplayName("카카오 인가 코드로 토큰 요청하면 OauthToken을 반환한다") @Test @@ -52,17 +42,13 @@ void whenRequestKakaoAccessToken_thenReturnOauthToken() throws Exception { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("code", "test_code"); params.add("grant_type", "authorization_code"); - params.add("client_id", clientId); - params.add("redirect_uri", redirectUri); + params.add("client_id", kakaoProperties.clientId()); + params.add("redirect_uri", kakaoProperties.redirectUri()); String expectResponse = """ { "access_token": "test_token", - "token_type": "bearer", - "refresh_token": "refresh_token", - "expires_in": 3600, - "scope": "profile", - "refresh_token_expires_in": 7200 + "expires_in": 3600 } """; @@ -76,7 +62,7 @@ void whenRequestKakaoAccessToken_thenReturnOauthToken() throws Exception { // then assertThat(result).isNotNull(); - assertThat(result.access_token()).isEqualTo("test_token"); + assertThat(result.accessToken()).isEqualTo("test_token"); } @DisplayName("잘못된 인가 코드로 토큰 요청 시 IllegalArgumentException이 발생한다") @@ -87,8 +73,8 @@ void whenRequestKakaoAccessTokenWithInvalidCode_thenThrowException() { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); - params.add("client_id", clientId); - params.add("redirect_uri", redirectUri); + params.add("client_id", kakaoProperties.clientId()); + params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", "invalid_code"); mockServer.expect(requestTo("https://kauth.kakao.com/oauth/token")) diff --git a/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java b/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java index 5771c45..9221917 100644 --- a/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java +++ b/src/test/java/com/dnd/moddo/integration/CacheIntegrationTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -64,8 +64,8 @@ void setUp() { groupRepository.save(GroupTestFactory.createDefault()); } - @AfterEach - void tearDown() { + @AfterAll + static void tearDown() { redis.close(); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 18cea29..0b1d1a3 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -47,5 +47,7 @@ cookie: kakao: auth: - client_id: clientidclientidclientidclientidclientidclientidclientid - redirect_uri: http://localhost:8080/api/v1/login/kakao/callback \ No newline at end of file + client-id: clientidclientidclientidclientidclientidclientidclientid + redirect-uri: http://localhost:8080/api/v1/login/kakao/callback + token-request-uri: https://kauth.kakao.com/oauth/token + profile-request-uri: https://kapi.kakao.com/v2/user/me \ No newline at end of file From 4475648f977fa66a619b89f138a176ab57c607b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 00:19:00 +0900 Subject: [PATCH 03/11] =?UTF-8?q?test:=20User=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B9=8C=EB=8D=94=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moddo/global/support/UserTestFactory.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java index 12c756f..c667cbc 100644 --- a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java +++ b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java @@ -3,6 +3,7 @@ import static com.dnd.moddo.domain.user.entity.type.Authority.*; import java.time.LocalDateTime; +import java.util.UUID; import com.dnd.moddo.domain.user.entity.User; @@ -10,26 +11,30 @@ public class UserTestFactory { public static User createGuestDefault() { LocalDateTime time = LocalDateTime.now(); - return new User( - "연노른자", - "guest-UUID1@guest.com", - "profile.png", - false, - USER, - time, - time.plusDays(7)); + return User + .builder() + .name("연노른자") + .email("guest-" + UUID.randomUUID() + "@guest.com") + .profile("profile.png") + .isMember(false) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); } public static User createWithEmail(String email) { LocalDateTime time = LocalDateTime.now(); - - return new User( - "연노른자", - email, - "profile.png", - true, - USER, - time, - time.plusDays(7)); + return User + .builder() + .name("연노른자") + .email(email) + .profile("profile.png") + .isMember(true) + .kakaoId(1234565L) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); } } From a3e0a4ae2c9d79e73257c8d7ef921b8fcdc4dab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 15:41:54 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8/?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EC=9C=A0=EC=A0=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=9A=94=EC=B2=AD=20DTO=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20User=20=EA=B8=B0=EB=B0=98=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 -- .../domain/auth/service/AuthService.java | 27 ++++++++------ .../auth/service/RefreshTokenService.java | 36 ++++++++++--------- .../dto/request/GuestUserSaveRequest.java | 21 +++++++++++ ...reateRequest.java => UserSaveRequest.java} | 7 ++-- ...erService.java => CommandUserService.java} | 11 +++--- .../moddo/global/jwt/utill/JwtProvider.java | 18 ++++++---- 7 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java rename src/main/java/com/dnd/moddo/domain/user/dto/request/{UserCreateRequest.java => UserSaveRequest.java} (84%) rename src/main/java/com/dnd/moddo/domain/user/service/{UserService.java => CommandUserService.java} (65%) diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index b36c268..ac597ab 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RestController; import com.dnd.moddo.domain.auth.service.AuthService; -import com.dnd.moddo.domain.auth.service.KakaoClient; import com.dnd.moddo.domain.auth.service.RefreshTokenService; import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.dto.RefreshResponse; @@ -29,7 +28,6 @@ public class AuthController { private final AuthService authService; private final RefreshTokenService refreshTokenService; - private final KakaoClient kakaoClient; private final CookieProperties cookieProperties; @GetMapping("/user/guest/token") diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index 611b6b9..22497de 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -2,14 +2,17 @@ import java.util.UUID; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; -import com.dnd.moddo.domain.user.dto.request.UserCreateRequest; +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.service.UserService; +import com.dnd.moddo.domain.user.service.CommandUserService; +import com.dnd.moddo.global.exception.ModdoException; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @@ -21,19 +24,18 @@ @Slf4j public class AuthService { - private final UserService userService; + private final CommandUserService commandUserService; private final JwtProvider jwtProvider; private final KakaoClient kakaoClient; @Transactional public TokenResponse loginWithGuest() { String guestEmail = "guest-" + UUID.randomUUID() + "@guest.com"; - UserCreateRequest request = new UserCreateRequest(guestEmail, "Guest", null, false); + GuestUserSaveRequest request = new GuestUserSaveRequest(guestEmail, "Guest"); - User user = userService.createGuestUser(request); + User user = commandUserService.createGuestUser(request); - return jwtProvider.generateToken(user.getId(), user.getEmail(), - user.getAuthority().toString(), user.getIsMember()); + return jwtProvider.generateToken(user); } @Transactional @@ -45,13 +47,16 @@ public TokenResponse loginOrRegisterWithKakao(String code) { String nickname = kakaoProfile.properties().nickname(); Long kakaoId = kakaoProfile.id(); - UserCreateRequest request = new UserCreateRequest(email, nickname, kakaoId, true); - User user = userService.getOrCreateUser(request); + if (email == null || nickname == null || kakaoId == null) { + throw new ModdoException(HttpStatus.BAD_REQUEST, "카카오 프로필 정보가 누락되었습니다."); + } + + UserSaveRequest request = new UserSaveRequest(email, nickname, kakaoId); + User user = commandUserService.getOrCreateUser(request); log.info("[USER_LOGIN] 로그인 성공 : code = {}, kakaoId = {}, nickname = {}", code, kakaoId, nickname); - return jwtProvider.generateToken(user.getId(), user.getEmail(), user.getAuthority().toString(), - user.getIsMember()); + return jwtProvider.generateToken(user); } } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java b/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java index 47b2504..458e6ef 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/RefreshTokenService.java @@ -1,5 +1,7 @@ package com.dnd.moddo.domain.auth.service; +import org.springframework.stereotype.Service; + import com.dnd.moddo.domain.auth.exception.TokenInvalidException; import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.repository.UserRepository; @@ -7,32 +9,32 @@ import com.dnd.moddo.global.jwt.properties.JwtConstants; import com.dnd.moddo.global.jwt.utill.JwtProvider; import com.dnd.moddo.global.jwt.utill.JwtUtil; + import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class RefreshTokenService { - private final JwtUtil jwtUtil; - private final UserRepository userRepository; - private final JwtProvider jwtProvider; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final JwtProvider jwtProvider; - public RefreshResponse execute(String token) { + public RefreshResponse execute(String token) { - String email; + String email; - try { - email = jwtUtil.getJwt(jwtUtil.parseToken(token)).getBody().get(JwtConstants.EMAIL.message).toString(); - } catch (Exception e) { - throw new TokenInvalidException(); - } + try { + email = jwtUtil.getJwt(jwtUtil.parseToken(token)).getBody().get(JwtConstants.EMAIL.message).toString(); + } catch (Exception e) { + throw new TokenInvalidException(); + } - User user = userRepository.getByEmail(email); - String newAccessToken = jwtProvider.generateAccessToken(user.getId(), user.getEmail(), user.getAuthority().toString()); + User user = userRepository.getByEmail(email); + String newAccessToken = jwtProvider.generateAccessToken(user.getId(), user.getAuthority().toString()); - return RefreshResponse.builder() - .accessToken(newAccessToken) - .build(); - } + return RefreshResponse.builder() + .accessToken(newAccessToken) + .build(); + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java b/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java new file mode 100644 index 0000000..a5f4b67 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/dto/request/GuestUserSaveRequest.java @@ -0,0 +1,21 @@ +package com.dnd.moddo.domain.user.dto.request; + +import java.time.LocalDateTime; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.entity.type.Authority; + +public record GuestUserSaveRequest(String email, String name) { + public User toEntity() { + return User.builder() + .email(email) + .name(name) + .kakaoId(null) + .isMember(false) + .authority(Authority.USER) + .profile(null) + .createdAt(LocalDateTime.now()) + .expiredAt(LocalDateTime.now().plusMonths(1)) + .build(); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java similarity index 84% rename from src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java rename to src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java index 8f599fe..0b91093 100644 --- a/src/main/java/com/dnd/moddo/domain/user/dto/request/UserCreateRequest.java +++ b/src/main/java/com/dnd/moddo/domain/user/dto/request/UserSaveRequest.java @@ -5,18 +5,17 @@ import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.entity.type.Authority; -public record UserCreateRequest( +public record UserSaveRequest( String email, String name, - Long kakaoId, - boolean isMember + Long kakaoId ) { public User toEntity() { return User.builder() .email(email) .name(name) .kakaoId(kakaoId) - .isMember(isMember) + .isMember(true) .authority(Authority.USER) .profile(null) .createdAt(LocalDateTime.now()) diff --git a/src/main/java/com/dnd/moddo/domain/user/service/UserService.java b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java similarity index 65% rename from src/main/java/com/dnd/moddo/domain/user/service/UserService.java rename to src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java index 079a85d..5b14004 100644 --- a/src/main/java/com/dnd/moddo/domain/user/service/UserService.java +++ b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java @@ -3,7 +3,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dnd.moddo.domain.user.dto.request.UserCreateRequest; +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.repository.UserRepository; @@ -11,21 +12,21 @@ @RequiredArgsConstructor @Service -public class UserService { +public class CommandUserService { private final UserRepository userRepository; @Transactional - public User createGuestUser(UserCreateRequest request) { + public User createGuestUser(GuestUserSaveRequest request) { return userRepository.save(request.toEntity()); } @Transactional - public User createKakaoUser(UserCreateRequest request) { + public User createKakaoUser(UserSaveRequest request) { return userRepository.save(request.toEntity()); } @Transactional - public User getOrCreateUser(UserCreateRequest request) { + public User getOrCreateUser(UserSaveRequest request) { return userRepository.findByKakaoId(request.kakaoId()) .orElseGet(() -> createKakaoUser(request)); } diff --git a/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java b/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java index 96a69c4..eea1801 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java +++ b/src/main/java/com/dnd/moddo/global/jwt/utill/JwtProvider.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; +import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.properties.JwtProperties; @@ -22,14 +23,18 @@ public class JwtProvider { private final JwtProperties jwtProperties; - public String generateAccessToken(Long id, String email, String role) { - return generateToken(id, email, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); + public String generateAccessToken(Long id, String role) { + return generateToken(id, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); } - public TokenResponse generateToken(Long id, String email, String role, Boolean isMember) { - String accessToken = generateToken(id, email, role, ACCESS_KEY.getMessage(), + public TokenResponse generateToken(User user) { + return generateToken(user.getId(), user.getAuthority().toString(), user.getIsMember()); + } + + public TokenResponse generateToken(Long id, String role, Boolean isMember) { + String accessToken = generateToken(id, role, ACCESS_KEY.getMessage(), jwtProperties.getAccessExpiration()); - String refreshToken = generateToken(id, email, role, REFRESH_KEY.getMessage(), + String refreshToken = generateToken(id, role, REFRESH_KEY.getMessage(), jwtProperties.getRefreshExpiration()); return new TokenResponse(accessToken, refreshToken, getExpiredTime(), isMember); @@ -39,10 +44,9 @@ public String generateGroupToken(Long groupId) { return generateGroupToken(groupId, GROUP_KEY.getMessage()); } - private String generateToken(Long id, String email, String role, String type, Long exp) { + private String generateToken(Long id, String role, String type, Long exp) { return Jwts.builder() .claim(AUTH_ID.getMessage(), id) - .claim(EMAIL.getMessage(), email) .setHeaderParam(TYPE.message, type) .claim(ROLE.getMessage(), role) .signWith(jwtProperties.getSecretKey(), SignatureAlgorithm.HS256) From 400df1781769a3b7dcac98d3a8a5b3c9b54cded9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 18:04:17 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20property=EC=84=A4=EC=A0=95=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 -- .../moddo/global/config/CookieProperties.java | 1 - .../moddo/global/config/PropertiesConfig.java | 10 ------ .../global/jwt/properties/JwtProperties.java | 32 ++++++++++--------- 4 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index ac597ab..9329abb 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -72,7 +72,6 @@ private ResponseCookie createCookie(String name, String key) { .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) .path(cookieProperties.path()) - .domain(cookieProperties.domain()) .sameSite(cookieProperties.sameSite()) .maxAge(cookieProperties.maxAge()) .build(); @@ -84,7 +83,6 @@ private ResponseCookie expireCookie(String name) { .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) .path(cookieProperties.path()) - .domain(cookieProperties.domain()) .sameSite(cookieProperties.sameSite()) .maxAge(0L) .build(); diff --git a/src/main/java/com/dnd/moddo/global/config/CookieProperties.java b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java index e274108..53ae9f6 100644 --- a/src/main/java/com/dnd/moddo/global/config/CookieProperties.java +++ b/src/main/java/com/dnd/moddo/global/config/CookieProperties.java @@ -8,7 +8,6 @@ public record CookieProperties( boolean httpOnly, boolean secure, - String domain, String path, String sameSite, Duration maxAge diff --git a/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java b/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java deleted file mode 100644 index 57019aa..0000000 --- a/src/main/java/com/dnd/moddo/global/config/PropertiesConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.dnd.moddo.global.config; - -import com.dnd.moddo.global.jwt.properties.JwtProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties({JwtProperties.class}) -public class PropertiesConfig { -} \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java b/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java index fbf5440..edc07f7 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java +++ b/src/main/java/com/dnd/moddo/global/jwt/properties/JwtProperties.java @@ -1,27 +1,29 @@ package com.dnd.moddo.global.jwt.properties; +import javax.crypto.SecretKey; + +import org.springframework.boot.context.properties.ConfigurationProperties; + import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.Getter; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import javax.crypto.SecretKey; @Getter @ConfigurationProperties(prefix = "jwt") public class JwtProperties { - private final String header; - private final String prefix; - private final SecretKey secretKey; - private final Long accessExpiration; - private final Long refreshExpiration; + private final String header; + private final String prefix; + private final SecretKey secretKey; + private final Long accessExpiration; + private final Long refreshExpiration; - public JwtProperties(String header, String prefix, String secretKey, Long accessExpiration, Long refreshExpiration) { - this.header = header; - this.prefix = prefix; - this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); - this.accessExpiration = accessExpiration; - this.refreshExpiration = refreshExpiration; - } + public JwtProperties(String header, String prefix, String secretKey, Long accessExpiration, + Long refreshExpiration) { + this.header = header; + this.prefix = prefix; + this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + this.accessExpiration = accessExpiration; + this.refreshExpiration = refreshExpiration; + } } From 27f0fbb1120d375fe09a2384ffdcc8ac5436aaa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 18:04:53 +0900 Subject: [PATCH 06/11] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthServiceTest.java | 53 +++-------- .../domain/auth/service/KakaoClientTest.java | 34 +++----- .../auth/service/RefreshTokenServiceTest.java | 11 +-- .../implementation/GroupCreatorTest.java | 5 +- .../moddo/domain/user/entity/UserTest.java | 66 +++++++------- .../user/service/CommandUserServiceTest.java | 87 +++++++++++++++++++ .../moddo/global/support/UserTestFactory.java | 15 ++++ .../dnd/moddo/global/util/ControllerTest.java | 5 +- src/test/resources/application.yml | 9 +- 9 files changed, 170 insertions(+), 115 deletions(-) create mode 100644 src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index e6ef58e..1f77d9b 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -3,8 +3,6 @@ import static com.dnd.moddo.global.support.UserTestFactory.*; import static org.mockito.Mockito.*; -import java.util.Optional; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,18 +11,19 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.dnd.moddo.domain.auth.dto.KakaoProfile; +import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.CommandUserService; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @ExtendWith(MockitoExtension.class) public class AuthServiceTest { - @Mock - private UserRepository userRepository; @Mock private JwtProvider jwtProvider; @Mock + CommandUserService commandUserService; + @Mock private KakaoClient kakaoClient; @InjectMocks private AuthService authService; @@ -34,14 +33,15 @@ public class AuthServiceTest { void whenCreateGuestUser_thenSaveAndIssueToken() { //given User user = createGuestDefault(); - when(userRepository.save(any(User.class))).thenReturn(user); + when(commandUserService.createGuestUser(any())).thenReturn(user); //when TokenResponse response = authService.loginWithGuest(); //then - verify(userRepository, times(1)).save(any(User.class)); + verify(jwtProvider, times(1)).generateToken(any()); + verify(commandUserService, times(1)).createGuestUser(any()); } - @DisplayName("기존 카카오 사용자가 로그인하면 토큰을 발급한다") + @DisplayName("카카오 사용자가 로그인하면 토큰을 발급한다") @Test void whenKakaoUserExists_thenTokenIsIssued() { //given @@ -58,47 +58,18 @@ void whenKakaoUserExists_thenTokenIsIssued() { "테스트유저" ) ); - String email = kakaoProfile.kakaoAccount().email(); - User user = createWithEmail(email); - when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); - - //when - TokenResponse response = authService.loginOrRegisterWithKakao(token); - - //then - verify(jwtProvider, times(1)).generateToken(any(), anyString(), anyString(), anyBoolean()); - } - - @DisplayName("신규 카카오 사용자가 로그인하면 회원가입 후 토큰을 발급한다") - @Test - void whenNewKakaoUser_thenRegisterAndIssueToken() { - //given - String token = "test_token"; - KakaoProfile kakaoProfile = new KakaoProfile( - 12345L, - new KakaoProfile.KakaoAccount( - "test@example.com", - new KakaoProfile.Profile( - "테스트 유저" - ) - ), - new KakaoProfile.Properties( - "테스트유저" - ) - ); + KakaoTokenResponse kakaoTokenResponse = new KakaoTokenResponse("access-token", 3600); String email = kakaoProfile.kakaoAccount().email(); User user = createWithEmail(email); + when(kakaoClient.join(anyString())).thenReturn(kakaoTokenResponse); when(kakaoClient.getKakaoProfile(anyString())).thenReturn(kakaoProfile); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); - when(userRepository.save(any(User.class))).thenReturn(user); + when(commandUserService.getOrCreateUser(any())).thenReturn(user); //when TokenResponse response = authService.loginOrRegisterWithKakao(token); //then - verify(userRepository, times(1)).save(any(User.class)); - verify(jwtProvider, times(1)).generateToken(any(), anyString(), anyString(), anyBoolean()); + verify(jwtProvider, times(1)).generateToken(any()); } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java index 55b906b..389e6a9 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -24,6 +25,7 @@ @ExtendWith(SpringExtension.class) @RestClientTest(value = KakaoClient.class) +@EnableConfigurationProperties(KakaoProperties.class) public class KakaoClientTest { @Autowired private KakaoClient kakaoClient; @@ -31,6 +33,7 @@ public class KakaoClientTest { @Autowired private MockRestServiceServer mockServer; + @Autowired private KakaoProperties kakaoProperties; @DisplayName("카카오 인가 코드로 토큰 요청하면 OauthToken을 반환한다") @@ -52,7 +55,7 @@ void whenRequestKakaoAccessToken_thenReturnOauthToken() throws Exception { } """; - mockServer.expect(requestTo("https://kauth.kakao.com/oauth/token")) + mockServer.expect(requestTo(kakaoProperties.tokenRequestUri())) .andExpect(method(HttpMethod.POST)) .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(content().formData(params)) @@ -77,7 +80,7 @@ void whenRequestKakaoAccessTokenWithInvalidCode_thenThrowException() { params.add("redirect_uri", kakaoProperties.redirectUri()); params.add("code", "invalid_code"); - mockServer.expect(requestTo("https://kauth.kakao.com/oauth/token")) + mockServer.expect(requestTo(kakaoProperties.tokenRequestUri())) .andExpect(method(HttpMethod.POST)) .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(content().formData(params)) @@ -98,34 +101,20 @@ void whenGetKakaoProfile_thenReturnKakaoProfile() { String expectResponse = """ { "id": 12345, - "connected_at": "2025.06.29T00:00:00", "properties": { - "nickname": "테스트유저", - "profile_image": "profile_image", - "thumbnail_image": "thumbnail_image" + "nickname": "테스트유저" }, "kakao_account": { - "profile_nickname_needs_agreement": true, - "profile_image_needs_agreement": true, + "email": "test@example.com", "profile": { - "nickname": "테스트 유저", - "thumbnail_image_url": "thumbnail_image_url", - "profile_image_url": "profile_image_url", - "is_default_image": true, - "is_default_nickname": true - }, - "has_email": true, - "email_needs_agreement": true, - "is_email_valid": true, - "is_email_verified": true, - "email": "test@example.com" + "nickname": "테스트 유저" + } } } """; - mockServer.expect(requestTo("https://kapi.kakao.com/v2/user/me")) + mockServer.expect(requestTo(kakaoProperties.profileRequestUri())) .andExpect(method(HttpMethod.GET)) - .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(header("Authorization", "Bearer " + token)) .andRespond(withSuccess(expectResponse, MediaType.APPLICATION_JSON)); @@ -145,9 +134,8 @@ void whenGetKakaoProfileWithHttpError_thenThrowException() { // given String token = "test_token"; - mockServer.expect(requestTo("https://kapi.kakao.com/v2/user/me")) + mockServer.expect(requestTo(kakaoProperties.profileRequestUri())) .andExpect(method(HttpMethod.GET)) - .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) .andExpect(header("Authorization", "Bearer " + token)) .andRespond(withServerError()); diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java index f00adbb..9d73188 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/RefreshTokenServiceTest.java @@ -1,10 +1,9 @@ package com.dnd.moddo.domain.auth.service; +import static com.dnd.moddo.global.support.UserTestFactory.*; import static org.assertj.core.api.BDDAssertions.*; import static org.mockito.Mockito.*; -import java.time.LocalDateTime; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,7 +14,6 @@ import com.dnd.moddo.domain.auth.exception.TokenInvalidException; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; import com.dnd.moddo.domain.user.repository.UserRepository; import com.dnd.moddo.global.jwt.dto.RefreshResponse; import com.dnd.moddo.global.jwt.properties.JwtConstants; @@ -60,12 +58,11 @@ public void reissueAccessToken() { when(mockJws.getBody()).thenReturn(mockClaims); when(mockClaims.get(JwtConstants.EMAIL.message)).thenReturn(email); - User user = new User("name", email, role, true, Authority.USER, LocalDateTime.now(), - LocalDateTime.now().plusDays(1)); + User user = createGuestDefault(); ReflectionTestUtils.setField(user, "id", userId); when(userRepository.getByEmail(email)).thenReturn(user); - when(jwtProvider.generateAccessToken(userId, email, role)).thenReturn(newAccessToken); + when(jwtProvider.generateAccessToken(userId, role)).thenReturn(newAccessToken); // when RefreshResponse response = refreshTokenService.execute(validToken); @@ -73,7 +70,7 @@ public void reissueAccessToken() { // then then(response.getAccessToken()).isEqualTo(newAccessToken); verify(userRepository, times(1)).getByEmail(email); - verify(jwtProvider, times(1)).generateAccessToken(userId, email, role); + verify(jwtProvider, times(1)).generateAccessToken(userId, role); } @Test diff --git a/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java b/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java index 1c72ad1..dd27754 100644 --- a/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java +++ b/src/test/java/com/dnd/moddo/domain/group/service/implementation/GroupCreatorTest.java @@ -1,5 +1,6 @@ package com.dnd.moddo.domain.group.service.implementation; +import static com.dnd.moddo.global.support.UserTestFactory.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -22,7 +23,6 @@ import com.dnd.moddo.domain.image.dto.CharacterResponse; import com.dnd.moddo.domain.image.service.implementation.ImageReader; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.entity.type.Authority; import com.dnd.moddo.domain.user.repository.UserRepository; @ExtendWith(MockitoExtension.class) @@ -57,8 +57,7 @@ void setUp() { userId = 1L; request = new GroupRequest("groupName", "password"); - mockUser = new User(userId, "test@example.com", "닉네임", "프로필", false, LocalDateTime.now(), - LocalDateTime.now().plusDays(1), Authority.USER); + mockUser = createGuestDefault(); encodedPassword = "encryptedPassword"; diff --git a/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java b/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java index 36e14f7..f1f5638 100644 --- a/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java +++ b/src/test/java/com/dnd/moddo/domain/user/entity/UserTest.java @@ -1,57 +1,53 @@ package com.dnd.moddo.domain.user.entity; -import com.dnd.moddo.ModdoApplication; -import com.dnd.moddo.domain.user.exception.UserNotFoundException; -import com.dnd.moddo.domain.user.repository.UserRepository; +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - -import static com.dnd.moddo.domain.user.entity.type.Authority.USER; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import com.dnd.moddo.domain.user.exception.UserNotFoundException; +import com.dnd.moddo.domain.user.repository.UserRepository; @ExtendWith(SpringExtension.class) @DataJpaTest public class UserTest { - @Autowired - private UserRepository userRepository; - - @DisplayName("이메일로 사용자를 조회할 수 있다.") - @Test - public void findByUser() { - // Given - LocalDateTime time = LocalDateTime.now(); + @Autowired + private UserRepository userRepository; - User user1 = new User("홍길동", "guest-UUID1@guest.com", "profile.png", false, USER, time, time.plusDays(7)); - User user2 = new User("심청이", "guest-UUID2@guest.com", "profile.png", false, USER, time, time.plusDays(7)); + @DisplayName("이메일로 사용자를 조회할 수 있다.") + @Test + public void findByUser() { + // Given + LocalDateTime time = LocalDateTime.now(); - userRepository.save(user1); - userRepository.save(user2); + User user1 = createGuestWithNameAndEmail("홍길동", "guest-UUID1@guest.com"); + User user2 = createGuestWithNameAndEmail("심청이", "guest-UUID2@guest.com"); - // When - User foundUser = userRepository.getByEmail("guest-UUID2@guest.com"); + userRepository.save(user1); + userRepository.save(user2); - // Then - assertThat(foundUser.getName()).isEqualTo("심청이"); - assertThat(foundUser.getEmail()).isEqualTo("guest-UUID2@guest.com"); - } + // When + User foundUser = userRepository.getByEmail("guest-UUID2@guest.com"); + // Then + assertThat(foundUser.getName()).isEqualTo("심청이"); + assertThat(foundUser.getEmail()).isEqualTo("guest-UUID2@guest.com"); + } - @DisplayName("이메일로 사용자를 조회할 때, 사용자가 없으면 예외를 발생시킨다.") - @Test - public void getByEmailNotFound() { - // When & Then - assertThrows(UserNotFoundException.class, () -> userRepository.getByEmail("exception@guest.com")); - } + @DisplayName("이메일로 사용자를 조회할 때, 사용자가 없으면 예외를 발생시킨다.") + @Test + public void getByEmailNotFound() { + // When & Then + assertThrows(UserNotFoundException.class, () -> userRepository.getByEmail("exception@guest.com")); + } } diff --git a/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java new file mode 100644 index 0000000..06045e7 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java @@ -0,0 +1,87 @@ +package com.dnd.moddo.domain.user.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; +import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class CommandUserServiceTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private CommandUserService commandUserService; + + @DisplayName("유효한 요청으로 게스트 유저를 생성할 수 있다.") + @Test + void whenSaveRequestIsValid_thenGuestUserIsSaved() { + //given + GuestUserSaveRequest request = new GuestUserSaveRequest("email", "Guest"); + when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.createGuestUser(request); + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Guest"); + assertThat(result.getIsMember()).isFalse(); + } + + @DisplayName("유효한 요청으로 카카오 유저를 생성할 수 있다.") + @Test + void whenSaveRequestIsValid_thenKakaoUserIsSaved() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.createKakaoUser(request); + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } + + @DisplayName("카카오 ID로 조회 시 유저가 없으면 새로 생성한다") + @Test + void whenUserDoesNotExist_thenCreateNewUser() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + + when(userRepository.findByKakaoId(anyLong())).thenReturn(Optional.empty()); + when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + //when + User result = commandUserService.getOrCreateUser(request); + + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } + + @DisplayName("카카오 ID로 유저 조회 시 이미 존재하면 기존 유저를 반환한다") + @Test + void getOrCreateUser() { + //given + UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); + + when(userRepository.findByKakaoId(anyLong())).thenReturn(Optional.of(request.toEntity())); + //when + User result = commandUserService.getOrCreateUser(request); + + //then + assertThat(result.getEmail()).isEqualTo("email"); + assertThat(result.getName()).isEqualTo("Kakao"); + assertThat(result.getKakaoId()).isEqualTo(123456L); + } +} diff --git a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java index c667cbc..dcd1041 100644 --- a/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java +++ b/src/test/java/com/dnd/moddo/global/support/UserTestFactory.java @@ -23,6 +23,21 @@ public static User createGuestDefault() { .build(); } + public static User createGuestWithNameAndEmail(String name, String email) { + LocalDateTime time = LocalDateTime.now(); + + return User + .builder() + .name(name) + .email(email) + .profile("profile.png") + .isMember(false) + .authority(USER) + .createdAt(time) + .expiredAt(time.plusDays(7)) + .build(); + } + public static User createWithEmail(String email) { LocalDateTime time = LocalDateTime.now(); return User diff --git a/src/test/java/com/dnd/moddo/global/util/ControllerTest.java b/src/test/java/com/dnd/moddo/global/util/ControllerTest.java index 0006619..fed0773 100644 --- a/src/test/java/com/dnd/moddo/global/util/ControllerTest.java +++ b/src/test/java/com/dnd/moddo/global/util/ControllerTest.java @@ -25,6 +25,7 @@ import com.dnd.moddo.domain.image.service.CommandImageService; import com.dnd.moddo.domain.memberExpense.controller.MemberExpenseController; import com.dnd.moddo.domain.memberExpense.service.QueryMemberExpenseService; +import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.auth.JwtAuth; import com.dnd.moddo.global.jwt.auth.JwtFilter; import com.dnd.moddo.global.jwt.service.JwtService; @@ -88,7 +89,9 @@ public abstract class ControllerTest { @MockBean protected QueryMemberExpenseService queryMemberExpenseService; - + + @MockBean + protected CookieProperties cookieProperties; // Jwt @MockBean protected JwtAuth jwtAuth; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 0b1d1a3..2227526 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -46,8 +46,7 @@ cookie: kakao: - auth: - client-id: clientidclientidclientidclientidclientidclientidclientid - redirect-uri: http://localhost:8080/api/v1/login/kakao/callback - token-request-uri: https://kauth.kakao.com/oauth/token - profile-request-uri: https://kapi.kakao.com/v2/user/me \ No newline at end of file + client-id: clientidclientidclientidclientidclientidclientidclientid + redirect-uri: http://localhost:8080/api/v1/login/kakao/callback + token-request-uri: https://kauth.kakao.com/oauth/token + profile-request-uri: https://kapi.kakao.com/v2/user/me \ No newline at end of file From ad45cef61f72ae20bf904f404f5b290761e7db92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Fri, 11 Jul 2025 22:32:32 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20User=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20CQRS=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20Us?= =?UTF-8?q?erCreator/UserReader=EB=A1=9C=20=EB=AA=85=EB=A0=B9=EA=B3=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/CommandUserService.java | 12 +++++---- .../domain/user/service/QueryUserService.java | 19 ++++++++++++++ .../service/implementation/UserCreator.java | 21 +++++++++++++++ .../service/implementation/UserReader.java | 26 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java create mode 100644 src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java create mode 100644 src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java diff --git a/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java index 5b14004..bcafbb0 100644 --- a/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java +++ b/src/main/java/com/dnd/moddo/domain/user/service/CommandUserService.java @@ -6,28 +6,30 @@ import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.implementation.UserCreator; +import com.dnd.moddo.domain.user.service.implementation.UserReader; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class CommandUserService { - private final UserRepository userRepository; + private final UserCreator userCreator; + private final UserReader userReader; @Transactional public User createGuestUser(GuestUserSaveRequest request) { - return userRepository.save(request.toEntity()); + return userCreator.createUser(request.toEntity()); } @Transactional public User createKakaoUser(UserSaveRequest request) { - return userRepository.save(request.toEntity()); + return userCreator.createUser(request.toEntity()); } @Transactional public User getOrCreateUser(UserSaveRequest request) { - return userRepository.findByKakaoId(request.kakaoId()) + return userReader.findByKakaoId(request.kakaoId()) .orElseGet(() -> createKakaoUser(request)); } } diff --git a/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java new file mode 100644 index 0000000..5b2854e --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java @@ -0,0 +1,19 @@ +package com.dnd.moddo.domain.user.service; + +import org.springframework.stereotype.Service; + +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class QueryUserService { + private final UserReader userReader; + + public Long findKakaoIdById(Long userId) { + return userReader.findKakaoIdById(userId) + .orElse(null); + } +} + diff --git a/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java new file mode 100644 index 0000000..feb9fda --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserCreator.java @@ -0,0 +1,21 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +@Transactional +public class UserCreator { + + private final UserRepository userRepository; + + public User createUser(User user) { + return userRepository.save(user); + } +} diff --git a/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java new file mode 100644 index 0000000..3a5327d --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/user/service/implementation/UserReader.java @@ -0,0 +1,26 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserReader { + private final UserRepository userRepository; + + public Optional findByKakaoId(Long kakaoId) { + return userRepository.findByKakaoId(kakaoId); + } + + public Optional findKakaoIdById(Long userId) { + return userRepository.findKakaoIdById(userId); + } +} From ed17bd3795d5f60a79feccb6d88ec92c18e8de66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Tue, 15 Jul 2025 15:02:03 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20+?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 12 ++++++-- .../domain/auth/dto/KakaoLogoutResponse.java | 4 +++ .../domain/auth/service/AuthService.java | 17 +++++++++++ .../domain/auth/service/KakaoClient.java | 26 +++++++++++++++- .../user/repository/UserRepository.java | 4 +++ .../moddo/global/config/KakaoProperties.java | 4 ++- .../moddo/global/jwt/service/JwtService.java | 30 +++++++++++-------- src/main/resources/config | 2 +- 8 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java diff --git a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java index 9329abb..4aed35f 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/moddo/domain/auth/controller/AuthController.java @@ -1,9 +1,12 @@ package com.dnd.moddo.domain.auth.controller; +import java.util.Collections; + import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -16,6 +19,7 @@ import com.dnd.moddo.global.config.CookieProperties; import com.dnd.moddo.global.jwt.dto.RefreshResponse; import com.dnd.moddo.global.jwt.dto.TokenResponse; +import com.dnd.moddo.global.jwt.service.JwtService; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @@ -29,6 +33,7 @@ public class AuthController { private final AuthService authService; private final RefreshTokenService refreshTokenService; private final CookieProperties cookieProperties; + private final JwtService jwtService; @GetMapping("/user/guest/token") public ResponseEntity getGuestToken() { @@ -59,12 +64,13 @@ public ResponseEntity kakaoLoginCallback(@RequestParam @NotBlank String co } @GetMapping("/logout") - public ResponseEntity kakaoLogout() { + public ResponseEntity kakaoLogout(@CookieValue(value = "accessToken") String token) { String cookie = expireCookie("accessToken").toString(); - + Long userId = jwtService.getUserId(token); + authService.logout(userId); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie) - .build(); + .body(Collections.singletonMap("message", "Logout successful")); } private ResponseCookie createCookie(String name, String key) { diff --git a/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java new file mode 100644 index 0000000..14fa575 --- /dev/null +++ b/src/main/java/com/dnd/moddo/domain/auth/dto/KakaoLogoutResponse.java @@ -0,0 +1,4 @@ +package com.dnd.moddo.domain.auth.dto; + +public record KakaoLogoutResponse(Long id) { +} diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index 22497de..bc78c07 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -6,12 +6,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.service.CommandUserService; +import com.dnd.moddo.domain.user.service.QueryUserService; import com.dnd.moddo.global.exception.ModdoException; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @@ -25,6 +27,7 @@ public class AuthService { private final CommandUserService commandUserService; + private final QueryUserService queryUserService; private final JwtProvider jwtProvider; private final KakaoClient kakaoClient; @@ -59,4 +62,18 @@ public TokenResponse loginOrRegisterWithKakao(String code) { return jwtProvider.generateToken(user); } + public void logout(Long userId) { + Long kakaoId = queryUserService.findKakaoIdById(userId); + if (kakaoId == null) + return; + + KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId); + + if (!kakaoId.equals(logoutResponse.id())) { + throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치"); + } + + log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId); + } + } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java index 366a7dd..07c480a 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java @@ -9,6 +9,7 @@ import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientResponseException; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.global.config.KakaoProperties; @@ -38,7 +39,7 @@ public KakaoTokenResponse join(String code) { try { return restClient.post() .uri(uri) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(params) .retrieve() .body(KakaoTokenResponse.class); @@ -74,4 +75,27 @@ public KakaoProfile getKakaoProfile(String token) { throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패"); } } + + public KakaoLogoutResponse logout(Long kakaoId) { + RestClient restClient = builder.build(); + String uri = kakaoProperties.logoutRequestUri(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("Authorization", "KakaoAK " + kakaoProperties.adminKey()); + params.add("target_id_type", "user_id"); + params.add("target_id", kakaoId.toString()); + + try { + return restClient.post() + .uri(uri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoProperties.adminKey()) + .body(params) + .retrieve() + .body(KakaoLogoutResponse.class); + } catch (Exception e) { + log.error("[KAKAO_CALLBACK_ERROR] 카카오 콜백 처리 실패", e.getMessage()); + throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패"); + } + } } diff --git a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java index 2c69fec..65afb20 100644 --- a/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java +++ b/src/main/java/com/dnd/moddo/domain/user/repository/UserRepository.java @@ -3,6 +3,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.dnd.moddo.domain.user.entity.User; @@ -15,6 +16,9 @@ public interface UserRepository extends JpaRepository { Optional findByKakaoId(Long kakaoId); + @Query("SELECT u.kakaoId FROM User u WHERE u.id = :userId") + Optional findKakaoIdById(Long userId); + default User getByEmail(String email) { return findByEmail(email) .orElseThrow(() -> new UserNotFoundException(email)); diff --git a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java index 409a9b4..cfe47d4 100644 --- a/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java +++ b/src/main/java/com/dnd/moddo/global/config/KakaoProperties.java @@ -6,8 +6,10 @@ public record KakaoProperties( String redirectUri, String clientId, + String adminKey, String tokenRequestUri, - String profileRequestUri + String profileRequestUri, + String logoutRequestUri ) { } diff --git a/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java b/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java index d4d04b9..8ca40c4 100644 --- a/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java +++ b/src/main/java/com/dnd/moddo/global/jwt/service/JwtService.java @@ -1,25 +1,31 @@ package com.dnd.moddo.global.jwt.service; +import org.springframework.stereotype.Service; + import com.dnd.moddo.global.jwt.utill.JwtUtil; + import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class JwtService { - private final JwtUtil jwtUtil; + private final JwtUtil jwtUtil; + + public Long getId(HttpServletRequest request, String key) { + String token = jwtUtil.resolveToken(request); + return jwtUtil.getIdFromToken(token, key); + } - public Long getId(HttpServletRequest request, String key) { - String token = jwtUtil.resolveToken(request); - return jwtUtil.getIdFromToken(token, key); - } + public Long getUserId(HttpServletRequest request) { + return getId(request, "userId"); + } - public Long getUserId(HttpServletRequest request) { - return getId(request, "userId"); - } + public Long getUserId(String token) { + return jwtUtil.getIdFromToken(token, "userId"); + } - public Long getGroupId(String groupToken) { - return jwtUtil.getIdFromToken(groupToken, "groupId"); - } + public Long getGroupId(String groupToken) { + return jwtUtil.getIdFromToken(groupToken, "groupId"); + } } diff --git a/src/main/resources/config b/src/main/resources/config index 1ec2dcd..6515d7b 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 1ec2dcd3d9e19a35e2488ca6dc2946014c8b162c +Subproject commit 6515d7b8afc3d52cef1f35923ab9cf0429b9302e From 0b6b55d87a6b2ed639213e48b0c4dc4cf538f71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Tue, 15 Jul 2025 15:02:37 +0900 Subject: [PATCH 09/11] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?,=EC=A1=B0=ED=9A=8C=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthControllerTest.java | 24 +++++++++ .../domain/auth/service/AuthServiceTest.java | 47 ++++++++++++++++- .../domain/auth/service/KakaoClientTest.java | 42 +++++++++++++++ .../group/controller/GroupControllerTest.java | 10 ++-- .../user/service/CommandUserServiceTest.java | 17 ++++--- .../implementation/UserCreatorTest.java | 40 +++++++++++++++ .../implementation/UserReaderTest.java | 41 +++++++++++++++ .../user/service/queryUserServiceTest.java | 51 +++++++++++++++++++ src/test/resources/application.yml | 4 +- 9 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java create mode 100644 src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java create mode 100644 src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java diff --git a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java index 995176f..8b81cab 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java @@ -1,6 +1,7 @@ package com.dnd.moddo.domain.auth.controller; import static org.mockito.BDDMockito.*; +import static org.springframework.restdocs.cookies.CookieDocumentation.*; import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; @@ -18,6 +19,8 @@ import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.util.RestDocsTestSupport; +import jakarta.servlet.http.Cookie; + class AuthControllerTest extends RestDocsTestSupport { @Test @@ -99,4 +102,25 @@ void kakaoLoginCallback() throws Exception { ) )); } + + @Test + @DisplayName("카카오에서 인가코드를 통해 토큰을 발급받아 사용자 정보를 가져와 등록시킨 뒤 엑세스 토큰을 발급하여 쿠키로 전달한다.") + void kakaoLooutCallback() throws Exception { + //given + given(jwtService.getUserId(anyString())).willReturn(1L); + doNothing().when(authService).logout(any()); + + //when & then + mockMvc.perform(get("/api/v1/logout") + .cookie(new Cookie("accessToken", "access-token"))) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestCookies( + cookieWithName("accessToken").description("액세스 토큰") + ), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING).description("로그아웃 성공 메시지") + ) + )); + } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index 1f77d9b..e20aa9a 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -1,6 +1,7 @@ package com.dnd.moddo.domain.auth.service; import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; import org.junit.jupiter.api.DisplayName; @@ -10,10 +11,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.domain.user.entity.User; import com.dnd.moddo.domain.user.service.CommandUserService; +import com.dnd.moddo.domain.user.service.QueryUserService; import com.dnd.moddo.global.jwt.dto.TokenResponse; import com.dnd.moddo.global.jwt.utill.JwtProvider; @@ -22,7 +25,9 @@ public class AuthServiceTest { @Mock private JwtProvider jwtProvider; @Mock - CommandUserService commandUserService; + private CommandUserService commandUserService; + @Mock + private QueryUserService queryUserService; @Mock private KakaoClient kakaoClient; @InjectMocks @@ -72,4 +77,44 @@ void whenKakaoUserExists_thenTokenIsIssued() { //then verify(jwtProvider, times(1)).generateToken(any()); } + + @DisplayName("카카오ID와 응답ID가 같을 때 카카오 로그아웃 성공한다.") + @Test + void whenKakaoIdMatches_thenKakaoLogoutSuccess() { + //given + Long kakaoId = 123456L; + when(queryUserService.findKakaoIdById(any())).thenReturn(kakaoId); + when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(kakaoId)); + //when + authService.logout(1L); + //then + verify(queryUserService, times(1)).findKakaoIdById(1L); + verify(kakaoClient, times(1)).logout(kakaoId); + } + + @DisplayName("카카오ID가 null일 때 게스트 로그아웃 성공한다.") + @Test + void whenKakaoIdNull_thenNoAction() { + //given + when(queryUserService.findKakaoIdById(any())).thenReturn(null); + //when + authService.logout(1L); + //then + verify(queryUserService, times(1)).findKakaoIdById(1L); + verify(kakaoClient, times(0)).logout(any()); + } + + @DisplayName("카카오ID와 응답ID가 다를 때 예외 발생한다.") + @Test + void whenKakaoIdDiffers_thenThrowsException() { + //given + Long kakaoId = 123456L; + when(queryUserService.findKakaoIdById(any())).thenReturn(kakaoId); + when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(234567L)); + + //when & then + assertThatThrownBy(() -> authService.logout(1L)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("로그아웃 실패"); + } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java index 389e6a9..52c8bd7 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/KakaoClientTest.java @@ -18,6 +18,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse; import com.dnd.moddo.domain.auth.dto.KakaoProfile; import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse; import com.dnd.moddo.global.config.KakaoProperties; @@ -143,4 +144,45 @@ void whenGetKakaoProfileWithHttpError_thenThrowException() { assertThatThrownBy(() -> kakaoClient.getKakaoProfile(token)) .isInstanceOf(ModdoException.class); } + + @DisplayName("카카오 로그아웃 API 호출 시 정상 응답을 반환한다") + @Test + void whenCallKakaoLogout_thenReturnValidResponse() { + //given + Long kakaoId = 123456L; + String expectResponse = """ + { + "id": 123456 + } + """; + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("target_id_type", "user_id"); + params.add("target_id", "123456"); + + mockServer.expect(requestTo(kakaoProperties.logoutRequestUri())) + .andExpect(method(HttpMethod.POST)) + .andExpect(header("Authorization", "KakaoAK " + kakaoProperties.adminKey())) + .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) + .andRespond(withSuccess(expectResponse, MediaType.APPLICATION_JSON)); + //when + KakaoLogoutResponse response = kakaoClient.logout(123456L); + //then + assertThat(response.id()).isEqualTo(kakaoId); + } + + @DisplayName("카카오 로그아웃 API 호출 시 서버 오류가 발생하면 예외를 던진다") + @Test + void henCallKakaoLogout_withServerError_thenThrowException() { + //given + mockServer.expect(requestTo(kakaoProperties.logoutRequestUri())) + .andExpect(method(HttpMethod.POST)) + .andExpect(header("Authorization", "KakaoAK " + kakaoProperties.adminKey())) + .andExpect(header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")) + .andRespond(withServerError()); + //when & then + + assertThatThrownBy(() -> kakaoClient.logout(123456L)) + .hasMessageContaining("카카오 콜백 처리 실패"); + } } diff --git a/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java b/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java index e326ed6..21c6be0 100644 --- a/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/group/controller/GroupControllerTest.java @@ -23,6 +23,8 @@ import com.dnd.moddo.domain.groupMember.dto.response.GroupMemberResponse; import com.dnd.moddo.global.util.RestDocsTestSupport; +import jakarta.servlet.http.HttpServletRequest; + public class GroupControllerTest extends RestDocsTestSupport { @Test @@ -34,7 +36,7 @@ void saveGroup() throws Exception { 1L, MANAGER, "김모또", "https://moddo-s3.s3.amazonaws.com/profile/MODDO.png", true, LocalDateTime.now() )); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(commandGroupService.createGroup(any(), eq(1L))).willReturn(response); // when & then @@ -55,7 +57,7 @@ void updateAccount() throws Exception { LocalDateTime.now().plusDays(1) ); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(commandGroupService.updateAccount(any(), eq(1L), eq(100L))).willReturn(response); @@ -76,7 +78,7 @@ void getGroup() throws Exception { LocalDateTime.now()) )); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(queryGroupService.findOne(100L, 1L)).willReturn(response); @@ -93,7 +95,7 @@ void isPasswordMatch() throws Exception { GroupPasswordRequest request = new GroupPasswordRequest("1234"); GroupPasswordResponse response = GroupPasswordResponse.from("확인되었습니다."); - given(jwtService.getUserId(any())).willReturn(1L); + given(jwtService.getUserId(any(HttpServletRequest.class))).willReturn(1L); given(queryGroupService.findIdByCode(anyString())).willReturn(100L); given(commandGroupService.isPasswordMatch(100L, 1L, request)).willReturn(response); diff --git a/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java index 06045e7..13c1d94 100644 --- a/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/user/service/CommandUserServiceTest.java @@ -15,12 +15,15 @@ import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest; import com.dnd.moddo.domain.user.dto.request.UserSaveRequest; import com.dnd.moddo.domain.user.entity.User; -import com.dnd.moddo.domain.user.repository.UserRepository; +import com.dnd.moddo.domain.user.service.implementation.UserCreator; +import com.dnd.moddo.domain.user.service.implementation.UserReader; @ExtendWith(MockitoExtension.class) public class CommandUserServiceTest { @Mock - private UserRepository userRepository; + private UserCreator userCreator; + @Mock + private UserReader userReader; @InjectMocks private CommandUserService commandUserService; @@ -29,7 +32,7 @@ public class CommandUserServiceTest { void whenSaveRequestIsValid_thenGuestUserIsSaved() { //given GuestUserSaveRequest request = new GuestUserSaveRequest("email", "Guest"); - when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); //when User result = commandUserService.createGuestUser(request); //then @@ -43,7 +46,7 @@ void whenSaveRequestIsValid_thenGuestUserIsSaved() { void whenSaveRequestIsValid_thenKakaoUserIsSaved() { //given UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); - when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); //when User result = commandUserService.createKakaoUser(request); //then @@ -58,8 +61,8 @@ void whenUserDoesNotExist_thenCreateNewUser() { //given UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); - when(userRepository.findByKakaoId(anyLong())).thenReturn(Optional.empty()); - when(userRepository.save(any(User.class))).thenReturn(request.toEntity()); + when(userReader.findByKakaoId(anyLong())).thenReturn(Optional.empty()); + when(userCreator.createUser(any(User.class))).thenReturn(request.toEntity()); //when User result = commandUserService.getOrCreateUser(request); @@ -75,7 +78,7 @@ void getOrCreateUser() { //given UserSaveRequest request = new UserSaveRequest("email", "Kakao", 123456L); - when(userRepository.findByKakaoId(anyLong())).thenReturn(Optional.of(request.toEntity())); + when(userReader.findByKakaoId(anyLong())).thenReturn(Optional.of(request.toEntity())); //when User result = commandUserService.getOrCreateUser(request); diff --git a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java new file mode 100644 index 0000000..ca14b3d --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserCreatorTest.java @@ -0,0 +1,40 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class UserCreatorTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private UserCreator userCreator; + + @Test + @DisplayName("사용자 생성 시 userRepository.save가 호출되고 저장된 User를 반환한다") + void whenCreateUser_thenReturnSavedUser() { + // given + User user = createWithEmail("test@example.com"); + + when(userRepository.save(user)).thenReturn(user); + + // when + User result = userCreator.createUser(user); + + // then + assertThat(result).isNotNull(); + assertThat(result.getEmail()).isEqualTo("test@example.com"); + verify(userRepository, times(1)).save(user); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java new file mode 100644 index 0000000..bce9895 --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java @@ -0,0 +1,41 @@ +package com.dnd.moddo.domain.user.service.implementation; + +import static com.dnd.moddo.global.support.UserTestFactory.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.entity.User; +import com.dnd.moddo.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class UserReaderTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private UserReader userReader; + + @DisplayName("kakaoId로 User를 조회하면 해당 User를 반환한다") + @Test + void whenFindByKakaoId_thenReturnUser() { + //given + User user = createWithEmail("test@example.com"); + Long kakaoId = user.getKakaoId(); + + when(userRepository.findByKakaoId(any())).thenReturn(Optional.of(user)); + //when + Optional result = userReader.findByKakaoId(kakaoId); + //then + assertThat(result).isPresent(); + assertThat(result.get().getKakaoId()).isEqualTo(kakaoId); + verify(userRepository, times(1)).findByKakaoId(kakaoId); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java new file mode 100644 index 0000000..589645f --- /dev/null +++ b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java @@ -0,0 +1,51 @@ +package com.dnd.moddo.domain.user.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.moddo.domain.user.service.implementation.UserReader; + +@ExtendWith(MockitoExtension.class) +public class queryUserServiceTest { + @Mock + private UserReader userReader; + @InjectMocks + private QueryUserService queryUserService; + + @DisplayName("userId로 kakaoId를 조회하면 해당 kakaoId를 반환한다") + @Test + void whenFindKakaoIdById_thenReturnKakaoId() { + //given + Long userId = 1L; + Long kakaoId = 123456L; + when(userReader.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); + //when + Long result = queryUserService.findKakaoIdById(userId); + //then + assertThat(result).isEqualTo(kakaoId); + verify(userReader, times(1)).findKakaoIdById(userId); + } + + @DisplayName("userId가 없을 때 null을 반환한다") + @Test + void whenUserIdNotFound_thenReturnNull() { + //given + when(userReader.findKakaoIdById(any())).thenReturn(Optional.empty()); + + //when + Long result = queryUserService.findKakaoIdById(1L); + + //then + assertThat(result).isNull(); + verify(userReader, times(1)).findKakaoIdById(1L); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 2227526..a5f2c38 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -47,6 +47,8 @@ cookie: kakao: client-id: clientidclientidclientidclientidclientidclientidclientid + admin-key: adminkeyadminkeyadminkeyadminkeyadminkey redirect-uri: http://localhost:8080/api/v1/login/kakao/callback token-request-uri: https://kauth.kakao.com/oauth/token - profile-request-uri: https://kapi.kakao.com/v2/user/me \ No newline at end of file + profile-request-uri: https://kapi.kakao.com/v2/user/me + logout-request-uri: https://kapi.kakao.com/v1/user/logout \ No newline at end of file From b830b3906d0ce0fb1bc56b2049bed52fce7cc847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Tue, 15 Jul 2025 15:02:54 +0900 Subject: [PATCH 10/11] =?UTF-8?q?infra:=20cicd=20=EC=9B=8C=ED=81=AC=20?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index d0aea34..7a8dedf 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -73,4 +73,5 @@ jobs: sudo docker compose stop app sudo docker compose rm -f app sudo docker rmi ${{env.DOCKER_IMAGE_NAME}}:latest + sudo docker compose pull app sudo docker compose up -d app From db01dd3a07e234a51acea1efe98b331a1b91c780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Tue, 15 Jul 2025 15:24:34 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moddo/domain/auth/service/AuthService.java | 16 +++++++--------- .../moddo/domain/auth/service/KakaoClient.java | 1 - .../domain/user/service/QueryUserService.java | 7 ++++--- .../auth/controller/AuthControllerTest.java | 4 ++-- .../domain/auth/service/AuthServiceTest.java | 8 +++++--- .../service/implementation/UserReaderTest.java | 2 +- .../user/service/queryUserServiceTest.java | 9 +++++---- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java index bc78c07..8aba9c9 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java @@ -63,17 +63,15 @@ public TokenResponse loginOrRegisterWithKakao(String code) { } public void logout(Long userId) { - Long kakaoId = queryUserService.findKakaoIdById(userId); - if (kakaoId == null) - return; + queryUserService.findKakaoIdById(userId).ifPresent(kakaoId -> { + KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId); - KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId); + if (!kakaoId.equals(logoutResponse.id())) { + throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치"); + } - if (!kakaoId.equals(logoutResponse.id())) { - throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치"); - } - - log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId); + log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId); + }); } } \ No newline at end of file diff --git a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java index 07c480a..f846bd6 100644 --- a/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java +++ b/src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java @@ -81,7 +81,6 @@ public KakaoLogoutResponse logout(Long kakaoId) { String uri = kakaoProperties.logoutRequestUri(); MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("Authorization", "KakaoAK " + kakaoProperties.adminKey()); params.add("target_id_type", "user_id"); params.add("target_id", kakaoId.toString()); diff --git a/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java index 5b2854e..d127d6d 100644 --- a/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java +++ b/src/main/java/com/dnd/moddo/domain/user/service/QueryUserService.java @@ -1,5 +1,7 @@ package com.dnd.moddo.domain.user.service; +import java.util.Optional; + import org.springframework.stereotype.Service; import com.dnd.moddo.domain.user.service.implementation.UserReader; @@ -11,9 +13,8 @@ public class QueryUserService { private final UserReader userReader; - public Long findKakaoIdById(Long userId) { - return userReader.findKakaoIdById(userId) - .orElse(null); + public Optional findKakaoIdById(Long userId) { + return userReader.findKakaoIdById(userId); } } diff --git a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java index 8b81cab..3a440ac 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/controller/AuthControllerTest.java @@ -104,8 +104,8 @@ void kakaoLoginCallback() throws Exception { } @Test - @DisplayName("카카오에서 인가코드를 통해 토큰을 발급받아 사용자 정보를 가져와 등록시킨 뒤 엑세스 토큰을 발급하여 쿠키로 전달한다.") - void kakaoLooutCallback() throws Exception { + @DisplayName("액세스 토큰 쿠키를 통해 카카오 로그아웃을 성공적으로 수행한다.") + void kakaoLogout() throws Exception { //given given(jwtService.getUserId(anyString())).willReturn(1L); doNothing().when(authService).logout(any()); diff --git a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java index e20aa9a..deb65e0 100644 --- a/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/auth/service/AuthServiceTest.java @@ -4,6 +4,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; +import java.util.Optional; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -83,7 +85,7 @@ void whenKakaoUserExists_thenTokenIsIssued() { void whenKakaoIdMatches_thenKakaoLogoutSuccess() { //given Long kakaoId = 123456L; - when(queryUserService.findKakaoIdById(any())).thenReturn(kakaoId); + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(kakaoId)); //when authService.logout(1L); @@ -96,7 +98,7 @@ void whenKakaoIdMatches_thenKakaoLogoutSuccess() { @Test void whenKakaoIdNull_thenNoAction() { //given - when(queryUserService.findKakaoIdById(any())).thenReturn(null); + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.empty()); //when authService.logout(1L); //then @@ -109,7 +111,7 @@ void whenKakaoIdNull_thenNoAction() { void whenKakaoIdDiffers_thenThrowsException() { //given Long kakaoId = 123456L; - when(queryUserService.findKakaoIdById(any())).thenReturn(kakaoId); + when(queryUserService.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); when(kakaoClient.logout(any())).thenReturn(new KakaoLogoutResponse(234567L)); //when & then diff --git a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java index bce9895..b38ecc9 100644 --- a/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java +++ b/src/test/java/com/dnd/moddo/domain/user/service/implementation/UserReaderTest.java @@ -30,7 +30,7 @@ void whenFindByKakaoId_thenReturnUser() { User user = createWithEmail("test@example.com"); Long kakaoId = user.getKakaoId(); - when(userRepository.findByKakaoId(any())).thenReturn(Optional.of(user)); + when(userRepository.findByKakaoId(kakaoId)).thenReturn(Optional.of(user)); //when Optional result = userReader.findByKakaoId(kakaoId); //then diff --git a/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java index 589645f..6ada626 100644 --- a/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/user/service/queryUserServiceTest.java @@ -29,9 +29,10 @@ void whenFindKakaoIdById_thenReturnKakaoId() { Long kakaoId = 123456L; when(userReader.findKakaoIdById(any())).thenReturn(Optional.of(kakaoId)); //when - Long result = queryUserService.findKakaoIdById(userId); + Optional result = queryUserService.findKakaoIdById(userId); //then - assertThat(result).isEqualTo(kakaoId); + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(kakaoId); verify(userReader, times(1)).findKakaoIdById(userId); } @@ -42,10 +43,10 @@ void whenUserIdNotFound_thenReturnNull() { when(userReader.findKakaoIdById(any())).thenReturn(Optional.empty()); //when - Long result = queryUserService.findKakaoIdById(1L); + Optional result = queryUserService.findKakaoIdById(1L); //then - assertThat(result).isNull(); + assertThat(result).isEmpty(); verify(userReader, times(1)).findKakaoIdById(1L); } }