diff --git a/.gitignore b/.gitignore index 94d228a..0173ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ out/ **/application.properties /lib/ +/url/ diff --git a/build.gradle b/build.gradle index fee0b8a..7d21ce8 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.locationtech.jts:jts-core:1.19.0' // Swagger-ui - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' // EXIF metadata extraction implementation 'com.drewnoakes:metadata-extractor:2.19.0' diff --git a/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java b/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java index f4c3c89..52c8a90 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java +++ b/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java @@ -3,7 +3,11 @@ import jakarta.validation.Valid; import kr.kro.photoliner.domain.album.dto.request.AlbumCreateRequest; import kr.kro.photoliner.domain.album.dto.request.AlbumDeleteRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumItemCreateRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumItemDeleteRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumTitleUpdateRequest; import kr.kro.photoliner.domain.album.dto.response.AlbumCreateResponse; +import kr.kro.photoliner.domain.album.dto.response.AlbumPhotoItemsResponse; import kr.kro.photoliner.domain.album.dto.response.AlbumsResponse; import kr.kro.photoliner.domain.album.service.AlbumService; import lombok.RequiredArgsConstructor; @@ -14,6 +18,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -43,6 +49,15 @@ public ResponseEntity getAlbums( return ResponseEntity.ok(albumService.getAlbums(userId, pageable)); } + @PatchMapping("/{albumId}/title") + public ResponseEntity updateAlbumTitle( + @PathVariable Long albumId, + @RequestBody @Valid AlbumTitleUpdateRequest request + ) { + albumService.updateAlbumTitle(albumId, request); + return ResponseEntity.noContent().build(); + } + @DeleteMapping public ResponseEntity deletePhoto( @Valid @RequestBody AlbumDeleteRequest request @@ -50,4 +65,30 @@ public ResponseEntity deletePhoto( albumService.deleteAlbums(request); return ResponseEntity.noContent().build(); } + + @GetMapping("/{albumId}/photos") + public ResponseEntity getAlbumItems( + @PathVariable Long albumId, + @PageableDefault(sort = "capturedDt", direction = Sort.Direction.DESC) Pageable pageable + ) { + return ResponseEntity.ok(albumService.getAlbumPhotoItems(albumId, pageable)); + } + + @PostMapping("/{albumId}/photos") + public ResponseEntity createAlbumItems( + @PathVariable Long albumId, + @RequestBody @Valid AlbumItemCreateRequest request + ) { + albumService.createAlbumItems(albumId, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{albumId}/photos") + public ResponseEntity deleteAlbumItems( + @PathVariable Long albumId, + @RequestBody @Valid AlbumItemDeleteRequest request + ) { + albumService.deleteAlbumItems(albumId, request); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java index 87476fd..953c4dd 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java @@ -7,6 +7,6 @@ public record AlbumCreateRequest( @NotNull Long userId, @NotEmpty - String name + String title ) { } diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemCreateRequest.java b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemCreateRequest.java new file mode 100644 index 0000000..0057f7f --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemCreateRequest.java @@ -0,0 +1,10 @@ +package kr.kro.photoliner.domain.album.dto.request; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record AlbumItemCreateRequest( + @NotNull + List ids +) { +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemDeleteRequest.java b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemDeleteRequest.java new file mode 100644 index 0000000..1eada1d --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumItemDeleteRequest.java @@ -0,0 +1,10 @@ +package kr.kro.photoliner.domain.album.dto.request; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record AlbumItemDeleteRequest( + @NotNull + List ids +) { +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumTitleUpdateRequest.java b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumTitleUpdateRequest.java new file mode 100644 index 0000000..926b39f --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumTitleUpdateRequest.java @@ -0,0 +1,9 @@ +package kr.kro.photoliner.domain.album.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record AlbumTitleUpdateRequest( + @NotNull + String title +) { +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumCreateResponse.java b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumCreateResponse.java index 019d19f..77de7b5 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumCreateResponse.java +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumCreateResponse.java @@ -10,7 +10,7 @@ public static AlbumCreateResponse from(Album album) { return new AlbumCreateResponse( new InnerAlbum( album.getId(), - album.getName() + album.getTitle() ) ); } diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumPhotoItemsResponse.java b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumPhotoItemsResponse.java new file mode 100644 index 0000000..6b28cfd --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumPhotoItemsResponse.java @@ -0,0 +1,40 @@ +package kr.kro.photoliner.domain.album.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoView; +import org.springframework.data.domain.Page; + +public record AlbumPhotoItemsResponse( + List items +) { + + public static AlbumPhotoItemsResponse from(Page albumPhotoViews) { + return new AlbumPhotoItemsResponse( + albumPhotoViews.stream() + .map(InnerAlbumPhotoItem::from) + .toList() + ); + } + + public record InnerAlbumPhotoItem( + Long id, + Long photoId, + String fileName, + String filePath, + String thumbnailPath, + LocalDateTime capturedDt + ) { + + public static InnerAlbumPhotoItem from(AlbumPhotoView albumPhotoView) { + return new InnerAlbumPhotoItem( + albumPhotoView.getId(), + albumPhotoView.getPhotoId(), + albumPhotoView.getFileName(), + albumPhotoView.getFilePath(), + albumPhotoView.getThumbnailPath(), + albumPhotoView.getCapturedDt() + ); + } + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumsResponse.java b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumsResponse.java index bb33ce3..22c40e2 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumsResponse.java +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/response/AlbumsResponse.java @@ -26,7 +26,7 @@ public record InnerAlbum( public static InnerAlbum from(Album album) { return new InnerAlbum( album.getId(), - album.getName() + album.getTitle() ); } } diff --git a/src/main/java/kr/kro/photoliner/domain/album/model/Album.java b/src/main/java/kr/kro/photoliner/domain/album/model/Album.java index 1156163..bc4a613 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/model/Album.java +++ b/src/main/java/kr/kro/photoliner/domain/album/model/Album.java @@ -1,5 +1,6 @@ package kr.kro.photoliner.domain.album.model; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -8,8 +9,12 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import kr.kro.photoliner.common.model.BaseEntity; import kr.kro.photoliner.domain.user.model.User; import lombok.AccessLevel; @@ -31,10 +36,39 @@ public class Album extends BaseEntity { private Long id; @NotNull - @Column(name = "name", nullable = false) - private String name; + @Column(name = "title", nullable = false) + private String title; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", referencedColumnName = "id") private User user; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "album") + private List items = new ArrayList<>(); + + public void addPhotos(List photoIds) { + photoIds.forEach(this::addPhoto); + } + + private void addPhoto(Long photoId) { + items.add(PhotoItem.of(this, photoId)); + } + + public void updateTitle(String title) { + this.title = title; + } + + public void removePhotos(List photoIds) { + photoIds.forEach(this::removePhoto); + } + + private void removePhoto(Long photoId) { + items.removeIf(item -> { + if (Objects.equals(item.getPhotoId(), photoId)) { + item.removeAlbum(); + return true; + } + return false; + }); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/album/model/Albums.java b/src/main/java/kr/kro/photoliner/domain/album/model/Albums.java deleted file mode 100644 index 0309497..0000000 --- a/src/main/java/kr/kro/photoliner/domain/album/model/Albums.java +++ /dev/null @@ -1,12 +0,0 @@ -package kr.kro.photoliner.domain.album.model; - -import java.util.List; - -public record Albums( - List albums -) { - - public int count() { - return albums.size(); - } -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhoto.java b/src/main/java/kr/kro/photoliner/domain/album/model/PhotoItem.java similarity index 62% rename from src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhoto.java rename to src/main/java/kr/kro/photoliner/domain/album/model/PhotoItem.java index 14c3a57..a8f4a9b 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhoto.java +++ b/src/main/java/kr/kro/photoliner/domain/album/model/PhotoItem.java @@ -1,17 +1,15 @@ -package kr.kro.photoliner.domain.photo.model; +package kr.kro.photoliner.domain.album.model; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import java.util.Objects; -import kr.kro.photoliner.domain.album.model.Album; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,20 +18,29 @@ @AllArgsConstructor @Table(name = "albums_photos") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class AlbumPhoto { +@Builder +public class PhotoItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne @JoinColumn(name = "album_id", nullable = false) private Album album; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "photo_id", nullable = false) - private Photo photo; + private Long photoId; - public boolean isIncludedInAlbum(Long albumId) { - return Objects.equals(album.getId(), albumId); + public PhotoItem(Album album, Long photoId) { + this.album = album; + this.photoId = photoId; + } + + public static PhotoItem of(Album album, Long photoId) { + return new PhotoItem(album, photoId); + } + + public void removeAlbum() { + this.album = null; } } diff --git a/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoView.java b/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoView.java new file mode 100644 index 0000000..8028d98 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoView.java @@ -0,0 +1,59 @@ +package kr.kro.photoliner.domain.album.model.view; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.Getter; +import org.hibernate.annotations.Immutable; +import org.locationtech.jts.geom.Point; + +@Entity +@Immutable +@Getter +@Table(name = "vw_album_photos") +public class AlbumPhotoView { + @Id + @Column(name = "id") + private Long id; + + @Column(name = "photo_id", nullable = false) + private Long photoId; + + @Column(name = "file_name") + private String fileName; + + @Column(name = "file_path") + private String filePath; + + @Column(name = "thumbnail_path") + private String thumbnailPath; + + @Column(name = "captured_dt") + private LocalDateTime capturedDt; + + @Column(name = "location") + private Point location; + + @Column(name = "album_id") + private Long albumId; + + @Column(name = "user_id") + private Long userId; + + public Double getLatitude() { + if (Objects.isNull(location)) { + return null; + } + return location.getX(); + } + + public Double getLongitude() { + if (Objects.isNull(location)) { + return null; + } + return location.getY(); + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoViews.java b/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoViews.java new file mode 100644 index 0000000..812e03b --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/model/view/AlbumPhotoViews.java @@ -0,0 +1,21 @@ +package kr.kro.photoliner.domain.album.model.view; + +import java.util.List; +import java.util.Objects; + +public record AlbumPhotoViews( + List albumPhotoViews +) { + + public List filterIncludedInAlbum(Long albumId) { + return albumPhotoViews.stream() + .filter(albumPhotoView -> Objects.equals(albumPhotoView.getAlbumId(), albumId)) + .toList(); + } + + public List filterExcludedFromAlbum(Long albumId) { + return albumPhotoViews.stream() + .filter(albumPhotoView -> !Objects.equals(albumPhotoView.getAlbumId(), albumId)) + .toList(); + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumPhotoRepository.java b/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumPhotoRepository.java new file mode 100644 index 0000000..09158a8 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumPhotoRepository.java @@ -0,0 +1,30 @@ +package kr.kro.photoliner.domain.album.repository; + +import java.util.List; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoView; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoViews; +import org.locationtech.jts.geom.Point; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface AlbumPhotoRepository extends JpaRepository { + Page findByAlbumId(Long albumId, Pageable pageable); + + @Query(""" + select apv + from AlbumPhotoView apv + where apv.userId = :userId + and function('st_x', apv.location) between function('st_x', :sw) and function('st_x', :ne) + and function('st_y', apv.location) between function('st_y', :sw) and function('st_y', :ne) + order by apv.capturedDt desc + """) + List findByUserIdInBox(Long userId, Point sw, Point ne); + + default AlbumPhotoViews getByUserIdInBox(Long userId, Point sw, Point ne) { + return new AlbumPhotoViews( + findByUserIdInBox(userId, sw, ne) + ); + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumRepository.java b/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumRepository.java index 861939c..ff82907 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/album/repository/AlbumRepository.java @@ -10,4 +10,5 @@ public interface AlbumRepository extends JpaRepository { Album save(Album album); Page findByUserId(Long userId, Pageable pageable); + } diff --git a/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java b/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java index 5c1df17..ed2b411 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java +++ b/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java @@ -2,9 +2,15 @@ import kr.kro.photoliner.domain.album.dto.request.AlbumCreateRequest; import kr.kro.photoliner.domain.album.dto.request.AlbumDeleteRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumItemCreateRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumItemDeleteRequest; +import kr.kro.photoliner.domain.album.dto.request.AlbumTitleUpdateRequest; import kr.kro.photoliner.domain.album.dto.response.AlbumCreateResponse; +import kr.kro.photoliner.domain.album.dto.response.AlbumPhotoItemsResponse; import kr.kro.photoliner.domain.album.dto.response.AlbumsResponse; import kr.kro.photoliner.domain.album.model.Album; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoView; +import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; import kr.kro.photoliner.domain.album.repository.AlbumRepository; import kr.kro.photoliner.domain.user.model.User; import kr.kro.photoliner.domain.user.repository.UserRepository; @@ -22,13 +28,14 @@ public class AlbumService { private final UserRepository userRepository; private final AlbumRepository albumRepository; + private final AlbumPhotoRepository albumPhotoRepository; @Transactional public AlbumCreateResponse createAlbum(AlbumCreateRequest request) { User user = userRepository.findUserById(request.userId()) .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + request.userId())); Album album = Album.builder() - .name(request.name()) + .title(request.title()) .user(user) .build(); Album savedAlbum = albumRepository.save(album); @@ -41,8 +48,35 @@ public AlbumsResponse getAlbums(Long userId, Pageable pageable) { return AlbumsResponse.from(albums); } + @Transactional(readOnly = true) + public AlbumPhotoItemsResponse getAlbumPhotoItems(Long albumId, Pageable pageable) { + Page albumPhotoViews = albumPhotoRepository.findByAlbumId(albumId, pageable); + return AlbumPhotoItemsResponse.from(albumPhotoViews); + } + + @Transactional + public void updateAlbumTitle(Long albumId, AlbumTitleUpdateRequest request) { + Album album = albumRepository.findById(albumId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_ALBUM, "album id: " + albumId)); + album.updateTitle(request.title()); + } + @Transactional public void deleteAlbums(AlbumDeleteRequest request) { albumRepository.deleteAllByIdInBatch(request.ids()); } + + @Transactional + public void createAlbumItems(Long albumId, AlbumItemCreateRequest request) { + Album album = albumRepository.findById(albumId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_ALBUM, "album id: " + albumId)); + album.addPhotos(request.ids()); + } + + @Transactional + public void deleteAlbumItems(Long albumId, AlbumItemDeleteRequest request) { + Album album = albumRepository.findById(albumId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_ALBUM, "album id: " + albumId)); + album.removePhotos(request.ids()); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 9e61d68..34d0cb0 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -43,16 +43,7 @@ public ResponseEntity getPhotos( @RequestParam Long userId, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable ) { - return ResponseEntity.ok(photoService.getPhotos(userId, pageable)); - } - - @GetMapping("/albums/{albumId}") - public ResponseEntity getPhotosByAlbumId( - @PathVariable Long albumId, - @RequestParam Long userId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable - ) { - return ResponseEntity.ok(photoService.getPhotosByAlbumId(userId, albumId, pageable)); + return ResponseEntity.ok(photoService.getPhotosByIds(userId, pageable)); } @GetMapping("/markers") diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/MapMarkersResponse.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/MapMarkersResponse.java index 2b1778a..6c23ace 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/MapMarkersResponse.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/MapMarkersResponse.java @@ -2,17 +2,18 @@ import java.time.LocalDateTime; import java.util.List; -import kr.kro.photoliner.domain.photo.model.Photo; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoView; public record MapMarkersResponse( InnerPhotoMarkers innerPhotoMarkers, InnerPoiMarkers innerPoiMarkers ) { - public static MapMarkersResponse of(List photosInDate, List photosOutOfDate) { + public static MapMarkersResponse of(List photosIncludedAlbum, + List photosExcludedAlbum) { return new MapMarkersResponse( - InnerPhotoMarkers.from(photosInDate), - InnerPoiMarkers.from(photosOutOfDate) + InnerPhotoMarkers.from(photosIncludedAlbum), + InnerPoiMarkers.from(photosExcludedAlbum) ); } @@ -21,7 +22,7 @@ public record InnerPhotoMarkers( List photoMarkers ) { - public static InnerPhotoMarkers from(List photos) { + public static InnerPhotoMarkers from(List photos) { return new InnerPhotoMarkers( photos.size(), photos.stream() @@ -39,7 +40,7 @@ public record InnerPhotoMarker( Double lng ) { - public static InnerPhotoMarker from(Photo photo) { + public static InnerPhotoMarker from(AlbumPhotoView photo) { return new InnerPhotoMarker( photo.getId(), photo.getCapturedDt(), @@ -57,7 +58,7 @@ public record InnerPoiMarkers( List markers ) { - public static InnerPoiMarkers from(List photos) { + public static InnerPoiMarkers from(List photos) { return new InnerPoiMarkers( photos.size(), photos.stream() @@ -75,7 +76,7 @@ public record InnerPoiMarker( Double lng ) { - public static InnerPoiMarker from(Photo photo) { + public static InnerPoiMarker from(AlbumPhotoView photo) { return new InnerPoiMarker( photo.getId(), photo.getCapturedDt(), diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java index 57b9c64..ea3a005 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java @@ -35,7 +35,7 @@ public ExifData extract(MultipartFile file) { Point location = extractGpsLocation(metadata); return new ExifData(capturedDt, location); } catch (ImageProcessingException | IOException e) { - throw CustomException.of(ApiResponseCode.FILE_PROCESSING_ERROR, "file name: " + file.getOriginalFilename(), + throw CustomException.of(ApiResponseCode.FILE_PROCESSING_ERROR, "file title: " + file.getOriginalFilename(), e); } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java index a41ac18..4a6b346 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java @@ -94,7 +94,7 @@ private void validateFile(MultipartFile file) { String originalFilename = file.getOriginalFilename(); if (originalFilename == null || originalFilename.contains("..")) { - throw CustomException.of(ApiResponseCode.INVALID_FILE_NAME, "file name: " + originalFilename); + throw CustomException.of(ApiResponseCode.INVALID_FILE_NAME, "file title: " + originalFilename); } } @@ -110,7 +110,7 @@ private void saveFile(MultipartFile file, Path directory, String fileName) { Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { - throw CustomException.of(ApiResponseCode.FILE_STORE_ERROR, "file name: " + file.getOriginalFilename(), e); + throw CustomException.of(ApiResponseCode.FILE_STORE_ERROR, "file title: " + file.getOriginalFilename(), e); } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhotos.java b/src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhotos.java deleted file mode 100644 index 4385cdd..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/model/AlbumPhotos.java +++ /dev/null @@ -1,22 +0,0 @@ -package kr.kro.photoliner.domain.photo.model; - -import java.util.List; - -public record AlbumPhotos( - List albumPhotos -) { - public List getPhotoIncludedAlbum(Long albumId) { - return albumPhotos.stream() - .filter(albumPhoto -> albumPhoto.isIncludedInAlbum(albumId)) - .map(AlbumPhoto::getPhoto) - .toList(); - } - - public List getPhotoNotIncludedAlbum(Long albumId) { - return albumPhotos.stream() - .filter(albumPhoto -> albumPhoto.isIncludedInAlbum(albumId)) - .map(AlbumPhoto::getPhoto) - .toList(); - } - -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java b/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java index 8c9cc47..8ba6830 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java @@ -10,10 +10,8 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Objects; -import java.util.Optional; import kr.kro.photoliner.common.model.BaseEntity; import kr.kro.photoliner.domain.user.model.User; import lombok.AccessLevel; @@ -57,13 +55,6 @@ public class Photo extends BaseEntity { @JoinColumn(name = "user_id", referencedColumnName = "id") private User user; - public boolean isBetween(LocalDate start, LocalDate end) { - return Optional.ofNullable(capturedDt) - .map(LocalDateTime::toLocalDate) - .filter(localDate -> localDate.isAfter(start) && localDate.isBefore(end)) - .isPresent(); - } - public void updateCapturedDate(LocalDateTime capturedDt) { this.capturedDt = capturedDt; } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/model/Photos.java b/src/main/java/kr/kro/photoliner/domain/photo/model/Photos.java index 9b97f8a..19ae167 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/model/Photos.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/model/Photos.java @@ -10,9 +10,4 @@ public int count() { return photos.size(); } - public List getPhotoIds() { - return photos.stream() - .map(Photo::getId) - .toList(); - } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/repository/AlbumPhotoRepository.java b/src/main/java/kr/kro/photoliner/domain/photo/repository/AlbumPhotoRepository.java deleted file mode 100644 index a376104..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/repository/AlbumPhotoRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package kr.kro.photoliner.domain.photo.repository; - -import java.util.List; -import kr.kro.photoliner.domain.photo.model.AlbumPhoto; -import kr.kro.photoliner.domain.photo.model.AlbumPhotos; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AlbumPhotoRepository extends JpaRepository { - - List findByPhotoIdIn(List ids); - - default AlbumPhotos getByPhotoIdIn(List ids) { - return new AlbumPhotos( - findByPhotoIdIn(ids) - ); - } -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java b/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java index 0e65278..c7123c9 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java @@ -1,14 +1,10 @@ package kr.kro.photoliner.domain.photo.repository; -import java.util.List; import java.util.Optional; import kr.kro.photoliner.domain.photo.model.Photo; -import kr.kro.photoliner.domain.photo.model.Photos; -import org.locationtech.jts.geom.Point; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; public interface PhotoRepository extends JpaRepository { @@ -17,33 +13,8 @@ Page findByUserId( Pageable pageable ); - @Query(""" - select p - from Photo p - inner join AlbumPhoto ap on ap.photo.id = p.id - where ap.album.id = :albumId - """) - Page findByUserIdAndAlbumId(Long userId, Long albumId, Pageable pageable); - - @Query(""" - select p - from Photo p - where p.user.id = :userId - and function('st_x', p.location) between function('st_x', :sw) and function('st_x', :ne) - and function('st_y', p.location) between function('st_y', :sw) and function('st_y', :ne) - order by p.capturedDt desc - """) - List findByUserIdInBox( - Long userId, - Point sw, - Point ne - ); - - default Photos getPhotosByUserIdInBox(Long userId, Point sw, Point ne) { - return new Photos(findByUserIdInBox(userId, sw, ne)); - } - Photo save(Photo photo); Optional findById(Long photoId); + } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 0867d17..07021e1 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -1,6 +1,8 @@ package kr.kro.photoliner.domain.photo.service; import java.util.List; +import kr.kro.photoliner.domain.album.model.view.AlbumPhotoViews; +import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; import kr.kro.photoliner.domain.photo.dto.DeletePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.MapMarkersRequest; import kr.kro.photoliner.domain.photo.dto.request.PhotoCapturedDateUpdateRequest; @@ -8,10 +10,7 @@ import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; import kr.kro.photoliner.domain.photo.infra.FileStorage; -import kr.kro.photoliner.domain.photo.model.AlbumPhotos; import kr.kro.photoliner.domain.photo.model.Photo; -import kr.kro.photoliner.domain.photo.model.Photos; -import kr.kro.photoliner.domain.photo.repository.AlbumPhotoRepository; import kr.kro.photoliner.domain.photo.repository.PhotoRepository; import kr.kro.photoliner.global.code.ApiResponseCode; import kr.kro.photoliner.global.exception.CustomException; @@ -27,32 +26,26 @@ @RequiredArgsConstructor public class PhotoService { - private final AlbumPhotoRepository albumPhotoRepository; private final PhotoRepository photoRepository; + private final AlbumPhotoRepository albumPhotoRepository; private final GeometryFactory geometryFactory; private final FileStorage fileStorage; @Transactional(readOnly = true) - public PhotosResponse getPhotos(Long userId, Pageable pageable) { + public PhotosResponse getPhotosByIds(Long userId, Pageable pageable) { return PhotosResponse.from(photoRepository.findByUserId(userId, pageable)); } - @Transactional(readOnly = true) - public PhotosResponse getPhotosByAlbumId(Long userId, Long albumId, Pageable pageable) { - return PhotosResponse.from(photoRepository.findByUserIdAndAlbumId(userId, albumId, pageable)); - } - @Transactional(readOnly = true) public MapMarkersResponse getMarkersInViewport(MapMarkersRequest request) { Point sw = geometryFactory.createPoint(request.getSouthWestCoordinate()); Point ne = geometryFactory.createPoint(request.getNorthEastCoordinate()); - Photos photos = photoRepository.getPhotosByUserIdInBox(request.userId(), sw, ne); - AlbumPhotos albumPhotos = albumPhotoRepository.getByPhotoIdIn(photos.getPhotoIds()); + AlbumPhotoViews albumPhotoViews = albumPhotoRepository.getByUserIdInBox(request.userId(), sw, ne); return MapMarkersResponse.of( - albumPhotos.getPhotoIncludedAlbum(request.albumId()), - albumPhotos.getPhotoNotIncludedAlbum(request.albumId()) + albumPhotoViews.filterIncludedInAlbum(request.albumId()), + albumPhotoViews.filterExcludedFromAlbum(request.albumId()) ); } diff --git a/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java b/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java index 71903c9..f783982 100644 --- a/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java +++ b/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java @@ -40,6 +40,7 @@ public enum ApiResponseCode { */ NOT_FOUND_USER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."), NOT_FOUND_PHOTO(HttpStatus.NOT_FOUND, "사진이 존재하지 않습니다."), + NOT_FOUND_ALBUM(HttpStatus.NOT_FOUND, "앨범이 존재하지 않습니다."), /** * 409 CONFLICT (중복 혹은 충돌) @@ -62,8 +63,7 @@ public enum ApiResponseCode { FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다."), FILE_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 생성 중 오류가 발생했습니다."), DIRECTORY_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다."), - FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다.") - ; + FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/resources/db/migration/V6__alter_albums_table.sql b/src/main/resources/db/migration/V6__alter_albums_table.sql new file mode 100644 index 0000000..22788ae --- /dev/null +++ b/src/main/resources/db/migration/V6__alter_albums_table.sql @@ -0,0 +1,2 @@ +alter table albums + change name title varchar(20) diff --git a/src/main/resources/db/migration/V7__create_album_photos_view.sql b/src/main/resources/db/migration/V7__create_album_photos_view.sql new file mode 100644 index 0000000..c7fe49a --- /dev/null +++ b/src/main/resources/db/migration/V7__create_album_photos_view.sql @@ -0,0 +1,13 @@ +create view vw_album_photos +as +select ap.id, + p.id as photo_id, + p.file_name, + p.file_path, + p.thumbnail_path, + p.captured_dt, + p.location, + ap.album_id, + p.user_id +from photos p + left outer join albums_photos ap on ap.photo_id = p.id