Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9f2fe3c
#59 feat: 사용자 정보 제공 API 구현
dahyun24 Nov 14, 2025
a2c95b8
#59 feat: 마이페이지 관련 성공 코드 추가
dahyun24 Nov 14, 2025
6aebc2c
#59 fix: 유저 정보 제공 수정
dahyun24 Nov 14, 2025
db69786
#59 feat: 닫힌 앨범 조회 response
dahyun24 Nov 15, 2025
2692cb6
#59 feat: 열린 앨범 조회 response
dahyun24 Nov 15, 2025
d615694
#59 feat: 마이페이지-앨범 조회 API 구현
dahyun24 Nov 15, 2025
a0d0407
#59 remove: 사용하지 않는 import문 및 메서드 삭제
dahyun24 Nov 15, 2025
2f4d6ab
#59 remove: 치즈네컷 presigned url 기능 제거
dahyun24 Nov 15, 2025
f167e1d
#59 fix: 피드백 반영
dahyun24 Nov 15, 2025
8677cd4
#59 fix: controller 수정에 따른 swagger 수정
dahyun24 Nov 15, 2025
842df06
#59 remove: 불필요한 import문 제거
dahyun24 Nov 15, 2025
1ccab8b
#59 fix: AlbumQueryService N+1 문제 해결
dahyun24 Nov 15, 2025
7f88dee
#59 fix: 앨범당 최대 3개의 썸네일만 조회하는 피드백 반영
dahyun24 Nov 15, 2025
5210175
#59 feat: album DTO에 requiredProperties 블록 생성
dahyun24 Nov 15, 2025
746ebc8
#59 feat: auth DTO에 requiredProperties 블록 생성
dahyun24 Nov 15, 2025
ea6df9e
#59 remove: 치즈네컷 업로드 로직 response 제거
dahyun24 Nov 15, 2025
22a7cfa
#59 feat: cheese4cut DTO에 requiredProperties 블록 생성
dahyun24 Nov 15, 2025
b1e63bb
#59 feat: photo DTO에 requiredProperties 블록 생성
dahyun24 Nov 15, 2025
4c689fd
#59 feat: user DTO에 requiredProperties 블록 생성
dahyun24 Nov 15, 2025
e85990c
#59 fix: 피드백 반영
dahyun24 Nov 15, 2025
0cd1281
#59 fix: 피드백 반영
dahyun24 Nov 15, 2025
9f45bfc
#59 fix: 피드백 재반영
dahyun24 Nov 15, 2025
6d251a7
Merge branch 'develop' into feat/#59-mypage
dahyun24 Nov 15, 2025
8b2d228
#59 fix: 지워진 스키마 복구
dahyun24 Nov 16, 2025
ad9bc10
Merge branch 'feat/#59-mypage' of https://github.com/Say-Cheeeese/BE …
dahyun24 Nov 16, 2025
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
187 changes: 187 additions & 0 deletions src/main/java/com/cheeeese/album/application/AlbumQueryService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.cheeeese.album.application;

import com.cheeeese.album.domain.Album;
import com.cheeeese.album.domain.type.Role;
import com.cheeeese.album.dto.response.ClosedAlbumPageResponse;
import com.cheeeese.album.dto.response.ClosedAlbumSummaryResponse;
import com.cheeeese.album.dto.response.OpenAlbumPageResponse;
import com.cheeeese.album.dto.response.OpenAlbumSummaryResponse;
import com.cheeeese.album.infrastructure.mapper.AlbumQueryMapper;
import com.cheeeese.album.infrastructure.persistence.UserAlbumRepository;
import com.cheeeese.cheese4cut.domain.Cheese4cutPhoto;
import com.cheeeese.cheese4cut.infrastructure.persistence.Cheese4cutPhotoRepository;
import com.cheeeese.global.util.resolver.CdnUrlResolver;
import com.cheeeese.photo.domain.Photo;
import com.cheeeese.photo.domain.PhotoStatus;
import com.cheeeese.photo.infrastructure.persistence.PhotoRepository;
import com.cheeeese.user.domain.User;
import com.cheeeese.user.exception.UserException;
import com.cheeeese.user.exception.code.UserErrorCode;
import com.cheeeese.user.infrastructure.persistence.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AlbumQueryService {

private static final int RECENT_THUMBNAIL_COUNT = 3;

private final UserAlbumRepository userAlbumRepository;
private final UserRepository userRepository;
private final PhotoRepository photoRepository;
private final Cheese4cutPhotoRepository cheese4cutPhotoRepository;
private final CdnUrlResolver cdnUrlResolver;

public OpenAlbumPageResponse getOpenAlbums(User user, int page, int size) {
Pageable pageable = PageRequest.of(page, size);

Slice<Album> albums = userAlbumRepository.findOpenAlbumsByUserId(
user.getId(),
Album.AlbumStatus.ACTIVE,
LocalDateTime.now(),
pageable
);

List<OpenAlbumSummaryResponse> responses = buildOpenAlbumResponses(albums.getContent());

return AlbumQueryMapper.toOpenAlbumPageResponse(responses, albums);
}

public OpenAlbumPageResponse getMyOpenAlbums(User user, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Slice<Album> albums = userAlbumRepository.findOpenAlbumsByUserIdAndRole(
user.getId(),
Role.MAKER,
Album.AlbumStatus.ACTIVE,
LocalDateTime.now(),
pageable
);

List<OpenAlbumSummaryResponse> responses = buildOpenAlbumResponses(albums.getContent());

return AlbumQueryMapper.toOpenAlbumPageResponse(responses, albums);
}

public ClosedAlbumPageResponse getClosedAlbums(User user, int page, int size) {
Pageable pageable = PageRequest.of(page, size);

Slice<Album> expiredAlbums = userAlbumRepository.findClosedAlbumsByUserId(
user.getId(),
Album.AlbumStatus.EXPIRED,
pageable
);

List<Long> albumIds = expiredAlbums.getContent().stream()
.map(Album::getId)
.toList();

if (albumIds.isEmpty()) {
return AlbumQueryMapper.toClosedAlbumPageResponse(List.of(), expiredAlbums);
}

Map<Long, User> makerMap = getMakers(expiredAlbums.getContent());

List<Cheese4cutPhoto> allCheese4cutPhotos = cheese4cutPhotoRepository.findAllCheese4cutPhotosByAlbumIds(albumIds);

Map<Long, List<Cheese4cutPhoto>> cheese4cutPhotoMap = allCheese4cutPhotos.stream()
.collect(Collectors.groupingBy(
c4p -> c4p.getCheese4cut().getAlbum().getId(),
Collectors.toList()
));

List<ClosedAlbumSummaryResponse> responses = expiredAlbums.getContent().stream()
.map(album -> {
List<Cheese4cutPhoto> c4pList = cheese4cutPhotoMap.getOrDefault(album.getId(), List.of());

User maker = Optional.ofNullable(makerMap.get(album.getMakerId()))
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));

List<String> thumbnails = c4pList.stream()
.sorted(Comparator.comparingInt(Cheese4cutPhoto::getPhotoRank)) // photoRank 순으로 정렬
.map(Cheese4cutPhoto::getThumbnailImageUrl)
.map(cdnUrlResolver::resolveThumbnail)
.collect(Collectors.toList());

return AlbumQueryMapper.toClosedAlbumSummaryResponse(album, maker, thumbnails);
})
.collect(Collectors.toList());

return AlbumQueryMapper.toClosedAlbumPageResponse(responses, expiredAlbums);
}

private List<OpenAlbumSummaryResponse> buildOpenAlbumResponses(List<Album> albums) {
if (albums.isEmpty()) {
return List.of();
}

Map<Long, User> makerMap = getMakers(albums);
Map<Long, List<String>> recentThumbnailsMap = getRecentThumbnailsMap(albums);

return albums.stream()
.map(album -> {
User maker = Optional.ofNullable(makerMap.get(album.getMakerId()))
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));

List<String> recentThumbnails = recentThumbnailsMap.getOrDefault(album.getId(), List.of());
return AlbumQueryMapper.toOpenAlbumSummaryResponse(album, maker, recentThumbnails);
})
.toList();
}

private Map<Long, User> getMakers(List<Album> albums) {
List<Long> makerIds = albums.stream()
.map(Album::getMakerId)
.distinct()
.toList();
Map<Long, User> makers = userRepository.findAllById(makerIds).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
if (makers.size() != makerIds.size()) {
throw new UserException(UserErrorCode.USER_NOT_FOUND);
}
return makers;
}

private Map<Long, List<String>> getRecentThumbnailsMap(List<Album> albums) {
List<Long> albumIds = albums.stream()
.map(Album::getId)
.toList();

if (albumIds.isEmpty()) {
return Map.of();
}

List<Photo> photos = photoRepository.findTop3RecentPhotosInEachAlbum(
albumIds,
PhotoStatus.COMPLETED
);

Map<Long, List<String>> thumbnailsMap = new HashMap<>();

for (Photo photo : photos) {
Long albumId = photo.getAlbum().getId();
List<String> thumbnails = thumbnailsMap.computeIfAbsent(albumId, key -> new ArrayList<>());

if (thumbnails.size() < RECENT_THUMBNAIL_COUNT) {
thumbnails.add(cdnUrlResolver.resolveThumbnail(photo.getThumbnailUrl()));
}
}

return thumbnailsMap.entrySet().stream()
.filter(entry -> entry.getValue().size() == RECENT_THUMBNAIL_COUNT)
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> List.copyOf(entry.getValue())
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
import java.time.LocalDate;

@Builder
@Schema(description = "앨범 생성 API")
@Schema(
description = "앨범 생성 요청",
requiredProperties = {
"themeEmoji",
"title",
"participant",
"eventDate"
}
)
public record AlbumCreationRequest(
@Schema(description = "앨범 테마 이모지", example = "U+1F9C0")
String themeEmoji,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
import java.time.LocalDate;

@Builder
@Schema(description = "앨범 생성 API")
public record AlbumCreationResponse(
@Schema(
description = "앨범 생성 응답",
requiredProperties = {
"themeEmoji",
"title",
"eventDate",
"currentPhotoCnt",
"code"
}
)public record AlbumCreationResponse(
@Schema(description = "앨범 테마 이모지", example = "U+1F9C0")
String themeEmoji,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@
import java.time.LocalDateTime;

@Builder
@Schema(description = "앨범 초대장 확인 응답 DTO")
public record AlbumInvitationResponse(
@Schema(
description = "앨범 초대장 확인 응답 DTO",
requiredProperties = {
"title",
"themeEmoji",
"eventDate",
"expiredAt",
"makerName",
"makerProfileImage",
"isExpired"
}
)public record AlbumInvitationResponse(
@Schema(description = "앨범 제목", example = "경영학부 졸업식")
String title,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Schema(description = "앨범 메이커 정보 응답 DTO")
@Schema(
description = "앨범 메이커 정보 응답 DTO",
requiredProperties = {
"makerName",
"makerProfileImage"
}
)
@Builder
public record AlbumMakerInfo(
@Schema(description = "메이커 이름", example = "우다현")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@
import java.util.List;

@Builder
@Schema(description = "앨범 참여자 공통 정보 구조")
@Schema(
description = "앨범 참여자 공통 정보 구조",
requiredProperties = {
"participants"
}
)
public record AlbumParticipantListResponse(
@Schema(description = "참가자 목록 (정렬 포함)")
List<ParticipantInfo> participants
) {
@Builder
@Schema(description = "참가자 개별 정보")
@Schema(
description = "참가자 개별 정보",
requiredProperties = {
"name",
"profileImage",
"role",
"isMe"
}
)
public record ParticipantInfo(
@Schema(description = "이름", example = "우다현")
String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@
import java.util.List;

@Builder
@Schema(description = "앨범 참여자 목록 응답 DTO (활성/만료 공용)")
@Schema(
description = "앨범 참여자 목록 응답 DTO (활성/만료 공용)",
requiredProperties = {
"isExpired",
"title",
"themeEmoji",
"eventDate",
"expiredAt",
"maxParticipantCount",
"currentParticipantCount",
"participants"
}
)
public record AlbumParticipantResponse(

@Schema(description = "만료 여부", example = "false")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.cheeeese.album.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.util.List;

@Builder
@Schema(
description = "닫힌 앨범 목록 페이지 응답",
requiredProperties = {
"responses",
"listSize",
"isFirst",
"isLast",
"hasNext"
}
)
public record ClosedAlbumPageResponse(
@Schema(description = "닫힌 앨범 목록")
List<ClosedAlbumSummaryResponse> responses,

@Schema(description = "현재 페이지의 앨범 수", example = "2")
int listSize,

@Schema(description = "첫 페이지 여부", example = "true")
boolean isFirst,

@Schema(description = "마지막 페이지 여부", example = "false")
boolean isLast,

@Schema(description = "다음 페이지 존재 여부", example = "true")
boolean hasNext
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.cheeeese.album.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.time.LocalDate;
import java.util.List;

@Builder
@Schema(
description = "닫힌 앨범 요약 정보",
requiredProperties = {
"code",
"title",
"makerName",
"eventDate"
}
)
public record ClosedAlbumSummaryResponse(
@Schema(description = "앨범 코드", example = "786ccd09-...")
String code,

@Schema(description = "앨범 제목", example = "봄 소풍")
String title,

@Schema(description = "앨범 생성자 이름", example = "치즈메이커")
String makerName,

@Schema(description = "이벤트 날짜", example = "2025-05-01")
LocalDate eventDate,

@Schema(description = "치즈네컷 썸네일 목록 (4개)", nullable = true)
List<String> thumbnails
) {}
Loading