Skip to content
Merged
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
@@ -1,8 +1,12 @@
package com.meetkey.server.domain.member.controller;

import com.meetkey.server.domain.member.dto.ProfileReqDTO;
import com.meetkey.server.domain.member.dto.ProfileResDTO;
import com.meetkey.server.domain.member.entity.mapping.MemberPhoto;
import com.meetkey.server.domain.member.service.ProfileService;
import com.meetkey.server.global.apiPayload.response.BasicResponse;
import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus;
import com.meetkey.server.global.s3.S3Service;
import com.meetkey.server.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -16,6 +20,9 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

import static com.meetkey.server.domain.member.dto.ProfileReqDTO.*;
import static com.meetkey.server.domain.member.dto.ProfileResDTO.*;

Expand All @@ -26,6 +33,7 @@
public class ProfileController {

private final ProfileService profileService;
private final S3Service s3Service;

@Operation(summary = "프로필 정보 수정 API", description = "사용자의 활동 지역(문자열), 한줄 소개, 언어 정보를 변경합니다.")
@ApiResponses(value = {
Expand Down Expand Up @@ -190,5 +198,43 @@ public ResponseEntity<BasicResponse<String>> toggleEvaluation(
.ok()
.body(BasicResponse.success(CommonSuccessStatus._OK, "평가가 반영되었습니다."));
}

@Operation(summary = "업로드용 Presigned Url 발급 API", description = "프론트에서 파일 업로드 전에 요청")
@PostMapping("/photos")
public BasicResponse<List<ProfileResDTO.MemberPhotoUrl>> getMemberPhotoUploadUrl(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@RequestBody List<ProfileReqDTO.PhotoInfo> photoInfos
){
Long memberId = customUserDetails.getMemberId();

List<ProfileResDTO.MemberPhotoUrl> responses = photoInfos.stream()
.map(info -> s3Service.generateMemberPhotoPresignedUrl(memberId, info.fileName(), info.contentType()))
.collect(Collectors.toList());

return BasicResponse.success(CommonSuccessStatus._OK, responses);
}

@PostMapping("/photos/register")
public BasicResponse<Void> registerMemberPhotos(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@RequestBody List<String> s3Keys // 프론트가 업로드 성공 후 보낸 key 리스트
) {
// 2. 새로운 s3Keys들을 MemberPhoto 엔티티로 만들어 저장
Long memberId = customUserDetails.getMemberId();
s3Service.registerMemberPhotoKeys(memberId, s3Keys);

return BasicResponse.success(CommonSuccessStatus._OK, null);
}

@Operation(summary = "내 프로필 사진 조회 API", description = "로그인한 사용자의 프로필 사진 URL 리스트를 가져옵니다.")
@GetMapping("/photos")
public BasicResponse<List<String>> getMyPhotos(
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
Long memberId = customUserDetails.getMemberId();
List<String> photoUrls = s3Service.getMemberPhotoUrls(memberId);

return BasicResponse.success(CommonSuccessStatus._OK, photoUrls);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,9 @@ public record LocationUpdateRequest(
@Schema(description = "경도", example = "126.9410")
Double longitude
) {}

public record PhotoInfo(
String fileName,
String contentType
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,10 @@ public record OtherProfileResponse(
String bio
) {}

@Builder
public record MemberPhotoUrl(
String url,
String key
){}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum MemberErrorStatus implements BaseCode {

// 회원 관련
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER4041", "해당 사용자를 찾을 수 없습니다."),
INVALID_S3_KEY(HttpStatus.NOT_FOUND, "MEMBER4042", "저장된 프로필 사진을 찾을 수 없습니다.");
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.meetkey.server.domain.member.repository;

import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.member.entity.mapping.MemberPhoto;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemberPhotoRepository extends JpaRepository<MemberPhoto, Long> {
List<MemberPhoto> findAllByMember(Member member);
}
31 changes: 0 additions & 31 deletions src/main/java/com/meetkey/server/global/s3/S3Controller.java

This file was deleted.

11 changes: 0 additions & 11 deletions src/main/java/com/meetkey/server/global/s3/S3ResDTO.java

This file was deleted.

64 changes: 53 additions & 11 deletions src/main/java/com/meetkey/server/global/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.meetkey.server.global.s3;

import com.meetkey.server.domain.member.dto.ProfileResDTO;
import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.member.entity.mapping.MemberPhoto;
import com.meetkey.server.domain.member.exception.MemberErrorStatus;
import com.meetkey.server.domain.member.exception.MemberException;
import com.meetkey.server.domain.member.repository.MemberPhotoRepository;
import com.meetkey.server.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
Expand All @@ -15,20 +23,29 @@
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class S3Service {
private final S3Presigner s3Presigner;
private final S3Client s3Client;
private final MemberRepository memberRepository;
private final MemberPhotoRepository memberPhotoRepository;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

// S3에 이미지 업로드용 Presigned url 발급
public S3ResDTO.PresignedUrl generateUploadPresignedUrl(String folder, String originalFileName, String contentType){
String key = folder + "/" + UUID.randomUUID() +"-" + originalFileName;
public ProfileResDTO.MemberPhotoUrl generateMemberPhotoPresignedUrl(
Long memberId,
String originalFileName,
String contentType
){
// 유저별 폴더 구조 생성 : profiles/{memberId}/{UUID}-{fileName}
String key = "profiles/" + memberId + "/" + UUID.randomUUID() + "-" + originalFileName;

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
Expand All @@ -41,9 +58,10 @@ public S3ResDTO.PresignedUrl generateUploadPresignedUrl(String folder, String or
.signatureDuration(Duration.ofMinutes(5))
);

return S3ResDTO.PresignedUrl.builder()
return ProfileResDTO.MemberPhotoUrl.builder()
.url(presignedPutObjectRequest.url().toString())
.key(key).build();
.key(key)
.build();
}

// S3 이미지 조회용 Presigned url 발급
Expand All @@ -63,19 +81,43 @@ public String generateGetPresignedUrl(String key){
}

// s3에서 이미지 삭제
public void deleteFile(String imageUrl){
String key = extractKeyFromUrl(imageUrl);

public void deleteFile(String key){
s3Client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucket)
.key(key)
.build()
);
}

// s3 url에서 key 추출
private String extractKeyFromUrl(String imageUrl){
int idx = imageUrl.indexOf(".amazonaws.com/") + ".amazonaws.com/".length();
return imageUrl.substring(idx);
@Transactional
public void registerMemberPhotoKeys(Long memberId, List<String> s3Keys){
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));

List<MemberPhoto> oldPhotos = memberPhotoRepository.findAllByMember(member);

if (!oldPhotos.isEmpty()) {
oldPhotos.forEach(photo -> deleteFile(photo.getMemberPhotoUrl()));
memberPhotoRepository.deleteAllInBatch(oldPhotos);
}

List<MemberPhoto> photos = s3Keys.stream()
.map(key -> MemberPhoto.builder()
.member(member)
.memberPhotoUrl(key)
.build())
.collect(Collectors.toList());

memberPhotoRepository.saveAll(photos);
}


public List<String> getMemberPhotoUrls(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));

return memberPhotoRepository.findAllByMember(member).stream()
.map(photo -> generateGetPresignedUrl(photo.getMemberPhotoUrl()))
.collect(Collectors.toList());
}
}