diff --git a/src/main/java/org/ioteatime/meonghanyangserver/common/type/ImageSuccessType.java b/src/main/java/org/ioteatime/meonghanyangserver/common/type/ImageSuccessType.java index b1a638c3..b781d320 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/common/type/ImageSuccessType.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/common/type/ImageSuccessType.java @@ -1,6 +1,7 @@ package org.ioteatime.meonghanyangserver.common.type; public enum ImageSuccessType implements SuccessTypeCode { + GET_LIST_OF_DATE(200, "OK", "날짜에 해당하는 이미지 목록 조회에 성공하였습니다."), CREATE_PRESIGNED_URL(200, "OK", "Presigned Url 생성에 성공하였습니다."); private final Integer code; diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageApi.java b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageApi.java new file mode 100644 index 00000000..bd18c69b --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageApi.java @@ -0,0 +1,21 @@ +package org.ioteatime.meonghanyangserver.image.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.ioteatime.meonghanyangserver.common.api.Api; +import org.ioteatime.meonghanyangserver.common.utils.LoginMember; +import org.ioteatime.meonghanyangserver.image.dto.response.GroupDateImageResponse; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "Image Api", description = "Image 관련 API 목록입니다.") +public interface ImageApi { + @Operation( + summary = "회원이 속한 그룹의 날짜별 객체 탐지 이미지 목록을 최근 시간 순서대로 조회합니다.", + description = + "담당자: 양원채\n\n달력의 날짜를 클릭했을 때 해당 날짜의 이미지 목록을 조회할 떄 사용합니다.\n\nURL QueryParameter로 날짜를 담아 요청 주시면 됩니다.") + Api groupDateImageList( + @LoginMember Long memberId, + @RequestParam("year") Short year, + @RequestParam("month") Byte month, + @RequestParam("day") Byte day); +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageController.java b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageController.java new file mode 100644 index 00000000..58852534 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageController.java @@ -0,0 +1,35 @@ +package org.ioteatime.meonghanyangserver.image.controller; + +import lombok.RequiredArgsConstructor; +import org.ioteatime.meonghanyangserver.common.api.Api; +import org.ioteatime.meonghanyangserver.common.type.ImageSuccessType; +import org.ioteatime.meonghanyangserver.common.utils.LoginMember; +import org.ioteatime.meonghanyangserver.image.dto.response.GroupDateImageResponse; +import org.ioteatime.meonghanyangserver.image.dto.response.ImageSaveUrlResponse; +import org.ioteatime.meonghanyangserver.image.service.ImageService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/image") +public class ImageController implements ImageApi { + private final ImageService imageService; + + @GetMapping + public Api groupDateImageList( + @LoginMember Long memberId, + @RequestParam("year") Short year, + @RequestParam("month") Byte month, + @RequestParam("day") Byte day) { + GroupDateImageResponse response = + imageService.findAllByMemberIdAndDate(memberId, year, month, day); + return Api.success(ImageSuccessType.GET_LIST_OF_DATE, response); + } + + @GetMapping("/{fileName}") + public Api getImageSaveUrl( + @LoginMember Long cctvId, @PathVariable String fileName) { + ImageSaveUrlResponse imageSaveUrlResponse = imageService.getImageSaveUrl(cctvId, fileName); + return Api.success(ImageSuccessType.CREATE_PRESIGNED_URL, imageSaveUrlResponse); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceApi.java b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceApi.java index cd435434..98ef9f5d 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceApi.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceApi.java @@ -3,12 +3,16 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.ioteatime.meonghanyangserver.common.api.Api; +import org.ioteatime.meonghanyangserver.common.utils.LoginMember; import org.ioteatime.meonghanyangserver.image.dto.response.ImageSaveUrlResponse; import org.springframework.web.bind.annotation.PathVariable; @Tag(name = "Image Api", description = "Image 관련 API 목록입니다.") public interface ImageDeviceApi { - @Operation(summary = "이미지 저장을 위한 presigned url을 발급 받습니다.", description = "담당자: 임지인") + @Operation( + summary = "이미지 저장을 위한 presigned url을 발급 받습니다.", + description = + "담당자: 임지인\n\n캡쳐 이미지 저장을 위한 URL 발급 요청입니다.\n\nCCTV의 Access Token과 함께 요청 하시면 됩니다.") public Api getImageSaveUrl( - @PathVariable Long cctvId, @PathVariable String fileName); + @LoginMember Long cctvId, @PathVariable String fileName); } diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceController.java b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceController.java index 1564a2ff..1b11abcb 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceController.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/controller/ImageDeviceController.java @@ -12,11 +12,11 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/open-api/image") +@RequestMapping("/api/image-device") public class ImageDeviceController implements ImageDeviceApi { private final ImageService imageService; - @GetMapping("/{cctvId}/{fileName}") + @GetMapping("/{fileName}") public Api getImageSaveUrl( @PathVariable Long cctvId, @PathVariable String fileName) { ImageSaveUrlResponse imageSaveUrlResponse = imageService.getImageSaveUrl(cctvId, fileName); diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/GroupDateImageResponse.java b/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/GroupDateImageResponse.java new file mode 100644 index 00000000..b0c38bb2 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/GroupDateImageResponse.java @@ -0,0 +1,8 @@ +package org.ioteatime.meonghanyangserver.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "날짜에 해당하는 그룹의 이미지 목록 응답") +public record GroupDateImageResponse( + @Schema(description = "날짜에 해당하는 그룹의 이미지 목록") List images) {} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/ImageResponse.java b/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/ImageResponse.java new file mode 100644 index 00000000..9e4e5e1d --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/dto/response/ImageResponse.java @@ -0,0 +1,42 @@ +package org.ioteatime.meonghanyangserver.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Schema(description = "이미지 단일 응답") +public class ImageResponse { + @NotNull + @Schema(description = "이미지 ID", example = "1") + private final Long imageId; + + @NotNull + @Schema(description = "이미지 원본 이름", example = "test-image.jpg") + private final String imageName; + + @NotNull + @Schema( + description = "이미지 URL", + example = "https://bucket.s3.ap-northeast-2.amazonaws.com/path/to/test-image.jpg") + private String imagePath; + + @NotNull + @Schema(description = "형식이 있는 이미지 생성 시각", example = "2024.10.22.13:00") + private final String formattedCreatedAt; + + @Builder + public ImageResponse( + Long imageId, String imageName, String imagePath, String formattedCreatedAt) { + this.imageId = imageId; + this.imageName = imageName; + this.imagePath = imagePath; + this.formattedCreatedAt = formattedCreatedAt; + } + + public ImageResponse updateToPresignedUrl(String imagePresignedPath) { + this.imagePath = imagePresignedPath; + return this; + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/mapper/ImageResponseMapper.java b/src/main/java/org/ioteatime/meonghanyangserver/image/mapper/ImageResponseMapper.java index c2019050..41a914eb 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/image/mapper/ImageResponseMapper.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/mapper/ImageResponseMapper.java @@ -1,8 +1,15 @@ package org.ioteatime.meonghanyangserver.image.mapper; +import java.util.List; +import org.ioteatime.meonghanyangserver.image.dto.response.GroupDateImageResponse; +import org.ioteatime.meonghanyangserver.image.dto.response.ImageResponse; import org.ioteatime.meonghanyangserver.image.dto.response.ImageSaveUrlResponse; public class ImageResponseMapper { + public static GroupDateImageResponse from(List images) { + return new GroupDateImageResponse(images); + } + public static ImageSaveUrlResponse form(String presignedUrl) { return new ImageSaveUrlResponse(presignedUrl); } diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageJpaRepository.java b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageJpaRepository.java new file mode 100644 index 00000000..41005fbc --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageJpaRepository.java @@ -0,0 +1,6 @@ +package org.ioteatime.meonghanyangserver.image.repository; + +import org.ioteatime.meonghanyangserver.image.domain.ImageEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ImageJpaRepository extends JpaRepository {} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepository.java b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepository.java new file mode 100644 index 00000000..d5324752 --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepository.java @@ -0,0 +1,9 @@ +package org.ioteatime.meonghanyangserver.image.repository; + +import java.time.LocalDateTime; +import java.util.List; +import org.ioteatime.meonghanyangserver.image.dto.response.ImageResponse; + +public interface ImageRepository { + List findAllByGroupIdAndDate(Long groupId, LocalDateTime searchDate); +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepositoryImpl.java b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepositoryImpl.java new file mode 100644 index 00000000..7c4dc90c --- /dev/null +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/repository/ImageRepositoryImpl.java @@ -0,0 +1,50 @@ +package org.ioteatime.meonghanyangserver.image.repository; + +import static org.ioteatime.meonghanyangserver.group.domain.QGroupEntity.groupEntity; +import static org.ioteatime.meonghanyangserver.image.domain.QImageEntity.imageEntity; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.ioteatime.meonghanyangserver.image.dto.response.ImageResponse; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ImageRepositoryImpl implements ImageRepository { + private final JPAQueryFactory queryFactory; + private final ImageJpaRepository imageJpaRepository; + + @Override + public List findAllByGroupIdAndDate(Long groupId, LocalDateTime searchDate) { + String dateFormat = "DATE_FORMAT({0}, '%Y.%m.%d.%H:%i')"; + return queryFactory + .select( + Projections.constructor( + ImageResponse.class, + imageEntity.id.as("imageId"), + imageEntity.imageName.as("imageName"), + imageEntity.imagePath.as("imagePath"), + Expressions.stringTemplate(dateFormat, imageEntity.createdAt) + .as("formattedCreatedAt"))) + .from(imageEntity) + .join(imageEntity.group, groupEntity) + .where( + imageEntity + .group + .id + .eq(groupId) + .and(imageEntity.createdAt.year().eq(searchDate.getYear())) + .and(imageEntity.createdAt.month().eq(searchDate.getMonthValue())) + .and( + imageEntity + .createdAt + .dayOfMonth() + .eq(searchDate.getDayOfMonth()))) + .orderBy(imageEntity.createdAt.desc()) + .fetch(); + } +} diff --git a/src/main/java/org/ioteatime/meonghanyangserver/image/service/ImageService.java b/src/main/java/org/ioteatime/meonghanyangserver/image/service/ImageService.java index 21b1420f..c195d674 100644 --- a/src/main/java/org/ioteatime/meonghanyangserver/image/service/ImageService.java +++ b/src/main/java/org/ioteatime/meonghanyangserver/image/service/ImageService.java @@ -1,13 +1,21 @@ package org.ioteatime.meonghanyangserver.image.service; import com.amazonaws.HttpMethod; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.ioteatime.meonghanyangserver.cctv.repository.CctvRepository; import org.ioteatime.meonghanyangserver.clients.s3.S3Client; import org.ioteatime.meonghanyangserver.common.exception.NotFoundException; import org.ioteatime.meonghanyangserver.common.type.CctvErrorType; +import org.ioteatime.meonghanyangserver.common.type.GroupErrorType; +import org.ioteatime.meonghanyangserver.group.domain.GroupEntity; +import org.ioteatime.meonghanyangserver.groupmember.repository.GroupMemberRepository; +import org.ioteatime.meonghanyangserver.image.dto.response.GroupDateImageResponse; +import org.ioteatime.meonghanyangserver.image.dto.response.ImageResponse; import org.ioteatime.meonghanyangserver.image.dto.response.ImageSaveUrlResponse; import org.ioteatime.meonghanyangserver.image.mapper.ImageResponseMapper; +import org.ioteatime.meonghanyangserver.image.repository.ImageRepository; import org.springframework.stereotype.Service; @Service @@ -15,6 +23,8 @@ public class ImageService { private final S3Client s3Client; private final CctvRepository cctvRepository; + private final ImageRepository imageRepository; + private final GroupMemberRepository groupMemberRepository; public ImageSaveUrlResponse getImageSaveUrl(Long cctvId, String fileName) { if (!cctvRepository.existsById(cctvId)) { @@ -24,4 +34,30 @@ public ImageSaveUrlResponse getImageSaveUrl(Long cctvId, String fileName) { String presignedUrl = s3Client.generatePreSignUrl(fileName, HttpMethod.PUT); return ImageResponseMapper.form(presignedUrl); } + + public GroupDateImageResponse findAllByMemberIdAndDate( + Long memberId, Short year, Byte month, Byte day) { + // 그룹멤버를 찾아 그룹 id 확인 + // 없으면 에러, 이미지를 조회할 수 없음 + // 그룹 id와 날짜를 기준으로 이미지 리스트 조회 + GroupEntity groupEntity = + groupMemberRepository + .findGroupFromGroupMember(memberId) + .orElseThrow( + () -> new NotFoundException(GroupErrorType.GROUP_MEMBER_NOT_FOUND)); + LocalDateTime searchDate = + LocalDateTime.of(year.intValue(), month.intValue(), day.intValue(), 0, 0); + List imageResponses = + imageRepository.findAllByGroupIdAndDate(groupEntity.getId(), searchDate).stream() + .map( + image -> { + // 경로를 PresignedUrl로 변경 + String preSignedUrl = + s3Client.generatePreSignUrl( + image.getImagePath(), HttpMethod.GET); + return image.updateToPresignedUrl(preSignedUrl); + }) + .toList(); + return ImageResponseMapper.from(imageResponses); + } } diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml index d5662518..5ee4caf8 100644 --- a/src/main/resources/application-prod.yaml +++ b/src/main/resources/application-prod.yaml @@ -10,4 +10,4 @@ spring: format_sql: true default_batch_fetch_size: 100 hibernate: - ddl-auto: update \ No newline at end of file + ddl-auto: none \ No newline at end of file