Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions src/main/java/com/cheeeese/album/application/AlbumService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@
import com.cheeeese.album.domain.type.AlbumJoinStatus;
import com.cheeeese.album.domain.type.Role;
import com.cheeeese.album.dto.request.AlbumCreationRequest;
import com.cheeeese.album.dto.response.AlbumCreationResponse;
import com.cheeeese.album.dto.response.AlbumInvitationResponse;
import com.cheeeese.album.dto.response.*;
import com.cheeeese.album.exception.AlbumException;
import com.cheeeese.album.exception.code.AlbumErrorCode;
import com.cheeeese.album.infrastructure.mapper.AlbumMapper;
import com.cheeeese.album.infrastructure.mapper.UserAlbumMapper;
import com.cheeeese.album.infrastructure.persistence.AlbumRepository;
import com.cheeeese.photo.application.PhotoService;
import com.cheeeese.album.domain.UserAlbum;
import com.cheeeese.album.dto.response.AlbumEnterResponse;
import com.cheeeese.album.dto.response.AlbumMakerInfo;
import com.cheeeese.album.dto.response.UploadAvailableCountResponse;
import com.cheeeese.album.infrastructure.persistence.UserAlbumRepository;
import com.cheeeese.photo.domain.Photo;
import com.cheeeese.user.domain.User;
import com.cheeeese.user.exception.UserException;
import com.cheeeese.user.exception.code.UserErrorCode;
Expand All @@ -34,6 +31,7 @@
import java.time.LocalTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
Expand Down Expand Up @@ -69,8 +67,8 @@ public AlbumCreationResponse createAlbum(User user, AlbumCreationRequest request
albumRepository.save(album);

userAlbumRepository.save(UserAlbumMapper.toEntity(
user.getId(),
album.getId(),
user,
album,
Role.MAKER
));

Expand Down Expand Up @@ -110,19 +108,22 @@ public AlbumEnterResponse enterAlbum(String code, User currentUser) {

// Case 2: 신규 참여
albumValidator.validateAlbumCapacity(album);
UserAlbum newUserAlbum = UserAlbumMapper.toGuestUserAlbum(currentUser, album);
userAlbumRepository.save(newUserAlbum);
userAlbumRepository.save(UserAlbumMapper.toEntity(
currentUser,
album,
Role.GUEST
));

int updated = albumRepository.incrementParticipantCountAtomically(album.getId());
if (updated == 0) {
throw new AlbumException(AlbumErrorCode.ALBUM_MAX_PARTICIPANT_REACHED);
}

List<String> recentThumbnails = photoService.getRecentThumbnailUrls(album.getId());
List<NewEnterResponse.RecentPhotoResponse> recentPhotos = getRecentPhotosWithUploaderInfo(album.getId());

int remainingUploadSlots = calculateRemainingUploadSlots(album);

return AlbumMapper.toNewResponse(album, makerInfo, remainingUploadSlots, recentThumbnails);
return AlbumMapper.toNewResponse(album, makerInfo, remainingUploadSlots, recentPhotos);
}

public UploadAvailableCountResponse getAvailablePhotoCount(User user, String code) {
Expand Down Expand Up @@ -156,4 +157,23 @@ private User getMaker(Long makerId) {
return userRepository.findById(makerId)
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
}

private List<NewEnterResponse.RecentPhotoResponse> getRecentPhotosWithUploaderInfo(Long albumId) {
List<Photo> photos = photoService.getRecentPhotosForNewEnter(albumId);

if (photos.isEmpty()) {
return List.of();
}

// 1~4개인 경우, 1개만 반환하는 비즈니스 로직 적용
if (photos.size() < 5) {
Photo photo = photos.get(0);
return List.of(AlbumMapper.toRecentPhotoResponse(photo));
}

// 5개인 경우, 5개 모두 반환
return photos.stream()
.map(AlbumMapper::toRecentPhotoResponse)
.collect(Collectors.toList());
}
}
19 changes: 10 additions & 9 deletions src/main/java/com/cheeeese/album/domain/UserAlbum.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cheeeese.album.domain.type.Role;
import com.cheeeese.global.domain.BaseEntity;
import com.cheeeese.user.domain.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
Expand All @@ -19,13 +20,13 @@ public class UserAlbum extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// TODO: 추후 필요시 ManyToOne, JoinColumn 넣을 예정
@Column(name = "user_id", nullable = false)
private Long userId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

// TODO: 추후 필요시 ManyToOne, JoinColumn 넣을 예정
@Column(name = "album_id", nullable = false)
private Long albumId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id", nullable = false)
private Album album;

@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
Expand All @@ -35,9 +36,9 @@ public class UserAlbum extends BaseEntity {
private boolean isVisible;

@Builder
private UserAlbum(Long userId, Long albumId, Role role, boolean isVisible) {
this.userId = userId;
this.albumId = albumId;
private UserAlbum(User user, Album album, Role role, boolean isVisible) {
this.user = user;
this.album = album;
this.role = role;
this.isVisible = isVisible;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cheeeese.album.domain.type.AlbumJoinStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;

import java.time.LocalDateTime;
Expand Down Expand Up @@ -31,6 +32,22 @@ public record NewEnterResponse(
@Schema(description = "남은 업로드 가능 사진 수", example = "5")
Integer remainingUploadSlots,

@Schema(description = "최근 업로드된 사진 썸네일 URL 목록 (최대 5개)", example = "[\"https://cdn.cheeeese.com/album/1/thumb1.jpg\"]")
List<String> recentPhotoUrls
) implements AlbumEnterResponse {}
@Schema(description = "최근 업로드된 사진 목록 (최대 5개)", implementation = RecentPhotoResponse.class)
List<RecentPhotoResponse> recentPhotos
) implements AlbumEnterResponse {
@Builder
@Schema(description = "최근 업로드 사진 정보 DTO")
public record RecentPhotoResponse(
@NotNull
@Schema(description = "사진 썸네일 URL", example = "https://cdn.cheeeese.com/album/1/thumb1.jpg")
String thumbnailUrl,

@NotNull
@Schema(description = "업로드 사용자 이름", example = "김치즈")
String uploaderName,

@NotNull
@Schema(description = "업로드 사용자 프로필 이미지 URL", example = "https://cdn.cheeeese.com/user/1/profile.jpg")
String uploaderProfileImage
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.cheeeese.album.domain.Album;
import com.cheeeese.album.domain.type.AlbumJoinStatus;
import com.cheeeese.album.dto.response.*;
import com.cheeeese.photo.domain.Photo;
import com.cheeeese.user.domain.User;

import java.time.LocalDate;
Expand Down Expand Up @@ -100,7 +101,7 @@ public static NewEnterResponse toNewResponse(
Album album,
AlbumMakerInfo makerInfo,
int remainingUploadSlots,
List<String> recentPhotoUrls
List<NewEnterResponse.RecentPhotoResponse> recentPhotos
) {
return NewEnterResponse.builder()
.joinStatus(AlbumJoinStatus.NEW)
Expand All @@ -110,7 +111,17 @@ public static NewEnterResponse toNewResponse(
.expiredAt(album.getExpiredAt())
.makerInfo(makerInfo)
.remainingUploadSlots(remainingUploadSlots)
.recentPhotoUrls(recentPhotoUrls)
.recentPhotos(recentPhotos)
.build();
}

public static NewEnterResponse.RecentPhotoResponse toRecentPhotoResponse(Photo photo) {
User uploader = photo.getUser();

return NewEnterResponse.RecentPhotoResponse.builder()
.thumbnailUrl(photo.getThumbnailUrl())
.uploaderName(uploader.getName())
.uploaderProfileImage(uploader.getProfileImage())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,12 @@

public class UserAlbumMapper {

public static UserAlbum toEntity(Long userId, Long albumId, Role role) {
public static UserAlbum toEntity(User user, Album album, Role role) {
return UserAlbum.builder()
.userId(userId)
.albumId(albumId)
.user(user)
.album(album)
.role(role)
.isVisible(true)
.build();
}

/**
* User와 Album 엔티티를 기반으로 GUEST 역할의 UserAlbum 엔티티를 생성합니다.
*/
public static UserAlbum toGuestUserAlbum(User user, Album album) {
return UserAlbum.builder()
.userId(user.getId())
.albumId(album.getId())
.role(Role.GUEST)
.isVisible(true)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import com.cheeeese.album.domain.UserAlbum;
import com.cheeeese.album.domain.type.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface UserAlbumRepository extends JpaRepository<UserAlbum, Long> {
Optional<UserAlbum> findByUserIdAndAlbumId(Long userId, Long albumId);
@Query("SELECT ua FROM UserAlbum ua WHERE ua.user.id = :userId AND ua.album.id = :albumId")
Optional<UserAlbum> findByUserIdAndAlbumId(@Param("userId") Long userId, @Param("albumId") Long albumId);

List<UserAlbum> findAllByAlbumId(Long albumId);
@Query("SELECT ua FROM UserAlbum ua WHERE ua.album.id = :albumId")
List<UserAlbum> findAllByAlbumId(@Param("albumId") Long albumId);

Optional<UserAlbum> findByAlbumIdAndUserIdAndRole(Long albumId, Long userId, Role role);
@Query("SELECT ua FROM UserAlbum ua WHERE ua.album.id = :albumId AND ua.user.id = :userId AND ua.role = :role")
Optional<UserAlbum> findByAlbumIdAndUserIdAndRole(@Param("albumId") Long albumId, @Param("userId") Long userId, @Param("role") Role role);
}
24 changes: 8 additions & 16 deletions src/main/java/com/cheeeese/photo/application/PhotoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.cheeeese.photo.infrastructure.persistence.PhotoRepository;
import com.cheeeese.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -35,21 +36,12 @@ public class PhotoService {

private static final String ORIGINAL_PHOTO_PATH_FORMAT = "album/%s/original/%d_%s";

public List<String> getRecentThumbnailUrls(Long albumId) {
List<Photo> photos = photoRepository
.findTop5ByAlbumIdAndIsDeletedFalseAndStatusOrderByCreatedAtDesc(albumId, PhotoStatus.COMPLETED);

if (photos.isEmpty()) {
return List.of();
}

if (photos.size() < 5) {
return List.of(photos.get(0).getThumbnailUrl());
}

return photos.stream()
.map(Photo::getThumbnailUrl)
.collect(Collectors.toList());
public List<Photo> getRecentPhotosForNewEnter(Long albumId) {
return photoRepository.findRecentPhotosByAlbumIdAndStatus(
albumId,
PhotoStatus.COMPLETED,
PageRequest.of(0, 5)
);
}

@Transactional
Expand Down Expand Up @@ -106,7 +98,7 @@ private PhotoPresignedUrlResponse.PresignedUrlInfo createPresignedUrlForFile(
Album album,
PhotoPresignedUrlRequest.FileInfo file
) {
Photo photo = PhotoMapper.toEntity(user.getId(), album.getId());
Photo photo = PhotoMapper.toEntity(user, album);
photoRepository.save(photo);

String safeFileName = sanitizeFileName(file.fileName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private List<Photo> findAndValidateExistence(List<Long> photoIds) {
*/
private void validateOwnership(List<Photo> photos, Long userId) {
boolean invalidOwner = photos.stream()
.anyMatch(photo -> !photo.getUserId().equals(userId));
.anyMatch(photo -> !photo.getUser().getId().equals(userId));

if (invalidOwner) {
throw new PhotoException(PhotoErrorCode.PHOTO_OWNER_MISMATCH);
Expand All @@ -115,7 +115,7 @@ private void validateOwnership(List<Photo> photos, Long userId) {
*/
private Long validateSingleAlbum(List<Photo> photos) {
Set<Long> albumIds = photos.stream()
.map(Photo::getAlbumId)
.map(photo -> photo.getAlbum().getId())
.collect(Collectors.toSet());

if (albumIds.size() != 1) {
Expand Down
24 changes: 13 additions & 11 deletions src/main/java/com/cheeeese/photo/domain/Photo.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.cheeeese.photo.domain;

import com.cheeeese.album.domain.Album;
import com.cheeeese.global.domain.BaseEntity;
import com.cheeeese.user.domain.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
Expand All @@ -20,15 +22,15 @@ public class Photo extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// TODO: 추후 필요시 ManyToOne, JoinColumn 넣을 예정
@Column(name = "user_id", nullable = false)
private Long userId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

// TODO: 추후 필요시 ManyToOne, JoinColumn 넣을 예정
@Column(name = "album_id", nullable = false)
private Long albumId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id", nullable = false)
private Album album;

@Column(name = "image_url", nullable = true, columnDefinition = "TEXT")
@Column(name = "image_url", columnDefinition = "TEXT")
private String imageUrl;

@Column(name = "thumbnail_url", columnDefinition = "TEXT")
Expand All @@ -49,15 +51,15 @@ public class Photo extends BaseEntity {

@Builder
private Photo(
Long userId,
Long albumId,
User user,
Album album,
String imageUrl,
String thumbnailUrl,
LocalDateTime captureTime,
PhotoStatus status
) {
this.userId = userId;
this.albumId = albumId;
this.user = user;
this.album = album;
this.imageUrl = imageUrl;
this.thumbnailUrl = thumbnailUrl;
this.captureTime = captureTime;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.cheeeese.photo.infrastructure.mapper;

import com.cheeeese.album.domain.Album;
import com.cheeeese.photo.domain.Photo;
import com.cheeeese.photo.domain.PhotoStatus;
import com.cheeeese.photo.dto.response.PhotoPresignedUrlResponse;
import com.cheeeese.user.domain.User;

import java.time.LocalDateTime;
import java.util.List;

public class PhotoMapper {

public static Photo toEntity(Long userId, Long albumId) {
public static Photo toEntity(User user, Album album) {
return Photo.builder()
.userId(userId)
.albumId(albumId)
.user(user)
.album(album)
.imageUrl(null) // presigned URL 생성 후 updateImageUrl()로 세팅됨
.thumbnailUrl(null)
.captureTime(LocalDateTime.now())
Expand Down
Loading