Skip to content

Commit

Permalink
Merge pull request #53 from Mu-necting/feat/#52
Browse files Browse the repository at this point in the history
프로필 업데이트 API 구현 완료
  • Loading branch information
mingmingmon authored Nov 1, 2024
2 parents 04185cb + 5860969 commit 6190baf
Show file tree
Hide file tree
Showing 20 changed files with 463 additions and 7 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ dependencies {
// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//aws
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4'

// Etc
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.munecting.api.domain.user.controller;


import com.munecting.api.domain.user.dto.request.UpdateProfileRequestDto;
import com.munecting.api.domain.user.dto.response.UpdateProfileResponseDto;
import com.munecting.api.domain.user.service.UserService;
import com.munecting.api.global.auth.user.UserId;
import com.munecting.api.global.common.dto.response.ApiResponse;
import com.munecting.api.global.common.dto.response.Status;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@RequiredArgsConstructor
Expand All @@ -30,6 +35,7 @@ public ApiResponse<?> test(
return ApiResponse.onSuccess(Status.OK.getCode(), Status.OK.getMessage(), userId);
}

//TODO: 엔드포인트에 userId 노출 제거
@DeleteMapping("/{userId}")
@Operation(summary = "회원 탈퇴")
public ApiResponse<?> deleteUser(
Expand All @@ -38,4 +44,22 @@ public ApiResponse<?> deleteUser(
userService.deleteUser(userId);
return ApiResponse.onSuccess(Status.OK.getCode(), Status.OK.getMessage(), null);
}

@PatchMapping(value = "/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "프로필 업데이트")
public ApiResponse<?> updateProfile(
@UserId Long userId,

@Schema(description = "새로운 프로필 이미지 파일", nullable = true, type = "string", format = "binary")
@RequestPart(value = "profileImage", required = false)
MultipartFile profileImage,

@Schema(description = "새로운 닉네임", nullable = true)
@RequestPart(value = "nickname", required = false)
String nickname
){
UpdateProfileRequestDto requestDto = new UpdateProfileRequestDto(nickname, profileImage);
UpdateProfileResponseDto responseDto = userService.updateProfile(userId, requestDto);
return ApiResponse.ok(responseDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findBySocialId(String sub);

boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.munecting.api.domain.user.dao;

import com.munecting.api.domain.user.entity.Uuid;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UuidRepository extends JpaRepository<Uuid, Long> {

Optional<Uuid> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.munecting.api.domain.user.dto.request;

import org.springframework.web.multipart.MultipartFile;

public record UpdateProfileRequestDto(
String nickname,
MultipartFile profileImage
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.munecting.api.domain.user.dto.response;

import lombok.Builder;

@Builder
public record UpdateProfileResponseDto(
String nickname,
String imageUrl
) {

public static UpdateProfileResponseDto of(String nickname, String imgUrl) {
return UpdateProfileResponseDto.builder()
.nickname(nickname)
.imageUrl(imgUrl)
.build();
}
}
15 changes: 14 additions & 1 deletion src/main/java/com/munecting/api/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.munecting.api.domain.user.constant.SocialType;
import com.munecting.api.global.common.domain.BaseEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import lombok.*;
Expand All @@ -23,7 +24,7 @@ public class User extends BaseEntity {
@NotNull
private String socialId;

@Column(nullable = true)
@NotBlank
private String nickname;

@Column(nullable = true)
Expand All @@ -36,4 +37,16 @@ public class User extends BaseEntity {
@NotNull
@Enumerated(EnumType.STRING)
private SocialType socialType;

public String updateNickname(String nickname) {
this.nickname = nickname;

return this.nickname;
}

public String updateProfileImageUrl(String imgUrl) {
this.profileImageUrl = imgUrl;

return this.profileImageUrl;
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/munecting/api/domain/user/entity/Uuid.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.munecting.api.domain.user.entity;

import com.munecting.api.global.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.util.UUID;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Uuid extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String uuid;

private Long userId;

public static Uuid toEntity(Long userId) {
return Uuid.builder()
.uuid(UUID.randomUUID().toString())
.userId(userId)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public UserTokenResponseDto getOrCreateUser(LoginRequestDto dto) {
return issueTokensForUser(user);
}

//TODO: 이메일 제거, 닉네임 자체 생성
private User createUser(String socialId, String email, SocialType socialType) {
User newUser = User.builder()
.socialId(socialId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.munecting.api.domain.user.service;

import com.munecting.api.domain.user.entity.User;
import com.munecting.api.domain.user.entity.Uuid;
import com.munecting.api.global.aws.s3.AmazonS3Manager;
import com.munecting.api.global.error.exception.InvalidValueException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.Objects;

import static com.munecting.api.global.common.dto.response.Status.BAD_REQUEST;

@Service
@RequiredArgsConstructor
public class UserProfileImageService {

private final UuidService uuidService;
private final AmazonS3Manager s3Manager;

private static final long MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB
private static final String OVER_FILE_SIZE_ERROR_MESSAGE = "업로드 가능한 파일 사이즈는 최대 3MB입니다.";

public String updateImage(User user, MultipartFile imgFile) {
if (Objects.isNull(imgFile)) {
return getImageUrl(user);
}

if (user.getProfileImageUrl() != null) {
deleteOldImage(user);
}

String imageUrl = uploadNewImage(user.getId(), imgFile);
return user.updateProfileImageUrl(imageUrl);
}

private String getImageUrl(User user) {
if (user.getProfileImageUrl() == null) {
// 프론트엔드 측 기본 이미지 사용
return null;
}

return user.getProfileImageUrl();
}

private void deleteOldImage(User user) {
Uuid uuid = uuidService.findByUserId(user.getId());
String presentKeyName = s3Manager.generateProfileImageKeyName(uuid);
s3Manager.deleteFile(presentKeyName);
uuidService.delete(uuid);
}

private String uploadNewImage(Long userId, MultipartFile image) {
validateImageSize(image);
Uuid uuid = uuidService.createUuid(userId);
String keyName = s3Manager.generateProfileImageKeyName(uuid);

return s3Manager.uploadFile(keyName, image);
}

private void validateImageSize(MultipartFile image) {
if (image.getSize() > MAX_FILE_SIZE) {
throw new InvalidValueException(BAD_REQUEST, OVER_FILE_SIZE_ERROR_MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
import com.munecting.api.domain.like.dao.LikeRepository;
import com.munecting.api.domain.uploadedMusic.dao.UploadedMusicRepository;
import com.munecting.api.domain.user.dao.UserRepository;
import com.munecting.api.domain.user.dto.request.UpdateProfileRequestDto;
import com.munecting.api.domain.user.dto.response.UpdateProfileResponseDto;
import com.munecting.api.domain.user.entity.User;
import com.munecting.api.global.common.dto.response.Status;
import com.munecting.api.global.error.exception.EntityNotFoundException;
import com.munecting.api.global.error.exception.InvalidValueException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import static com.munecting.api.global.common.dto.response.Status.USER_NOT_FOUND;

Expand All @@ -22,6 +28,17 @@ public class UserService {
private final CommentRepository commentRepository;
private final LikeRepository likeRepository;
private final UploadedMusicRepository uploadedMusicRepository;
private final UserProfileImageService userProfileImageService;

private static final int MIN_LENGTH = 2;
private static final int MAX_LENGTH = 15;

private static final String NAME_VALUE_RULES = "^[a-zA-Z0-9가-힣]+$";

private static final String NICKNAME_LENGTH_ERROR_MESSAGE = "닉네임은 2-15자 사이여야 합니다.";
private static final String NICKNAME_WRONG_VALUE_ERROR_MESSAGE = "닉네임은 한글, 영문, 숫자만 포함할 수 있습니다.";
private static final String NICKNAME_DUPLICATED_ERROR_MESSAGE = "이미 사용 중인 닉네임입니다.";


@Transactional
public void deleteUser(Long userId) {
Expand All @@ -42,4 +59,42 @@ public void validateUserExists(Long userId) {
throw new EntityNotFoundException(USER_NOT_FOUND);
}
}

@Transactional
public UpdateProfileResponseDto updateProfile(Long userId, UpdateProfileRequestDto requestDto) {
User user = userRepository.findById(userId).orElseThrow(EntityNotFoundException::new);
String nickname = updateNickname(user, requestDto.nickname());
String profileImageUrl = updateProfileImage(user, requestDto.profileImage());

return UpdateProfileResponseDto.of(nickname, profileImageUrl);
}

private String updateNickname(User user, String nickname) {
if (!StringUtils.hasText(nickname)) {
return user.getNickname();
}

validateNickname(user, nickname);
return user.updateNickname(nickname);
}

private void validateNickname(User existingUser, String nickname) {
if (nickname.length() < MIN_LENGTH || nickname.length() > MAX_LENGTH) {
throw new InvalidValueException(Status.BAD_REQUEST, NICKNAME_LENGTH_ERROR_MESSAGE);
}

if (!nickname.matches(NAME_VALUE_RULES)) {
throw new InvalidValueException(Status.BAD_REQUEST, NICKNAME_WRONG_VALUE_ERROR_MESSAGE);
}

// 중복 검사
if (!existingUser.getNickname().equals(nickname)
&& userRepository.existsByNickname(nickname)) {
throw new InvalidValueException(Status.CONFLICT, NICKNAME_DUPLICATED_ERROR_MESSAGE);
}
}

private String updateProfileImage(User user, MultipartFile imgFile) {
return userProfileImageService.updateImage(user, imgFile);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.munecting.api.domain.user.service;

import com.munecting.api.domain.user.entity.Uuid;
import com.munecting.api.domain.user.dao.UuidRepository;
import com.munecting.api.global.error.exception.InternalServerException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class UuidService {

private final UuidRepository uuidRepository;

public Uuid createUuid(Long userId) {
Uuid uuid = Uuid.toEntity(userId);

return uuidRepository.save(uuid);
}

public Uuid findByUserId(Long userId) {
return uuidRepository.findByUserId(userId).orElseThrow(()-> {
log.warn("userId {}과 매핑된 UUID 객체가 존재하지 않습니다.",userId);
throw new InternalServerException();
});
}

public void delete(Uuid uuid) {
uuidRepository.delete(uuid);
}
}
Loading

0 comments on commit 6190baf

Please sign in to comment.