From 4a1d3bc5cf06e9b9488b2ea83a3990becb39403e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 04:01:00 +0900 Subject: [PATCH 01/13] =?UTF-8?q?:heavy=5Fplus=5Fsign:=20Dependency:=20web?= =?UTF-8?q?p=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 3e59600..9eec12a 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,10 @@ dependencies { // AWS S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // Image + implementation("com.sksamuel.scrimage:scrimage-core:4.2.0") + implementation("com.sksamuel.scrimage:scrimage-webp:4.2.0") } tasks.named('test') { From cf30f5e271e4adc28e2974218e67f75593f344ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 04:01:33 +0900 Subject: [PATCH 02/13] =?UTF-8?q?:wrench:=20Settings:=20=EC=84=9C=EB=B8=8C?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources b/src/main/resources index 5471022..d23b7c2 160000 --- a/src/main/resources +++ b/src/main/resources @@ -1 +1 @@ -Subproject commit 547102251cecc07b04c80516470cd71d4f4a2145 +Subproject commit d23b7c2b63504e84ea800b139b7e9235d4bf045f From 1adcfb69e7c2f194b10d6c83e6746562107d0e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 04:02:36 +0900 Subject: [PATCH 03/13] =?UTF-8?q?:sparkles:=20Feat:=20=ED=96=89=EC=82=AC?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sku/refit/global/config/S3Config.java | 3 + .../sku/refit/global/s3/entity/PathName.java | 2 + .../refit/global/s3/service/S3Service.java | 2 + .../global/s3/service/S3ServiceImpl.java | 81 +++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/src/main/java/com/sku/refit/global/config/S3Config.java b/src/main/java/com/sku/refit/global/config/S3Config.java index dd829a9..9ff4c50 100644 --- a/src/main/java/com/sku/refit/global/config/S3Config.java +++ b/src/main/java/com/sku/refit/global/config/S3Config.java @@ -45,6 +45,9 @@ public class S3Config { @Value("${cloud.aws.s3.path.cloth}") private String clothPath; + @Value("${cloud.aws.s3.path.event}") + private String eventPath; + @PostConstruct public void init() { this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); diff --git a/src/main/java/com/sku/refit/global/s3/entity/PathName.java b/src/main/java/com/sku/refit/global/s3/entity/PathName.java index 6f75c12..9c40689 100644 --- a/src/main/java/com/sku/refit/global/s3/entity/PathName.java +++ b/src/main/java/com/sku/refit/global/s3/entity/PathName.java @@ -12,4 +12,6 @@ public enum PathName { POST, @Schema(description = "옷") CLOTH, + @Schema(description = "행사") + EVENT } diff --git a/src/main/java/com/sku/refit/global/s3/service/S3Service.java b/src/main/java/com/sku/refit/global/s3/service/S3Service.java index 71258ac..08b02f4 100644 --- a/src/main/java/com/sku/refit/global/s3/service/S3Service.java +++ b/src/main/java/com/sku/refit/global/s3/service/S3Service.java @@ -39,4 +39,6 @@ String uploadFile( String extractKeyNameFromUrl(String imageUrl); void fileExists(String keyName); + + String uploadFileAsWebp(PathName pathName, MultipartFile file); } diff --git a/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java b/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java index d9b8864..f40ca23 100644 --- a/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java +++ b/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java @@ -3,6 +3,11 @@ */ package com.sku.refit.global.s3.service; +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.webp.WebpWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.UUID; @@ -30,6 +35,8 @@ @RequiredArgsConstructor public class S3ServiceImpl implements S3Service { + private static final int WEBP_QUALITY = 90; + private final AmazonS3 amazonS3; private final S3Config s3Config; @@ -194,6 +201,80 @@ private String getPrefix(PathName pathName) { case PROFILE_IMAGE -> s3Config.getProfileImagePath(); case POST -> s3Config.getPostPath(); case CLOTH -> s3Config.getClothPath(); + case EVENT -> s3Config.getEventPath(); }; } + + @Override + public String uploadFileAsWebp(PathName pathName, MultipartFile file) { + + validateFile(file); + + byte[] webpBytes = convertToWebp(file); + + String keyName = getPrefix(pathName) + "/" + UUID.randomUUID() + ".webp"; + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(webpBytes.length); + metadata.setContentType("image/webp"); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(webpBytes)) { + amazonS3.putObject( + new PutObjectRequest(s3Config.getBucket(), keyName, inputStream, metadata) + ); + + log.info("파일(WebP) 업로드 성공 - bucket: {}, keyName: {}", s3Config.getBucket(), keyName); + + return amazonS3.getUrl(s3Config.getBucket(), keyName).toString(); + + } catch (Exception e) { + log.error( + "S3 WebP 업로드 중 오류 발생 - bucket: {}, keyName: {}", s3Config.getBucket(), keyName, e); + throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); + } + } + + private byte[] convertToWebp(MultipartFile file) { + + try { + ImmutableImage image; + try { + image = ImmutableImage.loader().fromStream(file.getInputStream()); + } catch (IOException e) { + log.error( + "이미지 디코딩 오류 - originalFilename: {}, message: {}", + file.getOriginalFilename(), + e.getMessage()); + throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); + } + + if (image == null) { + log.warn("이미지 디코딩 실패 - originalFilename: {}", file.getOriginalFilename()); + throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); + } + + WebpWriter writer = WebpWriter.DEFAULT.withQ(WEBP_QUALITY); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + image.forWriter(writer).write(baos); + } catch (IOException e) { + log.error( + "WebP 변환 중 IO 오류 - originalFilename: {}, message: {}", + file.getOriginalFilename(), e.getMessage()); + throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); + } + + return baos.toByteArray(); + + } catch (CustomException e) { + throw e; + + } catch (Exception e) { + log.error( + "WebP 변환 중 예기치 않은 오류 - originalFilename: {}, message: {}", + file.getOriginalFilename(), e.getMessage()); + throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); + } + } } From 8b1c6cc4369635b6e65ebc73bd57f304d380cded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 05:16:38 +0900 Subject: [PATCH 04/13] =?UTF-8?q?:sparkles:=20Feat:=20=ED=96=89=EC=82=AC?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventController.java | 101 ++++++ .../event/controller/EventControllerImpl.java | 84 +++++ .../event/dto/request/EventRequest.java | 68 ++++ .../event/dto/response/EventResponse.java | 148 ++++++++ .../sku/refit/domain/event/entity/Event.java | 68 ++++ .../domain/event/entity/EventReservation.java | 48 +++ .../event/entity/EventReservationImage.java | 42 +++ .../event/exception/EventErrorCode.java | 41 +++ .../domain/event/mapper/EventMapper.java | 109 ++++++ .../event/repository/EventRepository.java | 17 + .../EventReservationImageRepository.java | 38 ++ .../EventReservationRepository.java | 17 + .../domain/event/service/EventService.java | 162 +++++++++ .../event/service/EventServiceImpl.java | 335 ++++++++++++++++++ .../global/s3/service/S3ServiceImpl.java | 16 +- 15 files changed, 1286 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/sku/refit/domain/event/controller/EventController.java create mode 100644 src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java create mode 100644 src/main/java/com/sku/refit/domain/event/dto/request/EventRequest.java create mode 100644 src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java create mode 100644 src/main/java/com/sku/refit/domain/event/entity/Event.java create mode 100644 src/main/java/com/sku/refit/domain/event/entity/EventReservation.java create mode 100644 src/main/java/com/sku/refit/domain/event/entity/EventReservationImage.java create mode 100644 src/main/java/com/sku/refit/domain/event/exception/EventErrorCode.java create mode 100644 src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java create mode 100644 src/main/java/com/sku/refit/domain/event/repository/EventRepository.java create mode 100644 src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java create mode 100644 src/main/java/com/sku/refit/domain/event/repository/EventReservationRepository.java create mode 100644 src/main/java/com/sku/refit/domain/event/service/EventService.java create mode 100644 src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java diff --git a/src/main/java/com/sku/refit/domain/event/controller/EventController.java b/src/main/java/com/sku/refit/domain/event/controller/EventController.java new file mode 100644 index 0000000..8425543 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/controller/EventController.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.controller; + +import java.util.List; + +import jakarta.validation.Valid; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import com.sku.refit.domain.event.dto.request.EventRequest.*; +import com.sku.refit.domain.event.dto.response.EventResponse.*; +import com.sku.refit.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "행사", description = "행사 관련 API") +@RequestMapping("/api/events") +public interface EventController { + + /* ========================= + * Admin + * ========================= */ + + @PostMapping(value = "/admin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "[관리자] 행사 생성", description = "행사를 생성합니다.") + ResponseEntity> createEvent( + @Parameter( + description = "행사 정보", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) + @RequestPart("request") + @Valid + EventInfoRequest request, + @Parameter( + description = "대표 사진(썸네일)", + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) + @RequestPart("thumbnail") + MultipartFile thumbnail); + + @PutMapping(value = "/admin/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "[관리자] 행사 수정", description = "행사를 수정합니다.") + ResponseEntity> updateEvent( + @PathVariable Long id, + @RequestPart("request") @Valid EventInfoRequest request, + @RequestPart(value = "thumbnail", required = false) MultipartFile thumbnail); + + @DeleteMapping("/admin/{id}") + @Operation(summary = "[관리자] 행사 삭제", description = "행사 뿐만 아니라 대표사진 및 이미지들까지 모두 삭제합니다.") + ResponseEntity> deleteEvent(@PathVariable Long id); + + /* ========================= + * Public List + * ========================= */ + + @GetMapping("/upcoming") + @Operation(summary = "예정된 행사 리스트", description = "예정된 행사만 조회합니다. D-day가 가까운 순(오름차순)으로 정렬합니다.") + ResponseEntity>> getUpcomingEvents(); + + @GetMapping("/end") + @Operation(summary = "종료된 행사 리스트", description = "종료된 행사만 조회합니다. 최근 종료 순(내림차순)으로 정렬합니다.") + ResponseEntity>> getEndedEvents(); + + @GetMapping + @Operation(summary = "행사 3분류 조회", description = "다가오는/예정/종료 3분류로 반환합니다.") + ResponseEntity> getEventGroups(); + + /* ========================= + * Detail + * ========================= */ + + @GetMapping("/{id}") + @Operation( + summary = "행사 상세 조회", + description = "행사 예약 여부 + 행사 정보 + 최근 예약 이미지 4장 + 4장 제외 의류수를 반환합니다.") + ResponseEntity> getEventDetail(@PathVariable Long id); + + @GetMapping("/{id}/img") + @Operation( + summary = "행사 더보기 이미지 조회", + description = "해당 행사의 예약에서 업로드된 모든 옷 사진을 최신 등록순으로 반환합니다(페이징 없음).") + ResponseEntity>> getEventAllReservationImages( + @PathVariable Long id); + + /* ========================= + * Reservation + * ========================= */ + + @PostMapping(value = "/{id}/rsv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "행사 예약", description = "예약 정보를 저장하고 업로드 이미지는 WebP로 저장합니다.") + ResponseEntity> reserveEvent( + @PathVariable Long id, + @RequestPart("request") @Valid EventRsvRequest request, + @RequestPart(value = "clothImageList", required = false) List clothImageList); +} diff --git a/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java b/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java new file mode 100644 index 0000000..582823d --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.controller; + +import java.util.List; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.sku.refit.domain.event.dto.request.EventRequest.*; +import com.sku.refit.domain.event.dto.response.EventResponse.*; +import com.sku.refit.domain.event.service.EventService; +import com.sku.refit.global.response.BaseResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class EventControllerImpl implements EventController { + + private final EventService eventService; + + @Override + public ResponseEntity> createEvent( + @Valid EventInfoRequest request, MultipartFile thumbnail) { + + return ResponseEntity.ok(BaseResponse.success(eventService.createEvent(request, thumbnail))); + } + + @Override + public ResponseEntity> updateEvent( + Long id, @Valid EventInfoRequest request, MultipartFile thumbnail) { + + return ResponseEntity.ok( + BaseResponse.success(eventService.updateEvent(id, request, thumbnail))); + } + + @Override + public ResponseEntity> deleteEvent(Long id) { + eventService.deleteEvent(id); + return ResponseEntity.ok(BaseResponse.success(null)); + } + + @Override + public ResponseEntity>> getUpcomingEvents() { + return ResponseEntity.ok(BaseResponse.success(eventService.getUpcomingEvents())); + } + + @Override + public ResponseEntity>> getEndedEvents() { + return ResponseEntity.ok(BaseResponse.success(eventService.getEndedEvents())); + } + + @Override + public ResponseEntity> getEventGroups() { + return ResponseEntity.ok(BaseResponse.success(eventService.getEventGroups())); + } + + @Override + public ResponseEntity> getEventDetail(Long id) { + return ResponseEntity.ok(BaseResponse.success(eventService.getEventDetail(id))); + } + + @Override + public ResponseEntity>> getEventAllReservationImages( + Long id) { + + return ResponseEntity.ok(BaseResponse.success(eventService.getEventAllReservationImages(id))); + } + + @Override + public ResponseEntity> reserveEvent( + Long id, @Valid EventRsvRequest request, List clothImageList) { + + return ResponseEntity.ok( + BaseResponse.success(eventService.reserveEvent(id, request, clothImageList))); + } +} diff --git a/src/main/java/com/sku/refit/domain/event/dto/request/EventRequest.java b/src/main/java/com/sku/refit/domain/event/dto/request/EventRequest.java new file mode 100644 index 0000000..2a188b3 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/dto/request/EventRequest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.dto.request; + +import java.time.LocalDate; + +import jakarta.validation.constraints.*; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +public class EventRequest { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(title = "EventInfoRequest DTO", description = "행사 생성 및 수정 요청 데이터") + public static class EventInfoRequest { + + @NotBlank(message = "행사명은 필수입니다.") + @Schema(description = "행사명", example = "2025 21% 파티") + private String name; + + @NotBlank(message = "행사 설명은 필수입니다.") + @Schema(description = "행사 설명", example = "파티에 대한 짧은 설명입니다.") + private String description; + + @NotNull(message = "행사 날짜는 필수입니다.") @Schema(description = "행사 날짜", example = "2025-12-24") + private LocalDate date; + + @NotBlank(message = "행사 장소는 필수입니다.") + @Schema(description = "행사 장소", example = "서울 성동구") + private String location; + + @Schema(description = "행사 상세 링크", example = "https://wearagain.org/48") + private String detailLink; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(title = "EventRsvRequest DTO", description = "행사 예약 요청 데이터") + public static class EventRsvRequest { + + @NotBlank(message = "예약자 이름은 필수입니다.") + @Schema(description = "예약자 이름", example = "김재생") + private String name; + + @NotBlank(message = "연락처는 필수입니다.") + @Schema(description = "연락처", example = "010-1234-5678") + private String phone; + + @Email(message = "이메일 형식이 올바르지 않습니다.") + @NotBlank(message = "이메일은 필수입니다.") + @Schema(description = "이메일", example = "test@example.com") + private String email; + + @Min(value = 0, message = "옷 수량은 0 이상이어야 합니다.") + @Schema(description = "가져올 옷 수량", example = "3") + private Integer clothCount; + + @NotNull(message = "수신 동의 여부는 필수입니다.") @Schema(description = "마케팅/알림 수신 동의 여부", example = "true") + private Boolean marketingConsent; + } +} diff --git a/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java new file mode 100644 index 0000000..0e6b245 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(title = "EventResponse DTO", description = "행사 관련 응답 데이터") +public class EventResponse { + + @Getter + @Builder + @Schema(title = "EventDetailResponse DTO", description = "행사 상세 조회 응답") + public static class EventDetailResponse { + + @Schema(description = "현재 사용자가 해당 행사를 이미 예약했는지 여부 (비로그인 시 null)", example = "true") + private Boolean isReserved; + + /* ===== 상단 ===== */ + + @Schema(description = "누적 예약 인원(옷 수량 기준)", example = "25") + private Integer totalReservedCount; + + @Schema(description = "행사 대표 이미지 URL") + private String thumbnailUrl; + + @Schema(description = "행사명", example = "겨울 의류 나눔 행사") + private String name; + + @Schema(description = "행사 설명", example = "따뜻한 겨울을 위한 의류 기부 행사입니다.") + private String description; + + @Schema(description = "행사 상세 링크", example = "https://example.com/events/1") + private String detailLink; + + /* ===== 중간 ===== */ + + @Schema(description = "행사 날짜", example = "2025-12-24") + private LocalDate date; + + @Schema(description = "행사 장소", example = "서울 성동구") + private String location; + + /* ===== 하단 ===== */ + + @Schema(description = "최근 등록된 예약 이미지 4장 URL 리스트") + private List recentImageUrlList; + + @Schema(description = "최근 4장을 제외한 의류 수량", example = "21") + private Integer clothCountExceptRecent4; + } + + @Getter + @Builder + @Schema(title = "EventImageResponse DTO", description = "행사 예약 이미지 응답") + public static class EventImageResponse { + + @Schema(description = "이미지 정렬 순서(ID 기준 최신순)", example = "15") + private Long order; + + @Schema(description = "이미지 URL") + private String imageUrl; + } + + @Getter + @Builder + @Schema(title = "EventReservationResponse DTO", description = "행사 예약 결과 응답") + public static class EventReservationResponse { + + @Schema(description = "행사 식별자", example = "1") + private Long eventId; + + @Schema(description = "예약 완료 여부", example = "true") + private Boolean reserved; + + @Schema(description = "예약 완료 후 누적 예약 인원", example = "28") + private Integer totalReservedCount; + } + + /* ===== 리스트용 ===== */ + + @Getter + @Builder + @Schema(title = "EventCardResponse DTO", description = "다가오는 행사 카드형 응답") + public static class EventCardResponse { + + @Schema(description = "행사 식별자", example = "1") + private Long eventId; + + @Schema(description = "행사 대표 이미지 URL") + private String thumbnailUrl; + + @Schema(description = "D-day (행사까지 남은 일 수)", example = "5") + private Long dday; + + @Schema(description = "행사명", example = "겨울 의류 나눔 행사") + private String name; + + @Schema(description = "행사 설명", example = "따뜻한 겨울을 위한 의류 기부 행사입니다.") + private String description; + + @Schema(description = "행사 날짜", example = "2025-12-24") + private LocalDate date; + + @Schema(description = "행사 장소", example = "서울 성동구") + private String location; + } + + @Getter + @Builder + @Schema(title = "EventSimpleResponse DTO", description = "예정/종료 행사 간단 응답") + public static class EventSimpleResponse { + + @Schema(description = "행사 식별자", example = "1") + private Long eventId; + + @Schema(description = "행사 대표 이미지 URL") + private String thumbnailUrl; + + @Schema(description = "행사명", example = "겨울 의류 나눔 행사") + private String name; + + @Schema(description = "행사 날짜", example = "2025-12-24") + private LocalDate date; + + @Schema(description = "행사 장소", example = "서울 성동구") + private String location; + } + + @Getter + @Builder + @Schema(title = "EventGroupResponse DTO", description = "행사 분류별 리스트 응답") + public static class EventGroupResponse { + + @Schema(description = "다가오는 행사 리스트") + private List upcoming; + + @Schema(description = "예정된 행사 리스트") + private List scheduled; + + @Schema(description = "종료된 행사 리스트") + private List ended; + } +} diff --git a/src/main/java/com/sku/refit/domain/event/entity/Event.java b/src/main/java/com/sku/refit/domain/event/entity/Event.java new file mode 100644 index 0000000..8c8db3b --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/entity/Event.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.entity; + +import java.time.LocalDate; + +import jakarta.persistence.*; + +import com.sku.refit.global.common.BaseTimeEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "event") +public class Event extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, columnDefinition = "TEXT") + private String description; + + @Column(nullable = false) + private LocalDate date; + + @Column(nullable = false) + private String location; + + @Column private String detailLink; + + @Column(nullable = false) + private String thumbnailUrl; + + @Column(nullable = false) + @Builder.Default + private Integer totalReservedCount = 0; + + public void update( + String name, String description, LocalDate date, String location, String detailLink) { + this.name = name; + this.description = description; + this.date = date; + this.location = location; + this.detailLink = detailLink; + } + + public void updateThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } + + public void increaseReservedCount(int delta) { + this.totalReservedCount = + (this.totalReservedCount == null ? 0 : this.totalReservedCount) + delta; + } +} diff --git a/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java b/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java new file mode 100644 index 0000000..4669208 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.entity; + +import jakarta.persistence.*; + +import com.sku.refit.domain.user.entity.User; +import com.sku.refit.global.common.BaseTimeEntity; + +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "event_reservation") +public class EventReservation extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String phone; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + @Builder.Default + private Integer clothCount = 0; + + @Column(nullable = false) + private Boolean marketingConsent; +} diff --git a/src/main/java/com/sku/refit/domain/event/entity/EventReservationImage.java b/src/main/java/com/sku/refit/domain/event/entity/EventReservationImage.java new file mode 100644 index 0000000..14b484d --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/entity/EventReservationImage.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.entity; + +import jakarta.persistence.Column; +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 com.sku.refit.global.common.BaseTimeEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "event_reservation_image") +public class EventReservationImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reservation_id", nullable = false) + private EventReservation reservation; + + @Column(nullable = false) + private String imageUrl; +} diff --git a/src/main/java/com/sku/refit/domain/event/exception/EventErrorCode.java b/src/main/java/com/sku/refit/domain/event/exception/EventErrorCode.java new file mode 100644 index 0000000..c1d63af --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/exception/EventErrorCode.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.exception; + +import org.springframework.http.HttpStatus; + +import com.sku.refit.global.exception.model.BaseErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EventErrorCode implements BaseErrorCode { + EVENT_NOT_FOUND("EVENT001", "행사가 존재하지 않습니다.", HttpStatus.NOT_FOUND), + + EVENT_THUMBNAIL_REQUIRED("EVENT010", "행사 대표 사진은 필수입니다.", HttpStatus.BAD_REQUEST), + EVENT_THUMBNAIL_UPLOAD_FAILED( + "EVENT011", "행사 대표 사진 업로드에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EVENT_THUMBNAIL_DELETE_FAILED( + "EVENT012", "행사 대표 사진 삭제에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + + EVENT_CREATE_FAILED("EVENT013", "행사 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EVENT_UPDATE_FAILED("EVENT014", "행사 수정에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + + EVENT_DELETE_FAILED("EVENT015", "행사 삭제에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EVENT_RESERVATION_IMAGES_DELETE_FAILED( + "EVENT016", "예약 이미지 삭제에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + + EVENT_ALREADY_RESERVED("EVENT020", "이미 예약한 행사입니다.", HttpStatus.CONFLICT), + EVENT_RESERVATION_CREATE_FAILED( + "EVENT021", "행사 예약 저장에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EVENT_RESERVATION_IMAGE_UPLOAD_FAILED( + "EVENT022", "예약 이미지 업로드에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + ; + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java new file mode 100644 index 0000000..0d3d83b --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.mapper; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.sku.refit.domain.event.dto.request.EventRequest.EventInfoRequest; +import com.sku.refit.domain.event.dto.response.EventResponse.*; +import com.sku.refit.domain.event.entity.Event; +import com.sku.refit.domain.event.entity.EventReservationImage; + +@Component +public class EventMapper { + + public Event toEvent(EventInfoRequest req, String thumbnailUrl) { + return Event.builder() + .name(req.getName()) + .description(req.getDescription()) + .date(req.getDate()) + .location(req.getLocation()) + .detailLink(req.getDetailLink()) + .thumbnailUrl(thumbnailUrl) + .totalReservedCount(0) + .build(); + } + + public EventDetailResponse toDetail( + Event event, Boolean isReserved, List recent4, int clothCountExcept4) { + return EventDetailResponse.builder() + .isReserved(isReserved) + .totalReservedCount(event.getTotalReservedCount()) + .thumbnailUrl(event.getThumbnailUrl()) + .name(event.getName()) + .description(event.getDescription()) + .detailLink(event.getDetailLink()) + .date(event.getDate()) + .location(event.getLocation()) + .recentImageUrlList(recent4) + .clothCountExceptRecent4(clothCountExcept4) + .build(); + } + + public EventImageResponse toImageResponse(EventReservationImage img) { + return EventImageResponse.builder().order(img.getId()).imageUrl(img.getImageUrl()).build(); + } + + /* ========================= + * List Responses + * ========================= */ + + public EventCardResponse toUpcomingCard(Event event, LocalDate today) { + long dday = ChronoUnit.DAYS.between(today, event.getDate()); + return EventCardResponse.builder() + .eventId(event.getId()) + .thumbnailUrl(event.getThumbnailUrl()) + .dday(dday) + .name(event.getName()) + .description(event.getDescription()) + .date(event.getDate()) + .location(event.getLocation()) + .build(); + } + + public EventSimpleResponse toSimple(Event event) { + return EventSimpleResponse.builder() + .eventId(event.getId()) + .thumbnailUrl(event.getThumbnailUrl()) + .name(event.getName()) + .date(event.getDate()) + .location(event.getLocation()) + .build(); + } + + public List toUpcomingCardList(List events, LocalDate today) { + return events.stream().map(e -> toUpcomingCard(e, today)).toList(); + } + + public List toSimpleList(List events) { + return events.stream().map(this::toSimple).toList(); + } + + public EventGroupResponse toGroupResponse( + List upcoming, + List scheduled, + List ended) { + return EventGroupResponse.builder() + .upcoming(upcoming) + .scheduled(scheduled) + .ended(ended) + .build(); + } + + /* ========================= + * Reservation Response + * ========================= */ + + public EventReservationResponse toReservationResponse(Event event) { + return EventReservationResponse.builder() + .eventId(event.getId()) + .reserved(true) + .totalReservedCount(event.getTotalReservedCount()) + .build(); + } +} diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java new file mode 100644 index 0000000..da66e84 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.repository; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.sku.refit.domain.event.entity.Event; + +public interface EventRepository extends JpaRepository { + List findByDateGreaterThanEqualOrderByDateAsc(LocalDate today); + + List findByDateLessThanOrderByDateDesc(LocalDate today); +} diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java new file mode 100644 index 0000000..df79833 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.sku.refit.domain.event.entity.EventReservationImage; + +public interface EventReservationImageRepository + extends JpaRepository { + + // 행사(eventId)에 속한 "모든 예약 이미지"를 최신 등록순(id desc)으로 + @Query( + """ + select eri + from EventReservationImage eri + join eri.reservation r + where r.event.id = :eventId + order by eri.id desc + """) + List findAllByEventIdOrderByIdDesc(Long eventId); + + // 최근 4장 + @Query( + """ + select eri + from EventReservationImage eri + join eri.reservation r + where r.event.id = :eventId + order by eri.id desc + """) + List findTop4ByEventIdOrderByIdDesc(Long eventId, Pageable pageable); +} diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventReservationRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventReservationRepository.java new file mode 100644 index 0000000..325902c --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/repository/EventReservationRepository.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.sku.refit.domain.event.entity.EventReservation; + +public interface EventReservationRepository extends JpaRepository { + + boolean existsByEventIdAndUserId(Long eventId, Long userId); + + List findByEventId(Long eventId); +} diff --git a/src/main/java/com/sku/refit/domain/event/service/EventService.java b/src/main/java/com/sku/refit/domain/event/service/EventService.java new file mode 100644 index 0000000..69c28a4 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/service/EventService.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.service; + +import java.util.List; + +import org.springframework.web.multipart.MultipartFile; + +import com.sku.refit.domain.event.dto.request.EventRequest.EventInfoRequest; +import com.sku.refit.domain.event.dto.request.EventRequest.EventRsvRequest; +import com.sku.refit.domain.event.dto.response.EventResponse; + +/** + * 행사(Event) 관련 주요 기능을 제공하는 서비스 인터페이스입니다. + * + *

주요 기능: + * + *

    + *
  • 행사 생성 / 수정 / 삭제 (관리자) + *
  • 행사 목록 조회 (예정 / 종료 / 그룹) + *
  • 행사 상세 조회 + *
  • 행사 예약 및 예약 이미지 업로드 + *
  • 행사 예약 이미지 전체 조회 + *
+ */ +public interface EventService { + + /* ========================= + * Admin + * ========================= */ + + /** + * 새로운 행사를 생성합니다. (관리자 전용) + * + *

대표 사진은 WebP 포맷으로 변환되어 S3에 저장됩니다. + * + * @param request 행사 기본 정보 (행사명, 설명, 날짜, 장소, 상세 링크) + * @param thumbnail 행사 대표 이미지 + * @return 생성된 행사 상세 응답 + */ + EventResponse.EventDetailResponse createEvent(EventInfoRequest request, MultipartFile thumbnail); + + /** + * 기존 행사를 수정합니다. (관리자 전용) + * + *

대표 이미지를 함께 전달할 경우 기존 이미지는 삭제되고 새 이미지로 교체됩니다. + * + * @param id 수정할 행사 ID + * @param request 수정할 행사 정보 + * @param thumbnail 새 대표 이미지 (선택) + * @return 수정된 행사 상세 응답 + */ + EventResponse.EventDetailResponse updateEvent( + Long id, EventInfoRequest request, MultipartFile thumbnail); + + /** + * 행사를 삭제합니다. (관리자 전용) + * + *

대표 이미지 및 해당 행사에 등록된 모든 예약 이미지가 함께 삭제됩니다. + * + * @param id 삭제할 행사 ID + */ + void deleteEvent(Long id); + + /* ========================= + * List + * ========================= */ + + /** + * 예정된 행사 목록을 조회합니다. + * + *

행사 날짜 기준 오름차순으로 정렬되며, D-Day 정보가 포함된 카드 형태의 응답을 반환합니다. + * + * @return 예정된 행사 카드 응답 리스트 + */ + List getUpcomingEvents(); + + /** + * 종료된 행사 목록을 조회합니다. + * + *

행사 날짜 기준 내림차순으로 정렬되며, 간단한 행사 정보 형태의 응답을 반환합니다. + * + * @return 종료된 행사 간단 응답 리스트 + */ + List getEndedEvents(); + + /** + * 행사 목록을 그룹 형태로 조회합니다. + * + *

응답은 다음 세 그룹으로 구성됩니다. + * + *

    + *
  • 다가오는 행사 (카드 형태, D-Day 포함) + *
  • 예정된 행사 (간단 정보 형태) + *
  • 종료된 행사 (간단 정보 형태) + *
+ * + * @return 행사 그룹 응답 + */ + EventResponse.EventGroupResponse getEventGroups(); + + /* ========================= + * Detail + * ========================= */ + + /** + * 특정 행사에 대한 상세 정보를 조회합니다. + * + *

로그인 상태인 경우, 사용자의 해당 행사 예약 여부가 함께 반환됩니다. 비로그인 상태인 경우 예약 여부는 {@code null}로 반환됩니다. + * + *

상세 응답에는 다음 정보가 포함됩니다. + * + *

    + *
  • 누적 예약 인원 + *
  • 대표 이미지, 행사명, 설명, 상세 링크 + *
  • 행사 날짜 및 장소 + *
  • 최근 등록된 예약 이미지 4장 + *
  • 최근 4장을 제외한 의류 수량 + *
+ * + * @param id 조회할 행사 ID + * @return 행사 상세 응답 + */ + EventResponse.EventDetailResponse getEventDetail(Long id); + + /** + * 특정 행사에 등록된 모든 예약 이미지를 조회합니다. + * + *

이미지는 최신 등록 순으로 반환되며, 페이징 없이 전체 이미지를 조회합니다. + * + * @param eventId 행사 ID + * @return 행사 예약 이미지 응답 리스트 + */ + List getEventAllReservationImages(Long eventId); + + /* ========================= + * Reservation + * ========================= */ + + /** + * 특정 행사에 대해 예약을 진행합니다. + * + *

예약 시 다음 정보가 함께 저장됩니다. + * + *

    + *
  • 예약자 정보 (이름, 연락처, 이메일) + *
  • 의류 수량 + *
  • 마케팅 수신 동의 여부 + *
  • 행사에 가져올 의류 이미지 (WebP 변환 후 저장) + *
+ * + *

예약이 완료되면 행사 누적 예약 수량이 증가합니다. + * + * @param eventId 예약할 행사 ID + * @param request 예약 요청 정보 + * @param clothImageList 예약 시 업로드하는 의류 이미지 목록 + * @return 행사 예약 결과 응답 + */ + EventResponse.EventReservationResponse reserveEvent( + Long eventId, EventRsvRequest request, List clothImageList); +} diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java new file mode 100644 index 0000000..d64de5b --- /dev/null +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.event.service; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.sku.refit.domain.event.dto.request.EventRequest.*; +import com.sku.refit.domain.event.dto.response.EventResponse.*; +import com.sku.refit.domain.event.entity.Event; +import com.sku.refit.domain.event.entity.EventReservation; +import com.sku.refit.domain.event.entity.EventReservationImage; +import com.sku.refit.domain.event.exception.EventErrorCode; +import com.sku.refit.domain.event.mapper.EventMapper; +import com.sku.refit.domain.event.repository.EventRepository; +import com.sku.refit.domain.event.repository.EventReservationImageRepository; +import com.sku.refit.domain.event.repository.EventReservationRepository; +import com.sku.refit.domain.user.entity.User; +import com.sku.refit.domain.user.service.UserService; +import com.sku.refit.global.exception.CustomException; +import com.sku.refit.global.s3.entity.PathName; +import com.sku.refit.global.s3.service.S3Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class EventServiceImpl implements EventService { + + private final EventRepository eventRepository; + private final EventReservationRepository eventReservationRepository; + private final EventReservationImageRepository eventReservationImageRepository; + + private final S3Service s3Service; + private final UserService userService; + private final EventMapper eventMapper; + + /* ========================= + * Admin + * ========================= */ + + @Override + @Transactional + public EventDetailResponse createEvent(EventInfoRequest request, MultipartFile thumbnail) { + + if (thumbnail == null || thumbnail.isEmpty()) { + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_REQUIRED); + } + + String thumbnailUrl; + try { + thumbnailUrl = s3Service.uploadFileAsWebp(PathName.EVENT, thumbnail); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] createEvent - thumbnail upload failed", e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_UPLOAD_FAILED); + } + + try { + Event event = eventMapper.toEvent(request, thumbnailUrl); + eventRepository.save(event); + return getEventDetail(event.getId()); + + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] createEvent - failed", e); + throw new CustomException(EventErrorCode.EVENT_CREATE_FAILED); + } + } + + @Override + @Transactional + public EventDetailResponse updateEvent( + Long id, EventInfoRequest request, MultipartFile thumbnail) { + + Event event = + eventRepository + .findById(id) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + + try { + event.update( + request.getName(), + request.getDescription(), + request.getDate(), + request.getLocation(), + request.getDetailLink()); + if (thumbnail != null && !thumbnail.isEmpty()) { + String oldThumbUrl = event.getThumbnailUrl(); + if (oldThumbUrl != null) { + try { + s3Service.deleteFile(s3Service.extractKeyNameFromUrl(oldThumbUrl)); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] updateEvent - thumbnail delete failed, eventId={}", id, e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_DELETE_FAILED); + } + } + + try { + String newThumbUrl = s3Service.uploadFileAsWebp(PathName.EVENT, thumbnail); + event.updateThumbnailUrl(newThumbUrl); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] updateEvent - thumbnail upload failed, eventId={}", id, e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_UPLOAD_FAILED); + } + } + return getEventDetail(event.getId()); + + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] updateEvent - failed, eventId={}", id, e); + throw new CustomException(EventErrorCode.EVENT_UPDATE_FAILED); + } + } + + @Override + @Transactional + public void deleteEvent(Long id) { + + Event event = + eventRepository + .findById(id) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + + try { + if (event.getThumbnailUrl() != null) { + try { + s3Service.deleteFile(s3Service.extractKeyNameFromUrl(event.getThumbnailUrl())); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] deleteEvent - thumbnail delete failed, eventId={}", id, e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_DELETE_FAILED); + } + } + List images = + eventReservationImageRepository.findAllByEventIdOrderByIdDesc(id); + + for (EventReservationImage img : images) { + try { + s3Service.deleteFile(s3Service.extractKeyNameFromUrl(img.getImageUrl())); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error( + "[EVENT] deleteEvent - reservation image delete failed, eventId={}, imageId={}", + id, + img.getId(), + e); + throw new CustomException(EventErrorCode.EVENT_RESERVATION_IMAGES_DELETE_FAILED); + } + } + + eventReservationImageRepository.deleteAll(images); + + List reservations = eventReservationRepository.findByEventId(id); + eventReservationRepository.deleteAll(reservations); + + eventRepository.delete(event); + + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] deleteEvent - failed, eventId={}", id, e); + throw new CustomException(EventErrorCode.EVENT_DELETE_FAILED); + } + } + + /* ========================= + * List + * ========================= */ + + @Override + public List getUpcomingEvents() { + + LocalDate today = LocalDate.now(); + List upcoming = eventRepository.findByDateGreaterThanEqualOrderByDateAsc(today); + + return eventMapper.toUpcomingCardList(upcoming, today); + } + + @Override + public List getEndedEvents() { + + LocalDate today = LocalDate.now(); + List ended = eventRepository.findByDateLessThanOrderByDateDesc(today); + + return eventMapper.toSimpleList(ended); + } + + @Override + public EventGroupResponse getEventGroups() { + + LocalDate today = LocalDate.now(); + + List upcoming = eventRepository.findByDateGreaterThanEqualOrderByDateAsc(today); + List ended = eventRepository.findByDateLessThanOrderByDateDesc(today); + + List upcomingCards = eventMapper.toUpcomingCardList(upcoming, today); + List scheduled = eventMapper.toSimpleList(upcoming); + List endedSimple = eventMapper.toSimpleList(ended); + + return eventMapper.toGroupResponse(upcomingCards, scheduled, endedSimple); + } + + /* ========================= + * Detail + * ========================= */ + + @Override + public EventDetailResponse getEventDetail(Long id) { + + Event event = + eventRepository + .findById(id) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + + User user = null; + try { + user = userService.getCurrentUser(); + } catch (Exception e) { + } + + Boolean isReserved = null; + if (user != null) { + isReserved = eventReservationRepository.existsByEventIdAndUserId(id, user.getId()); + } + + List recent4 = + eventReservationImageRepository + .findTop4ByEventIdOrderByIdDesc(id, PageRequest.of(0, 4)) + .stream() + .map(EventReservationImage::getImageUrl) + .toList(); + + int totalClothCount = + eventReservationRepository.findByEventId(id).stream() + .mapToInt(r -> r.getClothCount() == null ? 0 : r.getClothCount()) + .sum(); + + int clothCountExcept4 = Math.max(0, totalClothCount - recent4.size()); + + return eventMapper.toDetail(event, isReserved, recent4, clothCountExcept4); + } + + @Override + public List getEventAllReservationImages(Long eventId) { + + eventRepository + .findById(eventId) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + + return eventReservationImageRepository.findAllByEventIdOrderByIdDesc(eventId).stream() + .map(eventMapper::toImageResponse) + .toList(); + } + + /* ========================= + * Reservation + * ========================= */ + + @Override + @Transactional + public EventReservationResponse reserveEvent( + Long eventId, EventRsvRequest request, List clothImageList) { + + Event event = + eventRepository + .findById(eventId) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + + User user = userService.getCurrentUser(); + + if (eventReservationRepository.existsByEventIdAndUserId(eventId, user.getId())) { + throw new CustomException(EventErrorCode.EVENT_ALREADY_RESERVED); + } + + try { + EventReservation reservation = + EventReservation.builder() + .event(event) + .user(user) + .name(request.getName()) + .phone(request.getPhone()) + .email(request.getEmail()) + .clothCount(request.getClothCount() == null ? 0 : request.getClothCount()) + .marketingConsent(request.getMarketingConsent()) + .build(); + + eventReservationRepository.save(reservation); + + if (clothImageList != null && !clothImageList.isEmpty()) { + for (MultipartFile f : clothImageList) { + try { + String url = s3Service.uploadFileAsWebp(PathName.EVENT, f); + + eventReservationImageRepository.save( + EventReservationImage.builder().reservation(reservation).imageUrl(url).build()); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] reserveEvent - image upload failed, eventId={}", eventId, e); + throw new CustomException(EventErrorCode.EVENT_RESERVATION_IMAGE_UPLOAD_FAILED); + } + } + } + + event.increaseReservedCount(reservation.getClothCount()); + + return eventMapper.toReservationResponse(event); + + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] reserveEvent - failed, eventId={}", eventId, e); + throw new CustomException(EventErrorCode.EVENT_RESERVATION_CREATE_FAILED); + } + } +} diff --git a/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java b/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java index f40ca23..d350341 100644 --- a/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java +++ b/src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java @@ -3,8 +3,6 @@ */ package com.sku.refit.global.s3.service; -import com.sksamuel.scrimage.ImmutableImage; -import com.sksamuel.scrimage.webp.WebpWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -21,6 +19,8 @@ import com.amazonaws.services.s3.model.ListObjectsV2Request; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.webp.WebpWriter; import com.sku.refit.global.config.S3Config; import com.sku.refit.global.exception.CustomException; import com.sku.refit.global.s3.dto.S3Response; @@ -220,16 +220,14 @@ public String uploadFileAsWebp(PathName pathName, MultipartFile file) { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(webpBytes)) { amazonS3.putObject( - new PutObjectRequest(s3Config.getBucket(), keyName, inputStream, metadata) - ); + new PutObjectRequest(s3Config.getBucket(), keyName, inputStream, metadata)); log.info("파일(WebP) 업로드 성공 - bucket: {}, keyName: {}", s3Config.getBucket(), keyName); return amazonS3.getUrl(s3Config.getBucket(), keyName).toString(); } catch (Exception e) { - log.error( - "S3 WebP 업로드 중 오류 발생 - bucket: {}, keyName: {}", s3Config.getBucket(), keyName, e); + log.error("S3 WebP 업로드 중 오류 발생 - bucket: {}, keyName: {}", s3Config.getBucket(), keyName, e); throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); } } @@ -261,7 +259,8 @@ private byte[] convertToWebp(MultipartFile file) { } catch (IOException e) { log.error( "WebP 변환 중 IO 오류 - originalFilename: {}, message: {}", - file.getOriginalFilename(), e.getMessage()); + file.getOriginalFilename(), + e.getMessage()); throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); } @@ -273,7 +272,8 @@ private byte[] convertToWebp(MultipartFile file) { } catch (Exception e) { log.error( "WebP 변환 중 예기치 않은 오류 - originalFilename: {}, message: {}", - file.getOriginalFilename(), e.getMessage()); + file.getOriginalFilename(), + e.getMessage()); throw new CustomException(S3ErrorStatus.FILE_SERVER_ERROR); } } From 45001558650419628cc41c72232c2fe24919a83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 05:29:25 +0900 Subject: [PATCH 05/13] =?UTF-8?q?:wrench:=20Settings:=20admin=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/sku/refit/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/sku/refit/global/config/SecurityConfig.java b/src/main/java/com/sku/refit/global/config/SecurityConfig.java index e17aeec..a03c6dd 100644 --- a/src/main/java/com/sku/refit/global/config/SecurityConfig.java +++ b/src/main/java/com/sku/refit/global/config/SecurityConfig.java @@ -125,7 +125,7 @@ private void configureAuthorization(HttpSecurity http) throws Exception { .permitAll() .requestMatchers("/error") .permitAll() - .requestMatchers(RegexRequestMatcher.regexMatcher(".*/admin/.*")) + .requestMatchers(RegexRequestMatcher.regexMatcher(".*/admin($|/.*)")) .hasRole("ADMIN") .requestMatchers(RegexRequestMatcher.regexMatcher(".*/dev/.*")) .hasRole("DEVELOPER") From e4c62ab44deffef1fc763879e8a32498c42c78e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 05:29:53 +0900 Subject: [PATCH 06/13] =?UTF-8?q?:recycle:=20Refactor:=20=ED=96=89?= =?UTF-8?q?=EC=82=AC=20=EA=B4=80=EB=A0=A8=20API=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventController.java | 6 +-- .../domain/event/mapper/EventMapper.java | 15 +++++- .../domain/event/service/EventService.java | 22 ++++---- .../event/service/EventServiceImpl.java | 53 +++++++------------ 4 files changed, 44 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/controller/EventController.java b/src/main/java/com/sku/refit/domain/event/controller/EventController.java index 8425543..667a1d0 100644 --- a/src/main/java/com/sku/refit/domain/event/controller/EventController.java +++ b/src/main/java/com/sku/refit/domain/event/controller/EventController.java @@ -68,7 +68,7 @@ ResponseEntity> updateEvent( ResponseEntity>> getEndedEvents(); @GetMapping - @Operation(summary = "행사 3분류 조회", description = "다가오는/예정/종료 3분류로 반환합니다.") + @Operation(summary = "다가오는/예정된/종료된 행사 조회", description = "다가오는/예정된/종료된 3분류로 행사를 하나씩 반환합니다.") ResponseEntity> getEventGroups(); /* ========================= @@ -82,9 +82,7 @@ ResponseEntity> updateEvent( ResponseEntity> getEventDetail(@PathVariable Long id); @GetMapping("/{id}/img") - @Operation( - summary = "행사 더보기 이미지 조회", - description = "해당 행사의 예약에서 업로드된 모든 옷 사진을 최신 등록순으로 반환합니다(페이징 없음).") + @Operation(summary = "행사 더보기 이미지 조회", description = "해당 행사의 예약에서 업로드된 모든 옷 사진을 최신 등록순으로 반환합니다.") ResponseEntity>> getEventAllReservationImages( @PathVariable Long id); diff --git a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java index 0d3d83b..8253020 100644 --- a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java +++ b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java @@ -9,10 +9,12 @@ import org.springframework.stereotype.Component; -import com.sku.refit.domain.event.dto.request.EventRequest.EventInfoRequest; +import com.sku.refit.domain.event.dto.request.EventRequest.*; import com.sku.refit.domain.event.dto.response.EventResponse.*; import com.sku.refit.domain.event.entity.Event; +import com.sku.refit.domain.event.entity.EventReservation; import com.sku.refit.domain.event.entity.EventReservationImage; +import com.sku.refit.domain.user.entity.User; @Component public class EventMapper { @@ -98,6 +100,17 @@ public EventGroupResponse toGroupResponse( /* ========================= * Reservation Response * ========================= */ + public EventReservation toReservation(Event event, User user, EventRsvRequest request) { + return EventReservation.builder() + .event(event) + .user(user) + .name(request.getName()) + .phone(request.getPhone()) + .email(request.getEmail()) + .clothCount(request.getClothCount() == null ? 0 : request.getClothCount()) + .marketingConsent(request.getMarketingConsent()) + .build(); + } public EventReservationResponse toReservationResponse(Event event) { return EventReservationResponse.builder() diff --git a/src/main/java/com/sku/refit/domain/event/service/EventService.java b/src/main/java/com/sku/refit/domain/event/service/EventService.java index 69c28a4..8b1acf6 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventService.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventService.java @@ -7,9 +7,8 @@ import org.springframework.web.multipart.MultipartFile; -import com.sku.refit.domain.event.dto.request.EventRequest.EventInfoRequest; -import com.sku.refit.domain.event.dto.request.EventRequest.EventRsvRequest; -import com.sku.refit.domain.event.dto.response.EventResponse; +import com.sku.refit.domain.event.dto.request.EventRequest.*; +import com.sku.refit.domain.event.dto.response.EventResponse.*; /** * 행사(Event) 관련 주요 기능을 제공하는 서비스 인터페이스입니다. @@ -39,7 +38,7 @@ public interface EventService { * @param thumbnail 행사 대표 이미지 * @return 생성된 행사 상세 응답 */ - EventResponse.EventDetailResponse createEvent(EventInfoRequest request, MultipartFile thumbnail); + EventDetailResponse createEvent(EventInfoRequest request, MultipartFile thumbnail); /** * 기존 행사를 수정합니다. (관리자 전용) @@ -51,8 +50,7 @@ public interface EventService { * @param thumbnail 새 대표 이미지 (선택) * @return 수정된 행사 상세 응답 */ - EventResponse.EventDetailResponse updateEvent( - Long id, EventInfoRequest request, MultipartFile thumbnail); + EventDetailResponse updateEvent(Long id, EventInfoRequest request, MultipartFile thumbnail); /** * 행사를 삭제합니다. (관리자 전용) @@ -74,7 +72,7 @@ EventResponse.EventDetailResponse updateEvent( * * @return 예정된 행사 카드 응답 리스트 */ - List getUpcomingEvents(); + List getUpcomingEvents(); /** * 종료된 행사 목록을 조회합니다. @@ -83,7 +81,7 @@ EventResponse.EventDetailResponse updateEvent( * * @return 종료된 행사 간단 응답 리스트 */ - List getEndedEvents(); + List getEndedEvents(); /** * 행사 목록을 그룹 형태로 조회합니다. @@ -98,7 +96,7 @@ EventResponse.EventDetailResponse updateEvent( * * @return 행사 그룹 응답 */ - EventResponse.EventGroupResponse getEventGroups(); + EventGroupResponse getEventGroups(); /* ========================= * Detail @@ -122,7 +120,7 @@ EventResponse.EventDetailResponse updateEvent( * @param id 조회할 행사 ID * @return 행사 상세 응답 */ - EventResponse.EventDetailResponse getEventDetail(Long id); + EventDetailResponse getEventDetail(Long id); /** * 특정 행사에 등록된 모든 예약 이미지를 조회합니다. @@ -132,7 +130,7 @@ EventResponse.EventDetailResponse updateEvent( * @param eventId 행사 ID * @return 행사 예약 이미지 응답 리스트 */ - List getEventAllReservationImages(Long eventId); + List getEventAllReservationImages(Long eventId); /* ========================= * Reservation @@ -157,6 +155,6 @@ EventResponse.EventDetailResponse updateEvent( * @param clothImageList 예약 시 업로드하는 의류 이미지 목록 * @return 행사 예약 결과 응답 */ - EventResponse.EventReservationResponse reserveEvent( + EventReservationResponse reserveEvent( Long eventId, EventRsvRequest request, List clothImageList); } diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index d64de5b..eeec707 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -291,45 +291,28 @@ public EventReservationResponse reserveEvent( throw new CustomException(EventErrorCode.EVENT_ALREADY_RESERVED); } - try { - EventReservation reservation = - EventReservation.builder() - .event(event) - .user(user) - .name(request.getName()) - .phone(request.getPhone()) - .email(request.getEmail()) - .clothCount(request.getClothCount() == null ? 0 : request.getClothCount()) - .marketingConsent(request.getMarketingConsent()) - .build(); - - eventReservationRepository.save(reservation); - - if (clothImageList != null && !clothImageList.isEmpty()) { - for (MultipartFile f : clothImageList) { - try { - String url = s3Service.uploadFileAsWebp(PathName.EVENT, f); + EventReservation reservation = eventMapper.toReservation(event, user, request); - eventReservationImageRepository.save( - EventReservationImage.builder().reservation(reservation).imageUrl(url).build()); - } catch (CustomException e) { - throw e; - } catch (Exception e) { - log.error("[EVENT] reserveEvent - image upload failed, eventId={}", eventId, e); - throw new CustomException(EventErrorCode.EVENT_RESERVATION_IMAGE_UPLOAD_FAILED); - } + eventReservationRepository.save(reservation); + + if (clothImageList != null && !clothImageList.isEmpty()) { + for (MultipartFile f : clothImageList) { + try { + String url = s3Service.uploadFileAsWebp(PathName.EVENT, f); + + eventReservationImageRepository.save( + EventReservationImage.builder().reservation(reservation).imageUrl(url).build()); + } catch (CustomException e) { + throw e; + } catch (Exception e) { + log.error("[EVENT] reserveEvent - image upload failed, eventId={}", eventId, e); + throw new CustomException(EventErrorCode.EVENT_RESERVATION_IMAGE_UPLOAD_FAILED); } } + } - event.increaseReservedCount(reservation.getClothCount()); - - return eventMapper.toReservationResponse(event); + event.increaseReservedCount(reservation.getClothCount()); - } catch (CustomException e) { - throw e; - } catch (Exception e) { - log.error("[EVENT] reserveEvent - failed, eventId={}", eventId, e); - throw new CustomException(EventErrorCode.EVENT_RESERVATION_CREATE_FAILED); - } + return eventMapper.toReservationResponse(event); } } From 21890840149d147591914468261d245edc3003fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 05:36:50 +0900 Subject: [PATCH 07/13] =?UTF-8?q?:recycle:=20Refactor:=20=ED=96=89?= =?UTF-8?q?=EC=82=AC=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9D=EB=B3=84=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sku/refit/domain/event/dto/response/EventResponse.java | 3 +++ .../java/com/sku/refit/domain/event/mapper/EventMapper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java index 0e6b245..f85c082 100644 --- a/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java +++ b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java @@ -22,6 +22,9 @@ public static class EventDetailResponse { /* ===== 상단 ===== */ + @Schema(description = "행사 식별자", example = "1") + private Long eventId; + @Schema(description = "누적 예약 인원(옷 수량 기준)", example = "25") private Integer totalReservedCount; diff --git a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java index 8253020..cd1ef9d 100644 --- a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java +++ b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java @@ -35,6 +35,7 @@ public EventDetailResponse toDetail( Event event, Boolean isReserved, List recent4, int clothCountExcept4) { return EventDetailResponse.builder() .isReserved(isReserved) + .eventId(event.getId()) .totalReservedCount(event.getTotalReservedCount()) .thumbnailUrl(event.getThumbnailUrl()) .name(event.getName()) From ead96992b75d657119279fa5c4794891b1d640c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:13:34 +0900 Subject: [PATCH 08/13] =?UTF-8?q?:sparkles:=20Feat:=20=ED=96=89=EC=82=AC?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventController.java | 4 +- .../event/dto/response/EventResponse.java | 12 +++--- .../sku/refit/domain/event/entity/Event.java | 5 +-- .../domain/event/entity/EventReservation.java | 10 ++++- .../domain/event/mapper/EventMapper.java | 6 +-- .../event/repository/EventRepository.java | 5 +++ .../EventReservationImageRepository.java | 26 +++--------- .../event/service/EventServiceImpl.java | 42 ++++++++++++------- 8 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/controller/EventController.java b/src/main/java/com/sku/refit/domain/event/controller/EventController.java index 667a1d0..5505622 100644 --- a/src/main/java/com/sku/refit/domain/event/controller/EventController.java +++ b/src/main/java/com/sku/refit/domain/event/controller/EventController.java @@ -68,7 +68,9 @@ ResponseEntity> updateEvent( ResponseEntity>> getEndedEvents(); @GetMapping - @Operation(summary = "다가오는/예정된/종료된 행사 조회", description = "다가오는/예정된/종료된 3분류로 행사를 하나씩 반환합니다.") + @Operation( + summary = "다가오는/예정된/종료된 행사 조회", + description = "다가오는(가장 가까운 D-day 1개) / 예정된(그 다음 1개) / 종료된(가장 최근 종료 1개)로 반환합니다.") ResponseEntity> getEventGroups(); /* ========================= diff --git a/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java index f85c082..d11fba6 100644 --- a/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java +++ b/src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java @@ -139,13 +139,13 @@ public static class EventSimpleResponse { @Schema(title = "EventGroupResponse DTO", description = "행사 분류별 리스트 응답") public static class EventGroupResponse { - @Schema(description = "다가오는 행사 리스트") - private List upcoming; + @Schema(description = "다가오는 행사") + private EventCardResponse upcoming; - @Schema(description = "예정된 행사 리스트") - private List scheduled; + @Schema(description = "예정된 행사") + private EventSimpleResponse scheduled; - @Schema(description = "종료된 행사 리스트") - private List ended; + @Schema(description = "종료된 행사") + private EventSimpleResponse ended; } } diff --git a/src/main/java/com/sku/refit/domain/event/entity/Event.java b/src/main/java/com/sku/refit/domain/event/entity/Event.java index 8c8db3b..c343597 100644 --- a/src/main/java/com/sku/refit/domain/event/entity/Event.java +++ b/src/main/java/com/sku/refit/domain/event/entity/Event.java @@ -61,8 +61,7 @@ public void updateThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } - public void increaseReservedCount(int delta) { - this.totalReservedCount = - (this.totalReservedCount == null ? 0 : this.totalReservedCount) + delta; + public void increaseReservedCount() { + this.totalReservedCount = (this.totalReservedCount == null ? 0 : this.totalReservedCount) + 1; } } diff --git a/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java b/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java index 4669208..6898072 100644 --- a/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java +++ b/src/main/java/com/sku/refit/domain/event/entity/EventReservation.java @@ -15,7 +15,13 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Table(name = "event_reservation") +@Table( + name = "event_reservation", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_event_user", + columnNames = {"event_id", "user_id"}) + }) public class EventReservation extends BaseTimeEntity { @Id @@ -27,7 +33,7 @@ public class EventReservation extends BaseTimeEntity { private Event event; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false, unique = true) + @JoinColumn(name = "user_id", nullable = false) private User user; @Column(nullable = false) diff --git a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java index cd1ef9d..3d86911 100644 --- a/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java +++ b/src/main/java/com/sku/refit/domain/event/mapper/EventMapper.java @@ -87,10 +87,8 @@ public List toSimpleList(List events) { return events.stream().map(this::toSimple).toList(); } - public EventGroupResponse toGroupResponse( - List upcoming, - List scheduled, - List ended) { + public EventGroupResponse toGroupResponseSingle( + EventCardResponse upcoming, EventSimpleResponse scheduled, EventSimpleResponse ended) { return EventGroupResponse.builder() .upcoming(upcoming) .scheduled(scheduled) diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java index da66e84..75649b1 100644 --- a/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java +++ b/src/main/java/com/sku/refit/domain/event/repository/EventRepository.java @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.sku.refit.domain.event.entity.Event; @@ -14,4 +15,8 @@ public interface EventRepository extends JpaRepository { List findByDateGreaterThanEqualOrderByDateAsc(LocalDate today); List findByDateLessThanOrderByDateDesc(LocalDate today); + + List findByDateGreaterThanEqualOrderByDateAsc(LocalDate date, Pageable pageable); + + List findByDateLessThanOrderByDateDesc(LocalDate date, Pageable pageable); } diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java index df79833..4c32213 100644 --- a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java +++ b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java @@ -7,32 +7,16 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import com.sku.refit.domain.event.entity.EventReservationImage; public interface EventReservationImageRepository extends JpaRepository { - // 행사(eventId)에 속한 "모든 예약 이미지"를 최신 등록순(id desc)으로 - @Query( - """ - select eri - from EventReservationImage eri - join eri.reservation r - where r.event.id = :eventId - order by eri.id desc - """) - List findAllByEventIdOrderByIdDesc(Long eventId); + List findTop4ByReservation_Event_IdOrderByIdDesc( + Long eventId, Pageable pageable); - // 최근 4장 - @Query( - """ - select eri - from EventReservationImage eri - join eri.reservation r - where r.event.id = :eventId - order by eri.id desc - """) - List findTop4ByEventIdOrderByIdDesc(Long eventId, Pageable pageable); + int countByReservation_Event_Id(Long eventId); + + List findAllByReservation_Event_IdOrderByIdDesc(Long eventId); } diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index eeec707..2b96e6b 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -150,7 +150,7 @@ public void deleteEvent(Long id) { } } List images = - eventReservationImageRepository.findAllByEventIdOrderByIdDesc(id); + eventReservationImageRepository.findAllByReservation_Event_IdOrderByIdDesc(id); for (EventReservationImage img : images) { try { @@ -206,17 +206,28 @@ public List getEndedEvents() { @Override public EventGroupResponse getEventGroups() { - LocalDate today = LocalDate.now(); - List upcoming = eventRepository.findByDateGreaterThanEqualOrderByDateAsc(today); - List ended = eventRepository.findByDateLessThanOrderByDateDesc(today); + List top2Upcoming = + eventRepository.findByDateGreaterThanEqualOrderByDateAsc(today, PageRequest.of(0, 2)); + + Event upcomingEvent = top2Upcoming.size() >= 1 ? top2Upcoming.get(0) : null; + Event scheduledEvent = top2Upcoming.size() >= 2 ? top2Upcoming.get(1) : null; + + Event endedEvent = + eventRepository.findByDateLessThanOrderByDateDesc(today, PageRequest.of(0, 1)).stream() + .findFirst() + .orElse(null); + + EventCardResponse upcoming = + (upcomingEvent == null) ? null : eventMapper.toUpcomingCard(upcomingEvent, today); + + EventSimpleResponse scheduled = + (scheduledEvent == null) ? null : eventMapper.toSimple(scheduledEvent); - List upcomingCards = eventMapper.toUpcomingCardList(upcoming, today); - List scheduled = eventMapper.toSimpleList(upcoming); - List endedSimple = eventMapper.toSimpleList(ended); + EventSimpleResponse ended = (endedEvent == null) ? null : eventMapper.toSimple(endedEvent); - return eventMapper.toGroupResponse(upcomingCards, scheduled, endedSimple); + return eventMapper.toGroupResponseSingle(upcoming, scheduled, ended); } /* ========================= @@ -244,17 +255,14 @@ public EventDetailResponse getEventDetail(Long id) { List recent4 = eventReservationImageRepository - .findTop4ByEventIdOrderByIdDesc(id, PageRequest.of(0, 4)) + .findTop4ByReservation_Event_IdOrderByIdDesc(id, PageRequest.of(0, 4)) .stream() .map(EventReservationImage::getImageUrl) .toList(); - int totalClothCount = - eventReservationRepository.findByEventId(id).stream() - .mapToInt(r -> r.getClothCount() == null ? 0 : r.getClothCount()) - .sum(); + int totalUploadedClothCount = eventReservationImageRepository.countByReservation_Event_Id(id); - int clothCountExcept4 = Math.max(0, totalClothCount - recent4.size()); + int clothCountExcept4 = Math.max(0, totalUploadedClothCount - 4); return eventMapper.toDetail(event, isReserved, recent4, clothCountExcept4); } @@ -266,7 +274,9 @@ public List getEventAllReservationImages(Long eventId) { .findById(eventId) .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); - return eventReservationImageRepository.findAllByEventIdOrderByIdDesc(eventId).stream() + return eventReservationImageRepository + .findAllByReservation_Event_IdOrderByIdDesc(eventId) + .stream() .map(eventMapper::toImageResponse) .toList(); } @@ -311,7 +321,7 @@ public EventReservationResponse reserveEvent( } } - event.increaseReservedCount(reservation.getClothCount()); + event.increaseReservedCount(); return eventMapper.toReservationResponse(event); } From 3fb55d57063ea2fe37c183e81a9caabdb0e388f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:33:40 +0900 Subject: [PATCH 09/13] =?UTF-8?q?:recycle:=20Refactor:=20EventReservationI?= =?UTF-8?q?mage=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=A4=91=EB=B3=B5=20top4?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/repository/EventReservationImageRepository.java | 2 +- .../com/sku/refit/domain/event/service/EventServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java index 4c32213..0b527bf 100644 --- a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java +++ b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java @@ -14,7 +14,7 @@ public interface EventReservationImageRepository extends JpaRepository { List findTop4ByReservation_Event_IdOrderByIdDesc( - Long eventId, Pageable pageable); + Long eventId); int countByReservation_Event_Id(Long eventId); diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index 2b96e6b..f166b2e 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -255,7 +255,7 @@ public EventDetailResponse getEventDetail(Long id) { List recent4 = eventReservationImageRepository - .findTop4ByReservation_Event_IdOrderByIdDesc(id, PageRequest.of(0, 4)) + .findTop4ByReservation_Event_IdOrderByIdDesc(id) .stream() .map(EventReservationImage::getImageUrl) .toList(); From 71cdf845aa1edc3986b86b01c39de94dc6ec4537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:38:58 +0900 Subject: [PATCH 10/13] =?UTF-8?q?:recycle:=20Refactor:=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B8=EB=94=A9=20=EC=95=88=EC=A0=95=EC=84=B1=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/controller/EventControllerImpl.java | 29 ++++++++++++++----- .../EventReservationImageRepository.java | 4 +-- .../event/service/EventServiceImpl.java | 4 +-- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java b/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java index 582823d..49a4bf9 100644 --- a/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java +++ b/src/main/java/com/sku/refit/domain/event/controller/EventControllerImpl.java @@ -8,11 +8,19 @@ import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.sku.refit.domain.event.dto.request.EventRequest.*; -import com.sku.refit.domain.event.dto.response.EventResponse.*; +import com.sku.refit.domain.event.dto.request.EventRequest.EventInfoRequest; +import com.sku.refit.domain.event.dto.request.EventRequest.EventRsvRequest; +import com.sku.refit.domain.event.dto.response.EventResponse.EventCardResponse; +import com.sku.refit.domain.event.dto.response.EventResponse.EventDetailResponse; +import com.sku.refit.domain.event.dto.response.EventResponse.EventGroupResponse; +import com.sku.refit.domain.event.dto.response.EventResponse.EventImageResponse; +import com.sku.refit.domain.event.dto.response.EventResponse.EventReservationResponse; +import com.sku.refit.domain.event.dto.response.EventResponse.EventSimpleResponse; import com.sku.refit.domain.event.service.EventService; import com.sku.refit.global.response.BaseResponse; @@ -28,21 +36,24 @@ public class EventControllerImpl implements EventController { @Override public ResponseEntity> createEvent( - @Valid EventInfoRequest request, MultipartFile thumbnail) { + @RequestPart("request") @Valid EventInfoRequest request, + @RequestPart("thumbnail") MultipartFile thumbnail) { return ResponseEntity.ok(BaseResponse.success(eventService.createEvent(request, thumbnail))); } @Override public ResponseEntity> updateEvent( - Long id, @Valid EventInfoRequest request, MultipartFile thumbnail) { + @PathVariable Long id, + @RequestPart("request") @Valid EventInfoRequest request, + @RequestPart(value = "thumbnail", required = false) MultipartFile thumbnail) { return ResponseEntity.ok( BaseResponse.success(eventService.updateEvent(id, request, thumbnail))); } @Override - public ResponseEntity> deleteEvent(Long id) { + public ResponseEntity> deleteEvent(@PathVariable Long id) { eventService.deleteEvent(id); return ResponseEntity.ok(BaseResponse.success(null)); } @@ -63,20 +74,22 @@ public ResponseEntity> getEventGroups() { } @Override - public ResponseEntity> getEventDetail(Long id) { + public ResponseEntity> getEventDetail(@PathVariable Long id) { return ResponseEntity.ok(BaseResponse.success(eventService.getEventDetail(id))); } @Override public ResponseEntity>> getEventAllReservationImages( - Long id) { + @PathVariable Long id) { return ResponseEntity.ok(BaseResponse.success(eventService.getEventAllReservationImages(id))); } @Override public ResponseEntity> reserveEvent( - Long id, @Valid EventRsvRequest request, List clothImageList) { + @PathVariable Long id, + @RequestPart("request") @Valid EventRsvRequest request, + @RequestPart(value = "clothImageList", required = false) List clothImageList) { return ResponseEntity.ok( BaseResponse.success(eventService.reserveEvent(id, request, clothImageList))); diff --git a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java index 0b527bf..23d73a2 100644 --- a/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java +++ b/src/main/java/com/sku/refit/domain/event/repository/EventReservationImageRepository.java @@ -5,7 +5,6 @@ import java.util.List; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.sku.refit.domain.event.entity.EventReservationImage; @@ -13,8 +12,7 @@ public interface EventReservationImageRepository extends JpaRepository { - List findTop4ByReservation_Event_IdOrderByIdDesc( - Long eventId); + List findTop4ByReservation_Event_IdOrderByIdDesc(Long eventId); int countByReservation_Event_Id(Long eventId); diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index f166b2e..8a101ce 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -254,9 +254,7 @@ public EventDetailResponse getEventDetail(Long id) { } List recent4 = - eventReservationImageRepository - .findTop4ByReservation_Event_IdOrderByIdDesc(id) - .stream() + eventReservationImageRepository.findTop4ByReservation_Event_IdOrderByIdDesc(id).stream() .map(EventReservationImage::getImageUrl) .toList(); From 178b828c9a08a81ecf2dfcdc76510d439557cd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:41:43 +0900 Subject: [PATCH 11/13] =?UTF-8?q?:recycle:=20Refactor:=20=ED=96=89?= =?UTF-8?q?=EC=82=AC=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=8C=80=ED=91=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/service/EventServiceImpl.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index 8a101ce..fb7e488 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -81,13 +81,10 @@ public EventDetailResponse createEvent(EventInfoRequest request, MultipartFile t @Override @Transactional - public EventDetailResponse updateEvent( - Long id, EventInfoRequest request, MultipartFile thumbnail) { + public EventDetailResponse updateEvent(Long id, EventInfoRequest request, MultipartFile thumbnail) { - Event event = - eventRepository - .findById(id) - .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + Event event = eventRepository.findById(id) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); try { event.update( @@ -95,30 +92,13 @@ public EventDetailResponse updateEvent( request.getDescription(), request.getDate(), request.getLocation(), - request.getDetailLink()); - if (thumbnail != null && !thumbnail.isEmpty()) { - String oldThumbUrl = event.getThumbnailUrl(); - if (oldThumbUrl != null) { - try { - s3Service.deleteFile(s3Service.extractKeyNameFromUrl(oldThumbUrl)); - } catch (CustomException e) { - throw e; - } catch (Exception e) { - log.error("[EVENT] updateEvent - thumbnail delete failed, eventId={}", id, e); - throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_DELETE_FAILED); - } - } + request.getDetailLink() + ); - try { - String newThumbUrl = s3Service.uploadFileAsWebp(PathName.EVENT, thumbnail); - event.updateThumbnailUrl(newThumbUrl); - } catch (CustomException e) { - throw e; - } catch (Exception e) { - log.error("[EVENT] updateEvent - thumbnail upload failed, eventId={}", id, e); - throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_UPLOAD_FAILED); - } + if (thumbnail != null && !thumbnail.isEmpty()) { + replaceThumbnail(event, id, thumbnail); } + return getEventDetail(event.getId()); } catch (CustomException e) { @@ -129,6 +109,29 @@ public EventDetailResponse updateEvent( } } + private void replaceThumbnail(Event event, Long eventId, MultipartFile thumbnail) { + String oldThumbUrl = event.getThumbnailUrl(); + + final String newThumbUrl; + try { + newThumbUrl = s3Service.uploadFileAsWebp(PathName.EVENT, thumbnail); + } catch (Exception e) { + log.error("[EVENT] updateEvent - thumbnail upload failed, eventId={}", eventId, e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_UPLOAD_FAILED); + } + + event.updateThumbnailUrl(newThumbUrl); + + if (oldThumbUrl != null) { + try { + s3Service.deleteFile(s3Service.extractKeyNameFromUrl(oldThumbUrl)); + } catch (Exception e) { + log.error("[EVENT] updateEvent - thumbnail delete failed, eventId={}", eventId, e); + throw new CustomException(EventErrorCode.EVENT_THUMBNAIL_DELETE_FAILED); + } + } + } + @Override @Transactional public void deleteEvent(Long id) { From fd5f723d44af58755f8c045af620f66cf394caaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:44:10 +0900 Subject: [PATCH 12/13] =?UTF-8?q?:wrench:=20Settings:=20srcimage=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9eec12a..3a450f4 100644 --- a/build.gradle +++ b/build.gradle @@ -59,8 +59,8 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' // Image - implementation("com.sksamuel.scrimage:scrimage-core:4.2.0") - implementation("com.sksamuel.scrimage:scrimage-webp:4.2.0") + implementation("com.sksamuel.scrimage:scrimage-core:4.3.5") + implementation("com.sksamuel.scrimage:scrimage-webp:4.3.5") } tasks.named('test') { From 8a55e9a46c15e7bd960ad1621c51cb0436bbd832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Sat, 13 Dec 2025 06:48:57 +0900 Subject: [PATCH 13/13] =?UTF-8?q?:recycle:=20Refactor:=20=EB=B9=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=96=89=EC=82=AC=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/event/service/EventServiceImpl.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java index fb7e488..8d253c5 100644 --- a/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/event/service/EventServiceImpl.java @@ -81,10 +81,13 @@ public EventDetailResponse createEvent(EventInfoRequest request, MultipartFile t @Override @Transactional - public EventDetailResponse updateEvent(Long id, EventInfoRequest request, MultipartFile thumbnail) { + public EventDetailResponse updateEvent( + Long id, EventInfoRequest request, MultipartFile thumbnail) { - Event event = eventRepository.findById(id) - .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); + Event event = + eventRepository + .findById(id) + .orElseThrow(() -> new CustomException(EventErrorCode.EVENT_NOT_FOUND)); try { event.update( @@ -92,8 +95,7 @@ public EventDetailResponse updateEvent(Long id, EventInfoRequest request, Multip request.getDescription(), request.getDate(), request.getLocation(), - request.getDetailLink() - ); + request.getDetailLink()); if (thumbnail != null && !thumbnail.isEmpty()) { replaceThumbnail(event, id, thumbnail); @@ -249,6 +251,7 @@ public EventDetailResponse getEventDetail(Long id) { try { user = userService.getCurrentUser(); } catch (Exception e) { + log.debug("[EVENT] getEventDetail - unauthenticated request"); } Boolean isReserved = null;