diff --git a/src/main/java/com/umc/naoman/domain/agenda/repository/AgendaPhotoRepository.java b/src/main/java/com/umc/naoman/domain/agenda/repository/AgendaPhotoRepository.java index 0c1ac74d..47e518f8 100644 --- a/src/main/java/com/umc/naoman/domain/agenda/repository/AgendaPhotoRepository.java +++ b/src/main/java/com/umc/naoman/domain/agenda/repository/AgendaPhotoRepository.java @@ -9,4 +9,5 @@ @Repository public interface AgendaPhotoRepository extends JpaRepository { List findByAgendaId(Long agendaId); + List findByPhotoId(Long photoId); } diff --git a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoService.java b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoService.java index 219caa7c..74ec9690 100644 --- a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoService.java +++ b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoService.java @@ -10,4 +10,5 @@ public interface AgendaPhotoService { AgendaPhoto findAgendaPhoto(Long agendaPhotoId); List findAgendaPhotoList(Long agendaId); + List findAgendaPhotoListByPhotoId(Long photoId); } diff --git a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoServiceImpl.java b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoServiceImpl.java index 4f28b6c5..8ad4211f 100644 --- a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaPhotoServiceImpl.java @@ -41,4 +41,9 @@ public AgendaPhoto findAgendaPhoto(Long agendaPhotoId) { public List findAgendaPhotoList(Long agendaId) { return agendaPhotoRepository.findByAgendaId(agendaId); } + + @Override + public List findAgendaPhotoListByPhotoId(Long photoId) { + return agendaPhotoRepository.findByPhotoId(photoId); + } } diff --git a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaService.java b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaService.java index fa345cb9..79beeb31 100644 --- a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaService.java +++ b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaService.java @@ -6,6 +6,8 @@ import com.umc.naoman.domain.member.entity.Member; import org.springframework.data.domain.Pageable; +import java.util.List; + public interface AgendaService { Agenda createAgenda(Member member, AgendaRequest.CreateAgendaRequest request); Agenda getAgendaDetailInfo(Long agendaId, Member member); @@ -13,4 +15,5 @@ public interface AgendaService { Agenda deleteAgenda(Long agendaId); Agenda findAgenda(Long agendaId); + List findAgendaListByPhotoId(Long photoId); } diff --git a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaServiceImpl.java b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaServiceImpl.java index 4019aee0..6bc386e3 100644 --- a/src/main/java/com/umc/naoman/domain/agenda/service/AgendaServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/agenda/service/AgendaServiceImpl.java @@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; @Service @Transactional(readOnly = true) @@ -76,4 +77,13 @@ public Agenda deleteAgenda(Long agendaId) { agenda.delete(); return agenda; } + + // 특정 사진이 안건 후보로 담겨 있는 안건 목록을 조회하는 함수 + @Override + public List findAgendaListByPhotoId(Long photoId) { + List agendaPhotoList = agendaPhotoService.findAgendaPhotoListByPhotoId(photoId); + return agendaPhotoList.stream() + .map(agendaPhoto -> agendaPhoto.getAgenda()) + .collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/main/java/com/umc/naoman/domain/member/controller/MemberController.java b/src/main/java/com/umc/naoman/domain/member/controller/MemberController.java index e3090902..b9959c0c 100644 --- a/src/main/java/com/umc/naoman/domain/member/controller/MemberController.java +++ b/src/main/java/com/umc/naoman/domain/member/controller/MemberController.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -31,13 +32,11 @@ public class MemberController { private final MemberConverter memberConverter; @GetMapping("/{memberId}") // memberId를 사용해 특정 회원 정보 조회 - @Operation( summary = "특정 회원 정보 조회 API", description = "[PathVariable]\n memberId\n[request]\n" + + @Operation(summary = "특정 회원 정보 조회 API", description = "[PathVariable]\n memberId\n[request]\n" + "[response]\n uesrname, email, 소셜 프로필 이미지 url") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse - (responseCode = "SM005", description = "특정 회원 정보 조회 성공."), - @io.swagger.v3.oas.annotations.responses.ApiResponse - (responseCode = "EM001", description = "해당 memberId를 가진 회원이 존재하지 않습니다.", + @ApiResponse(responseCode = "SM005", description = "특정 회원 정보 조회 성공."), + @ApiResponse(responseCode = "EM001", description = "해당 memberId를 가진 회원이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) public ResultResponse getMemberInfo(@PathVariable(name = "memberId") Long memberId) { @@ -55,11 +54,9 @@ public ResultResponse getMyInfo(@LoginMember Member member) { @GetMapping("/terms/{memberId}") @Operation(summary = "마케팅 약관 동의 여부 조회 API", description = "[PathVariable]\n memberId\n[request]\n" + "[response]\n 마케팅 동의 여부 -> 동의 => true, 비동의 => false") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse - (responseCode = "SM006", description = "마케팅 약관 동의 여부 조회 성공."), - @io.swagger.v3.oas.annotations.responses.ApiResponse - (responseCode = "EM001", description = "해당 memberId를 가진 회원이 존재하지 않습니다.", + @ApiResponses(value = { + @ApiResponse(responseCode = "SM006", description = "마케팅 약관 동의 여부 조회 성공."), + @ApiResponse(responseCode = "EM001", description = "해당 memberId를 가진 회원이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), }) public ResultResponse getMarketingAgreed(@PathVariable(name = "memberId") Long memberId) { diff --git a/src/main/java/com/umc/naoman/domain/photo/controller/PhotoController.java b/src/main/java/com/umc/naoman/domain/photo/controller/PhotoController.java index 004a5d9e..fe20b32e 100644 --- a/src/main/java/com/umc/naoman/domain/photo/controller/PhotoController.java +++ b/src/main/java/com/umc/naoman/domain/photo/controller/PhotoController.java @@ -2,8 +2,17 @@ import com.umc.naoman.domain.member.entity.Member; import com.umc.naoman.domain.photo.converter.PhotoConverter; -import com.umc.naoman.domain.photo.dto.PhotoRequest; -import com.umc.naoman.domain.photo.dto.PhotoResponse; +import com.umc.naoman.domain.photo.dto.PhotoRequest.PhotoDeletedRequest; +import com.umc.naoman.domain.photo.dto.PhotoRequest.PhotoUploadRequest; +import com.umc.naoman.domain.photo.dto.PhotoRequest.PreSignedUrlRequest; +import com.umc.naoman.domain.photo.dto.PhotoRequest.UploadSamplePhotoRequest; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PagedPhotoInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDeleteInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDownloadUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoUploadInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.SamplePhotoUploadInfo; import com.umc.naoman.domain.photo.elasticsearch.document.PhotoEs; import com.umc.naoman.domain.photo.elasticsearch.service.PhotoEsService; import com.umc.naoman.domain.photo.entity.Photo; @@ -44,17 +53,24 @@ public class PhotoController { @PostMapping("/preSignedUrl") @Operation(summary = "Presigned URL 요청 API", description = "Presigned URL을 요청하는 API입니다.") - public ResultResponse getPreSignedUrlList(@Valid @RequestBody PhotoRequest.PreSignedUrlRequest request, - @LoginMember Member member) { - List preSignedUrlList = photoService.getPreSignedUrlList(request, member); + public ResultResponse getPreSignedUrlList(@Valid @RequestBody PreSignedUrlRequest request, + @LoginMember Member member) { + List preSignedUrlList = photoService.getPreSignedUrlList(request, member); return ResultResponse.of(CREATE_PRESIGNED_URL, photoConverter.toPreSignedUrlListInfo(preSignedUrlList)); } + @PostMapping("/sample") + @Operation(summary = "샘플 사진 업로드 API", description = "얼굴 분류에 사용할 샘플 사진을 업로드하는 API입니다.") + public ResultResponse uploadSamplePhotoList(@Valid @RequestBody UploadSamplePhotoRequest request, + @LoginMember Member member) { + return ResultResponse.of(UPLOAD_SAMPLE_PHOTO, photoService.uploadSamplePhotoList(request, member)); + } + @PostMapping("/upload") @Operation(summary = "사진 업로드 API", description = "Presigned URL을 통해 업로드한 사진을 데이터베이스에 저장하는 API입니다.") - public ResultResponse uploadPhotoList(@Valid @RequestBody PhotoRequest.PhotoUploadRequest request, - @LoginMember Member member) { - PhotoResponse.PhotoUploadInfo photoUploadInfo = photoService.uploadPhotoList(request, member); + public ResultResponse uploadPhotoList(@Valid @RequestBody PhotoUploadRequest request, + @LoginMember Member member) { + PhotoUploadInfo photoUploadInfo = photoService.uploadPhotoList(request, member); return ResultResponse.of(UPLOAD_PHOTO, photoUploadInfo); } @@ -66,13 +82,13 @@ public ResultResponse uploadPhotoList(@Valid @Req @Parameter(name = "page", description = "조회할 페이지를 입력해 주세요.(0번부터 시작)"), @Parameter(name = "size", description = "한 페이지에 나타낼 사진 개수를 입력해주세요.") }) - public ResultResponse getPhotoListByShareGroupAndProfile(@RequestParam Long shareGroupId, - @RequestParam Long profileId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) - @Parameter(hidden = true) Pageable pageable, - @LoginMember Member member) { + public ResultResponse getPhotoListByShareGroupAndProfile(@RequestParam Long shareGroupId, + @RequestParam Long profileId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) + @Parameter(hidden = true) Pageable pageable, + @LoginMember Member member) { Page photoEsList = photoEsService.getPhotoEsListByShareGroupIdAndFaceTag(shareGroupId, profileId, member, pageable); - return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoEsInfo(photoEsList, member)); + return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoInfo(photoEsList, member)); } @GetMapping("/all") @@ -82,12 +98,12 @@ public ResultResponse getPhotoListByShareGroupAn @Parameter(name = "page", description = "조회할 페이지를 입력해 주세요.(0번부터 시작)"), @Parameter(name = "size", description = "한 페이지에 나타낼 사진 개수를 입력해주세요.") }) - public ResultResponse getAllPhotoListByShareGroup(@RequestParam Long shareGroupId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) - @Parameter(hidden = true) Pageable pageable, - @LoginMember Member member) { + public ResultResponse getAllPhotoListByShareGroup(@RequestParam Long shareGroupId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) + @Parameter(hidden = true) Pageable pageable, + @LoginMember Member member) { Page photoEsList = photoEsService.getAllPhotoEsListByShareGroupId(shareGroupId, member, pageable); - return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoEsInfo(photoEsList, member)); + return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoInfo(photoEsList, member)); } @GetMapping("/etc") @@ -97,27 +113,28 @@ public ResultResponse getAllPhotoListByShareGrou @Parameter(name = "page", description = "조회할 페이지를 입력해 주세요.(0번부터 시작)"), @Parameter(name = "size", description = "한 페이지에 나타낼 사진 개수를 입력해주세요.") }) - public ResultResponse getEtcPhotoListByShareGroup(@RequestParam Long shareGroupId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) - @Parameter(hidden = true) Pageable pageable, - @LoginMember Member member) { + public ResultResponse getEtcPhotoListByShareGroup(@RequestParam Long shareGroupId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) + @Parameter(hidden = true) Pageable pageable, + @LoginMember Member member) { Page photoEsList = photoEsService.getEtcPhotoEsListByShareGroupId(shareGroupId, member, pageable); - return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoEsInfo(photoEsList, member)); + return ResultResponse.of(RETRIEVE_PHOTO, photoConverter.toPagedPhotoInfo(photoEsList, member)); + } + + @GetMapping("/download") + @Operation(summary = "사진 다운로드 API", description = "여러장의 사진을 다운로드할 주소를 받는 API입니다. 해당 공유그룹에 속해있는 회원만 다운로드 요청할 수 있습니다.") + public ResultResponse getPhotoDownloadUrlList(@RequestParam List photoIdList, + @RequestParam Long shareGroupId, + @LoginMember Member member) { + PhotoDownloadUrlListInfo photoDownloadUrlList = photoService.getPhotoDownloadUrlList(photoIdList, shareGroupId, member); + return ResultResponse.of(DOWNLOAD_PHOTO, photoDownloadUrlList); } @DeleteMapping @Operation(summary = "사진 삭제 API", description = "사진을 삭제하는 API입니다. 해당 공유그룹에 속해있는 회원만 삭제할 수 있습니다.") - public ResultResponse deletePhotoList(@Valid @RequestBody PhotoRequest.PhotoDeletedRequest request, - @LoginMember Member member) { + public ResultResponse deletePhotoList(@Valid @RequestBody PhotoDeletedRequest request, + @LoginMember Member member) { List photoList = photoService.deletePhotoList(request, member); return ResultResponse.of(DELETE_PHOTO, photoConverter.toPhotoDeleteInfo(photoList)); } - - @GetMapping("/download") - @Operation(summary = "사진 다운로드 API", description = "여러장의 사진을 다운로드할 주소를 받는 API입니다. 해당 공유그룹에 속해있는 회원만 다운로드 요청할 수 있습니다.") - public ResultResponse getPhotoDownloadUrlList(@RequestParam List photoIdList, @RequestParam Long shareGroupId, - @LoginMember Member member) { - PhotoResponse.PhotoDownloadUrlListInfo photoDownloadUrlList = photoService.getPhotoDownloadUrlList(photoIdList, shareGroupId, member); - return ResultResponse.of(DOWNLOAD_PHOTO, photoDownloadUrlList); - } } diff --git a/src/main/java/com/umc/naoman/domain/photo/converter/PhotoConverter.java b/src/main/java/com/umc/naoman/domain/photo/converter/PhotoConverter.java index 7328d0d6..058d4157 100644 --- a/src/main/java/com/umc/naoman/domain/photo/converter/PhotoConverter.java +++ b/src/main/java/com/umc/naoman/domain/photo/converter/PhotoConverter.java @@ -1,7 +1,14 @@ package com.umc.naoman.domain.photo.converter; import com.umc.naoman.domain.member.entity.Member; -import com.umc.naoman.domain.photo.dto.PhotoResponse; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PagedPhotoInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDeleteInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDownloadUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoUploadInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.SamplePhotoUploadInfo; import com.umc.naoman.domain.photo.elasticsearch.document.PhotoEs; import com.umc.naoman.domain.photo.entity.Photo; import com.umc.naoman.domain.shareGroup.entity.ShareGroup; @@ -17,9 +24,16 @@ @Component public class PhotoConverter { + public Photo toEntity(String photoUrl, String photoName, ShareGroup shareGroup) { + return Photo.builder() + .url(photoUrl) + .name(photoName) + .shareGroup(shareGroup) + .build(); + } - public PhotoResponse.PreSignedUrlListInfo toPreSignedUrlListInfo(List preSignedUrlList) { - List preSignedUrlInfoList = preSignedUrlList.stream() + public PreSignedUrlListInfo toPreSignedUrlListInfo(List preSignedUrlList) { + List preSignedUrlInfoList = preSignedUrlList.stream() .map(preSignedUrlInfo -> toPreSignedUrlInfo( preSignedUrlInfo.getPreSignedUrl(), preSignedUrlInfo.getPhotoUrl(), @@ -27,48 +41,47 @@ public PhotoResponse.PreSignedUrlListInfo toPreSignedUrlListInfo(List photoEsList, Member member) { - List photoEsInfoList = photoEsList.stream() - .map(photoEs -> toPhotoEsInfo(photoEs, member)) + public PagedPhotoInfo toPagedPhotoInfo(Page photoEsList, Member member) { + List photoInfoList = photoEsList.stream() + .map(photoEs -> toPhotoInfo(photoEs, member)) .collect(Collectors.toList()); - return PhotoResponse.PagedPhotoEsInfo.builder() + return PagedPhotoInfo.builder() .isLast(photoEsList.isLast()) .isFirst(photoEsList.isFirst()) .totalPages(photoEsList.getTotalPages()) .totalElements(photoEsList.getTotalElements()) - .photoEsInfoList(photoEsInfoList) + .photoInfoList(photoInfoList) .build(); } - public PhotoResponse.PhotoEsInfo toPhotoEsInfo(PhotoEs photoEs, Member member) { + public PhotoInfo toPhotoInfo(PhotoEs photoEs, Member member) { String rawUrl = photoEs.getUrl(); Boolean isDownload = !photoEs.getDownloadTag().isEmpty() && photoEs.getDownloadTag().contains(member.getId()); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime createdAt = LocalDateTime.parse(photoEs.getCreatedAt(), dateTimeFormatter); - return PhotoResponse.PhotoEsInfo.builder() + return PhotoInfo.builder() .photoId(photoEs.getRdsId()) .rawPhotoUrl(rawUrl) .w200PhotoUrl(createResizedPhotoUrl(rawUrl, W200_PATH_PREFIX)) @@ -93,22 +106,22 @@ public String convertExtension(String photoUrl) { return photoUrl.replace(".HEIC", ".jpg"); } - public PhotoResponse.PhotoDeleteInfo toPhotoDeleteInfo(List photoList) { + public PhotoDeleteInfo toPhotoDeleteInfo(List photoList) { List photoIdList = photoList.stream() .map(Photo::getId) .collect(Collectors.toList()); - return PhotoResponse.PhotoDeleteInfo.builder() + return PhotoDeleteInfo.builder() .photoIdList(photoIdList) .build(); } - public PhotoResponse.PhotoDownloadUrlListInfo toPhotoDownloadUrlListInfo(List photoList) { + public PhotoDownloadUrlListInfo toPhotoDownloadUrlListInfo(List photoList) { List photoDownloadUrlList = photoList.stream() .map(Photo::getUrl) .collect(Collectors.toList()); - return PhotoResponse.PhotoDownloadUrlListInfo.builder() + return PhotoDownloadUrlListInfo.builder() .photoDownloadUrlList(photoDownloadUrlList) .build(); } diff --git a/src/main/java/com/umc/naoman/domain/photo/converter/SamplePhotoConverter.java b/src/main/java/com/umc/naoman/domain/photo/converter/SamplePhotoConverter.java new file mode 100644 index 00000000..adc3994f --- /dev/null +++ b/src/main/java/com/umc/naoman/domain/photo/converter/SamplePhotoConverter.java @@ -0,0 +1,24 @@ +package com.umc.naoman.domain.photo.converter; + +import com.umc.naoman.domain.member.entity.Member; +import com.umc.naoman.domain.photo.dto.PhotoResponse; +import com.umc.naoman.domain.photo.entity.SamplePhoto; +import org.springframework.stereotype.Component; + +@Component +public class SamplePhotoConverter { + public SamplePhoto toEntity(String photoUrl, String photoName, Member member) { + return SamplePhoto.builder() + .url(photoUrl) + .name(photoName) + .member(member) + .build(); + } + + public PhotoResponse.SamplePhotoUploadInfo toSamplePhotoUploadInfo(Long memberId, int uploadCount) { + return PhotoResponse.SamplePhotoUploadInfo.builder() + .memberId(memberId) + .uploadCount(uploadCount) + .build(); + } +} diff --git a/src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java b/src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java index 65d43e47..3fa2f630 100644 --- a/src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java +++ b/src/main/java/com/umc/naoman/domain/photo/dto/PhotoRequest.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -24,6 +25,15 @@ public static class PreSignedUrlRequest { } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class UploadSamplePhotoRequest { + @Size(min = 2, message = "샘플 사진의 개수는 최소 2개 이상이어야 합니다.") + private List photoUrlList; + } + @Getter @Builder @NoArgsConstructor diff --git a/src/main/java/com/umc/naoman/domain/photo/dto/PhotoResponse.java b/src/main/java/com/umc/naoman/domain/photo/dto/PhotoResponse.java index 7174ce0f..945846ca 100644 --- a/src/main/java/com/umc/naoman/domain/photo/dto/PhotoResponse.java +++ b/src/main/java/com/umc/naoman/domain/photo/dto/PhotoResponse.java @@ -42,8 +42,17 @@ public static class PhotoUploadInfo { @Builder @AllArgsConstructor @NoArgsConstructor - public static class PagedPhotoEsInfo { - private List photoEsInfoList; + public static class SamplePhotoUploadInfo { + private Long memberId; + private int uploadCount; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class PagedPhotoInfo { + private List photoInfoList; private Integer totalPages; private Long totalElements; private Boolean isFirst; @@ -54,7 +63,7 @@ public static class PagedPhotoEsInfo { @Builder @AllArgsConstructor @NoArgsConstructor - public static class PhotoEsInfo { + public static class PhotoInfo { private Long photoId; private String rawPhotoUrl; private String w200PhotoUrl; diff --git a/src/main/java/com/umc/naoman/domain/photo/repository/SamplePhotoRepository.java b/src/main/java/com/umc/naoman/domain/photo/repository/SamplePhotoRepository.java new file mode 100644 index 00000000..c335c51f --- /dev/null +++ b/src/main/java/com/umc/naoman/domain/photo/repository/SamplePhotoRepository.java @@ -0,0 +1,9 @@ +package com.umc.naoman.domain.photo.repository; + +import com.umc.naoman.domain.photo.entity.SamplePhoto; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SamplePhotoRepository extends JpaRepository { +} diff --git a/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionService.java b/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionService.java index 0d66aaf3..8303b27a 100644 --- a/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionService.java +++ b/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionService.java @@ -4,6 +4,7 @@ public interface FaceDetectionService { - void detectFaceUploadPhoto(List photoNameList, Long shareGroupId); + void detectFaceUploadPhoto(List photoNameList, Long shareGroupId, List memberIdList); void detectFaceJoinShareGroup(Long memberId, Long shareGroupId); + void detectFaceSamplePhoto(Long memberId, List photoNameList); } diff --git a/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionServiceImpl.java b/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionServiceImpl.java index 70508e51..8783d778 100644 --- a/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/photo/service/FaceDetectionServiceImpl.java @@ -1,18 +1,19 @@ package com.umc.naoman.domain.photo.service; + import com.amazonaws.services.lambda.AWSLambda; import com.amazonaws.services.lambda.model.InvocationType; import com.amazonaws.services.lambda.model.InvokeRequest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.umc.naoman.domain.shareGroup.service.ShareGroupService; import com.umc.naoman.global.error.BusinessException; import com.umc.naoman.global.error.code.AwsLambdaErrorCode; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -21,9 +22,10 @@ public class FaceDetectionServiceImpl implements FaceDetectionService { private String detectFaceUploadPhotoLambda; @Value("${spring.lambda.function.detect_face_join_share_group}") private String detectFaceJoinShareGroupLambda; + @Value("${spring.lambda.function.detect_face_sample_photo}") + private String detectFaceSamplePhotoLambda; private final AWSLambda awsLambda; private final ObjectMapper objectMapper = new ObjectMapper(); - private final ShareGroupService shareGroupService; @Getter @AllArgsConstructor @@ -40,11 +42,15 @@ private class DetectFaceShareGroupPayload { private Long shareGroupId; } + @Getter + @AllArgsConstructor + private class DetectFaceSamplePhotoPayload { + private long memberId; + private List photoNameList; + } + @Override - public void detectFaceUploadPhoto(List photoNameList, Long shareGroupId) { - List memberIdList = shareGroupService.findProfileListByShareGroupId(shareGroupId).stream() - .map(profile -> profile.getMember().getId()) - .collect(Collectors.toList()); + public void detectFaceUploadPhoto(List photoNameList, Long shareGroupId, List memberIdList) { DetectFacePhotoPayload payLoad = new DetectFacePhotoPayload(photoNameList, memberIdList, shareGroupId); String lambdaPayload = null; @@ -78,4 +84,21 @@ public void detectFaceJoinShareGroup(Long memberId, Long shareGroupId) { awsLambda.invoke(invokeRequest); } + + @Override + public void detectFaceSamplePhoto(Long memberId, List photoNameList) { + DetectFaceSamplePhotoPayload photoPayload = new DetectFaceSamplePhotoPayload(memberId,photoNameList); + String lambdaPayload = null; + try{ + lambdaPayload = objectMapper.writeValueAsString(photoPayload); + } catch (JsonProcessingException e) { + throw new BusinessException(AwsLambdaErrorCode.AWS_JsonProcessing_Exception, e); + } + InvokeRequest invokeRequest = new InvokeRequest() + .withInvocationType(InvocationType.Event) //비동기 호출 + .withFunctionName(detectFaceSamplePhotoLambda) + .withPayload(lambdaPayload); + + awsLambda.invoke(invokeRequest); + } } diff --git a/src/main/java/com/umc/naoman/domain/photo/service/PhotoService.java b/src/main/java/com/umc/naoman/domain/photo/service/PhotoService.java index b15f036b..befb0193 100644 --- a/src/main/java/com/umc/naoman/domain/photo/service/PhotoService.java +++ b/src/main/java/com/umc/naoman/domain/photo/service/PhotoService.java @@ -2,15 +2,21 @@ import com.umc.naoman.domain.member.entity.Member; import com.umc.naoman.domain.photo.dto.PhotoRequest; -import com.umc.naoman.domain.photo.dto.PhotoResponse; +import com.umc.naoman.domain.photo.dto.PhotoRequest.UploadSamplePhotoRequest; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDownloadUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoUploadInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.SamplePhotoUploadInfo; import com.umc.naoman.domain.photo.entity.Photo; import java.util.List; public interface PhotoService { - List getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member); - PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request, Member member); + List getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member); + SamplePhotoUploadInfo uploadSamplePhotoList(UploadSamplePhotoRequest request, Member member); + PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request, Member member); + PhotoDownloadUrlListInfo getPhotoDownloadUrlList(List photoIdList, Long shareGroupId, Member member); List deletePhotoList(PhotoRequest.PhotoDeletedRequest request, Member member); + Photo findPhoto(Long photoId); - PhotoResponse.PhotoDownloadUrlListInfo getPhotoDownloadUrlList(List photoIdList, Long shareGroupId, Member member); } diff --git a/src/main/java/com/umc/naoman/domain/photo/service/PhotoServiceImpl.java b/src/main/java/com/umc/naoman/domain/photo/service/PhotoServiceImpl.java index 0fec79ba..f2277d3d 100644 --- a/src/main/java/com/umc/naoman/domain/photo/service/PhotoServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/photo/service/PhotoServiceImpl.java @@ -7,11 +7,19 @@ import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.umc.naoman.domain.member.entity.Member; import com.umc.naoman.domain.photo.converter.PhotoConverter; +import com.umc.naoman.domain.photo.converter.SamplePhotoConverter; import com.umc.naoman.domain.photo.dto.PhotoRequest; -import com.umc.naoman.domain.photo.dto.PhotoResponse; +import com.umc.naoman.domain.photo.dto.PhotoRequest.PhotoUploadRequest; +import com.umc.naoman.domain.photo.dto.PhotoRequest.UploadSamplePhotoRequest; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoDownloadUrlListInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PhotoUploadInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.PreSignedUrlInfo; +import com.umc.naoman.domain.photo.dto.PhotoResponse.SamplePhotoUploadInfo; import com.umc.naoman.domain.photo.elasticsearch.repository.PhotoEsClientRepository; import com.umc.naoman.domain.photo.entity.Photo; +import com.umc.naoman.domain.photo.entity.SamplePhoto; import com.umc.naoman.domain.photo.repository.PhotoRepository; +import com.umc.naoman.domain.photo.repository.SamplePhotoRepository; import com.umc.naoman.domain.shareGroup.entity.ShareGroup; import com.umc.naoman.domain.shareGroup.service.ShareGroupService; import com.umc.naoman.global.error.BusinessException; @@ -27,19 +35,22 @@ import java.util.UUID; import java.util.stream.Collectors; -import static com.umc.naoman.global.error.code.S3ErrorCode.*; +import static com.umc.naoman.global.error.code.S3ErrorCode.PHOTO_NOT_FOUND; +import static com.umc.naoman.global.error.code.S3ErrorCode.PHOTO_NOT_FOUND_S3; @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class PhotoServiceImpl implements PhotoService { - private final AmazonS3 amazonS3; - private final S3Template s3Template; - private final PhotoRepository photoRepository; private final ShareGroupService shareGroupService; + private final FaceDetectionService faceDetectionService; + private final PhotoRepository photoRepository; + private final SamplePhotoRepository samplePhotoRepository; private final PhotoEsClientRepository photoEsClientRepository; private final PhotoConverter photoConverter; - private final FaceDetectionService faceDetectionService; + private final SamplePhotoConverter samplePhotoConverter; + private final AmazonS3 amazonS3; + private final S3Template s3Template; @Value("${spring.cloud.aws.s3.bucket}") private String bucketName; @@ -50,14 +61,9 @@ public class PhotoServiceImpl implements PhotoService { public static final String W200_PATH_PREFIX = "w200"; public static final String W400_PATH_PREFIX = "w400"; - @Override - public Photo findPhoto(Long photoId) { - return photoRepository.findById(photoId).orElseThrow(() -> new BusinessException(PHOTO_NOT_FOUND)); - } - @Override @Transactional - public List getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member) { + public List getPreSignedUrlList(PhotoRequest.PreSignedUrlRequest request, Member member) { validateShareGroupAndProfile(request.getShareGroupId(), member); return request.getPhotoNameList().stream() @@ -65,10 +71,79 @@ public List getPreSignedUrlList(PhotoRequest.Pre .collect(Collectors.toList()); } + private PreSignedUrlInfo getPreSignedUrl(String originalFilename) { + String fileName = createPath(originalFilename); + String photoName = fileName.split("/")[1]; + String photoUrl = generateFileAccessUrl(fileName); + + URL preSignedUrl = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucketName, fileName)); + return photoConverter.toPreSignedUrlInfo(preSignedUrl.toString(), photoUrl, photoName); + } + + // 원본 사진 전체 경로 생성 + private String createPath(String fileName) { + String fileId = createFileId(); + return String.format("%s/%s", RAW_PATH_PREFIX, fileId + fileName); + } + + // 사진 고유 ID 생성 + private String createFileId() { + return UUID.randomUUID().toString(); + } + + // 사진 업로드용(PUT) PreSigned URL 생성 + private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(getPreSignedUrlExpiration()); + generatePresignedUrlRequest.addRequestParameter(Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); + + return generatePresignedUrlRequest; + } + + // PreSigned URL 유효 기간 설정 + private Date getPreSignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 3; + expiration.setTime(expTimeMillis); + return expiration; + } + + // 원본 사진의 접근 URL 생성 + private String generateFileAccessUrl(String fileName) { + return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName); + } + @Override @Transactional - public PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadRequest request, Member member) { + public SamplePhotoUploadInfo uploadSamplePhotoList(UploadSamplePhotoRequest request, Member member) { + List samplePhotoList = request.getPhotoUrlList().stream() + .map(photoUrl -> checkAndSaveSamplePhotoInDB(photoUrl, extractPhotoNameFromUrl(photoUrl), member)) + .toList(); + + List samplePhotoNameList = samplePhotoList.stream() + .map(samplePhoto -> samplePhoto.getName()) + .toList(); + // Elasticsearch에 sample_face_vector를 저장하기 위한 트리거 실행 + faceDetectionService.detectFaceSamplePhoto(member.getId(), samplePhotoNameList); + + return samplePhotoConverter.toSamplePhotoUploadInfo(member.getId(), samplePhotoList.size()); + } + + private SamplePhoto checkAndSaveSamplePhotoInDB(String photoUrl, String photoName, Member member) { + if (!amazonS3.doesObjectExist(bucketName, RAW_PATH_PREFIX + "/" + photoName)) { + throw new BusinessException(PHOTO_NOT_FOUND_S3); + } + + SamplePhoto samplePhoto = samplePhotoConverter.toEntity(photoUrl, photoName, member); + return samplePhotoRepository.save(samplePhoto); + } + + @Override + @Transactional + public PhotoUploadInfo uploadPhotoList(PhotoUploadRequest request, Member member) { validateShareGroupAndProfile(request.getShareGroupId(), member); ShareGroup shareGroup = shareGroupService.findShareGroup(request.getShareGroupId()); @@ -85,10 +160,44 @@ public PhotoResponse.PhotoUploadInfo uploadPhotoList(PhotoRequest.PhotoUploadReq .map(Photo::getName) .toList(); + + List memberIdList = shareGroupService.findProfileListByShareGroupId(shareGroup.getId()).stream() + .filter(profile -> profile.getMember() != null) + .map(profile -> profile.getMember().getId()) + .collect(Collectors.toList()); // 얼굴 인식 서비스 호출 - faceDetectionService.detectFaceUploadPhoto(photoNameList, request.getShareGroupId()); + faceDetectionService.detectFaceUploadPhoto(photoNameList, shareGroup.getId(), memberIdList); + + return photoConverter.toPhotoUploadInfo(shareGroup.getId(), photoList.size()); + } + + // S3에 객체의 존재 여부 확인 및 DB에 사진을 저장하고 객체를 반환하는 메서드 + private Photo checkAndSavePhotoInDB(String photoUrl, String photoName, ShareGroup shareGroup) { + if (!amazonS3.doesObjectExist(bucketName, RAW_PATH_PREFIX + "/" + photoName)) { + throw new BusinessException(PHOTO_NOT_FOUND_S3); + } + + Photo photo = photoConverter.toEntity(photoUrl, photoName, shareGroup); + return photoRepository.save(photo); // 저장된 Photo 객체 반환 + } - return new PhotoResponse.PhotoUploadInfo(request.getShareGroupId(), photoList.size()); + // 사진 URL에서 사진 이름을 추출하는 메서드 + private String extractPhotoNameFromUrl(String photoUrl) { + int lastSlashIndex = photoUrl.lastIndexOf('/'); + return photoUrl.substring(lastSlashIndex + 1); + } + + @Override + public PhotoDownloadUrlListInfo getPhotoDownloadUrlList(List photoIdList, Long shareGroupId, Member member) { + validateShareGroupAndProfile(shareGroupId, member); + List photoList = photoRepository.findByIdIn(photoIdList); + + if (photoList.size() != photoIdList.size()) { + // 요청한 사진이 일부 또는 전부 없을 경우 예외 발생 + throw new BusinessException(PHOTO_NOT_FOUND); + } + + return photoConverter.toPhotoDownloadUrlListInfo(photoList); } @Override @@ -119,17 +228,11 @@ public List deletePhotoList(PhotoRequest.PhotoDeletedRequest request, Mem return photoList; // 삭제된 사진 목록 반환 } - @Override - public PhotoResponse.PhotoDownloadUrlListInfo getPhotoDownloadUrlList(List photoIdList, Long shareGroupId, Member member) { - validateShareGroupAndProfile(shareGroupId, member); - List photoList = photoRepository.findByIdIn(photoIdList); - - if (photoList.size() != photoIdList.size()) { - // 요청한 사진이 일부 또는 전부 없을 경우 예외 발생 - throw new BusinessException(PHOTO_NOT_FOUND); - } - - return photoConverter.toPhotoDownloadUrlListInfo(photoList); + private void deletePhoto(String photoName) { + // S3에서 원본 및 변환된 이미지 삭제 + s3Template.deleteObject(bucketName, RAW_PATH_PREFIX + "/" + photoName); + s3Template.deleteObject(bucketName, W200_PATH_PREFIX + "/" + photoConverter.convertExtension(photoName)); + s3Template.deleteObject(bucketName, W400_PATH_PREFIX + "/" + photoConverter.convertExtension(photoName)); } // 해당 공유 그룹이 존재하는지 확인 & 멤버가 해당 공유 그룹에 속해있는지 확인 @@ -138,71 +241,9 @@ private void validateShareGroupAndProfile(Long shareGroupId, Member member) { shareGroupService.findProfile(shareGroupId, member.getId()); } - private PhotoResponse.PreSignedUrlInfo getPreSignedUrl(String originalFilename) { - String fileName = createPath(originalFilename); - - String photoName = fileName.split("/")[1]; - String photoUrl = generateFileAccessUrl(fileName); - - URL preSignedUrl = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucketName, fileName)); - return photoConverter.toPreSignedUrlInfo(preSignedUrl.toString(), photoUrl, photoName); - } - - // 사진 업로드용(PUT) PreSigned URL 생성 - private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileName) - .withMethod(HttpMethod.PUT) - .withExpiration(getPreSignedUrlExpiration()); - generatePresignedUrlRequest.addRequestParameter(Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); - - return generatePresignedUrlRequest; - } - - // 원본 사진 전체 경로 생성 - private String createPath(String fileName) { - String fileId = createFileId(); - return String.format("%s/%s", RAW_PATH_PREFIX, fileId + fileName); - } - - // 사진 고유 ID 생성 - private String createFileId() { - return UUID.randomUUID().toString(); - } - - // 원본 사진의 접근 URL 생성 - private String generateFileAccessUrl(String fileName) { - return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName); - } - - // PreSigned URL 유효 기간 설정 - private Date getPreSignedUrlExpiration() { - Date expiration = new Date(); - long expTimeMillis = expiration.getTime(); - expTimeMillis += 1000 * 60 * 3; - expiration.setTime(expTimeMillis); - return expiration; - } - - // 사진 URL에서 사진 이름을 추출하는 메서드 - private String extractPhotoNameFromUrl(String photoUrl) { - int lastSlashIndex = photoUrl.lastIndexOf('/'); - return photoUrl.substring(lastSlashIndex + 1); - } - - // S3에 객체의 존재 여부 확인 및 DB에 사진을 저장하고 객체를 반환하는 메서드 - private Photo checkAndSavePhotoInDB(String photoUrl, String photoName, ShareGroup shareGroup) { - if (!amazonS3.doesObjectExist(bucketName, RAW_PATH_PREFIX + "/" + photoName)) { - throw new BusinessException(PHOTO_NOT_FOUND_S3); - } - - Photo photo = photoConverter.toEntity(photoUrl, photoName, shareGroup); - return photoRepository.save(photo); // 저장된 Photo 객체 반환 - } - - private void deletePhoto(String photoName) { - // S3에서 원본 및 변환된 이미지 삭제 - s3Template.deleteObject(bucketName, RAW_PATH_PREFIX + "/" + photoName); - s3Template.deleteObject(bucketName, W200_PATH_PREFIX + "/" + photoConverter.convertExtension(photoName)); - s3Template.deleteObject(bucketName, W400_PATH_PREFIX + "/" + photoConverter.convertExtension(photoName)); + @Override + public Photo findPhoto(Long photoId) { + return photoRepository.findById(photoId) + .orElseThrow(() -> new BusinessException(PHOTO_NOT_FOUND)); } } diff --git a/src/main/java/com/umc/naoman/domain/shareGroup/service/ShareGroupServiceImpl.java b/src/main/java/com/umc/naoman/domain/shareGroup/service/ShareGroupServiceImpl.java index f4ea239e..eec51173 100644 --- a/src/main/java/com/umc/naoman/domain/shareGroup/service/ShareGroupServiceImpl.java +++ b/src/main/java/com/umc/naoman/domain/shareGroup/service/ShareGroupServiceImpl.java @@ -26,7 +26,6 @@ @Transactional(readOnly = true) @RequiredArgsConstructor public class ShareGroupServiceImpl implements ShareGroupService { - private final ShareGroupRepository shareGroupRepository; private final ProfileRepository profileRepository; private final ShareGroupConverter shareGroupConverter; diff --git a/src/main/java/com/umc/naoman/global/result/code/PhotoResultCode.java b/src/main/java/com/umc/naoman/global/result/code/PhotoResultCode.java index 20a51ce0..934d1f20 100644 --- a/src/main/java/com/umc/naoman/global/result/code/PhotoResultCode.java +++ b/src/main/java/com/umc/naoman/global/result/code/PhotoResultCode.java @@ -10,11 +10,13 @@ public enum PhotoResultCode implements ResultCode { CREATE_PRESIGNED_URL(200, "SP000", "성공적으로 Presigned URL을 요청하였습니다."), UPLOAD_PHOTO(200, "SP000", "성공적으로 이미지를 업로드하였습니다."), + UPLOAD_SAMPLE_PHOTO(200, "SP000", "성공적으로 샘플 이미지를 업로드하였습니다."), RETRIEVE_PHOTO(200, "SP000", "성공적으로 이미지를 조회하였습니다."), DELETE_PHOTO(200, "SP000", "성공적으로 이미지를 삭제하였습니다."), - DOWNLOAD_PHOTO(200, "SP000", "성공적으로 이미지를 디운로드하였습니다.") + DOWNLOAD_PHOTO(200, "SP000", "성공적으로 이미지를 디운로드하였습니다."), ; + private final int status; private final String code; private final String message; diff --git a/src/main/java/com/umc/naoman/global/security/util/CookieUtils.java b/src/main/java/com/umc/naoman/global/security/util/CookieUtils.java index 2d8e3f06..eba6a9ae 100644 --- a/src/main/java/com/umc/naoman/global/security/util/CookieUtils.java +++ b/src/main/java/com/umc/naoman/global/security/util/CookieUtils.java @@ -16,9 +16,9 @@ public static void addCookie(HttpServletResponse response, String name, String v ResponseCookie cookie = ResponseCookie.from(name, value) .path("/") .maxAge(maxAge) - //.httpOnly(false) - //.secure(true) - //.sameSite("None") + .httpOnly(false) + .secure(true) + .sameSite("None") .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());