Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.chatserver.domain.member.controller;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.example.chatserver.domain.member.service.MemberService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/api/info")
@RequiredArgsConstructor
@Tag(name = "InfoChange", description = "νšŒμ› 정보 μˆ˜μ •")
public class MemberInfoController {
private final MemberService memberService; // 정보 μˆ˜μ • λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ‹΄λ‹Ή

@Operation(summary = "ν”„λ‘œν•„ 이미지 쑰회", description = "λ‘œκ·ΈμΈν•œ μœ μ €μ˜ ν”„λ‘œν•„ 이미지 URL을 μ‘°νšŒν•©λ‹ˆλ‹€.")
@GetMapping("/profile-image")
public ResponseEntity<String> getProfileImage(
@AuthenticationPrincipal UserDetails userDetails) {
String imageUrl = memberService.getProfileImage(userDetails.getUsername());
return ResponseEntity.ok(imageUrl);
}

@Operation(summary = "ν”„λ‘œν•„ 이미지 λ³€κ²½", description = "λ‘œκ·ΈμΈν•œ μœ μ €μ˜ ν”„λ‘œν•„ 이미지λ₯Ό S3에 μ—…λ‘œλ“œν•˜κ³  λ³€κ²½ν•©λ‹ˆλ‹€.")
@PostMapping(value = "/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> updateProfileImage(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam("file") MultipartFile file) {

// 1. 파일이 λΉ„μ–΄μžˆλŠ”μ§€ κΈ°λ³Έ 체크
if (file == null || file.isEmpty()) {
return ResponseEntity.badRequest().body("파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
}

// 2. userDetails.getUsername() (일반적으둜 μ΄λ©”μΌμ΄λ‚˜ PK)λ₯Ό λ„˜κ²¨μ„œ 식별
String newImageUrl = memberService.updateProfileImage(userDetails.getUsername(), file);

return ResponseEntity.ok(newImageUrl);
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.chatserver.domain.member.entity;

import com.example.chatserver.domain.profile.ProfileImage;
import com.example.chatserver.global.common.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.*;
Expand Down Expand Up @@ -27,15 +28,20 @@ public class Member extends BaseTimeEntity {
@Column(nullable = false, length = 100)
private String password;


@Column(nullable = false, unique = true, updatable = false)
private String publicId;

@OneToOne(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private ProfileImage profileImage;

@Enumerated(EnumType.STRING)
@Builder.Default
private Role role = Role.USER;


public void updateProfileImage(ProfileImage profileImage) {
this.profileImage = profileImage;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
import com.example.chatserver.domain.member.exception.AlreadyExistEmailException;
import com.example.chatserver.domain.member.exception.PasswordIncorrectException;
import com.example.chatserver.domain.member.repository.MemberRepository;
import com.example.chatserver.domain.profile.ProfileImage;
import com.example.chatserver.domain.profile.repository.ProfileImageRepository;
import com.example.chatserver.domain.profile.service.ImageStorage;
import com.example.chatserver.global.common.error.BaseException;
import com.example.chatserver.global.common.error.ErrorCode;
import com.example.chatserver.global.security.jwt.JwtUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.data.redis.core.StringRedisTemplate;
Expand All @@ -21,26 +25,23 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

private final MemberRepository memberRepository;
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
private final PasswordEncoder passwordEncoder;
private final ImageStorage imageStorage;
private final ProfileImageRepository profileImageRepository;

public MemberService(MemberRepository memberRepository, JwtUtil jwtUtil, StringRedisTemplate redisTemplate,
PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.jwtUtil = jwtUtil;
this.redisTemplate = redisTemplate;
this.passwordEncoder = passwordEncoder;
}

@Transactional
public ResponseEntity<LoginResponse> signup(SignupRequest signupRequest) {
Expand Down Expand Up @@ -153,4 +154,57 @@ public ResponseEntity<LoginResponse> reissueToken(String refreshToken) {
);
}

@Transactional
public String updateProfileImage(String publicId, MultipartFile file) {
// 1. νšŒμ› 쑰회
Member member = memberRepository.findByPublicId(publicId)
.orElseThrow(() -> new IllegalArgumentException("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));

// 2. 파일 μ €μž₯ (예: AWS S3, 둜컬 파일 μ‹œμŠ€ν…œ λ“±)
ProfileImage existingImage = member.getProfileImage();
if (existingImage != null) {
// S3 μ‚­μ œ λ‘œμ§μ€ ImageStorage μΈν„°νŽ˜μ΄μŠ€μ— delete λ©”μ„œλ“œλ₯Ό μΆ”κ°€λ‘œ κ΅¬ν˜„ μ˜ˆμ •
// imageStorage.delete("profiles/" + publicId + "/" + existingImage.getStoreFileName());
}

// 3. S3에 μƒˆ 이미지 μ—…λ‘œλ“œ (폴더 경둜: profiles/{publicId})
String directory = "profiles/" + member.getPublicId();
String imageUrl = imageStorage.upload(file, directory);

// 4. μ—”ν‹°ν‹° 생성 λ˜λŠ” μ—…λ°μ΄νŠΈ
String storeFileName = extractFileName(imageUrl);

if (existingImage == null) {
// 처음 ν”„λ‘œν•„ 이미지λ₯Ό λ“±λ‘ν•˜λŠ” 경우
ProfileImage newProfileImage = ProfileImage.builder()
.imageUrl(imageUrl)
.storeFileName(storeFileName)
.uploadFileName(file.getOriginalFilename())
.member(member)
.build();
member.updateProfileImage(newProfileImage);
profileImageRepository.save(newProfileImage);
} else {
// κΈ°μ‘΄ 이미지가 μžˆλŠ” 경우 (Dirty Checking으둜 μ—…λ°μ΄νŠΈ)
existingImage.updateImageInfo(imageUrl, storeFileName, file.getOriginalFilename());
}
return imageUrl;
}

// ν”„λ‘œν•„ 이미지 쑰회
public String getProfileImage(String publicId) {
Member member = memberRepository.findByPublicId(publicId)
.orElseThrow(() -> new IllegalArgumentException("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));

ProfileImage profileImage = member.getProfileImage();
if (profileImage == null) {
throw new IllegalArgumentException("ν”„λ‘œν•„ 이미지가 λ“±λ‘λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.");
}
return profileImage.getImageUrl();
}

// URLμ—μ„œ 파일λͺ…λ§Œ μΆ”μΆœν•˜λŠ” 편의 λ©”μ„œλ“œ
private String extractFileName(String url) {
return url.substring(url.lastIndexOf("/") + 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public class ProfileImage extends BaseTimeEntity {
@JoinColumn(name = "member_id")
private Member member;


public void updateImageInfo(String imageUrl, String storeFileName, String uploadFileName) {
this.imageUrl = imageUrl;
this.storeFileName = storeFileName;
this.uploadFileName = uploadFileName;
}

@Builder
public ProfileImage(String storeFileName, String uploadFileName, String imageUrl, Member member) {
this.storeFileName = storeFileName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.chatserver.domain.profile.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.chatserver.domain.profile.ProfileImage;

@Repository
public interface ProfileImageRepository extends JpaRepository<ProfileImage, Long> {

}