diff --git a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java index 7b7bfdac..8cbd7682 100644 --- a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java +++ b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java @@ -1,21 +1,28 @@ package org.cotato.csquiz.api.session.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.session.dto.AddSessionPhotoRequest; +import org.cotato.csquiz.api.session.dto.AddSessionPhotoResponse; import org.cotato.csquiz.api.session.dto.AddSessionRequest; import org.cotato.csquiz.api.session.dto.AddSessionResponse; import org.cotato.csquiz.api.session.dto.CsEducationOnSessionNumberResponse; +import org.cotato.csquiz.api.session.dto.DeleteSessionPhotoRequest; import org.cotato.csquiz.api.session.dto.SessionListResponse; import org.cotato.csquiz.api.session.dto.UpdateSessionDescriptionRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoOrderRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.domain.generation.service.SessionService; import org.cotato.csquiz.common.error.exception.ImageException; import org.springframework.http.HttpStatus; 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.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -33,11 +40,13 @@ public class SessionController { private final SessionService sessionService; + @Operation(summary = "Session 리스트 정보 얻기", description = "Get Session Infos") @GetMapping("") public ResponseEntity> findSessionsByGenerationId(@RequestParam Long generationId) { return ResponseEntity.status(HttpStatus.OK).body(sessionService.findSessionsByGenerationId(generationId)); } + @Operation(summary = "Session 추가하기", description = "세션 추가하기") @PostMapping(value = "/add", consumes = "multipart/form-data") public ResponseEntity addSession(@ModelAttribute @Valid AddSessionRequest request) throws ImageException { @@ -45,8 +54,7 @@ public ResponseEntity addSession(@ModelAttribute @Valid AddS } @PatchMapping(value = "/update", consumes = "multipart/form-data") - public ResponseEntity updateSession(@ModelAttribute @Valid UpdateSessionRequest request) - throws ImageException { + public ResponseEntity updateSession(@RequestBody @Valid UpdateSessionRequest request) { sessionService.updateSession(request); return ResponseEntity.noContent().build(); } @@ -57,16 +65,24 @@ public ResponseEntity updateSessionNumber(@RequestBody @Valid UpdateSessio return ResponseEntity.noContent().build(); } - @PatchMapping("/description") - public ResponseEntity updateSessionDescription(@RequestBody @Valid UpdateSessionDescriptionRequest request) { - sessionService.updateSessionDescription(request); + @Operation(summary = "Session 수정 - 사진 순서", description = "세션 사진 순서 바꾸기") + @PatchMapping("/photo/order") + public ResponseEntity updateSessionPhotoOrder(@RequestBody UpdateSessionPhotoOrderRequest request) { + sessionService.updateSessionPhotoOrder(request); return ResponseEntity.noContent().build(); } - @PatchMapping(value = "/update/photo", consumes = "multipart/form-data") - public ResponseEntity updateSessionPhoto(@ModelAttribute @Valid UpdateSessionPhotoRequest request) + @Operation(summary = "Session 수정 - 사진 추가하기", description = "세션 수정 시 사진 추가하기, photoId 반환") + @PostMapping(value = "/photo", consumes = "multipart/form-data") + public ResponseEntity additionalSessionPhoto(@ModelAttribute @Valid AddSessionPhotoRequest request) throws ImageException { - sessionService.updateSessionPhoto(request); + return ResponseEntity.status(HttpStatus.CREATED).body(sessionService.additionalSessionPhoto(request)); + } + + @Operation(summary = "Session 수정 - 사진 삭제하기", description = "사진 삭제하기") + @DeleteMapping(value = "/photo") + public ResponseEntity deleteSessionPhoto(@RequestBody DeleteSessionPhotoRequest request) { + sessionService.deleteSessionPhoto(request); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoRequest.java new file mode 100644 index 00000000..7fe1ee71 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoRequest.java @@ -0,0 +1,13 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record AddSessionPhotoRequest( + + @NotNull + Long sessionId, + @NotNull + MultipartFile photo +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoResponse.java new file mode 100644 index 00000000..6da92af3 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionPhotoResponse.java @@ -0,0 +1,17 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; +import org.cotato.csquiz.domain.generation.entity.SessionPhoto; +import org.springframework.web.multipart.MultipartFile; + +public record AddSessionPhotoResponse( + Long photoId, + String photoUrl, + Integer order +) { + public static AddSessionPhotoResponse from(SessionPhoto sessionPhoto) { + return new AddSessionPhotoResponse(sessionPhoto.getId(), + sessionPhoto.getS3Info().getUrl(), + sessionPhoto.getOrder()); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java index a8f10f0d..b266462f 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.api.session.dto; +import java.util.List; import org.cotato.csquiz.domain.generation.enums.CSEducation; import org.cotato.csquiz.domain.generation.enums.DevTalk; import org.cotato.csquiz.domain.generation.enums.ItIssue; @@ -10,7 +11,7 @@ public record AddSessionRequest( @NotNull Long generationId, - MultipartFile sessionImage, + List photos, @NotNull String title, @NotNull diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionPhotoRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionPhotoRequest.java new file mode 100644 index 00000000..aa254a1c --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionPhotoRequest.java @@ -0,0 +1,9 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; + +public record DeleteSessionPhotoRequest( + @NotNull + Long photoId +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListPhotoInfoResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListPhotoInfoResponse.java new file mode 100644 index 00000000..4841e3ca --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListPhotoInfoResponse.java @@ -0,0 +1,23 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import org.cotato.csquiz.domain.generation.entity.SessionPhoto; + +public record SessionListPhotoInfoResponse( + Long photoId, + String photoUrl, + Integer order +) { + public static SessionListPhotoInfoResponse from(SessionPhoto sessionPhoto) { + return new SessionListPhotoInfoResponse(sessionPhoto.getId(), + sessionPhoto.getS3Info().getUrl(), + sessionPhoto.getOrder()); + } + + public static List from(List sessionPhotos) { + return sessionPhotos.stream() + .map(SessionListPhotoInfoResponse::from) + .toList(); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java index 987ecb8c..a8f0317e 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.api.session.dto; +import java.util.List; import org.cotato.csquiz.domain.generation.embedded.SessionContents; import org.cotato.csquiz.domain.generation.entity.Session; @@ -7,7 +8,7 @@ public record SessionListResponse( Long sessionId, Integer sessionNumber, String title, - String photoUrl, + List photoInfos, String description, Long generationId, SessionContents sessionContents @@ -17,7 +18,7 @@ public static SessionListResponse from(Session session) { session.getId(), session.getNumber(), session.getTitle(), - (session.getPhotoS3Info() != null) ? session.getPhotoS3Info().getUrl() : null, + SessionListPhotoInfoResponse.from(session.getSessionPhotos()), session.getDescription(), session.getGeneration().getId(), session.getSessionContents() diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderInfoRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderInfoRequest.java new file mode 100644 index 00000000..a0554184 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderInfoRequest.java @@ -0,0 +1,11 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; + +public record UpdateSessionPhotoOrderInfoRequest( + @NotNull + Long photoId, + @NotNull + Integer order +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderRequest.java new file mode 100644 index 00000000..73d487d9 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoOrderRequest.java @@ -0,0 +1,9 @@ +package org.cotato.csquiz.api.session.dto; + +import java.util.List; + +public record UpdateSessionPhotoOrderRequest( + Long sessionId, + List orderInfos +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java index e11f7d15..4cddfcb3 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java @@ -8,6 +8,6 @@ public record UpdateSessionPhotoRequest( @NotNull Long sessionId, - MultipartFile sessionImage + MultipartFile photo ) { } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java index 7044b43b..995566ec 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java @@ -5,14 +5,10 @@ import org.cotato.csquiz.domain.generation.enums.ItIssue; import org.cotato.csquiz.domain.generation.enums.Networking; import jakarta.validation.constraints.NotNull; -import org.springframework.web.multipart.MultipartFile; public record UpdateSessionRequest( @NotNull Long sessionId, - MultipartFile sessionImage, - @NotNull - Boolean isPhotoUpdated, String title, String description, @NotNull @@ -21,7 +17,6 @@ public record UpdateSessionRequest( Networking networking, @NotNull CSEducation csEducation, - @NotNull DevTalk devTalk ) { diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 512592f4..a77d458e 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -45,8 +45,13 @@ public enum ErrorCode { EDUCATION_STATUS_NOT_BEFORE(HttpStatus.BAD_REQUEST, "E-402", "이미 시작한 적이 있는 교육입니다."), MEMBER_CANT_ACCESS(HttpStatus.BAD_REQUEST, "E-403", "해당 멤버의 ROLE로 접근할 수 없습니다"), + //세션 사진 + SESSION_PHOTO_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "P-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), + SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "P-102", "입력한 순서는 유효하지 않습니다."), + FILE_EXTENSION_FAULT(HttpStatus.BAD_REQUEST, "F-001", "해당 파일은 등록 할 수 없는 확장자명입니다."), + INVALID_ANSWER(HttpStatus.BAD_REQUEST, "Q-101", "객관식 문제는 숫자 형식의 값만 정답으로 추가할 수 있습니다."), CONTENT_IS_NOT_ANSWER(HttpStatus.BAD_REQUEST, "Q-201", "추가되지 않은 정답을 추가할 수 없습니다."), QUIZ_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "Q-301", "퀴즈 번호는 중복될 수 없습니다."), diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java index 847563d1..449e4352 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java @@ -12,6 +12,9 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -38,8 +41,8 @@ public class Session extends BaseTimeEntity { @Column(name = "session_title", length = 100) private String title; - @Embedded - private S3Info photoS3Info; + @OneToMany(mappedBy = "session", orphanRemoval = true) + private List sessionPhotos = new ArrayList<>(); @Column(name = "session_description") private String description; @@ -61,9 +64,8 @@ public class Session extends BaseTimeEntity { private SessionContents sessionContents; @Builder - public Session(Integer number, S3Info s3Info, String title, String description, Generation generation, SessionContents sessionContents) { + public Session(Integer number, String title, String description, Generation generation, SessionContents sessionContents) { this.number = number; - this.photoS3Info = s3Info; this.title = title; this.description = description; this.generation = generation; @@ -78,10 +80,6 @@ public void updateDescription(String description) { this.description = description; } - public void changePhotoUrl(S3Info photoUrl) { - this.photoS3Info = photoUrl; - } - public void updateSessionContents(SessionContents sessionContents) { this.sessionContents = sessionContents; } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionPhoto.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionPhoto.java new file mode 100644 index 00000000..6903ca63 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionPhoto.java @@ -0,0 +1,58 @@ +package org.cotato.csquiz.domain.generation.entity; + +import static jakarta.persistence.FetchType.LAZY; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.cotato.csquiz.api.session.dto.SessionListPhotoInfoResponse; +import org.cotato.csquiz.common.entity.BaseTimeEntity; +import org.cotato.csquiz.common.entity.S3Info; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SessionPhoto extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "session_photo_id") + private Long id; + + @Embedded + private S3Info s3Info; + + @Column(name = "session_photo_order") + private Integer order; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "session_id") + private Session session; + + @Builder + public SessionPhoto(Session session, Integer order, S3Info s3Info) { + this.session = session; + this.order = order; + this.s3Info = s3Info; + } + + public void updateOrder(Integer order) { + this.order = order; + } + + public void decreaseOrder() { + if (order > 0) { + order--; + } + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionPhotoRepository.java b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionPhotoRepository.java new file mode 100644 index 00000000..c14d607d --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionPhotoRepository.java @@ -0,0 +1,13 @@ +package org.cotato.csquiz.domain.generation.repository; + +import java.util.List; +import java.util.Optional; +import org.cotato.csquiz.domain.generation.entity.Session; +import org.cotato.csquiz.domain.generation.entity.SessionPhoto; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SessionPhotoRepository extends JpaRepository { + List findAllBySession(Session session); + + Optional findFirstBySessionOrderByOrderDesc(Session session); +} diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java index f6525bfe..b1b1f50f 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java @@ -1,27 +1,41 @@ package org.cotato.csquiz.domain.generation.service; import jakarta.persistence.EntityNotFoundException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.session.dto.AddSessionPhotoResponse; +import org.cotato.csquiz.api.session.dto.DeleteSessionPhotoRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoOrderInfoRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoOrderRequest; +import org.cotato.csquiz.api.session.dto.AddSessionPhotoRequest; import org.cotato.csquiz.api.session.dto.AddSessionRequest; import org.cotato.csquiz.api.session.dto.AddSessionResponse; import org.cotato.csquiz.api.session.dto.CsEducationOnSessionNumberResponse; import org.cotato.csquiz.api.session.dto.SessionListResponse; -import org.cotato.csquiz.api.session.dto.UpdateSessionDescriptionRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest; -import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.common.entity.S3Info; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.domain.education.entity.Education; import org.cotato.csquiz.domain.education.service.EducationService; import org.cotato.csquiz.domain.generation.embedded.SessionContents; +import org.cotato.csquiz.domain.generation.entity.SessionPhoto; import org.cotato.csquiz.domain.generation.enums.CSEducation; import org.cotato.csquiz.domain.generation.entity.Generation; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.common.error.exception.ImageException; import org.cotato.csquiz.common.S3.S3Uploader; import org.cotato.csquiz.domain.generation.repository.GenerationRepository; +import org.cotato.csquiz.domain.generation.repository.SessionPhotoRepository; import org.cotato.csquiz.domain.generation.repository.SessionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,23 +50,20 @@ public class SessionService { private static final String SESSION_BUCKET_DIRECTORY = "session"; private final SessionRepository sessionRepository; private final GenerationRepository generationRepository; + private final SessionPhotoRepository sessionPhotoRepository; private final EducationService educationService; private final S3Uploader s3Uploader; @Transactional public AddSessionResponse addSession(AddSessionRequest request) throws ImageException { - S3Info s3Info = null; - if (isImageExist(request.sessionImage())) { - s3Info = s3Uploader.uploadFiles(request.sessionImage(), SESSION_BUCKET_DIRECTORY); - } Generation findGeneration = generationRepository.findById(request.generationId()) .orElseThrow(() -> new EntityNotFoundException("해당 기수를 찾을 수 없습니다.")); int sessionNumber = calculateLastSessionNumber(findGeneration); log.info("해당 기수에 추가된 마지막 세션 : {}", sessionNumber); + Session session = Session.builder() .number(sessionNumber + 1) - .s3Info(s3Info) .description(request.description()) .generation(findGeneration) .title(request.title()) @@ -66,6 +77,27 @@ public AddSessionResponse addSession(AddSessionRequest request) throws ImageExce Session savedSession = sessionRepository.save(session); log.info("세션 생성 완료"); + if (request.photos() != null && !request.photos().isEmpty()) { + AtomicInteger index = new AtomicInteger(0); + + List sessionPhotos = new ArrayList<>(); + + for (MultipartFile photoFile : request.photos()) { + S3Info s3Info = s3Uploader.uploadFiles(photoFile, SESSION_BUCKET_DIRECTORY); + + SessionPhoto sessionPhoto = SessionPhoto.builder() + .session(savedSession) + .s3Info(s3Info) + .order(index.getAndIncrement()) + .build(); + + sessionPhotos.add(sessionPhoto); + } + + sessionPhotoRepository.saveAll(sessionPhotos); + log.info("세션 이미지 생성 완료"); + } + return AddSessionResponse.from(savedSession); } @@ -82,13 +114,7 @@ public void updateSessionNumber(UpdateSessionNumberRequest request) { } @Transactional - public void updateSessionDescription(UpdateSessionDescriptionRequest request) { - Session session = findSessionById(request.sessionId()); - session.updateDescription(request.description()); - } - - @Transactional - public void updateSession(UpdateSessionRequest request) throws ImageException { + public void updateSession(UpdateSessionRequest request) { Session session = findSessionById(request.sessionId()); session.updateDescription(request.description()); @@ -99,35 +125,88 @@ public void updateSession(UpdateSessionRequest request) throws ImageException { .itIssue(request.itIssue()) .networking(request.networking()) .build()); - if (request.isPhotoUpdated()) { - updatePhoto(session, request.sessionImage()); - } sessionRepository.save(session); } @Transactional - public void updateSessionPhoto(UpdateSessionPhotoRequest request) throws ImageException { + public AddSessionPhotoResponse additionalSessionPhoto(AddSessionPhotoRequest request) throws ImageException { Session session = findSessionById(request.sessionId()); - updatePhoto(session, request.sessionImage()); + + S3Info imageInfo = s3Uploader.uploadFiles(request.photo(), SESSION_BUCKET_DIRECTORY); + + Integer imageOrder = sessionPhotoRepository.findFirstBySessionOrderByOrderDesc(session) + .map(sessionPhoto -> sessionPhoto.getOrder() + 1).orElse(0); + + SessionPhoto sessionPhoto = SessionPhoto.builder() + .session(session) + .s3Info(imageInfo) + .order(imageOrder) + .build(); + + return AddSessionPhotoResponse.from(sessionPhotoRepository.save(sessionPhoto)); } - private void updatePhoto(Session session, MultipartFile sessionImage) throws ImageException { - if (isImageExist(sessionImage)) { - S3Info s3Info = s3Uploader.uploadFiles(sessionImage, SESSION_BUCKET_DIRECTORY); - deleteOldImage(session); - session.changePhotoUrl(s3Info); + @Transactional + public void deleteSessionPhoto(DeleteSessionPhotoRequest request) { + SessionPhoto deletePhoto = sessionPhotoRepository.findById(request.photoId()) + .orElseThrow(() -> new EntityNotFoundException("해당 사진을 찾을 수 없습니다.")); + s3Uploader.deleteFile(deletePhoto.getS3Info()); + sessionPhotoRepository.delete(deletePhoto); + + List reOrderPhotos = sessionPhotoRepository.findAllBySession(deletePhoto.getSession()).stream() + .filter(photo -> photo.getOrder() > deletePhoto.getOrder()) + .toList(); + + for (SessionPhoto sessionPhoto : reOrderPhotos) { + sessionPhoto.decreaseOrder(); + } + } + + @Transactional + public void updateSessionPhotoOrder(UpdateSessionPhotoOrderRequest request) { + Session sessionById = findSessionById(request.sessionId()); + List orderList = request.orderInfos(); + + List savedPhotos = sessionPhotoRepository.findAllBySession(sessionById); + + if (savedPhotos.size() != orderList.size()) { + throw new AppException(ErrorCode.SESSION_PHOTO_COUNT_MISMATCH); + } + + if (checkValidOrderRange(orderList)) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); + } + + if (!checkOrderUnique(orderList)) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); } - if (!isImageExist(sessionImage)) { - deleteOldImage(session); - session.changePhotoUrl(null); + + Map orderMap = orderList.stream() + .collect(Collectors.toMap(UpdateSessionPhotoOrderInfoRequest::photoId, Function.identity())); + + for (SessionPhoto savedPhoto : savedPhotos) { + if (orderMap.get(savedPhoto.getId()) == null) { + throw new EntityNotFoundException("해당 사진을 찾을 수 없습니다."); + } + savedPhoto.updateOrder(orderMap.get(savedPhoto.getId()).order()); } } - private void deleteOldImage(Session session) { - if (session.getPhotoS3Info() != null) { - s3Uploader.deleteFile(session.getPhotoS3Info()); + private boolean checkValidOrderRange(List orderList) { + return orderList.stream().noneMatch(orderInfo -> + orderInfo.order() < 0 || orderInfo.order() >= orderList.size()); + } + + private boolean checkOrderUnique(List orderList) { + Set uniqueOrders = new HashSet<>(); + for (UpdateSessionPhotoOrderInfoRequest orderInfo : orderList) { + if (!uniqueOrders.add(orderInfo.order())) { + return false; + } } + + return true; } public List findSessionsByGenerationId(Long generationId) { @@ -160,8 +239,4 @@ public List findAllNotLinkedCsOnSessionsByGe .map(CsEducationOnSessionNumberResponse::from) .toList(); } - - private boolean isImageExist(MultipartFile sessionImage) { - return sessionImage != null && !sessionImage.isEmpty(); - } }