From e00099422207f0213ce9224e7e5a57db354d07a8 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:19:42 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:#391=20signUp=20session=20repository?= =?UTF-8?q?=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/SignupSessionRepository.java | 48 +++++++++++++++++++ .../domain/user/service/SignupSession.java | 12 +++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java create mode 100644 src/main/java/or/sopt/houme/domain/user/service/SignupSession.java diff --git a/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java new file mode 100644 index 00000000..78aa98d6 --- /dev/null +++ b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java @@ -0,0 +1,48 @@ +package or.sopt.houme.domain.user.repository; + +import lombok.RequiredArgsConstructor; +import or.sopt.houme.domain.user.service.SignupSession; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class SignupSessionRepository { + + private final RedisTemplate redisTemplate; + + private static final String PREFIX = "signup-session:"; + + public void save(String signupToken, SignupSession session, long ttlSeconds) { + if (!StringUtils.hasText(signupToken) || session == null) { + return; + } + String key = PREFIX + signupToken; + redisTemplate.opsForValue().set(key, session, Duration.ofSeconds(ttlSeconds)); + } + + public Optional consume(String signupToken) { + if (!StringUtils.hasText(signupToken)) { + return Optional.empty(); + } + String key = PREFIX + signupToken; + + Object value; + try { + value = redisTemplate.opsForValue().getAndDelete(key); + } catch (Exception e) { + value = redisTemplate.opsForValue().get(key); + redisTemplate.delete(key); + } + + if (value instanceof SignupSession session) { + return Optional.of(session); + } + return Optional.empty(); + } +} + diff --git a/src/main/java/or/sopt/houme/domain/user/service/SignupSession.java b/src/main/java/or/sopt/houme/domain/user/service/SignupSession.java new file mode 100644 index 00000000..a1da837d --- /dev/null +++ b/src/main/java/or/sopt/houme/domain/user/service/SignupSession.java @@ -0,0 +1,12 @@ +package or.sopt.houme.domain.user.service; + +public record SignupSession( + Long kakaoId, + String email, + String nickname +) { + public static SignupSession of(Long kakaoId, String email, String nickname) { + return new SignupSession(kakaoId, email, nickname); + } +} + From 1a079c3b931d4452b9bb83aaa265357124717cc8 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:21:00 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:#319=20record=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/{service => entity/record}/SignupSession.java | 2 +- .../houme/domain/user/repository/SignupSessionRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/or/sopt/houme/domain/user/{service => entity/record}/SignupSession.java (83%) diff --git a/src/main/java/or/sopt/houme/domain/user/service/SignupSession.java b/src/main/java/or/sopt/houme/domain/user/entity/record/SignupSession.java similarity index 83% rename from src/main/java/or/sopt/houme/domain/user/service/SignupSession.java rename to src/main/java/or/sopt/houme/domain/user/entity/record/SignupSession.java index a1da837d..3defba25 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/SignupSession.java +++ b/src/main/java/or/sopt/houme/domain/user/entity/record/SignupSession.java @@ -1,4 +1,4 @@ -package or.sopt.houme.domain.user.service; +package or.sopt.houme.domain.user.entity.record; public record SignupSession( Long kakaoId, diff --git a/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java index 78aa98d6..6637a67c 100644 --- a/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java +++ b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java @@ -1,7 +1,7 @@ package or.sopt.houme.domain.user.repository; import lombok.RequiredArgsConstructor; -import or.sopt.houme.domain.user.service.SignupSession; +import or.sopt.houme.domain.user.entity.record.SignupSession; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; From f9715dae140cd33080ffb59ce94349e30de279cc Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:26:53 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:#391=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/KakaoLoginResponse.java | 22 +++++++ .../domain/user/service/OAuthService.java | 61 ++++++++++++------- 2 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 src/main/java/or/sopt/houme/domain/user/controller/dto/KakaoLoginResponse.java diff --git a/src/main/java/or/sopt/houme/domain/user/controller/dto/KakaoLoginResponse.java b/src/main/java/or/sopt/houme/domain/user/controller/dto/KakaoLoginResponse.java new file mode 100644 index 00000000..9a3dd297 --- /dev/null +++ b/src/main/java/or/sopt/houme/domain/user/controller/dto/KakaoLoginResponse.java @@ -0,0 +1,22 @@ +package or.sopt.houme.domain.user.controller.dto; + +public record KakaoLoginResponse( + boolean isNewUser, + String signupToken, + Prefill prefill +) { + public static KakaoLoginResponse newUser(String signupToken, String email, String nickname) { + return new KakaoLoginResponse(true, signupToken, new Prefill(email, nickname)); + } + + public static KakaoLoginResponse existingUser() { + return new KakaoLoginResponse(false, null, null); + } + + public record Prefill( + String email, + String nickname + ) { + } +} + diff --git a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java index f5611350..31f40d83 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java +++ b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java @@ -8,14 +8,17 @@ import or.sopt.houme.domain.user.client.KaKaoOAuthClient; import or.sopt.houme.domain.user.client.KaKaoUserInfoClient; import or.sopt.houme.domain.user.controller.dto.CustomUserDetails; +import or.sopt.houme.domain.user.controller.dto.KakaoLoginResponse; import or.sopt.houme.domain.user.controller.dto.KaKaoOAuthTokenDTO; import or.sopt.houme.domain.user.controller.dto.KaKaoUserInfoResponse; import or.sopt.houme.domain.user.entity.Role; import or.sopt.houme.domain.user.entity.SocialType; import or.sopt.houme.domain.user.entity.User; import or.sopt.houme.domain.user.entity.UserStatus; +import or.sopt.houme.domain.user.entity.record.SignupSession; import or.sopt.houme.domain.user.repository.BlacklistTokenRepository; import or.sopt.houme.domain.user.repository.RefreshTokenRepository; +import or.sopt.houme.domain.user.repository.SignupSessionRepository; import or.sopt.houme.domain.user.repository.UserRepository; import or.sopt.houme.global.api.ErrorCode; import or.sopt.houme.global.api.handler.UserException; @@ -26,10 +29,13 @@ import or.sopt.houme.global.util.CookieUtil; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; import jakarta.servlet.http.HttpServletRequest; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; import java.util.List; @Service @@ -45,10 +51,13 @@ public class OAuthService { private final RefreshTokenRepository refreshTokenRepository; private final BlacklistTokenRepository blacklistTokenRepository; + private final SignupSessionRepository signupSessionRepository; private final KaKaoConfig kaKaoConfig; private final CookieConfig cookieConfig; + @Value("${auth.signup-token.ttl-seconds:600}") + private long signupTokenTtlSeconds; /** * 카카오 인증 서버에 인가코드를 요청하는 메서드입니다 @@ -80,10 +89,7 @@ public String requestRedirect(HttpServletRequest request, String env) { } - public Boolean kakaoLogin(String accessCode, String env, HttpServletRequest request, HttpServletResponse response) { - - // 신규회원인지 검증하는 필드 - Boolean isNewUser = false; + public KakaoLoginResponse kakaoLogin(String accessCode, String env, HttpServletRequest request, HttpServletResponse response) { // 인가코드가 비어있다면 예외발생 if (accessCode == null || accessCode.isEmpty()) { @@ -115,27 +121,36 @@ public Boolean kakaoLogin(String accessCode, String env, HttpServletRequest requ throw new UserException(ErrorCode.KAKAO_ACCESSTOKEN_INVALID); } - // 만약 해당 이메일을 통해 회원가입된 회원이 존재하지 않는다면, 새로운 회원을 생성합니다 - Boolean userExist = userRepository.existsByEmail(userInfo.getKakao_account().getEmail()); + String email = Optional.ofNullable(userInfo.getKakao_account()) + .map(KaKaoUserInfoResponse.KakaoAccount::getEmail) + .orElse(null); + if (!StringUtils.hasText(email)) { + throw new UserException(ErrorCode.NOT_VALID_EXCEPTION); + } + + String nickname = Optional.ofNullable(userInfo.getKakao_account()) + .map(KaKaoUserInfoResponse.KakaoAccount::getProfile) + .map(KaKaoUserInfoResponse.KakaoAccount.Profile::getNickname) + .orElse(null); + Boolean userExist = userRepository.existsByEmail(email); + + // 회원 정보가 없는 경우 -> 임시토큰을 발급하여 반환합니다 if (userExist == Boolean.FALSE) { - User newUser = User.builder() -// 이름은 자체 회원가입시 입력 받습니다. - .password(null) - .email(userInfo.getKakao_account().getEmail()) - .role(Role.ROLE_USER) - .socialType(SocialType.KAKAO) - .status(UserStatus.ACTIVE) - .hasGeneratedImage(Boolean.FALSE) - .build(); - - userRepository.save(newUser); - - isNewUser = true; + String signupToken = UUID.randomUUID().toString().replace("-", ""); + + SignupSession signupSession = SignupSession.of( + userInfo.getId(), + email, + nickname + ); + signupSessionRepository.save(signupToken, signupSession, signupTokenTtlSeconds); + + return KakaoLoginResponse.newUser(signupToken, email, nickname); } - // 그리고 회원 정보를 기반으로 액세스토큰을 발급하여 헤더에 넣습니다 - User byEmail = userRepository.findByEmail(userInfo.getKakao_account().getEmail()) + // 회원정보가 존재하는 경우 -> 회원 정보를 기반으로 액세스토큰을 발급하여 헤더에 넣습니다 + User byEmail = userRepository.findByEmail(email) .orElseThrow(()-> new UserException(ErrorCode.USER_NOT_FOUND)); String access = jwtUtil.createJwt("access", byEmail.getId(), byEmail.getRole().toString(), jwtConfig.getAccessTokenValidityInSeconds()); @@ -155,7 +170,7 @@ public Boolean kakaoLogin(String accessCode, String env, HttpServletRequest requ cookieConfig.getSameSite() ); - return isNewUser; + return KakaoLoginResponse.existingUser(); } // Backward-compatible overloads for existing tests and callers @@ -163,7 +178,7 @@ public String requestRedirect(HttpServletRequest request) { return requestRedirect(request, null); } - public Boolean kakaoLogin(String accessCode, HttpServletRequest request, HttpServletResponse response) { + public KakaoLoginResponse kakaoLogin(String accessCode, HttpServletRequest request, HttpServletResponse response) { return kakaoLogin(accessCode, null, request, response); } From 42bdb895052d8aa4538830be91a399e7ee28b582 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:28:18 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:#391=20controller=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../houme/domain/user/controller/OAuthController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/or/sopt/houme/domain/user/controller/OAuthController.java b/src/main/java/or/sopt/houme/domain/user/controller/OAuthController.java index 91d9ac31..784d5975 100644 --- a/src/main/java/or/sopt/houme/domain/user/controller/OAuthController.java +++ b/src/main/java/or/sopt/houme/domain/user/controller/OAuthController.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import or.sopt.houme.domain.user.controller.dto.CustomUserDetails; +import or.sopt.houme.domain.user.controller.dto.KakaoLoginResponse; import or.sopt.houme.domain.user.service.OAuthService; import or.sopt.houme.global.api.ApiResponse; import org.springframework.http.ResponseEntity; @@ -45,14 +46,15 @@ public void kakaoOAuthCallback(@RequestParam(value = "env", required = false) St @Operation(summary = "카카오 인증서버 토큰 검증 API", description = "프론트에서 전달한 code를 이용해 토큰 교환 및 로그인 처리를 수행합니다.

" + - "액세스 토큰은 헤더에, 리프레시 토큰은 쿠키에 담아 반환합니다.") + "기존회원이면 액세스 토큰은 헤더에, 리프레시 토큰은 쿠키에 담아 반환합니다.
" + + "신규회원이면 signupToken(임시토큰)과 prefill 정보를 반환합니다.") @GetMapping("/oauth/kakao/callback") - public ResponseEntity> kakaoLogin(@RequestParam("code") String accessCode, + public ResponseEntity> kakaoLogin(@RequestParam("code") String accessCode, @RequestParam(value = "env", required = false) String env, HttpServletRequest request, HttpServletResponse response) { - Boolean result = (env == null) + KakaoLoginResponse result = (env == null) ? oAuthService.kakaoLogin(accessCode, request, response) : oAuthService.kakaoLogin(accessCode, env, request, response); From 984488aae00f0327aa12a76f1f1c8da66fb640ae Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:28:55 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:#391=20request=20DTO=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/SocialSignUpRequest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/or/sopt/houme/domain/user/controller/dto/SocialSignUpRequest.java diff --git a/src/main/java/or/sopt/houme/domain/user/controller/dto/SocialSignUpRequest.java b/src/main/java/or/sopt/houme/domain/user/controller/dto/SocialSignUpRequest.java new file mode 100644 index 00000000..120c10f5 --- /dev/null +++ b/src/main/java/or/sopt/houme/domain/user/controller/dto/SocialSignUpRequest.java @@ -0,0 +1,27 @@ +package or.sopt.houme.domain.user.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import or.sopt.houme.domain.user.valid.ValidBirthday; + +public record SocialSignUpRequest( + @NotBlank(message = "signupToken은 필수 입력값입니다.") + String signupToken, + + @NotBlank(message = "이름은 필수 입력값입니다.") + @Pattern(regexp = "^[가-힣a-zA-Z]+$", message = "숫자, 특수문자는 입력할 수 없어요.") + String name, + + @NotBlank(message = "성별은 필수 입력값입니다.") + @Pattern( + regexp = "MALE|FEMALE|NONBINARY", + message = "성별은 MALE, FEMALE, NONBINARY 중 하나여야 해요." + ) + String gender, + + @NotBlank(message = "생년월일은 필수 입력값입니다.") + @ValidBirthday + String birthday +) { +} + From edc89318aa03f7ff5a94b7204d7e9f785678f403 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:35:01 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:#391=20=EC=9E=84=EC=8B=9C=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 34 +++++++++ .../domain/user/service/OAuthService.java | 70 +++++++++++++++++-- .../or/sopt/houme/global/api/ErrorCode.java | 4 ++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/main/java/or/sopt/houme/domain/user/controller/UserController.java b/src/main/java/or/sopt/houme/domain/user/controller/UserController.java index 2095f436..1769386f 100644 --- a/src/main/java/or/sopt/houme/domain/user/controller/UserController.java +++ b/src/main/java/or/sopt/houme/domain/user/controller/UserController.java @@ -2,12 +2,14 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import or.sopt.houme.domain.user.controller.dto.*; import or.sopt.houme.domain.user.entity.Gender; import or.sopt.houme.domain.user.service.UserDeletionService; import or.sopt.houme.domain.user.service.UserService; +import or.sopt.houme.domain.user.service.OAuthService; import or.sopt.houme.global.api.ApiResponse; import or.sopt.houme.global.api.ErrorCode; import or.sopt.houme.global.api.GeneralException; @@ -25,6 +27,7 @@ public class UserController { private final UserService userService; private final UserDeletionService userDeletionService; + private final OAuthService oAuthService; @GetMapping(value = "/mypage/user") // 유저의 이름, 사용가능한 크레딧 개수 조회 @Operation(summary = "마이페이지 기본 정보 제공 API") @@ -72,6 +75,37 @@ public ResponseEntity> updateUser(@AuthenticationPrincipal C return ResponseEntity.ok(ApiResponse.ok(username)); } + @PostMapping(value = "/sign-up") + @Operation(summary = "소셜 회원가입 API", + description = "카카오 소셜로그인 완료 후 발급된 signupToken(임시토큰)과 함께 호출하면 회원을 생성합니다.

" + + "성공 시 access-token 헤더와 refresh-token 쿠키를 함께 반환합니다.") + public ResponseEntity> signUp(@RequestBody @Valid SocialSignUpRequest signUpRequest, + HttpServletResponse response) { + Gender gender; + LocalDate birthday; + + try { + gender = Gender.valueOf(signUpRequest.gender()); + } catch (IllegalArgumentException e) { + throw new GeneralException(ErrorCode.NOT_VALID_EXCEPTION); + } + try { + birthday = LocalDate.parse(signUpRequest.birthday()); + } catch (IllegalArgumentException e) { + throw new GeneralException(ErrorCode.NOT_VALID_EXCEPTION); + } + + String username = oAuthService.signUpWithToken( + signUpRequest.signupToken(), + signUpRequest.name(), + gender, + birthday, + response + ); + + return ResponseEntity.ok(ApiResponse.ok(username)); + } + @DeleteMapping("/user") @Operation(summary = "회원 탈퇴 API", description = "회원을 삭제합니다.

" + diff --git a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java index 31f40d83..265936d1 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java +++ b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java @@ -1,32 +1,33 @@ package or.sopt.houme.domain.user.service; import feign.FeignException; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import or.sopt.houme.domain.credit.entity.Credit; +import or.sopt.houme.domain.credit.entity.CreditStatus; +import or.sopt.houme.domain.credit.repository.CreditRepository; import or.sopt.houme.domain.user.client.KaKaoOAuthClient; import or.sopt.houme.domain.user.client.KaKaoUserInfoClient; import or.sopt.houme.domain.user.controller.dto.CustomUserDetails; import or.sopt.houme.domain.user.controller.dto.KakaoLoginResponse; import or.sopt.houme.domain.user.controller.dto.KaKaoOAuthTokenDTO; import or.sopt.houme.domain.user.controller.dto.KaKaoUserInfoResponse; -import or.sopt.houme.domain.user.entity.Role; -import or.sopt.houme.domain.user.entity.SocialType; -import or.sopt.houme.domain.user.entity.User; -import or.sopt.houme.domain.user.entity.UserStatus; +import or.sopt.houme.domain.user.entity.*; import or.sopt.houme.domain.user.entity.record.SignupSession; import or.sopt.houme.domain.user.repository.BlacklistTokenRepository; import or.sopt.houme.domain.user.repository.RefreshTokenRepository; import or.sopt.houme.domain.user.repository.SignupSessionRepository; import or.sopt.houme.domain.user.repository.UserRepository; import or.sopt.houme.global.api.ErrorCode; +import or.sopt.houme.global.api.handler.CreditException; import or.sopt.houme.global.api.handler.UserException; import or.sopt.houme.global.config.CookieConfig; import or.sopt.houme.global.config.JWTConfig; import or.sopt.houme.global.config.KaKaoConfig; import or.sopt.houme.global.jwt.JWTUtil; import or.sopt.houme.global.util.CookieUtil; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.beans.factory.annotation.Value; @@ -34,6 +35,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.util.Optional; import java.util.UUID; import java.util.List; @@ -46,6 +48,7 @@ public class OAuthService { private final KaKaoOAuthClient kaKaoOAuthClient; private final KaKaoUserInfoClient kaKaoUserInfoClient; private final UserRepository userRepository; + private final CreditRepository creditRepository; private final JWTUtil jwtUtil; private final JWTConfig jwtConfig; @@ -182,6 +185,63 @@ public KakaoLoginResponse kakaoLogin(String accessCode, HttpServletRequest reque return kakaoLogin(accessCode, null, request, response); } + @Transactional + public String signUpWithToken(String signupToken, String name, Gender gender, LocalDate birthday, HttpServletResponse response) { + SignupSession signupSession = signupSessionRepository.consume(signupToken) + .orElseThrow(() -> new UserException(ErrorCode.SIGNUP_TOKEN_INVALID)); + + if (!StringUtils.hasText(signupSession.email())) { + throw new UserException(ErrorCode.NOT_VALID_EXCEPTION); + } + if (userRepository.existsByEmail(signupSession.email())) { + throw new UserException(ErrorCode.EMAIL_ALREADY_EXISTS); + } + + User savedUser = userRepository.save( + User.builder() + .password(null) + .email(signupSession.email()) + .name(name) + .birthday(birthday) + .gender(gender) + .role(Role.ROLE_USER) + .socialType(SocialType.KAKAO) + .status(UserStatus.ACTIVE) + .hasGeneratedImage(Boolean.FALSE) + .build() + ); + + try { + creditRepository.save( + Credit.builder() + .status(CreditStatus.ACTIVE) + .user(savedUser) + .build() + ); + } catch (Exception e) { + throw new CreditException(ErrorCode.CREDIT_CREATE_EXCEPTION); + } + + String access = jwtUtil.createJwt("access", savedUser.getId(), savedUser.getRole().toString(), jwtConfig.getAccessTokenValidityInSeconds()); + String refresh = jwtUtil.createJwt("refresh", savedUser.getId(), savedUser.getRole().toString(), jwtConfig.getRefreshTokenValidityInSeconds()); + + refreshTokenRepository.saveRefreshToken(savedUser.getId(), refresh, jwtConfig.getRefreshTokenValidityInSeconds()); + + response.setHeader("access-token", access); + + CookieUtil.addSameSiteCookie( + response, + "refresh-token", + refresh, + jwtConfig.getRefreshTokenValidityInSeconds().intValue(), + cookieConfig.getDomain(), + cookieConfig.isSecure(), + cookieConfig.getSameSite() + ); + + return savedUser.getName(); + } + /** * @param userDetails userDetails 에서 회원의 id를 받아서 그걸로 리프레시 토큰을 삭제합니다 diff --git a/src/main/java/or/sopt/houme/global/api/ErrorCode.java b/src/main/java/or/sopt/houme/global/api/ErrorCode.java index 1e642666..eeb60bcd 100644 --- a/src/main/java/or/sopt/houme/global/api/ErrorCode.java +++ b/src/main/java/or/sopt/houme/global/api/ErrorCode.java @@ -32,6 +32,10 @@ public enum ErrorCode { // 회원관련 USERNAME_DUPLICATE(HttpStatus.BAD_REQUEST,40009,"username이 중복되었습니다."), + EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40021, "이미 가입된 이메일입니다."), + + // 소셜 회원가입 관련 + SIGNUP_TOKEN_INVALID(HttpStatus.BAD_REQUEST, 40020, "회원가입 토큰이 유효하지 않습니다."), // 좋아요 관련 MISMATCHED_IS_LIKE(HttpStatus.BAD_REQUEST,40011 ,"좋아요와 요인의 선호 여부가 일치하지 않습니다."), From afef07cb6d989b510afa3a547d99b10027368ca6 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:37:34 +0900 Subject: [PATCH 07/13] =?UTF-8?q?chore:#391=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=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/user/service/OAuthService.java | 4 +- .../houme/global/config/SecurityConfig.java | 1 + .../domain/user/service/OAuthServiceTest.java | 62 ++++++++++++++++--- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java index 265936d1..c5158622 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java +++ b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java @@ -124,6 +124,7 @@ public KakaoLoginResponse kakaoLogin(String accessCode, String env, HttpServletR throw new UserException(ErrorCode.KAKAO_ACCESSTOKEN_INVALID); } + // 응답값에서 이메일을 파싱 String email = Optional.ofNullable(userInfo.getKakao_account()) .map(KaKaoUserInfoResponse.KakaoAccount::getEmail) .orElse(null); @@ -131,6 +132,7 @@ public KakaoLoginResponse kakaoLogin(String accessCode, String env, HttpServletR throw new UserException(ErrorCode.NOT_VALID_EXCEPTION); } + // nickname 파싱 String nickname = Optional.ofNullable(userInfo.getKakao_account()) .map(KaKaoUserInfoResponse.KakaoAccount::getProfile) .map(KaKaoUserInfoResponse.KakaoAccount.Profile::getNickname) @@ -138,7 +140,7 @@ public KakaoLoginResponse kakaoLogin(String accessCode, String env, HttpServletR Boolean userExist = userRepository.existsByEmail(email); - // 회원 정보가 없는 경우 -> 임시토큰을 발급하여 반환합니다 + // 회원 정보가 없는 경우 (이메일이 존재하지 않음) -> 임시토큰을 발급하여 반환합니다 if (userExist == Boolean.FALSE) { String signupToken = UUID.randomUUID().toString().replace("-", ""); diff --git a/src/main/java/or/sopt/houme/global/config/SecurityConfig.java b/src/main/java/or/sopt/houme/global/config/SecurityConfig.java index ba86e8a5..d3e00d0f 100644 --- a/src/main/java/or/sopt/houme/global/config/SecurityConfig.java +++ b/src/main/java/or/sopt/houme/global/config/SecurityConfig.java @@ -99,6 +99,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { // 인가 경로 설정 http.authorizeHttpRequests((auth)->auth + .requestMatchers(HttpMethod.POST, "/api/v1/sign-up").permitAll() .requestMatchers(WhiteListConfig.swaggerWhitelist().toArray(new String[0])).permitAll() .requestMatchers(WhiteListConfig.oauthWhitelist().toArray(new String[0])).permitAll() .requestMatchers(WhiteListConfig.serverWhitelist().toArray(new String[0])).permitAll() diff --git a/src/test/java/or/sopt/houme/domain/user/service/OAuthServiceTest.java b/src/test/java/or/sopt/houme/domain/user/service/OAuthServiceTest.java index a7889d6a..ebead387 100644 --- a/src/test/java/or/sopt/houme/domain/user/service/OAuthServiceTest.java +++ b/src/test/java/or/sopt/houme/domain/user/service/OAuthServiceTest.java @@ -7,12 +7,15 @@ import or.sopt.houme.domain.user.client.KaKaoOAuthClient; import or.sopt.houme.domain.user.client.KaKaoUserInfoClient; import or.sopt.houme.domain.user.controller.dto.CustomUserDetails; +import or.sopt.houme.domain.user.controller.dto.KakaoLoginResponse; import or.sopt.houme.domain.user.controller.dto.KaKaoOAuthTokenDTO; import or.sopt.houme.domain.user.controller.dto.KaKaoUserInfoResponse; +import or.sopt.houme.domain.credit.repository.CreditRepository; import or.sopt.houme.domain.user.entity.Role; import or.sopt.houme.domain.user.entity.User; import or.sopt.houme.domain.user.repository.BlacklistTokenRepository; import or.sopt.houme.domain.user.repository.RefreshTokenRepository; +import or.sopt.houme.domain.user.repository.SignupSessionRepository; import or.sopt.houme.domain.user.repository.UserRepository; import or.sopt.houme.global.api.handler.UserException; import or.sopt.houme.global.config.CookieConfig; @@ -46,6 +49,8 @@ class OAuthServiceTest { @Mock private UserRepository userRepository; @Mock + private CreditRepository creditRepository; + @Mock private JWTUtil jwtUtil; @Mock private JWTConfig jwtConfig; @@ -54,6 +59,8 @@ class OAuthServiceTest { @Mock private BlacklistTokenRepository blacklistTokenRepository; @Mock + private SignupSessionRepository signupSessionRepository; + @Mock private KaKaoConfig kaKaoConfig; @Mock private CookieConfig cookieConfig; @@ -84,8 +91,8 @@ void requestRedirectTest() { @Test - @DisplayName("kakaoLogin() 을 이용해서 소셜로그인을 통해 회원을 생성 할 수 있다") - void kakaoLogin_success() { + @DisplayName("kakaoLogin() 신규회원이면 회원을 생성하지 않고 signupToken을 발급한다") + void kakaoLogin_newUser_issueSignupToken() { // Given String code = "authCode"; KaKaoOAuthTokenDTO tokenDTO = new KaKaoOAuthTokenDTO(); @@ -94,17 +101,55 @@ void kakaoLogin_success() { KaKaoUserInfoResponse.KakaoAccount kakaoAccount = new KaKaoUserInfoResponse.KakaoAccount(); kakaoAccount.setEmail("test@houme.kr"); - KaKaoUserInfoResponse.Properties properties = new KaKaoUserInfoResponse.Properties(); - properties.setNickname("테스트닉네임"); + KaKaoUserInfoResponse.KakaoAccount.Profile profile = new KaKaoUserInfoResponse.KakaoAccount.Profile(); + profile.setNickname("테스트닉네임"); + kakaoAccount.setProfile(profile); KaKaoUserInfoResponse userInfo = new KaKaoUserInfoResponse(); + userInfo.setId(1234L); userInfo.setKakao_account(kakaoAccount); - userInfo.setProperties(properties); when(kaKaoOAuthClient.getToken(any(), any(), any(), any())).thenReturn(tokenDTO); when(kaKaoUserInfoClient.getUserInfo("Bearer kakaoAccessToken")).thenReturn(userInfo); when(userRepository.existsByEmail("test@houme.kr")).thenReturn(false); - when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); + ReflectionTestUtils.setField(oAuthService, "signupTokenTtlSeconds", 600L); + + // When + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Origin")).thenReturn("http://localhost:5173"); + + KakaoLoginResponse result = oAuthService.kakaoLogin(code, request, response); + + // Then + assertTrue(result.isNewUser()); + assertNotNull(result.signupToken()); + assertEquals("test@houme.kr", result.prefill().email()); + assertEquals("테스트닉네임", result.prefill().nickname()); + + verify(signupSessionRepository).save(anyString(), any(or.sopt.houme.domain.user.entity.record.SignupSession.class), eq(600L)); + verify(userRepository, never()).save(any(User.class)); + verify(refreshTokenRepository, never()).saveRefreshToken(anyLong(), anyString(), anyLong()); + verify(response, never()).setHeader(eq("access-token"), anyString()); + } + + @Test + @DisplayName("kakaoLogin() 기존회원이면 access/refresh 토큰을 발급한다") + void kakaoLogin_existingUser_issueJwtTokens() { + // Given + String code = "authCode"; + KaKaoOAuthTokenDTO tokenDTO = new KaKaoOAuthTokenDTO(); + tokenDTO.setAccess_token("kakaoAccessToken"); + + KaKaoUserInfoResponse.KakaoAccount kakaoAccount = new KaKaoUserInfoResponse.KakaoAccount(); + kakaoAccount.setEmail("test@houme.kr"); + + KaKaoUserInfoResponse userInfo = new KaKaoUserInfoResponse(); + userInfo.setId(1234L); + userInfo.setKakao_account(kakaoAccount); + + when(kaKaoOAuthClient.getToken(any(), any(), any(), any())).thenReturn(tokenDTO); + when(kaKaoUserInfoClient.getUserInfo("Bearer kakaoAccessToken")).thenReturn(userInfo); + when(userRepository.existsByEmail("test@houme.kr")).thenReturn(true); when(userRepository.findByEmail("test@houme.kr")).thenReturn(Optional.of( User.builder().id(1L).email("test@houme.kr").role(Role.ROLE_USER).build() )); @@ -121,11 +166,10 @@ void kakaoLogin_success() { HttpServletRequest request = mock(HttpServletRequest.class); when(request.getHeader("Origin")).thenReturn("http://localhost:5173"); - Boolean result = oAuthService.kakaoLogin(code, request, response); + KakaoLoginResponse result = oAuthService.kakaoLogin(code, request, response); // Then - assertTrue(result); // 신규 회원이므로 true - verify(userRepository).save(any(User.class)); + assertFalse(result.isNewUser()); verify(refreshTokenRepository).saveRefreshToken(eq(1L), eq("refreshToken"), eq(86400L)); verify(response).setHeader("access-token", "accessToken"); } From a9b089d403ff7d003289f257b534dde9e2a96575 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:39:16 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:#391=20controller=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=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 --- .../user/controller/OAuthControllerTest.java | 11 +++++-- .../user/controller/UserControllerTest.java | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/test/java/or/sopt/houme/domain/user/controller/OAuthControllerTest.java b/src/test/java/or/sopt/houme/domain/user/controller/OAuthControllerTest.java index 0b567d6d..88b3b2f0 100644 --- a/src/test/java/or/sopt/houme/domain/user/controller/OAuthControllerTest.java +++ b/src/test/java/or/sopt/houme/domain/user/controller/OAuthControllerTest.java @@ -1,6 +1,7 @@ package or.sopt.houme.domain.user.controller; import or.sopt.houme.domain.user.controller.dto.CustomUserDetails; +import or.sopt.houme.domain.user.controller.dto.KakaoLoginResponse; import or.sopt.houme.domain.user.entity.*; import or.sopt.houme.domain.user.service.OAuthService; import org.junit.jupiter.api.BeforeEach; @@ -71,10 +72,11 @@ void testKakaoOAuthRedirect() throws Exception { } @Test - @DisplayName("GET /oauth/kakao/callback 요청 시 카카오 로그인 성공 여부가 반환된다") + @DisplayName("GET /oauth/kakao/callback 요청 시 카카오 로그인 결과가 반환된다") void testKakaoLoginCallback() throws Exception { // given - when(oAuthService.kakaoLogin(eq("abc123"), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); + when(oAuthService.kakaoLogin(eq("abc123"), any(HttpServletRequest.class), any(HttpServletResponse.class))) + .thenReturn(KakaoLoginResponse.newUser("signup-token", "test@houme.kr", "닉네임")); // when & then mockMvc.perform(get("/oauth/kakao/callback") @@ -82,7 +84,10 @@ void testKakaoLoginCallback() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.msg").value("응답 성공")) - .andExpect(jsonPath("$.data").value(true)); + .andExpect(jsonPath("$.data.isNewUser").value(true)) + .andExpect(jsonPath("$.data.signupToken").value("signup-token")) + .andExpect(jsonPath("$.data.prefill.email").value("test@houme.kr")) + .andExpect(jsonPath("$.data.prefill.nickname").value("닉네임")); } @Test diff --git a/src/test/java/or/sopt/houme/domain/user/controller/UserControllerTest.java b/src/test/java/or/sopt/houme/domain/user/controller/UserControllerTest.java index ea6a41c1..e7b3169e 100644 --- a/src/test/java/or/sopt/houme/domain/user/controller/UserControllerTest.java +++ b/src/test/java/or/sopt/houme/domain/user/controller/UserControllerTest.java @@ -5,6 +5,7 @@ import or.sopt.houme.domain.user.controller.dto.MyPageInfoResponse; import or.sopt.houme.domain.user.entity.*; import or.sopt.houme.domain.user.repository.BlacklistTokenRepository; +import or.sopt.houme.domain.user.service.OAuthService; import or.sopt.houme.domain.user.service.UserDeletionService; import or.sopt.houme.domain.user.service.UserService; import or.sopt.houme.global.config.JWTConfig; @@ -23,12 +24,14 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import jakarta.servlet.http.HttpServletResponse; import java.time.LocalDate; import java.util.ArrayList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest( @@ -52,6 +55,9 @@ class UserControllerTest { @MockBean private UserDeletionService userDeletionService; + @MockBean + private OAuthService oAuthService; + @MockBean private JWTConfig jwtConfig; @@ -108,4 +114,31 @@ void getMyPageInfo_Success() throws Exception { .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.msg").value("응답 성공")); } + + @Test + @DisplayName("POST /api/v1/sign-up 요청 시 소셜 회원가입이 처리된다") + void socialSignUp_Success() throws Exception { + given(oAuthService.signUpWithToken( + any(String.class), + any(String.class), + any(Gender.class), + any(LocalDate.class), + any(HttpServletResponse.class) + )).willReturn("테스트유저"); + + mockMvc.perform(post("/api/v1/sign-up") + .contentType(org.springframework.http.MediaType.APPLICATION_JSON) + .content(""" + { + "signupToken": "signup-token", + "name": "테스트유저", + "gender": "MALE", + "birthday": "2000-01-01" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.msg").value("응답 성공")) + .andExpect(jsonPath("$.data").value("테스트유저")); + } } From f9065e294808cf645cc2a493197fc2f490d89485 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 22:59:20 +0900 Subject: [PATCH 09/13] =?UTF-8?q?signup=20token=20repository=20map=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/repository/SignupSessionRepository.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java index 6637a67c..c4670c12 100644 --- a/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java +++ b/src/main/java/or/sopt/houme/domain/user/repository/SignupSessionRepository.java @@ -1,5 +1,6 @@ package or.sopt.houme.domain.user.repository; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import or.sopt.houme.domain.user.entity.record.SignupSession; import org.springframework.data.redis.core.RedisTemplate; @@ -7,6 +8,7 @@ import org.springframework.util.StringUtils; import java.time.Duration; +import java.util.Map; import java.util.Optional; @Component @@ -14,6 +16,7 @@ public class SignupSessionRepository { private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; private static final String PREFIX = "signup-session:"; @@ -42,7 +45,9 @@ public Optional consume(String signupToken) { if (value instanceof SignupSession session) { return Optional.of(session); } + if (value instanceof Map map) { + return Optional.ofNullable(objectMapper.convertValue(map, SignupSession.class)); + } return Optional.empty(); } } - From 9f18b397bbe69d6e68a74c0824980e9e2f89b771 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Thu, 8 Jan 2026 23:05:29 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore:#391=20=ED=81=AC=EB=A0=88=EB=94=A7?= =?UTF-8?q?=20=EA=B0=9C=EC=88=98=205=EA=B0=9C=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/UserController.java | 2 +- .../houme/domain/user/service/OAuthService.java | 17 ++++++++--------- .../domain/user/service/UserServiceImpl.java | 15 +++++++++------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/or/sopt/houme/domain/user/controller/UserController.java b/src/main/java/or/sopt/houme/domain/user/controller/UserController.java index 1769386f..5306a2c8 100644 --- a/src/main/java/or/sopt/houme/domain/user/controller/UserController.java +++ b/src/main/java/or/sopt/houme/domain/user/controller/UserController.java @@ -76,7 +76,7 @@ public ResponseEntity> updateUser(@AuthenticationPrincipal C } @PostMapping(value = "/sign-up") - @Operation(summary = "소셜 회원가입 API", + @Operation(summary = "소셜 자체 회원가입 API", description = "카카오 소셜로그인 완료 후 발급된 signupToken(임시토큰)과 함께 호출하면 회원을 생성합니다.

" + "성공 시 access-token 헤더와 refresh-token 쿠키를 함께 반환합니다.") public ResponseEntity> signUp(@RequestBody @Valid SocialSignUpRequest signUpRequest, diff --git a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java index c5158622..c072f4ab 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java +++ b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java @@ -39,12 +39,15 @@ import java.util.Optional; import java.util.UUID; import java.util.List; +import java.util.stream.IntStream; @Service @RequiredArgsConstructor @Slf4j public class OAuthService { + private static final int SIGN_UP_CREDIT_COUNT = 5; + private final KaKaoOAuthClient kaKaoOAuthClient; private final KaKaoUserInfoClient kaKaoUserInfoClient; private final UserRepository userRepository; @@ -71,11 +74,6 @@ public class OAuthService { * 현재 fallback 로직이 구현되어있지 않습니다. 아직 Feign의 fallback factory에 대한 학습이 부족해서... * 앱잼기간내에 구현해보겠습니다 FIXME * */ - - /** - * - * - * */ public String requestRedirect(HttpServletRequest request, String env) { String redirectBase = resolveRedirectBase(request, env); String redirectUri = redirectBase + "/oauth/kakao/callback"; @@ -214,12 +212,13 @@ public String signUpWithToken(String signupToken, String name, Gender gender, Lo ); try { - creditRepository.save( - Credit.builder() + List newCredits = IntStream.range(0, SIGN_UP_CREDIT_COUNT) + .mapToObj(i -> Credit.builder() .status(CreditStatus.ACTIVE) .user(savedUser) - .build() - ); + .build()) + .toList(); + creditRepository.saveAll(newCredits); } catch (Exception e) { throw new CreditException(ErrorCode.CREDIT_CREATE_EXCEPTION); } diff --git a/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java b/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java index ab4a0073..ca3e0604 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java +++ b/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java @@ -42,6 +42,8 @@ @Transactional public class UserServiceImpl implements UserService { + private static final int SIGN_UP_CREDIT_COUNT = 5; + private final UserRepository userRepository; private final HouseRepository houseRepository; private final TagRepository tagRepository; @@ -190,12 +192,13 @@ public String updateUser(User user, String name, Gender gender, LocalDate birthd findUser.updateUserFromSignUp(name, birthday, gender); try { - Credit newCredit = Credit.builder() - .status(CreditStatus.ACTIVE) - .user(findUser) - .build(); - - creditRepository.save(newCredit); + List newCredits = IntStream.range(0, SIGN_UP_CREDIT_COUNT) + .mapToObj(i -> Credit.builder() + .status(CreditStatus.ACTIVE) + .user(findUser) + .build()) + .toList(); + creditRepository.saveAll(newCredits); }catch (Exception e) { throw new CreditException(ErrorCode.CREDIT_CREATE_EXCEPTION); } From bd5314fb9f64dd91df110e6b5de7c0d8367d3a95 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Fri, 9 Jan 2026 12:36:15 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore:#391=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=B9=8C=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 --- .../houme/domain/user/service/UserServiceImplTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java b/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java index 30ae2157..2c551713 100644 --- a/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java +++ b/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java @@ -34,6 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.BDDMockito.*; class UserServiceImplTest { @@ -372,7 +374,8 @@ void updateUser_credit_create() { assertEquals(Gender.MALE, dbUser.getGender()); assertEquals(LocalDate.of(2000, 5, 15), dbUser.getBirthday()); - verify(creditRepository, times(1)).save(any(Credit.class)); + verify(creditRepository, times(1)) + .saveAll(argThat(credits -> credits instanceof java.util.Collection c && c.size() == 5)); } @@ -401,7 +404,7 @@ void updateUser_credit_create_fail() { // 크레딧 저장 시 RuntimeException 발생하도록 설정 willThrow(new RuntimeException("DB error")) - .given(creditRepository).save(any(Credit.class)); + .given(creditRepository).saveAll(anyList()); // when & then assertThatThrownBy(() -> userService.updateUser(inputUser, request.name(), Gender.MALE, LocalDate.of(2000, 5, 15) From 1a233207426f7f57479f9b00cfaab2fa487546c3 Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Tue, 13 Jan 2026 19:16:09 +0900 Subject: [PATCH 12/13] =?UTF-8?q?chore:#391=20=ED=81=AC=EB=A0=88=EB=94=A7?= =?UTF-8?q?=20=EA=B0=9C=EC=88=98=201=EA=B0=9C=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/or/sopt/houme/domain/user/service/OAuthService.java | 4 ++-- .../or/sopt/houme/domain/user/service/UserServiceImpl.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java index c072f4ab..1d29718a 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java +++ b/src/main/java/or/sopt/houme/domain/user/service/OAuthService.java @@ -46,7 +46,7 @@ @Slf4j public class OAuthService { - private static final int SIGN_UP_CREDIT_COUNT = 5; + private static final int SIGN_UP_CREDIT_COUNT = 1; private final KaKaoOAuthClient kaKaoOAuthClient; private final KaKaoUserInfoClient kaKaoUserInfoClient; @@ -325,4 +325,4 @@ private String resolveRedirectBase(HttpServletRequest request, String env) { } return resolveRedirectBase(request); } -} +} \ No newline at end of file diff --git a/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java b/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java index ca3e0604..fd5af56b 100644 --- a/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java +++ b/src/main/java/or/sopt/houme/domain/user/service/UserServiceImpl.java @@ -42,7 +42,7 @@ @Transactional public class UserServiceImpl implements UserService { - private static final int SIGN_UP_CREDIT_COUNT = 5; + private static final int SIGN_UP_CREDIT_COUNT = 1; private final UserRepository userRepository; private final HouseRepository houseRepository; From cf5ebd22398d6a3641f421d9f81da12101d3135a Mon Sep 17 00:00:00 2001 From: gdbs1107 Date: Tue, 13 Jan 2026 19:43:02 +0900 Subject: [PATCH 13/13] =?UTF-8?q?chore:#391=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=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 --- .../or/sopt/houme/domain/user/service/UserServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java b/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java index 2c551713..5733e4ff 100644 --- a/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java +++ b/src/test/java/or/sopt/houme/domain/user/service/UserServiceImplTest.java @@ -375,7 +375,7 @@ void updateUser_credit_create() { assertEquals(LocalDate.of(2000, 5, 15), dbUser.getBirthday()); verify(creditRepository, times(1)) - .saveAll(argThat(credits -> credits instanceof java.util.Collection c && c.size() == 5)); + .saveAll(argThat(credits -> credits instanceof java.util.Collection c && c.size() == 1)); }