diff --git a/build.gradle b/build.gradle index f0d5ffa..54e51b0 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'mysql:mysql-connector-java:8.0.33' implementation 'org.projectlombok:lombok' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'net.coobird:thumbnailator:0.4.8' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/src/main/java/com/prgrms2/java/bitta/member/controller/MemberController.java b/src/main/java/com/prgrms2/java/bitta/member/controller/MemberController.java index fcf0c85..e221fb4 100644 --- a/src/main/java/com/prgrms2/java/bitta/member/controller/MemberController.java +++ b/src/main/java/com/prgrms2/java/bitta/member/controller/MemberController.java @@ -4,6 +4,7 @@ import com.prgrms2.java.bitta.member.dto.MemberDTO; import com.prgrms2.java.bitta.member.dto.SignInDTO; import com.prgrms2.java.bitta.member.dto.SignUpDTO; +import com.prgrms2.java.bitta.member.exception.NoChangeException; import com.prgrms2.java.bitta.member.service.MemberService; import com.prgrms2.java.bitta.security.JwtToken; import com.prgrms2.java.bitta.security.JwtTokenProvider; @@ -135,8 +136,10 @@ public ResponseEntity updateMemberById(@PathVariable Long id, MemberDTO memberDTO = objectMapper.readValue(dtoJson, MemberDTO.class); MemberDTO updatedMember = memberService.updateMember(id, memberDTO, profileImage, removeProfileImage); return ResponseEntity.ok(updatedMember); + } catch (NoChangeException e) { + return ResponseEntity.ok().body(null); } catch (IOException e) { - log.error("Failed to update member profile", e); + log.error("파일 업데이트에 실패하였습니다.", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); } } diff --git a/src/main/java/com/prgrms2/java/bitta/member/dto/MemberDTO.java b/src/main/java/com/prgrms2/java/bitta/member/dto/MemberDTO.java index 0de3b46..e420a3f 100644 --- a/src/main/java/com/prgrms2/java/bitta/member/dto/MemberDTO.java +++ b/src/main/java/com/prgrms2/java/bitta/member/dto/MemberDTO.java @@ -5,6 +5,7 @@ import lombok.*; @Getter +@Setter @ToString @AllArgsConstructor @NoArgsConstructor @@ -17,6 +18,9 @@ public class MemberDTO { @Schema(title = "아이디", description = "로그인에 사용할 회원 아이디입니다.", example = "username") private String username; + @Schema(title = "비밀번호", description = "로그인에 사용할 회원 비밀번호입니다.", example = "password") + private String password; + @Schema(title = "별명", description = "회원의 별명입니다.", example = "Nickname") private String nickname; diff --git a/src/main/java/com/prgrms2/java/bitta/member/exception/NoChangeException.java b/src/main/java/com/prgrms2/java/bitta/member/exception/NoChangeException.java new file mode 100644 index 0000000..77216b1 --- /dev/null +++ b/src/main/java/com/prgrms2/java/bitta/member/exception/NoChangeException.java @@ -0,0 +1,7 @@ +package com.prgrms2.java.bitta.member.exception; + +public class NoChangeException extends RuntimeException { + public NoChangeException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/prgrms2/java/bitta/member/service/MemberServiceImpl.java b/src/main/java/com/prgrms2/java/bitta/member/service/MemberServiceImpl.java index 295b745..88e0203 100644 --- a/src/main/java/com/prgrms2/java/bitta/member/service/MemberServiceImpl.java +++ b/src/main/java/com/prgrms2/java/bitta/member/service/MemberServiceImpl.java @@ -3,12 +3,14 @@ import com.prgrms2.java.bitta.member.dto.MemberDTO; import com.prgrms2.java.bitta.member.dto.SignUpDTO; import com.prgrms2.java.bitta.member.entity.Member; +import com.prgrms2.java.bitta.member.exception.NoChangeException; import com.prgrms2.java.bitta.member.repository.MemberRepository; import com.prgrms2.java.bitta.security.JwtToken; import com.prgrms2.java.bitta.security.JwtTokenProvider; import com.prgrms2.java.bitta.security.exception.InvalidTokenException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -20,6 +22,7 @@ import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -39,7 +42,7 @@ public class MemberServiceImpl implements MemberService{ @Value("${file.root.path}") private String fileRootPath; - + @Transactional @Override public JwtToken signIn(String username, String password) { @@ -99,8 +102,16 @@ public MemberDTO getMemberById(Long id) { .orElseThrow(() -> new IllegalArgumentException("회원 정보를 찾을 수 없습니다.")); String profile = member.getProfile() != null ? member.getProfile() : defaultProfileImg; + File thumbnailFile = getThumbnailFile(profile); + + if (thumbnailFile.exists()) { + profile = thumbnailFile.getPath(); + } + + MemberDTO memberDTO = new MemberDTO(member); + memberDTO.setProfile(profile); - return new MemberDTO(member); + return memberDTO; } @Transactional @@ -109,19 +120,56 @@ public MemberDTO updateMember(Long id, MemberDTO memberDTO, MultipartFile profil Member member = memberRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("회원 정보를 찾을 수 없습니다.")); - if (removeProfileImage || profileImage == null) { + boolean isUpdated = false; + + if (removeProfileImage) { + deleteProfileImage(member.getProfile()); member.setProfile(defaultProfileImg); - } else { + isUpdated = true; + } else if (profileImage != null && !profileImage.isEmpty()) { + deleteProfileImage(member.getProfile()); String imagePath = saveProfileImage(profileImage); + + String thumbnailPath = createThumbnail(imagePath); member.setProfile(imagePath); + isUpdated = true; } - member.setNickname(memberDTO.getNickname()); - member.setAddress(memberDTO.getAddress()); + if (memberDTO.getNickname() != null && !memberDTO.getNickname().isBlank()) { + member.setNickname(memberDTO.getNickname()); + isUpdated = true; + } + + if (memberDTO.getAddress() != null && !memberDTO.getAddress().isBlank()) { + member.setAddress(memberDTO.getAddress()); + isUpdated = true; + } + if (memberDTO.getPassword() != null && !memberDTO.getPassword().isBlank()) { + member.setPassword(passwordEncoder.encode(memberDTO.getPassword())); + isUpdated = true; + } + + if (!isUpdated) { + throw new NoChangeException("변경된 내용이 없습니다."); + } + + memberRepository.save(member); return MemberDTO.toDTO(member); } + @Transactional + @Override + public void deleteMember(Long id) { + Member member = memberRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("ID가 " + id + "인 회원을 찾을 수 없습니다.")); + memberRepository.delete(member); + } + + private File getProfileImageFile(String profileImg) { + return new File(profileImg); + } + private String saveProfileImage(MultipartFile profileImage) throws IOException { String directory = fileRootPath + "/uploads/profile_images/"; Path uploadPath = Paths.get(directory); @@ -137,21 +185,68 @@ private String saveProfileImage(MultipartFile profileImage) throws IOException { return directory + fileName; } - public void deleteProfileImage(String profileImg) { - if (!profileImg.equals(defaultProfileImg)) { - File file = new File(profileImg); - if (file.exists()) { - file.delete(); + if (profileImg != null && !profileImg.equals(defaultProfileImg)) { + File profileFile = getProfileImageFile(profileImg); + File thumbnailFile = getThumbnailFile(profileImg); + + if (profileFile.exists() && profileFile.isFile()) { + profileFile.delete(); + } + + if (thumbnailFile.exists() && thumbnailFile.isFile()) { + thumbnailFile.delete(); } } } - @Transactional - @Override - public void deleteMember(Long id) { - Member member = memberRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("ID가 " + id + "인 회원을 찾을 수 없습니다.")); - memberRepository.delete(member); + private String createThumbnail(String imagePath) throws IOException { + String thumbnailDirectory = fileRootPath + "/uploads/profile_images/thumbnail/"; + Path thumbnailPath = Paths.get(thumbnailDirectory); + + if (!Files.exists(thumbnailPath)) { + Files.createDirectories(thumbnailPath); + } + + String originalFileName = Paths.get(imagePath).getFileName().toString(); + String thumbnailFileName = "thumb_" + originalFileName; + Path thumbnailFilePath = thumbnailPath.resolve(thumbnailFileName); + + Thumbnails.of(new File(imagePath)) + .size(200, 200) + .keepAspectRatio(true) + .toFile(thumbnailFilePath.toFile()); + + return thumbnailFilePath.toString(); + } + + public void outputThumbnail(String profileImagePath, OutputStream outputStream) throws IOException { + File thumbnailFile = getThumbnailFile(profileImagePath); + if (thumbnailFile.exists() && thumbnailFile.isFile()) { + Thumbnails.of(thumbnailFile) + .size(200, 200) + .keepAspectRatio(true) + .outputFormat("jpg") + .toOutputStream(outputStream); + } else { + throw new IOException("썸네일 파일을 찾을 수 없습니다: " + thumbnailFile.getAbsolutePath()); + } + } + + private File getThumbnailFile(String profileImg) { + String thumbnailImgPath = profileImg.replace("profile_images", "profile_images/thumbnail"); + String thumbnailFileName = "thumb_" + Paths.get(profileImg).getFileName().toString(); + return new File(thumbnailImgPath.replace(Paths.get(profileImg).getFileName().toString(), thumbnailFileName)); + } + + private Path getThumbnailDirectory() throws IOException { + String thumbnailDirectory = fileRootPath + "/uploads/profile_images/thumbnail/"; + Path thumbnailPath = Paths.get(thumbnailDirectory); + + if (!Files.exists(thumbnailPath)) { + Files.createDirectories(thumbnailPath); + } + + return thumbnailPath; } -} +} \ No newline at end of file diff --git a/src/main/resources/test.images/test1-avatar.png b/src/main/resources/test.images/test1-avatar.png new file mode 100644 index 0000000..a14bafc Binary files /dev/null and b/src/main/resources/test.images/test1-avatar.png differ diff --git a/src/main/resources/test.images/test2-avatar.png b/src/main/resources/test.images/test2-avatar.png new file mode 100644 index 0000000..b527288 Binary files /dev/null and b/src/main/resources/test.images/test2-avatar.png differ diff --git a/src/main/resources/test.images/test3_avatar.png b/src/main/resources/test.images/test3_avatar.png new file mode 100644 index 0000000..f6fd328 Binary files /dev/null and b/src/main/resources/test.images/test3_avatar.png differ diff --git a/src/main/resources/test.images/test4_avatar.png b/src/main/resources/test.images/test4_avatar.png new file mode 100644 index 0000000..65e37ca Binary files /dev/null and b/src/main/resources/test.images/test4_avatar.png differ