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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class PhotoCallbackService {
public void markUploadCompleted(PhotoCompleteRequest request) {
int updated = photoRepository.updateStatusAndUrl(
request.photoId(),
PhotoStatus.PROCESSING,
PhotoStatus.UPLOADING,
PhotoStatus.COMPLETED,
request.thumbnailUrl()
);
Expand Down
69 changes: 22 additions & 47 deletions src/main/java/com/cheeeese/photo/application/PhotoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@
import com.cheeeese.photo.exception.code.PhotoErrorCode;
import com.cheeeese.photo.infrastructure.mapper.PhotoMapper;
import com.cheeeese.photo.infrastructure.persistence.PhotoRepository;
import com.cheeeese.user.application.UserService;
import com.cheeeese.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

private final UserService userService;
private final PhotoRepository photoRepository;
private final PhotoValidator photoValidator;
private final AlbumValidator albumValidator;
private final AlbumRepository albumRepository;
private final PresignedUrlService presignedUrlService;

@Value("${ncp.object-storage.bucket}")
private String bucket;

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

public List<Photo> getRecentPhotosForNewEnter(Long albumId) {
Expand All @@ -49,23 +54,29 @@ public PhotoPresignedUrlResponse createPresignedUrls(User user, PhotoPresignedUr
Album album = validateAlbumAndPermission(user, request.albumCode());
validateUploadRequest(album, request);

int updatedRows = albumRepository.incrementPhotoCount(album.getId(), request.fileInfos().size());
int uploadCount = request.fileInfos().size();

int updatedRows = albumRepository.incrementPhotoCount(album.getId(), uploadCount);
if (updatedRows != 1) {
throw new PhotoException(PhotoErrorCode.PHOTO_COUNT_INCREMENT_FAILED);
}

userService.incrementPhotoCount(user.getId(), uploadCount);

List<PhotoPresignedUrlResponse.PresignedUrlInfo> presignedUrls = generatePresignedUrls(user, album, request.fileInfos());
return PhotoMapper.toPresignedUrlResponse(presignedUrls);
}

@Transactional
public void reportUploadResult(User user, PhotoUploadReportRequest request) {
PhotoValidator.ValidatedPhotos validated = validateRequestAndPhotos(user, request);
Long albumId = validated.albumId();
List<Long> failurePhotoIds = request.failurePhotoIds().stream()
.distinct()
.toList();

handleSuccessfulUploads(user.getId(), request.successPhotoIds());
PhotoValidator.ValidatedPhotos validated = photoValidator.validatePhotos(user.getId(), failurePhotoIds);
Long albumId = validated.albumId();

handleFailedUploads(user.getId(), albumId, request.failurePhotoIds());
handleFailedUploads(user, albumId, failurePhotoIds);
}

private Album validateAlbumAndPermission(User user, String albumCode) {
Expand Down Expand Up @@ -108,9 +119,10 @@ private PhotoPresignedUrlResponse.PresignedUrlInfo createPresignedUrlForFile(
photo.getId(),
safeFileName
);
String imageUrl = bucket + "/" + objectKey;
photo.updateImageUrl(imageUrl);

String uploadUrl = presignedUrlService.generatePresignedPutUrl(objectKey, file.contentType());
photo.updateImageUrl(objectKey);

return PhotoMapper.toPresignedUrlInfo(photo.getId(), uploadUrl);
}
Expand All @@ -125,48 +137,10 @@ private String sanitizeFileName(String raw) {
return name;
}

private PhotoValidator.ValidatedPhotos validateRequestAndPhotos(User user, PhotoUploadReportRequest request) {
var success = new java.util.HashSet<>(request.successPhotoIds());
var failure = new java.util.HashSet<>(request.failurePhotoIds());
success.retainAll(failure);

if (!success.isEmpty()) {
throw new PhotoException(PhotoErrorCode.PHOTO_REPORT_CONFLICTING_IDS);
}

List<Long> allPhotoIds = Stream.concat(
request.successPhotoIds().stream(),
request.failurePhotoIds().stream()
).toList();

return photoValidator.validatePhotos(user.getId(), allPhotoIds);
}

private void handleSuccessfulUploads(Long userId, List<Long> successPhotoIds) {
if (successPhotoIds == null || successPhotoIds.isEmpty()) {
return;
}

int updatedRows = photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus(
successPhotoIds,
userId,
PhotoStatus.PROCESSING,
PhotoStatus.UPLOADING
);

if (updatedRows != successPhotoIds.size()) {
throw new PhotoException(PhotoErrorCode.PHOTO_STATUS_UPDATE_FAILED);
}
}

private void handleFailedUploads(Long userId, Long albumId, List<Long> failurePhotoIds) {
if (failurePhotoIds == null || failurePhotoIds.isEmpty()) {
return;
}

private void handleFailedUploads(User user, Long albumId, List<Long> failurePhotoIds) {
int updatedRows = photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus(
failurePhotoIds,
userId,
user.getId(),
PhotoStatus.FAILED,
PhotoStatus.UPLOADING
);
Expand All @@ -180,6 +154,7 @@ private void handleFailedUploads(Long userId, Long albumId, List<Long> failurePh
if (decremented == 0) {
throw new PhotoException(PhotoErrorCode.PHOTO_COUNT_DECREMENT_FAILED);
}
userService.decrementPhotoCount(user.getId(), updatedRows);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,15 @@ private void validatePhotoIdsNotEmpty(List<Long> photoIds) {
* photoId 리스트 기반으로 존재하는 사진 조회 및 존재 검증
*/
private List<Photo> findAndValidateExistence(List<Long> photoIds) {
Set<Long> requestedIds = new HashSet<>(photoIds);
List<Photo> photos = photoRepository.findAllById(requestedIds);
List<Photo> photos = photoRepository.findAllById(photoIds);

Set<Long> uniqueRequestedIds = new HashSet<>(photoIds);

Set<Long> foundIds = photos.stream()
.map(Photo::getId)
.collect(Collectors.toSet());

Set<Long> missingIds = requestedIds.stream()
Set<Long> missingIds = uniqueRequestedIds.stream()
.filter(id -> !foundIds.contains(id))
.collect(Collectors.toSet());

Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/cheeeese/photo/domain/PhotoStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

public enum PhotoStatus {
UPLOADING,
PROCESSING,
COMPLETED,
FAILED
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@
import java.util.List;

@Builder
@Schema(description = "사진 업로드 결과 보고 요청 (부분 성공/실패 처리)")
@Schema(description = "사진 업로드 결과 보고 요청 (실패 처리)")
public record PhotoUploadReportRequest(
@NotNull
@Schema(description = "업로드가 성공적으로 완료된 사진 ID 목록 (UPLOADING -> PROCESSING)", example = "[100, 102]")
List<Long> successPhotoIds,

@NotNull
@Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백)", example = "[101, 103]")
@Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백)", example = "[1, 3]")
List<Long> failurePhotoIds
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,15 @@ CommonResponse<PhotoPresignedUrlResponse> createPresignedUrls(
);

@Operation(
summary = "사진 업로드 결과 보고 API (부분 성공/실패 처리)", // [추가]
summary = "사진 업로드 결과 보고 API (실패 처리)", // [추가]
description = """
### RequestBody
---
`successPhotoIds`: Object Storage 업로드 성공 ID 목록 \n
`failurePhotoIds`: Object Storage 업로드 실패 ID 목록 \n

### 로직 상세
---
1. **Success IDs 처리**: `Photo` 상태를 `UPLOADING`에서 `PROCESSING`으로 변경 (후처리 대기).
2. **Failure IDs 처리**: `Photo` 상태를 `UPLOADING`에서 `FAILED`으로 변경, 앨범의 `currentPhotoCount`를 **롤백** (감소)합니다.
1. **Failure IDs 처리**: `Photo` 상태를 `UPLOADING`에서 `FAILED`으로 변경, 앨범의 `currentPhotoCount`를 **롤백** (감소)합니다.
"""
)
@ApiResponses(value = {
Expand All @@ -106,20 +104,6 @@ CommonResponse<PhotoPresignedUrlResponse> createPresignedUrls(
""")
)
),
@ApiResponse(
responseCode = "400",
description = "업로드 결과(success/failure) 목록에 중복된 사진 ID가 포함되어 있습니다.",
content = @Content(
mediaType = "application/json",
schema = @Schema(example = """
{
"isSuccess": false,
"code": 400,
"message": "업로드 결과(success/failure) 목록에 중복된 사진 ID가 포함되어 있습니다."
}
""")
)
),
@ApiResponse(
responseCode = "403",
description = "사용자와 사진의 소유자가 일치하지 않습니다.",
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/cheeeese/user/application/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import com.cheeeese.user.domain.User;
import com.cheeeese.user.dto.request.UserAgreementRequest;
import com.cheeeese.user.dto.request.UserProfileRequest;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -14,6 +17,7 @@
public class UserService {

private final UserValidator userValidator;
private final UserRepository userRepository;

@Transactional
public void updateUserProfile(User user, UserProfileRequest request) {
Expand All @@ -33,4 +37,20 @@ public void saveUserAgreement(User user, UserAgreementRequest request) {
request.isThirdPartyAgreement()
);
}

@Transactional
public void incrementPhotoCount(Long userId, int count) {
int updated = userRepository.incrementPhotoCount(userId, count);
if (updated != 1) {
throw new UserException(UserErrorCode.USER_PHOTO_COUNT_INCREMENT_FAILED);
}
}

@Transactional
public void decrementPhotoCount(Long userId, int count) {
int updated = userRepository.decrementPhotoCount(userId, count);
if (updated != 1) {
throw new UserException(UserErrorCode.USER_PHOTO_COUNT_DECREMENT_FAILED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public enum UserErrorCode implements BaseCode {

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."),
REQUIRED_TERMS_NOT_AGREED(HttpStatus.BAD_REQUEST, "필수 약관에 동의하지 않았습니다."),
USER_PHOTO_COUNT_INCREMENT_FAILED(HttpStatus.CONFLICT, "유저의 앨범 사진 개수 증가에 실패했습니다."),
USER_PHOTO_COUNT_DECREMENT_FAILED(HttpStatus.CONFLICT, "유저의 앨범 사진 개수 감소에 실패했습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

import com.cheeeese.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByProviderId(String providerId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE User u SET u.photoCnt = u.photoCnt + :count WHERE u.id = :userId")
int incrementPhotoCount(@Param("userId") Long userId, @Param("count") int count);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE User u SET u.photoCnt = u.photoCnt - :count WHERE u.id = :userId AND u.photoCnt >= :count")
int decrementPhotoCount(@Param("userId") Long userId, @Param("count") int count);
}