diff --git a/SossBar/src/main/java/com/sossbar/oauth2/kakao/service/KakaoLoginService.java b/SossBar/src/main/java/com/sossbar/oauth2/kakao/service/KakaoLoginService.java index 7c05258..9c53057 100644 --- a/SossBar/src/main/java/com/sossbar/oauth2/kakao/service/KakaoLoginService.java +++ b/SossBar/src/main/java/com/sossbar/oauth2/kakao/service/KakaoLoginService.java @@ -104,7 +104,6 @@ public User processLogin(String code) { ).orElseGet(() -> userRepository.save(User.builder() .email(kakaoUserInfo.getKakaoAccount().getEmail()) - .nickname(kakaoUserInfo.getProperties().getNickname()) .profileImageUrl( kakaoUserInfo.getKakaoAccount() .getProfile() diff --git a/SossBar/src/main/java/com/sossbar/user/controller/UserController.java b/SossBar/src/main/java/com/sossbar/user/controller/UserController.java index 3e05aa2..b94c3e9 100644 --- a/SossBar/src/main/java/com/sossbar/user/controller/UserController.java +++ b/SossBar/src/main/java/com/sossbar/user/controller/UserController.java @@ -3,7 +3,6 @@ import com.sossbar.global.common.code.SuccessCode; import com.sossbar.global.common.template.ApiResTemplate; import com.sossbar.global.common.template.SwaggerApiResTemplate; -import com.sossbar.user.dto.request.UserOnboardingReqDto; import com.sossbar.user.dto.request.UserInfoUpdateReqDto; import com.sossbar.user.dto.response.UserInfoResDto; import com.sossbar.user.service.UserService; @@ -23,35 +22,25 @@ @RequiredArgsConstructor @RequestMapping("/api/v1/users") @SwaggerApiResTemplate -@Tag(name = "MyPage API", description = "내 프로필 관련 API") +@Tag(name = "User API", description = "사용자 관련 API - MyPage") public class UserController { private final UserService userService; - @Operation(summary = "사용자 정보 추가 입력", description = "카카오 로그인을 완료한 사용자가 최초 1회 추가 정보(이름, 한 줄 소개, 프로필 이미지)를 입력합니다." + + @Operation(summary = "사용자 정보 추가 입력", description = "카카오 로그인을 완료한 사용자가 최초 1회 추가 정보(실명, 한 줄 소개, 프로필 이미지)를 입력합니다." + "
이미지를 빈 값으로 보낼 시 url은 null로 저장됨") @PostMapping(value = "/onboarding", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResTemplate saveUserInfo(Principal principal, - @Valid @RequestPart("onboarding") UserOnboardingReqDto userOnboardingReqDto, + @Valid @RequestPart("onboarding") UserInfoUpdateReqDto userInfoUpdateReqDto, @RequestPart(value = "profileImage", required = false) MultipartFile profileImage) { - UserInfoResDto userInfoResDto = userService.onboarding(principal, userOnboardingReqDto, profileImage); + UserInfoResDto userInfoResDto = userService.onboarding(principal, userInfoUpdateReqDto, profileImage); return ApiResTemplate.successResponse(SuccessCode.SUCCESS, userInfoResDto); } - @Operation(summary = "내 정보 조회", description = "로그인한 사용자가 자신의 프로필 정보를 조회합니다.") + @Operation(summary = "내 정보 조회", description = "로그인한 사용자가 자신의 계정 정보를 조회합니다. (본인만 조회 가능, 이메일 포함)") @GetMapping("/profile") public ApiResTemplate getUserInfo(Principal principal) { UserInfoResDto userInfoResDto = userService.getUserInfo(principal); return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS, userInfoResDto); } - - @Operation(summary = "내 정보 수정", description = "로그인한 사용자가 자신의 프로필 정보를 수정합니다. (닉네임, 한 줄 소개, 프로필 이미지) " + - "
이미지를 빈 값으로 보낼 시 기존 프로필 이미지 유지") - @PatchMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ApiResTemplate updateUserInfo(Principal principal, - @Valid @RequestPart("info") UserInfoUpdateReqDto userInfoUpdateReqDto, - @RequestPart(value = "profileImage", required = false) MultipartFile profileImage) { - UserInfoResDto userInfoResDto = userService.updateUserInfo(principal, userInfoUpdateReqDto, profileImage); - return ApiResTemplate.successResponse(SuccessCode.UPDATE_SUCCESS, userInfoResDto); - } } diff --git a/SossBar/src/main/java/com/sossbar/user/controller/UserProfileController.java b/SossBar/src/main/java/com/sossbar/user/controller/UserProfileController.java new file mode 100644 index 0000000..b7937ce --- /dev/null +++ b/SossBar/src/main/java/com/sossbar/user/controller/UserProfileController.java @@ -0,0 +1,45 @@ +package com.sossbar.user.controller; + +import com.sossbar.global.common.code.SuccessCode; +import com.sossbar.global.common.template.ApiResTemplate; +import com.sossbar.global.common.template.SwaggerApiResTemplate; +import com.sossbar.user.dto.request.UserInfoUpdateReqDto; +import com.sossbar.user.dto.response.UserInfoResDto; +import com.sossbar.user.dto.response.UserProfileInfoResDto; +import com.sossbar.user.service.UserProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/users") +@SwaggerApiResTemplate +@Tag(name = "User Profile API", description = "사용자 프로필 관련 API") +public class UserProfileController { + + private final UserProfileService userProfileService; + + @Operation(summary = "사용자 프로필 정보 조회", description = "로그인한 사용자가 userId로 사용자 프로필 정보(실명, 프로필 사진, 한 줄 소개)를 조회합니다.") + @GetMapping("/profile/{userId}") + public ApiResTemplate getUserProfile(@PathVariable("userId") Long userId) { + UserProfileInfoResDto userProfileInfoResDto = userProfileService.getUserProfile(userId); + return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS, userProfileInfoResDto); + } + + @Operation(summary = "내 프로필 수정", description = "로그인한 사용자가 자신의 프로필 정보를 수정합니다. (실명, 한 줄 소개, 프로필 이미지)" + + "
이미지를 빈 값으로 보낼 시 기존 프로필 이미지 유지") + @PatchMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ApiResTemplate updateUserInfo(Principal principal, + @Valid @RequestPart("info") UserInfoUpdateReqDto userInfoUpdateReqDto, + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage) { + UserInfoResDto userInfoResDto = userProfileService.updateUserInfo(principal, userInfoUpdateReqDto, profileImage); + return ApiResTemplate.successResponse(SuccessCode.UPDATE_SUCCESS, userInfoResDto); + } +} diff --git a/SossBar/src/main/java/com/sossbar/user/dto/request/UserInfoUpdateReqDto.java b/SossBar/src/main/java/com/sossbar/user/dto/request/UserInfoUpdateReqDto.java index eb4dd1e..fd04245 100644 --- a/SossBar/src/main/java/com/sossbar/user/dto/request/UserInfoUpdateReqDto.java +++ b/SossBar/src/main/java/com/sossbar/user/dto/request/UserInfoUpdateReqDto.java @@ -4,11 +4,10 @@ import jakarta.validation.constraints.Size; public record UserInfoUpdateReqDto( - @NotBlank - @Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하로 입력해 주세요.") - String nickname, + @NotBlank(message = "실명만 입력해 주세요.") + @Size(min = 2, max = 20, message = "이름은 2자 이상 20자 이하로 입력해 주세요.") + String username, - @NotBlank @Size(max = 100, message = "한 줄 소개는 100자 이하로 입력해 주세요.") String bio ) { diff --git a/SossBar/src/main/java/com/sossbar/user/dto/request/UserOnboardingReqDto.java b/SossBar/src/main/java/com/sossbar/user/dto/request/UserOnboardingReqDto.java deleted file mode 100644 index 0582289..0000000 --- a/SossBar/src/main/java/com/sossbar/user/dto/request/UserOnboardingReqDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sossbar.user.dto.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -public record UserOnboardingReqDto( - @NotBlank - @Size(min = 2, max = 10, message = "이름은 2자 이상 10자 이하로 입력해 주세요.") - String username, - - @NotBlank - @Size(max = 100, message = "한 줄 소개는 100자 이하로 입력해 주세요.") - String bio -) { -} diff --git a/SossBar/src/main/java/com/sossbar/user/dto/response/UserInfoResDto.java b/SossBar/src/main/java/com/sossbar/user/dto/response/UserInfoResDto.java index f8885ec..37cb1bb 100644 --- a/SossBar/src/main/java/com/sossbar/user/dto/response/UserInfoResDto.java +++ b/SossBar/src/main/java/com/sossbar/user/dto/response/UserInfoResDto.java @@ -8,7 +8,6 @@ public record UserInfoResDto( Long userId, String username, - String nickname, String email, String bio, String profileImageUrl, @@ -18,7 +17,6 @@ public static UserInfoResDto from(User user) { return UserInfoResDto.builder() .userId(user.getId()) .username(user.getUsername()) - .nickname(user.getNickname()) .email(user.getEmail()) .bio(user.getBio()) .profileImageUrl(user.getProfileImageUrl()) diff --git a/SossBar/src/main/java/com/sossbar/user/dto/response/UserProfileInfoResDto.java b/SossBar/src/main/java/com/sossbar/user/dto/response/UserProfileInfoResDto.java new file mode 100644 index 0000000..b82869b --- /dev/null +++ b/SossBar/src/main/java/com/sossbar/user/dto/response/UserProfileInfoResDto.java @@ -0,0 +1,21 @@ +package com.sossbar.user.dto.response; + +import com.sossbar.user.entity.User; +import lombok.Builder; + +@Builder +public record UserProfileInfoResDto( + Long userId, + String username, + String bio, + String profileImageUrl +) { + public static UserProfileInfoResDto from(User user) { + return UserProfileInfoResDto.builder() + .userId(user.getId()) + .username(user.getUsername()) + .bio(user.getBio()) + .profileImageUrl(user.getProfileImageUrl()) + .build(); + } +} diff --git a/SossBar/src/main/java/com/sossbar/user/entity/User.java b/SossBar/src/main/java/com/sossbar/user/entity/User.java index 0032871..75ea1e6 100644 --- a/SossBar/src/main/java/com/sossbar/user/entity/User.java +++ b/SossBar/src/main/java/com/sossbar/user/entity/User.java @@ -2,7 +2,6 @@ import com.sossbar.global.common.template.BaseTimeEntity; import com.sossbar.user.dto.request.UserInfoUpdateReqDto; -import com.sossbar.user.dto.request.UserOnboardingReqDto; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -18,7 +17,6 @@ public class User extends BaseTimeEntity { @Column(name = "user_id") private Long id; private String username; - private String nickname; @Column(unique = true, nullable = false) private String email; @@ -30,9 +28,8 @@ public class User extends BaseTimeEntity { private String refreshToken; @Builder - public User(String username, String nickname, String email, String bio, String profileImageUrl, UserType userType, String refreshToken) { + public User(String username, String email, String bio, String profileImageUrl, UserType userType, String refreshToken) { this.username = username; - this.nickname = nickname; this.email = email; this.bio = bio; this.profileImageUrl = profileImageUrl; @@ -40,16 +37,18 @@ public User(String username, String nickname, String email, String bio, String p this.refreshToken = refreshToken; } - public void onboarding(UserOnboardingReqDto userInfoSaveReqDto, String profileImageUrl) { - this.username = userInfoSaveReqDto.username(); - this.bio = userInfoSaveReqDto.bio(); - this.profileImageUrl = profileImageUrl; - } - public void updateUserInfo(UserInfoUpdateReqDto userInfoUpdateReqDto, String profileImageUrl) { - this.nickname = userInfoUpdateReqDto.nickname(); - this.bio = userInfoUpdateReqDto.bio(); - this.profileImageUrl = profileImageUrl; + if (userInfoUpdateReqDto.username() != null) { + this.username = userInfoUpdateReqDto.username(); + } + + if (userInfoUpdateReqDto.bio() != null) { + this.bio = userInfoUpdateReqDto.bio(); + } + + if (profileImageUrl != null) { + this.profileImageUrl = profileImageUrl; + } } public void saveRefreshToken(String refreshToken) { diff --git a/SossBar/src/main/java/com/sossbar/user/repository/UserRepository.java b/SossBar/src/main/java/com/sossbar/user/repository/UserRepository.java index 4863ab4..a1b3085 100644 --- a/SossBar/src/main/java/com/sossbar/user/repository/UserRepository.java +++ b/SossBar/src/main/java/com/sossbar/user/repository/UserRepository.java @@ -9,6 +9,4 @@ @Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - boolean existsByNickname(String nickname); - boolean existsByNicknameAndIdNot(String nickname, Long userId); } diff --git a/SossBar/src/main/java/com/sossbar/user/service/UserProfileService.java b/SossBar/src/main/java/com/sossbar/user/service/UserProfileService.java new file mode 100644 index 0000000..ef476a6 --- /dev/null +++ b/SossBar/src/main/java/com/sossbar/user/service/UserProfileService.java @@ -0,0 +1,62 @@ +package com.sossbar.user.service; + +import com.sossbar.global.common.code.ErrorCode; +import com.sossbar.global.common.exception.BusinessException; +import com.sossbar.global.config.S3Service; +import com.sossbar.user.dto.request.UserInfoUpdateReqDto; +import com.sossbar.user.dto.response.UserInfoResDto; +import com.sossbar.user.dto.response.UserProfileInfoResDto; +import com.sossbar.user.entity.User; +import com.sossbar.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserProfileService { + + private final UserRepository userRepository; + private final S3Service s3Service; + + // 프로필 페이지 - 내 프로필 수정(실명, 한 줄 소개, 프로필 이미지) + @Transactional + public UserInfoResDto updateUserInfo(Principal principal, UserInfoUpdateReqDto userInfoUpdateReqDto, MultipartFile profileImage) { + Long id = Long.parseLong(principal.getName()); + User user = getUserById(id); + + // 프로필 이미지 수정 처리 (기존 이미지 삭제 후 새 이미지 업로드) + String newProfileImageUrl = user.getProfileImageUrl(); + + if (profileImage != null && !profileImage.isEmpty()) { + // 기존 이미지가 존재한다면 S3에서 삭제 + if (newProfileImageUrl != null && !newProfileImageUrl.isBlank()) { + s3Service.deleteFile(newProfileImageUrl); + } + // 새 이미지 업로드 + newProfileImageUrl = s3Service.uploadFile(profileImage, "sossbar/profile"); + } + + user.updateUserInfo(userInfoUpdateReqDto, newProfileImageUrl); + + return UserInfoResDto.from(user); + } + + // 프로필 페이지 - 사용자 프로필 조회, userId로 구분 + public UserProfileInfoResDto getUserProfile(Long userId) { + User user = getUserById(userId); + + return UserProfileInfoResDto.from(user); + } + + // entity 찾는 공통 메소드 + private User getUserById(Long userId) { + return userRepository.findById(userId).orElseThrow( + () -> new BusinessException(ErrorCode.USER_NOT_FOUND_EXCEPTION + , ErrorCode.USER_NOT_FOUND_EXCEPTION.getMessage() + userId)); + } +} diff --git a/SossBar/src/main/java/com/sossbar/user/service/UserService.java b/SossBar/src/main/java/com/sossbar/user/service/UserService.java index 54f5a4a..8b7abe9 100644 --- a/SossBar/src/main/java/com/sossbar/user/service/UserService.java +++ b/SossBar/src/main/java/com/sossbar/user/service/UserService.java @@ -3,7 +3,6 @@ import com.sossbar.global.common.code.ErrorCode; import com.sossbar.global.common.exception.BusinessException; import com.sossbar.global.config.S3Service; -import com.sossbar.user.dto.request.UserOnboardingReqDto; import com.sossbar.user.dto.request.UserInfoUpdateReqDto; import com.sossbar.user.dto.response.UserInfoResDto; import com.sossbar.user.entity.User; @@ -23,9 +22,9 @@ public class UserService { private final UserRepository userRepository; private final S3Service s3Service; - // 온보딩 - 사용자 추가 정보 입력 (이름, 한 줄 소개, 프로필 이미지) + // 온보딩 - 사용자 추가 정보 입력 (실명, 한 줄 소개, 프로필 이미지) @Transactional - public UserInfoResDto onboarding(Principal principal, UserOnboardingReqDto userOnboardingReqDto, MultipartFile profileImage) { + public UserInfoResDto onboarding(Principal principal, UserInfoUpdateReqDto userInfoUpdateReqDto, MultipartFile profileImage) { Long id = Long.parseLong(principal.getName()); User user = getUserById(id); @@ -43,12 +42,12 @@ public UserInfoResDto onboarding(Principal principal, UserOnboardingReqDto userO profileImageUrl = s3Service.uploadFile(profileImage, "sossbar/profile"); } - user.onboarding(userOnboardingReqDto, profileImageUrl); + user.updateUserInfo(userInfoUpdateReqDto, profileImageUrl); return UserInfoResDto.from(user); } - // 마이페이지 - 내 프로필 조회 + // 마이페이지 - 내 계정 정보 조회(실명, 이메일) public UserInfoResDto getUserInfo(Principal principal) { Long id = Long.parseLong(principal.getName()); User user = getUserById(id); @@ -56,40 +55,6 @@ public UserInfoResDto getUserInfo(Principal principal) { return UserInfoResDto.from(user); } - // 마이페이지 - 내 프로필 수정 - @Transactional - public UserInfoResDto updateUserInfo(Principal principal, UserInfoUpdateReqDto userInfoUpdateReqDto, MultipartFile profileImage) { - Long id = Long.parseLong(principal.getName()); - User user = getUserById(id); - - String newNickname = userInfoUpdateReqDto.nickname(); - - // nickname 중복 체크 - 자신 제외 - if (newNickname != null && - userRepository.existsByNicknameAndIdNot(newNickname, user.getId())) { - throw new BusinessException( - ErrorCode.VALIDATION_ERROR, - "이미 사용 중인 닉네임입니다." - ); - } - - // 프로필 이미지 수정 처리 (기존 이미지 삭제 후 새 이미지 업로드) - String newProfileImageUrl = user.getProfileImageUrl(); - - if (profileImage != null && !profileImage.isEmpty()) { - // 기존 이미지가 존재한다면 S3에서 삭제 - if (newProfileImageUrl != null && !newProfileImageUrl.isBlank()) { - s3Service.deleteFile(newProfileImageUrl); - } - // 새 이미지 업로드 - newProfileImageUrl = s3Service.uploadFile(profileImage, "sossbar/profile"); - } - - user.updateUserInfo(userInfoUpdateReqDto, newProfileImageUrl); - - return UserInfoResDto.from(user); - } - // entity 찾는 공통 메소드 private User getUserById(Long userId) { return userRepository.findById(userId).orElseThrow(