diff --git a/src/main/java/com/divary/domain/image/service/ImageService.java b/src/main/java/com/divary/domain/image/service/ImageService.java index 57826225..676f3418 100644 --- a/src/main/java/com/divary/domain/image/service/ImageService.java +++ b/src/main/java/com/divary/domain/image/service/ImageService.java @@ -17,6 +17,7 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; +import java.time.Duration; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -237,10 +238,25 @@ public ImageResponse uploadImage(ImageUploadRequest request) { } // 경로 패턴으로 이미지 목록 조회 + @Transactional(readOnly = true) public List getImagesByPath(String pathPattern) { List images = imageRepository.findByS3KeyStartingWith(pathPattern); + return images.stream() - .map(image -> ImageResponse.from(image, imageStorageService.generatePublicUrl(image.getS3Key()))) + .map(image -> { + String s3Key = image.getS3Key(); + String fileUrl; + + //s3Key에 license가 포함된 경우 Pre-signed URL 생성 + if (s3Key != null && s3Key.contains("/license/")) { + fileUrl = imageStorageService.generatePreSignedUrl(s3Key, Duration.ofMinutes(10)); + log.info("라이센스 이미지 Pre-signed URL 생성 완료: {}", fileUrl); + } else { + fileUrl = imageStorageService.generatePublicUrl(s3Key); + }//이외의 경우에는 일반 public url 생성 + + return ImageResponse.from(image, fileUrl); + }) .collect(Collectors.toList()); } @@ -252,7 +268,7 @@ public ImageResponse getImageById(Long imageId) { String fileUrl = imageStorageService.generatePublicUrl(image.getS3Key()); return ImageResponse.from(image, fileUrl); } - + // 이미지 삭제 @Transactional public void deleteImage(Long imageId) { diff --git a/src/main/java/com/divary/domain/image/service/ImageStorageService.java b/src/main/java/com/divary/domain/image/service/ImageStorageService.java index 22fd003e..817beb0e 100644 --- a/src/main/java/com/divary/domain/image/service/ImageStorageService.java +++ b/src/main/java/com/divary/domain/image/service/ImageStorageService.java @@ -7,14 +7,18 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; -import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; import java.io.IOException; +import java.time.Duration; import java.util.List; import java.util.UUID; @@ -128,6 +132,32 @@ public String generatePublicUrl(String s3Key) { return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, s3Key); } + public String generatePreSignedUrl(String s3Key, Duration expiry) { + try (S3Presigner presigner = S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build()) { + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(expiry) + .getObjectRequest(getObjectRequest) + .build(); + + PresignedGetObjectRequest presigned = presigner.presignGetObject(presignRequest); + return presigned.url().toString(); + + } catch (Exception e) { + log.error("Pre-signed URL 생성 실패: {}", e.getMessage()); + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + // S3 URL에서 S3 키 추출 public String extractS3KeyFromUrl(String imageUrl) { diff --git a/src/main/java/com/divary/domain/logbase/LogBaseInfoRepository.java b/src/main/java/com/divary/domain/logbase/LogBaseInfoRepository.java index 4134c56e..b66a3950 100644 --- a/src/main/java/com/divary/domain/logbase/LogBaseInfoRepository.java +++ b/src/main/java/com/divary/domain/logbase/LogBaseInfoRepository.java @@ -8,13 +8,14 @@ import java.util.Optional; import com.mysql.cj.log.Log; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface LogBaseInfoRepository extends JpaRepository { - @Query("SELECT l FROM LogBaseInfo l WHERE YEAR(l.date) = :year AND l.saveStatus = :status AND l.member = :member ORDER BY l.date DESC") - List findByYearAndStatusAndMember(@Param("year") int year, @Param("status") SaveStatus status, @Param("member") Member member); + @Query("SELECT l FROM LogBaseInfo l WHERE YEAR(l.date) = :year AND l.saveStatus = :status AND l.member = :member") + List findByYearAndStatusAndMember(@Param("year") int year, @Param("status") SaveStatus status, @Param("member") Member member, Sort sort); @Query("SELECT l FROM LogBaseInfo l WHERE YEAR(l.date) = :year AND l.member = :member ORDER BY l.date DESC") List findByYearAndMember(@Param("year") int year, @Param("member") Member member); diff --git a/src/main/java/com/divary/domain/logbase/logbook/controller/LogBookController.java b/src/main/java/com/divary/domain/logbase/logbook/controller/LogBookController.java index 058e8a45..1c191a51 100644 --- a/src/main/java/com/divary/domain/logbase/logbook/controller/LogBookController.java +++ b/src/main/java/com/divary/domain/logbase/logbook/controller/LogBookController.java @@ -19,6 +19,7 @@ import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; @@ -54,11 +55,21 @@ public class LogBookController { public ApiResponse> getLogListByYearAndStatus( @RequestParam int year, @RequestParam(required = false) SaveStatus saveStatus, - @AuthenticationPrincipal CustomUserPrincipal userPrincipal) { + @AuthenticationPrincipal CustomUserPrincipal userPrincipal, + @RequestParam(required = false) String sort) + { + Sort sortOption; + + if ("oldest".equalsIgnoreCase(sort)) { + sortOption = Sort.by(Sort.Direction.ASC, "date"); + } + else { + sortOption = Sort.by(Sort.Direction.DESC, "date"); + } Long userId = userPrincipal.getId(); - List result = logBookService.getLogBooksByYearAndStatus(year, saveStatus, userId); + List result = logBookService.getLogBooksByYearAndStatus(year, saveStatus, userId, sortOption); return ApiResponse.success(result); } diff --git a/src/main/java/com/divary/domain/logbase/logbook/service/LogBookService.java b/src/main/java/com/divary/domain/logbase/logbook/service/LogBookService.java index 8d5bd8f4..46aaa698 100644 --- a/src/main/java/com/divary/domain/logbase/logbook/service/LogBookService.java +++ b/src/main/java/com/divary/domain/logbase/logbook/service/LogBookService.java @@ -16,6 +16,7 @@ import com.divary.global.exception.ErrorCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -59,7 +60,7 @@ public class LogBookService { } @Transactional(readOnly = true) - public List getLogBooksByYearAndStatus(int year, SaveStatus status, Long userId) { + public List getLogBooksByYearAndStatus(int year, SaveStatus status, Long userId, Sort sort) { List logBaseInfoList; @@ -70,7 +71,7 @@ public List getLogBooksByYearAndStatus(int year, SaveStatu // 쿼리스트링 없을 경우 전체 조회 logBaseInfoList = logBaseInfoRepository.findByYearAndMember(year,member); } else { - logBaseInfoList = logBaseInfoRepository.findByYearAndStatusAndMember(year,status,member); + logBaseInfoList = logBaseInfoRepository.findByYearAndStatusAndMember(year,status,member,sort); } return logBaseInfoList.stream() @@ -131,11 +132,6 @@ public LogDetailCreateResultDTO createLogDetail(Long logBaseInfoId, Long userId) LogBaseInfo base = logBaseInfoRepository.findByIdAndMemberId(logBaseInfoId,userId) .orElseThrow(() -> new BusinessException(ErrorCode.LOG_BASE_NOT_FOUND)); - // 연결된 기존의 로그북 개수 확인 - if (logBookRepository.countByLogBaseInfo(base) >= 3) { - throw new BusinessException(ErrorCode.LOG_LIMIT_EXCEEDED); - }//하루 최대 3개 넘으면 에러 던지기 - LogBook logBook = LogBook.builder() .logBaseInfo(base) .build(); @@ -170,17 +166,12 @@ public void calculateLogBaseStatus(LogBaseInfo base) { } return; // 베이스를 TEMP로 맞췄으니 종료 } - - // 2. 연결된 로그북들 모두 COMPLETE인지 확인 - long total = logBookRepository.countByLogBaseInfoId(base.getId()); - if (total > 0) { - long completeCount = logBookRepository.countByLogBaseInfoIdAndSaveStatus(base.getId(), SaveStatus.COMPLETE); - if (completeCount == total) { - if (base.getSaveStatus() != SaveStatus.COMPLETE) { - base.setSaveStatus(SaveStatus.COMPLETE); - } + else { + if (base.getSaveStatus() != SaveStatus.COMPLETE) { + base.setSaveStatus(SaveStatus.COMPLETE); } } + } @Transactional diff --git a/src/main/java/com/divary/domain/member/controller/MemberController.java b/src/main/java/com/divary/domain/member/controller/MemberController.java index 67ee5c45..12d9fc03 100644 --- a/src/main/java/com/divary/domain/member/controller/MemberController.java +++ b/src/main/java/com/divary/domain/member/controller/MemberController.java @@ -1,8 +1,10 @@ package com.divary.domain.member.controller; import com.divary.common.response.ApiResponse; +import com.divary.domain.member.dto.requestDTO.MyPageGroupRequestDTO; import com.divary.domain.member.dto.requestDTO.MyPageLevelRequestDTO; import com.divary.domain.member.dto.response.MyPageImageResponseDTO; +import com.divary.domain.member.dto.response.MyPageProfileResponseDTO; import com.divary.global.config.SwaggerConfig; import com.divary.global.config.security.CustomUserPrincipal; import com.divary.global.exception.ErrorCode; @@ -39,4 +41,27 @@ public ApiResponse licenseUpload(@AuthenticationPrincipal CustomUserPrincipal us return ApiResponse.success(response); } + @PatchMapping("/group") + @SwaggerConfig.ApiSuccessResponse(dataType = Void.class) + @SwaggerConfig.ApiErrorExamples(value = {ErrorCode.INVALID_INPUT_VALUE, ErrorCode.AUTHENTICATION_REQUIRED}) + public ApiResponse updateGroup(@AuthenticationPrincipal CustomUserPrincipal userPrincipal, @Valid @RequestBody MyPageGroupRequestDTO requestDTO) { + memberService.updateGroup(userPrincipal.getId(), requestDTO); + return ApiResponse.success(null); + } + + @GetMapping("/profile") + @SwaggerConfig.ApiSuccessResponse(dataType = Void.class) + @SwaggerConfig.ApiErrorExamples(value = {ErrorCode.AUTHENTICATION_REQUIRED}) + public ApiResponse getProfile(@AuthenticationPrincipal CustomUserPrincipal userPrincipal){ + MyPageProfileResponseDTO responseDTO = memberService.getMemberProfile(userPrincipal.getId()); + return ApiResponse.success(responseDTO); + } + + @GetMapping("/license") + @SwaggerConfig.ApiSuccessResponse(dataType = Void.class) + @SwaggerConfig.ApiErrorExamples(value = {ErrorCode.AUTHENTICATION_REQUIRED}) + public ApiResponse getLicense(@AuthenticationPrincipal CustomUserPrincipal userPrincipal){ + MyPageImageResponseDTO responseDTO = memberService.getLicenseImage(userPrincipal.getId()); + return ApiResponse.success(responseDTO); + } } diff --git a/src/main/java/com/divary/domain/member/dto/requestDTO/MyPageGroupRequestDTO.java b/src/main/java/com/divary/domain/member/dto/requestDTO/MyPageGroupRequestDTO.java new file mode 100644 index 00000000..2207f16e --- /dev/null +++ b/src/main/java/com/divary/domain/member/dto/requestDTO/MyPageGroupRequestDTO.java @@ -0,0 +1,11 @@ +package com.divary.domain.member.dto.requestDTO; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class MyPageGroupRequestDTO { + + @Schema(description = "Group", example = "PADI") + private String memberGroup; +} diff --git a/src/main/java/com/divary/domain/member/dto/response/MyPageProfileResponseDTO.java b/src/main/java/com/divary/domain/member/dto/response/MyPageProfileResponseDTO.java new file mode 100644 index 00000000..33efb20f --- /dev/null +++ b/src/main/java/com/divary/domain/member/dto/response/MyPageProfileResponseDTO.java @@ -0,0 +1,25 @@ +package com.divary.domain.member.dto.response; + +import com.divary.domain.member.enums.Levels; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MyPageProfileResponseDTO { + + @Schema(description = "멤버 id", example = "user1234") + private String id; + + @Schema(description = "단체명", example = "PADI") + private String memberGroup; + + @Schema(description = "레벨", example = "오픈워터 다이버") + private Levels level; + +} diff --git a/src/main/java/com/divary/domain/member/entity/Member.java b/src/main/java/com/divary/domain/member/entity/Member.java index 1e143352..b0f4f610 100644 --- a/src/main/java/com/divary/domain/member/entity/Member.java +++ b/src/main/java/com/divary/domain/member/entity/Member.java @@ -37,6 +37,9 @@ public class Member extends BaseEntity { @NotNull Status status = Status.ACTIVE; // 사용자 상태 + @Column + private String memberGroup; + private LocalDateTime deactivatedAt; //비활성화 된 시간과 날짜 @Version @@ -54,4 +57,9 @@ public void cancelDeletion() { this.status = Status.ACTIVE; this.deactivatedAt = null; } + + public void updateGroup(String newGroup){ + this.memberGroup = newGroup; + } + } diff --git a/src/main/java/com/divary/domain/member/service/MemberService.java b/src/main/java/com/divary/domain/member/service/MemberService.java index ac57de97..287a5081 100644 --- a/src/main/java/com/divary/domain/member/service/MemberService.java +++ b/src/main/java/com/divary/domain/member/service/MemberService.java @@ -1,6 +1,8 @@ package com.divary.domain.member.service; +import com.divary.domain.member.dto.requestDTO.MyPageGroupRequestDTO; import com.divary.domain.member.dto.response.MyPageImageResponseDTO; +import com.divary.domain.member.dto.response.MyPageProfileResponseDTO; import com.divary.domain.member.entity.Member; import com.divary.domain.member.dto.requestDTO.MyPageLevelRequestDTO; import com.divary.global.oauth.dto.response.DeactivateResponse; @@ -16,5 +18,9 @@ public interface MemberService { void cancelDeleteMember(Long memberId); public Member findOrCreateMember(String email); + void updateGroup(Long userId, MyPageGroupRequestDTO requestDTO); + MyPageProfileResponseDTO getMemberProfile(Long userId); + + MyPageImageResponseDTO getLicenseImage(Long userId); } diff --git a/src/main/java/com/divary/domain/member/service/MemberServiceImpl.java b/src/main/java/com/divary/domain/member/service/MemberServiceImpl.java index f680897c..6f14ce6d 100644 --- a/src/main/java/com/divary/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/divary/domain/member/service/MemberServiceImpl.java @@ -2,9 +2,12 @@ import com.divary.common.util.EnumValidator; import com.divary.domain.image.dto.request.ImageUploadRequest; +import com.divary.domain.image.dto.response.ImageResponse; import com.divary.domain.image.service.ImageService; +import com.divary.domain.member.dto.requestDTO.MyPageGroupRequestDTO; import com.divary.domain.member.dto.requestDTO.MyPageLevelRequestDTO; import com.divary.domain.member.dto.response.MyPageImageResponseDTO; +import com.divary.domain.member.dto.response.MyPageProfileResponseDTO; import com.divary.domain.member.enums.Role; import com.divary.domain.member.enums.Status; import com.divary.global.exception.BusinessException; @@ -25,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Service @@ -145,4 +149,40 @@ public Member findOrCreateMember(String email) { return memberRepository.save(newMember); }); } + @Override + @CacheEvict(cacheNames = com.divary.global.config.CacheConfig.CACHE_MEMBER_BY_ID, key = "#userId") + public void updateGroup(Long userId, MyPageGroupRequestDTO requestDTO){ + String group = requestDTO.getMemberGroup(); + + Member member = memberRepository.findById(userId).orElseThrow(()-> new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + member.updateGroup(group); + } + + @Override + public MyPageProfileResponseDTO getMemberProfile(Long userId){ + Member member = memberRepository.findById(userId).orElseThrow(()->new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); + + String memberIdByEmail = member.getEmail().split("@")[0]; + // 프로필에 나오는 아이디: 이메일에서 @ 앞부분만 추출 + + return MyPageProfileResponseDTO.builder() + .memberGroup(member.getMemberGroup()) + .level(member.getLevel()) + .id(memberIdByEmail) + .build(); + } + + @Override + public MyPageImageResponseDTO getLicenseImage(Long userId){ + + String uploadPath = "users/" + userId + "/license/"; + + List imageResponses = imageService.getImagesByPath(uploadPath); + + String fileUrl = imageResponses.getFirst().getFileUrl(); + + return new MyPageImageResponseDTO(fileUrl); + } + + }