diff --git a/api/src/main/java/org/yapp/domain/auth/application/oauth/service/OauthService.java b/api/src/main/java/org/yapp/domain/auth/application/oauth/service/OauthService.java index 0c7422f5..3c6f6a12 100644 --- a/api/src/main/java/org/yapp/domain/auth/application/oauth/service/OauthService.java +++ b/api/src/main/java/org/yapp/domain/auth/application/oauth/service/OauthService.java @@ -1,43 +1,45 @@ package org.yapp.domain.auth.application.oauth.service; -import java.util.Optional; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.yapp.domain.auth.application.jwt.JwtUtil; import org.yapp.domain.auth.application.oauth.OauthProvider; import org.yapp.domain.auth.application.oauth.OauthProviderResolver; import org.yapp.domain.auth.dto.request.OauthLoginRequest; import org.yapp.domain.auth.dto.response.OauthLoginResponse; +import org.yapp.domain.user.User; import org.yapp.domain.user.dao.UserRepository; -import org.yapp.domain.user.domain.User; + +import java.util.Optional; + +import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class OauthService { - private final OauthProviderResolver oauthProviderResolver; - private final JwtUtil jwtUtil; - private final UserRepository userRepository; - - public OauthLoginResponse login(OauthLoginRequest request){ - OauthProvider oauthProvider = oauthProviderResolver.find(request.getProviderName()); - String oauthId = request.getProviderName() + oauthProvider.getOAuthProviderUserId(request.getToken()); - - //이미 가입된 유저인지 확인하고 가입되어 있지 않으면 회원가입 처리 - Optional userOptional = userRepository.findByOauthId(oauthId); - if (userOptional.isEmpty()){ - User newUser = User.builder().oauthId(oauthId).build(); - User savedUser = userRepository.save(newUser); - Long userId = savedUser.getId(); - String accessToken = jwtUtil.createJwt("access_token",userId, oauthId, "member", 600000L); - String refreshToken = jwtUtil.createJwt("refresh_token",userId, oauthId, "member", 864000000L); - return new OauthLoginResponse(true, accessToken, refreshToken); - } - - //이미 가입한 유저인 경우 로그인 처리 - Long userId = userOptional.get().getId(); - String accessToken = jwtUtil.createJwt("access_token", userId,oauthId, "member", 600000L); - String refreshToken = jwtUtil.createJwt("refresh_token", userId,oauthId, "member", 864000000L); - - return new OauthLoginResponse(false, accessToken, refreshToken); + private final OauthProviderResolver oauthProviderResolver; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + public OauthLoginResponse login(OauthLoginRequest request) { + OauthProvider oauthProvider = oauthProviderResolver.find(request.getProviderName()); + String oauthId = request.getProviderName() + oauthProvider.getOAuthProviderUserId(request.getToken()); + + //이미 가입된 유저인지 확인하고 가입되어 있지 않으면 회원가입 처리 + Optional userOptional = userRepository.findByOauthId(oauthId); + if (userOptional.isEmpty()) { + User newUser = User.builder().oauthId(oauthId).build(); + User savedUser = userRepository.save(newUser); + Long userId = savedUser.getId(); + String accessToken = jwtUtil.createJwt("access_token", userId, oauthId, "member", 600000L); + String refreshToken = jwtUtil.createJwt("refresh_token", userId, oauthId, "member", 864000000L); + return new OauthLoginResponse(true, accessToken, refreshToken); } + + //이미 가입한 유저인 경우 로그인 처리 + Long userId = userOptional.get().getId(); + String accessToken = jwtUtil.createJwt("access_token", userId, oauthId, "member", 600000L); + String refreshToken = jwtUtil.createJwt("refresh_token", userId, oauthId, "member", 864000000L); + + return new OauthLoginResponse(false, accessToken, refreshToken); + } } diff --git a/api/src/main/java/org/yapp/domain/profile/api/ProfileController.java b/api/src/main/java/org/yapp/domain/profile/api/ProfileController.java new file mode 100644 index 00000000..f253aed2 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/api/ProfileController.java @@ -0,0 +1,46 @@ +package org.yapp.domain.profile.api; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.yapp.domain.profile.Profile; +import org.yapp.domain.profile.api.request.ProfileUpdateRequest; +import org.yapp.domain.profile.api.response.ProfileResponse; +import org.yapp.domain.profile.application.ProfileService; +import org.yapp.domain.user.User; +import org.yapp.util.ApiResponse; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/profiles") +public class ProfileController { + private final ProfileService profileService; + + @GetMapping + @Operation(summary = "프로필 조회", description = "현재 로그인된 사용자의 프로필을 조회합니다.", tags = {"Profile"}) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "프로필이 성공적으로 조회되었습니다.") + public ResponseEntity> updateProfile(@AuthenticationPrincipal User user) { + Profile profile = profileService.getProfileById(user.getProfile().getId()); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success(ProfileResponse.from(profile))); + } + + @PutMapping() + @Operation(summary = "프로필 업데이트", description = "현재 로그인된 사용자의 프로필을 업데이트합니다.", tags = {"Profile"}) + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "프로필이 성공적으로 업데이트되었습니다.") + public ResponseEntity> updateProfile(@AuthenticationPrincipal User user, + @RequestBody @Valid ProfileUpdateRequest request) { + Profile profile = profileService.updateByUserId(user.getId(), request); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success(ProfileResponse.from(profile))); + } +} diff --git a/api/src/main/java/org/yapp/domain/profile/api/request/ProfileUpdateRequest.java b/api/src/main/java/org/yapp/domain/profile/api/request/ProfileUpdateRequest.java new file mode 100644 index 00000000..55326917 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/api/request/ProfileUpdateRequest.java @@ -0,0 +1,55 @@ +package org.yapp.domain.profile.api.request; + +import org.yapp.domain.profile.ProfileBasic; +import org.yapp.domain.profile.ProfileBio; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record ProfileUpdateRequest(@NotBlank(message = "닉네임은 비어있을 수 없습니다.") String nickname, + + @NotBlank(message = "생일은 비어있을 수 없습니다.") String birthdate, + + @Min(value = 50, message = "키는 최소 50cm 이상이어야 합니다.") @Max(value = 300, + message = "키는 250cm를 초과할 수 없습니다.") int height, + + @NotBlank(message = "직업은 비어있을 수 없습니다.") String job, + + @NotBlank(message = "위치는 비어있을 수 없습니다.") String location, + + String smokingStatus, String religion, String snsActivityLevel, + + @Pattern(regexp = "^\\d{10,11}$", + message = "전화번호는 10자리에서 11자리 숫자여야 합니다.") String phoneNumber, + + String imageUrl, + + @Size(max = 500, message = "자기소개는 500자를 초과할 수 없습니다.") String introduction, + + @Size(max = 500, message = "목표는 500자를 초과할 수 없습니다.") String goal, + + @Size(max = 500, message = "관심사는 500자를 초과할 수 없습니다.") String interest) { + + public ProfileBio toProfileBio() { + return ProfileBio.builder().introduction(introduction).goal(goal).interest(interest).build(); + } + + public ProfileBasic toProfileBasic() { + return ProfileBasic.builder() + .nickname(nickname) + .birthdate(java.sql.Date.valueOf(birthdate)) + .height(height) + .job(job) + .location(location) + .smokingStatus(smokingStatus) + .religion(religion) + .snsActivityLevel(snsActivityLevel) + .phoneNumber(phoneNumber) + .imageUrl(imageUrl) + .build(); + } + +} \ No newline at end of file diff --git a/api/src/main/java/org/yapp/domain/profile/api/response/ProfileResponse.java b/api/src/main/java/org/yapp/domain/profile/api/response/ProfileResponse.java new file mode 100644 index 00000000..557f31df --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/api/response/ProfileResponse.java @@ -0,0 +1,24 @@ +package org.yapp.domain.profile.api.response; + +import org.yapp.domain.profile.Profile; +import org.yapp.domain.profile.ProfileBasic; +import org.yapp.domain.profile.ProfileBio; + +import java.util.List; + +public record ProfileResponse(Long id, String nickname, String birthdate, int height, String job, String location, + String smokingStatus, String religion, String snsActivityLevel, String phoneNumber, + String imageUrl, String introduction, String goal, String interest, + List profileValues) { + + public static ProfileResponse from(Profile profile) { + ProfileBasic basic = profile.getProfileBasic(); + ProfileBio bio = profile.getProfileBio(); + + List values = profile.getProfileValues().stream().map(ProfileValueResponse::from).toList(); + + return new ProfileResponse(profile.getId(), basic.getNickname(), basic.getBirthdate().toString(), basic.getHeight(), + basic.getJob(), basic.getLocation(), basic.getSmokingStatus(), basic.getReligion(), basic.getSnsActivityLevel(), + basic.getPhoneNumber(), basic.getImageUrl(), bio.getIntroduction(), bio.getGoal(), bio.getInterest(), values); + } +} \ No newline at end of file diff --git a/api/src/main/java/org/yapp/domain/profile/api/response/ProfileValueResponse.java b/api/src/main/java/org/yapp/domain/profile/api/response/ProfileValueResponse.java new file mode 100644 index 00000000..ab2cd2c8 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/api/response/ProfileValueResponse.java @@ -0,0 +1,11 @@ +package org.yapp.domain.profile.api.response; + +import org.yapp.domain.profile.ProfileValue; +import org.yapp.domain.profile.ValueItem; + +public record ProfileValueResponse(Long id, ValueItem valueItem, Integer selectedValue) { + public static ProfileValueResponse from(ProfileValue profileValue) { + return new ProfileValueResponse(profileValue.getId(), profileValue.getValueItem(), + profileValue.getSelectedAnswer()); + } +} \ No newline at end of file diff --git a/api/src/main/java/org/yapp/domain/profile/application/ProfileService.java b/api/src/main/java/org/yapp/domain/profile/application/ProfileService.java new file mode 100644 index 00000000..ea0b4945 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/application/ProfileService.java @@ -0,0 +1,52 @@ +package org.yapp.domain.profile.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.yapp.domain.profile.Profile; +import org.yapp.domain.profile.ProfileBasic; +import org.yapp.domain.profile.ProfileBio; +import org.yapp.domain.profile.api.request.ProfileUpdateRequest; +import org.yapp.domain.profile.application.dto.ProfileCreateDto; +import org.yapp.domain.profile.dao.ProfileRepository; +import org.yapp.domain.user.User; +import org.yapp.domain.user.application.UserService; +import org.yapp.error.dto.ProfileErrorCode; +import org.yapp.error.exception.ApplicationException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ProfileService { + private final UserService userService; + private final ProfileRepository profileRepository; + + @Transactional + public Profile create(ProfileCreateDto dto) { + ProfileBasic profileBasic = ProfileBasic.builder().nickname(dto.nickName()).phoneNumber(dto.phoneNumber()).build(); + ProfileBio profileBio = ProfileBio.builder().build(); + Profile profile = Profile.builder().profileBasic(profileBasic).profileBio(profileBio).build(); + + return profileRepository.save(profile); + } + + @Transactional + public Profile getProfileById(long profileId) { + return profileRepository.findById(profileId) + .orElseThrow(() -> new ApplicationException(ProfileErrorCode.NOTFOUND_PROFILE)); + } + + @Transactional + public Profile updateByUserId(long userId, ProfileUpdateRequest dto) { + User user = this.userService.getUserById(userId); + Profile profile = getProfileById(user.getProfile().getId()); + + ProfileBasic profileBasic = dto.toProfileBasic(); + ProfileBio profileBio = dto.toProfileBio(); + + profile.updateBasic(profileBasic); + profile.updateBio(profileBio); + + return profile; + } +} diff --git a/api/src/main/java/org/yapp/domain/profile/application/dto/ProfileCreateDto.java b/api/src/main/java/org/yapp/domain/profile/application/dto/ProfileCreateDto.java new file mode 100644 index 00000000..e2cc9d31 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/application/dto/ProfileCreateDto.java @@ -0,0 +1,10 @@ +package org.yapp.domain.profile.application.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Builder; + +@Builder +public record ProfileCreateDto(@NotBlank String name, @NotBlank String nickName, + @NotBlank @Pattern(regexp = "\\d{3}-\\d{4}-\\d{4}") String phoneNumber) { +} diff --git a/api/src/main/java/org/yapp/domain/profile/application/util/NickNameGenerator.java b/api/src/main/java/org/yapp/domain/profile/application/util/NickNameGenerator.java new file mode 100644 index 00000000..f8e077d6 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/application/util/NickNameGenerator.java @@ -0,0 +1,15 @@ +package org.yapp.domain.profile.application.util; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class NickNameGenerator { + private static final List ANIMAL_NAMES = + List.of("사자", "호랑이", "곰", "독수리", "늑대", "판다", "여우", "사슴", "토끼", "돌고래"); + + public static String generateNickname() { + String animal = ANIMAL_NAMES.get(ThreadLocalRandom.current().nextInt(ANIMAL_NAMES.size())); + int number = ThreadLocalRandom.current().nextInt(10000) + 1; + return animal + number; + } +} diff --git a/api/src/main/java/org/yapp/domain/profile/dao/ProfileRepository.java b/api/src/main/java/org/yapp/domain/profile/dao/ProfileRepository.java new file mode 100644 index 00000000..3b50f136 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/profile/dao/ProfileRepository.java @@ -0,0 +1,9 @@ +package org.yapp.domain.profile.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.yapp.domain.profile.Profile; + +@Repository +public interface ProfileRepository extends JpaRepository { +} diff --git a/api/src/main/java/org/yapp/domain/user/application/UserService.java b/api/src/main/java/org/yapp/domain/user/application/UserService.java new file mode 100644 index 00000000..8a69de69 --- /dev/null +++ b/api/src/main/java/org/yapp/domain/user/application/UserService.java @@ -0,0 +1,19 @@ +package org.yapp.domain.user.application; + +import org.springframework.stereotype.Service; +import org.yapp.domain.user.User; +import org.yapp.domain.user.dao.UserRepository; +import org.yapp.error.dto.UserErrorCode; +import org.yapp.error.exception.ApplicationException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + public User getUserById(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> new ApplicationException(UserErrorCode.NOTFOUND_USER)); + } +} diff --git a/api/src/main/java/org/yapp/domain/user/dao/UserRepository.java b/api/src/main/java/org/yapp/domain/user/dao/UserRepository.java index e9bf509f..7cc581b6 100644 --- a/api/src/main/java/org/yapp/domain/user/dao/UserRepository.java +++ b/api/src/main/java/org/yapp/domain/user/dao/UserRepository.java @@ -1,9 +1,10 @@ package org.yapp.domain.user.dao; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.yapp.domain.user.domain.User; +import org.yapp.domain.user.User; + +import java.util.Optional; public interface UserRepository extends JpaRepository { - Optional findByOauthId(String oauthId); + Optional findByOauthId(String oauthId); } diff --git a/api/src/main/java/org/yapp/domain/user/domain/User.java b/api/src/main/java/org/yapp/domain/user/domain/User.java index 9e2e4486..40c16c10 100644 --- a/api/src/main/java/org/yapp/domain/user/domain/User.java +++ b/api/src/main/java/org/yapp/domain/user/domain/User.java @@ -1,29 +1,29 @@ -package org.yapp.domain.user.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Table(name = "USER") -@Entity -@Getter -@NoArgsConstructor -public class User { - @Id - @Column(name = "ID") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(name = "OAUTH_ID",nullable = false) - private String oauthId; - - @Builder - public User(String oauthId) { - this.oauthId = oauthId; - } -} +//package org.yapp.domain.user.domain; +// +//import jakarta.persistence.Column; +//import jakarta.persistence.Entity; +//import jakarta.persistence.GeneratedValue; +//import jakarta.persistence.GenerationType; +//import jakarta.persistence.Id; +//import jakarta.persistence.Table; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +// +//@Table(name = "USER") +//@Entity +//@Getter +//@NoArgsConstructor +//public class User { +// @Id +// @Column(name = "ID") +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private Long id; +// @Column(name = "OAUTH_ID",nullable = false) +// private String oauthId; +// +// @Builder +// public User(String oauthId) { +// this.oauthId = oauthId; +// } +//} diff --git a/api/src/main/java/org/yapp/global/config/SecurityConfig.java b/api/src/main/java/org/yapp/global/config/SecurityConfig.java index 4336d91b..8eceedcb 100644 --- a/api/src/main/java/org/yapp/global/config/SecurityConfig.java +++ b/api/src/main/java/org/yapp/global/config/SecurityConfig.java @@ -1,12 +1,7 @@ package org.yapp.global.config; -import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; - -import java.util.Collections; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -19,53 +14,58 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.yapp.domain.auth.application.jwt.JwtFilter; +import java.util.Collections; + +import lombok.RequiredArgsConstructor; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final JwtFilter jwtFilter; + private final JwtFilter jwtFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.csrf(AbstractHttpConfigurer::disable) + .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(registry -> registry.requestMatchers(getMatcherForUserAndAdmin()) + .hasAnyRole("USER", "ADMIN") + .requestMatchers(getMatcherForAnyone()) + .permitAll() + .requestMatchers(getMatcherForSwagger()) + .permitAll() + .anyRequest() + .hasAnyRole("ADMIN")) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http - .csrf(AbstractHttpConfigurer::disable) - .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) - .httpBasic(AbstractHttpConfigurer::disable) - .sessionManagement( - configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ) - .authorizeHttpRequests(registry -> registry - .requestMatchers(getMatcherForUserAndAdmin()) - .hasAnyRole("USER", "ADMIN") - .requestMatchers(getMatcherForAnyone()) - .permitAll() - .anyRequest() - .hasAnyRole("ADMIN") - ) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - .build(); - } + private CorsConfigurationSource corsConfigurationSource() { + return request -> { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedHeaders(Collections.singletonList("*")); + config.setAllowedMethods(Collections.singletonList("*")); + config.setAllowedOriginPatterns(Collections.singletonList("*")); + return config; + }; + } - private CorsConfigurationSource corsConfigurationSource() { - return request -> { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedHeaders(Collections.singletonList("*")); - config.setAllowedMethods(Collections.singletonList("*")); - config.setAllowedOriginPatterns(Collections.singletonList("*")); - return config; - }; - } + private RequestMatcher getMatcherForAnyone() { + return RequestMatchers.anyOf(antMatcher("/login/oauth")); + } - private RequestMatcher getMatcherForAnyone() { - return RequestMatchers.anyOf( - antMatcher("/login/oauth") - ); - } + private RequestMatcher getMatcherForUserAndAdmin() { + return RequestMatchers.anyOf(antMatcher("/user") //TODO: 임시이며 추후 url에 따라 수정해야. + ); + } - private RequestMatcher getMatcherForUserAndAdmin() { - return RequestMatchers.anyOf( - antMatcher("/user") //TODO: 임시이며 추후 url에 따라 수정해야. - ); - } + private RequestMatcher getMatcherForSwagger() { + return RequestMatchers.anyOf(antMatcher("/swagger-ui/**"), antMatcher("/v3/api-docs/**"), + antMatcher("/swagger-ui.html")); + } } \ No newline at end of file diff --git a/api/src/test/java/org/yapp/domain/auth/application/oauth/service/OauthServiceTest.java b/api/src/test/java/org/yapp/domain/auth/application/oauth/service/OauthServiceTest.java index fb93c476..5ac40532 100644 --- a/api/src/test/java/org/yapp/domain/auth/application/oauth/service/OauthServiceTest.java +++ b/api/src/test/java/org/yapp/domain/auth/application/oauth/service/OauthServiceTest.java @@ -1,18 +1,10 @@ package org.yapp.domain.auth.application.oauth.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import org.junit.jupiter.api.Assertions; 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.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.yapp.domain.auth.application.jwt.JwtUtil; @@ -20,86 +12,94 @@ import org.yapp.domain.auth.application.oauth.OauthProviderResolver; import org.yapp.domain.auth.dto.request.OauthLoginRequest; import org.yapp.domain.auth.dto.response.OauthLoginResponse; +import org.yapp.domain.user.User; import org.yapp.domain.user.dao.UserRepository; -import org.yapp.domain.user.domain.User; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OauthServiceTest { - @InjectMocks - private OauthService oauthService; - @Mock - private OauthProviderResolver oauthProviderResolver; - @Mock - private JwtUtil jwtUtil; - @Mock - private UserRepository userRepository; - @Mock - private OauthProvider oauthProvider; - @Test - @DisplayName("처음 로그인에 성공하여 회원가입을 한 뒤 액세스,리프레시 토큰을 반환한다") - void testFirstLogin_Success(){ - //given - String providerName = "provider"; - String token = "oauth_token"; - OauthLoginRequest request = new OauthLoginRequest(providerName, token); - String oauthId = providerName + "123456"; + @InjectMocks + private OauthService oauthService; + @Mock + private OauthProviderResolver oauthProviderResolver; + @Mock + private JwtUtil jwtUtil; + @Mock + private UserRepository userRepository; + @Mock + private OauthProvider oauthProvider; + + @Test + @DisplayName("처음 로그인에 성공하여 회원가입을 한 뒤 액세스,리프레시 토큰을 반환한다") + void testFirstLogin_Success() { + //given + String providerName = "provider"; + String token = "oauth_token"; + OauthLoginRequest request = new OauthLoginRequest(providerName, token); + String oauthId = providerName + "123456"; - when(oauthProviderResolver.find(providerName)).thenReturn(oauthProvider); - when(oauthProvider.getOAuthProviderUserId(token)).thenReturn("123456"); - when(userRepository.findByOauthId(oauthId)).thenReturn(Optional.empty()); - User user = new User(oauthId); - ReflectionTestUtils.setField(user,"id",1L); - when(userRepository.save(any(User.class))).thenReturn(user); - when(jwtUtil.createJwt("access_token", 1L, oauthId, "member", 600000L)).thenReturn("access_token"); - when(jwtUtil.createJwt("refresh_token", 1L, oauthId, "member", 864000000L)).thenReturn("refresh_token"); + when(oauthProviderResolver.find(providerName)).thenReturn(oauthProvider); + when(oauthProvider.getOAuthProviderUserId(token)).thenReturn("123456"); + when(userRepository.findByOauthId(oauthId)).thenReturn(Optional.empty()); + User user = User.builder().oauthId(oauthId).build(); + ReflectionTestUtils.setField(user, "id", 1L); + when(userRepository.save(any(User.class))).thenReturn(user); + when(jwtUtil.createJwt("access_token", 1L, oauthId, "member", 600000L)).thenReturn("access_token"); + when(jwtUtil.createJwt("refresh_token", 1L, oauthId, "member", 864000000L)).thenReturn("refresh_token"); - // When - OauthLoginResponse response = oauthService.login(request); + // When + OauthLoginResponse response = oauthService.login(request); - // Then - assertThat(response.isRegistered()).isTrue(); - assertThat(response.getAccessToken()).isEqualTo("access_token"); - assertThat(response.getRefreshToken()).isEqualTo("refresh_token"); - } + // Then + assertThat(response.isRegistered()).isTrue(); + assertThat(response.getAccessToken()).isEqualTo("access_token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh_token"); + } - @Test - @DisplayName("회원가입이 된 상태에서 로그인에 성공하여 액세스, 리프레시 토큰을 반환한다.") - void testNotFirstLogin_Success(){ - //given - String providerName = "provider"; - String token = "oauth_token"; - OauthLoginRequest request = new OauthLoginRequest(providerName, token); - String oauthId = providerName + "123456"; + @Test + @DisplayName("회원가입이 된 상태에서 로그인에 성공하여 액세스, 리프레시 토큰을 반환한다.") + void testNotFirstLogin_Success() { + //given + String providerName = "provider"; + String token = "oauth_token"; + OauthLoginRequest request = new OauthLoginRequest(providerName, token); + String oauthId = providerName + "123456"; - when(oauthProviderResolver.find(providerName)).thenReturn(oauthProvider); - when(oauthProvider.getOAuthProviderUserId(token)).thenReturn("123456"); - User existingUser = User.builder().oauthId(oauthId).build(); - ReflectionTestUtils.setField(existingUser,"id",1L); - when(userRepository.findByOauthId(oauthId)).thenReturn(Optional.of(existingUser)); - when(jwtUtil.createJwt("access_token", 1L, oauthId, "member", 600000L)).thenReturn("access_token"); - when(jwtUtil.createJwt("refresh_token", 1L, oauthId, "member", 864000000L)).thenReturn("refresh_token"); + when(oauthProviderResolver.find(providerName)).thenReturn(oauthProvider); + when(oauthProvider.getOAuthProviderUserId(token)).thenReturn("123456"); + User existingUser = User.builder().oauthId(oauthId).build(); + ReflectionTestUtils.setField(existingUser, "id", 1L); + when(userRepository.findByOauthId(oauthId)).thenReturn(Optional.of(existingUser)); + when(jwtUtil.createJwt("access_token", 1L, oauthId, "member", 600000L)).thenReturn("access_token"); + when(jwtUtil.createJwt("refresh_token", 1L, oauthId, "member", 864000000L)).thenReturn("refresh_token"); - // When - OauthLoginResponse response = oauthService.login(request); + // When + OauthLoginResponse response = oauthService.login(request); - // Then - assertThat(response.isRegistered()).isFalse(); - assertThat(response.getAccessToken()).isEqualTo("access_token"); - assertThat(response.getRefreshToken()).isEqualTo("refresh_token"); - } + // Then + assertThat(response.isRegistered()).isFalse(); + assertThat(response.getAccessToken()).isEqualTo("access_token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh_token"); + } - @Test - @DisplayName("존재하지 않는 Provider로 로그인을 요청하여 로그인에 실패한다.") - void testLogin_Failure(){ - // Given - String providerName = "nonexistent"; - String token = "oauth_token"; - OauthLoginRequest request = new OauthLoginRequest(providerName, token); + @Test + @DisplayName("존재하지 않는 Provider로 로그인을 요청하여 로그인에 실패한다.") + void testLogin_Failure() { + // Given + String providerName = "nonexistent"; + String token = "oauth_token"; + OauthLoginRequest request = new OauthLoginRequest(providerName, token); - // Mocking - when(oauthProviderResolver.find(providerName)).thenThrow(new RuntimeException()); + // Mocking + when(oauthProviderResolver.find(providerName)).thenThrow(new RuntimeException()); - // When & Then - assertThrows(RuntimeException.class, () -> oauthService.login(request)); - } + // When & Then + assertThrows(RuntimeException.class, () -> oauthService.login(request)); + } } \ No newline at end of file diff --git a/api/src/test/java/org/yapp/domain/auth/application/profile/ProfileServiceTest.java b/api/src/test/java/org/yapp/domain/auth/application/profile/ProfileServiceTest.java new file mode 100644 index 00000000..77228d6b --- /dev/null +++ b/api/src/test/java/org/yapp/domain/auth/application/profile/ProfileServiceTest.java @@ -0,0 +1,92 @@ +package org.yapp.domain.auth.application.profile; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.yapp.domain.profile.Profile; +import org.yapp.domain.profile.api.request.ProfileUpdateRequest; +import org.yapp.domain.profile.application.ProfileService; +import org.yapp.domain.profile.application.dto.ProfileCreateDto; +import org.yapp.domain.profile.dao.ProfileRepository; +import org.yapp.domain.user.User; +import org.yapp.domain.user.dao.UserRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("ProfileService 통합 테스트") +@SpringBootTest +@Transactional +class ProfileServiceTest { + @Autowired + private ProfileService profileService; + + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private UserRepository userRepository; + + private User testUser; + private Profile testProfile; + + @BeforeEach + void setUp() { + profileRepository.deleteAll(); + + testUser = User.builder().name("test@naver.com").build(); + + ProfileCreateDto dto = ProfileCreateDto.builder().nickName("nickname123").phoneNumber("010-5133-2895").build(); + testProfile = profileService.create(dto); + + testUser.setProfile(testProfile); + userRepository.save(testUser); + } + + @Test + @DisplayName("프로필 생성 테스트 - 실제 DB에서 성공적으로 프로필을 생성한다.") + void shouldCreateProfileSuccessfully() { + // Given + ProfileCreateDto dto = new ProfileCreateDto("홍길동", "nickname123", "010-1234-5678"); + + // When + Profile savedProfile = profileService.create(dto); + + // Then + assertThat(savedProfile).isNotNull(); + assertThat(savedProfile.getProfileBasic().getNickname()).isEqualTo("nickname123"); + assertThat(savedProfile.getProfileBasic().getPhoneNumber()).isEqualTo("010-1234-5678"); + } + + @Test + @DisplayName("유저 ID로 프로필 업데이트 테스트") + void shouldUpdateProfileByUserId() { + // Given: 업데이트 요청 생성 + ProfileUpdateRequest updateRequest = + new ProfileUpdateRequest("updatedNickname", "1995-05-20", 172, "개발자", "서울시 강남구", "비흡연", "기독교", "활발", + "01011112222", "https://example.com/profile.jpg", "안녕하세요. 개발자입니다.", "10년 안에 CTO가 되는 것", "기술, 클라이밍, 여행"); + + // When: 업데이트 실행 + Profile updatedProfile = profileService.updateByUserId(testUser.getId(), updateRequest); + + // Then: 업데이트 결과 검증 (모든 필드) + assertThat(updatedProfile).isNotNull(); + + assertThat(updatedProfile.getProfileBasic().getNickname()).isEqualTo(updateRequest.nickname()); + assertThat(updatedProfile.getProfileBasic().getPhoneNumber()).isEqualTo(updateRequest.phoneNumber()); + assertThat(updatedProfile.getProfileBasic().getJob()).isEqualTo(updateRequest.job()); + assertThat(updatedProfile.getProfileBasic().getHeight()).isEqualTo(updateRequest.height()); + assertThat(updatedProfile.getProfileBasic().getBirthdate().toString()).isEqualTo(updateRequest.birthdate()); + assertThat(updatedProfile.getProfileBasic().getLocation()).isEqualTo(updateRequest.location()); + assertThat(updatedProfile.getProfileBasic().getSmokingStatus()).isEqualTo(updateRequest.smokingStatus()); + assertThat(updatedProfile.getProfileBasic().getReligion()).isEqualTo(updateRequest.religion()); + assertThat(updatedProfile.getProfileBasic().getSnsActivityLevel()).isEqualTo(updateRequest.snsActivityLevel()); + assertThat(updatedProfile.getProfileBasic().getImageUrl()).isEqualTo(updateRequest.imageUrl()); + + assertThat(updatedProfile.getProfileBio().getIntroduction()).isEqualTo(updateRequest.introduction()); + assertThat(updatedProfile.getProfileBio().getGoal()).isEqualTo(updateRequest.goal()); + assertThat(updatedProfile.getProfileBio().getInterest()).isEqualTo(updateRequest.interest()); + } +} \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle index 55ab5720..4b00bf14 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -14,6 +14,9 @@ repositories { dependencies { api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + api 'io.hypersistence:hypersistence-utils-hibernate-62:3.7.0' + api 'com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations' + testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' } diff --git a/common/src/main/java/org/yapp/domain/profile/Profile.java b/common/src/main/java/org/yapp/domain/profile/Profile.java new file mode 100644 index 00000000..4a8c4aa6 --- /dev/null +++ b/common/src/main/java/org/yapp/domain/profile/Profile.java @@ -0,0 +1,53 @@ +package org.yapp.domain.profile; + +import org.yapp.domain.user.User; + +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "profile") +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Profile { + @Id + @Column(name = "profile_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(mappedBy = "profile") + private User user; + + @Embedded + private ProfileBasic profileBasic; + + @Embedded + private ProfileBio profileBio; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + private List profileValues; + + public void updateBio(ProfileBio profileBio) { + this.profileBio = profileBio; + } + + public void updateBasic(ProfileBasic profileBasic) { + this.profileBasic = profileBasic; + } +} diff --git a/common/src/main/java/org/yapp/domain/profile/ProfileBasic.java b/common/src/main/java/org/yapp/domain/profile/ProfileBasic.java new file mode 100644 index 00000000..8b85b715 --- /dev/null +++ b/common/src/main/java/org/yapp/domain/profile/ProfileBasic.java @@ -0,0 +1,48 @@ +package org.yapp.domain.profile; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class ProfileBasic { + @Column(name = "nickname", nullable = false) + private String nickname; + + @Column(name = "birthdate") + private Date birthdate; + + @Column(name = "height") + private int height; + + @Column(name = "job") + private String job; + + @Column(name = "location") + private String location; + + @Column(name = "smoking_status") + private String smokingStatus; + + @Column(name = "religion") + private String religion; + + @Column(name = "sns_activity_level") + private String snsActivityLevel; + + @Column(name = "phone_number", nullable = false) + private String phoneNumber; + + @Column(name = "image_url") + private String imageUrl; +} diff --git a/common/src/main/java/org/yapp/domain/profile/ProfileBio.java b/common/src/main/java/org/yapp/domain/profile/ProfileBio.java new file mode 100644 index 00000000..01811f2e --- /dev/null +++ b/common/src/main/java/org/yapp/domain/profile/ProfileBio.java @@ -0,0 +1,25 @@ +package org.yapp.domain.profile; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class ProfileBio { + @Column(name = "introduction", length = 500) + private String introduction; + + @Column(name = "goal", length = 500) + private String goal; + + @Column(name = "interest", length = 500) + private String interest; +} \ No newline at end of file diff --git a/common/src/main/java/org/yapp/domain/profile/ProfileValue.java b/common/src/main/java/org/yapp/domain/profile/ProfileValue.java new file mode 100644 index 00000000..0c916a60 --- /dev/null +++ b/common/src/main/java/org/yapp/domain/profile/ProfileValue.java @@ -0,0 +1,43 @@ +package org.yapp.domain.profile; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "profile_value") +@Getter +@NoArgsConstructor +public class ProfileValue { + @Id + @Column(name = "profile_value_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private Profile profile; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "value_item_id", nullable = false) + private ValueItem valueItem; + + @Column(nullable = false) + private Integer selectedAnswer; + + @Builder + public ProfileValue(Profile profile, ValueItem valueItem, Integer selectedAnswer) { + this.profile = profile; + this.valueItem = valueItem; + this.selectedAnswer = selectedAnswer; + } +} diff --git a/common/src/main/java/org/yapp/domain/profile/ValueItem.java b/common/src/main/java/org/yapp/domain/profile/ValueItem.java new file mode 100644 index 00000000..050a2d5f --- /dev/null +++ b/common/src/main/java/org/yapp/domain/profile/ValueItem.java @@ -0,0 +1,40 @@ +package org.yapp.domain.profile; + +import org.hibernate.annotations.Type; + +import java.util.Map; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "value_item") +@Getter +@NoArgsConstructor +public class ValueItem { + @Id + @Column(name = "value_item_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String question; + + @Type(JsonType.class) + @Column(name = "answers", columnDefinition = "longtext", nullable = false) + private Map answers; + + @Builder + public ValueItem(String question, Map answers) { + this.question = question; + this.answers = answers; + } +} diff --git a/common/src/main/java/org/yapp/domain/user/User.java b/common/src/main/java/org/yapp/domain/user/User.java new file mode 100644 index 00000000..1eccfbcf --- /dev/null +++ b/common/src/main/java/org/yapp/domain/user/User.java @@ -0,0 +1,48 @@ +package org.yapp.domain.user; + +import org.yapp.domain.profile.Profile; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "user_common") +@Entity +@Getter +@NoArgsConstructor +public class User { + @Id + @Column(name = "user_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "oauth_id") + private String oauthId; + + @Column(name = "name") + private String name; + + @OneToOne + @JoinColumn(name = "profile_id", unique = true) // User가 profile_id를 FK로 가짐 + private Profile profile; + + @Builder + public User(String oauthId, String name, Profile profile) { + this.oauthId = oauthId; + this.name = name; + this.profile = profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } +} + diff --git a/common/src/main/java/org/yapp/error/dto/ProfileErrorCode.java b/common/src/main/java/org/yapp/error/dto/ProfileErrorCode.java new file mode 100644 index 00000000..d68f87a3 --- /dev/null +++ b/common/src/main/java/org/yapp/error/dto/ProfileErrorCode.java @@ -0,0 +1,17 @@ +package org.yapp.error.dto; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ProfileErrorCode implements ErrorCode { + INACTIVE_PROFILE(HttpStatus.FORBIDDEN, "Profile is inactive"), + NOTFOUND_PROFILE(HttpStatus.NOT_FOUND, "Profile not found"), + ; + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/common/src/main/java/org/yapp/error/dto/UserErrorCode.java b/common/src/main/java/org/yapp/error/dto/UserErrorCode.java index b0e42720..2d30d641 100644 --- a/common/src/main/java/org/yapp/error/dto/UserErrorCode.java +++ b/common/src/main/java/org/yapp/error/dto/UserErrorCode.java @@ -1,15 +1,17 @@ package org.yapp.error.dto; +import org.springframework.http.HttpStatus; + import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor public enum UserErrorCode implements ErrorCode { - INACTIVE_USER(HttpStatus.FORBIDDEN, "User is inactive"), - ; + INACTIVE_USER(HttpStatus.FORBIDDEN, "User is inactive"), + NOTFOUND_USER(HttpStatus.NOT_FOUND, "User not found"), + ; - private final HttpStatus httpStatus; - private final String message; + private final HttpStatus httpStatus; + private final String message; } diff --git a/common/src/main/java/org/yapp/error/exception/ApplicationException.java b/common/src/main/java/org/yapp/error/exception/ApplicationException.java index c60d8dc1..79427226 100644 --- a/common/src/main/java/org/yapp/error/exception/ApplicationException.java +++ b/common/src/main/java/org/yapp/error/exception/ApplicationException.java @@ -1,11 +1,12 @@ package org.yapp.error.exception; +import org.yapp.error.dto.ErrorCode; + import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.yapp.error.dto.CommonErrorCode; @Getter @RequiredArgsConstructor public class ApplicationException extends RuntimeException { - private final CommonErrorCode errorCode; + private final ErrorCode errorCode; } diff --git a/common/src/main/java/org/yapp/util/ApiResponse.java b/common/src/main/java/org/yapp/util/ApiResponse.java new file mode 100644 index 00000000..08f69a6c --- /dev/null +++ b/common/src/main/java/org/yapp/util/ApiResponse.java @@ -0,0 +1,18 @@ +package org.yapp.util; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ApiResponse { + private final HttpStatus status; + private final String message; + private final T data; + + public static ApiResponse success(T data) { + return new ApiResponse<>(HttpStatus.OK, "요청이 성공적으로 처리되었습니다.", data); + } +} \ No newline at end of file