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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 135 additions & 24 deletions src/main/java/com/sku/refit/domain/event/controller/EventController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@
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.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.EventPagedResponse;
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.entity.EventStatus;
import com.sku.refit.global.response.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -30,47 +38,132 @@ public interface EventController {
* ========================= */

@PostMapping(value = "/admin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "[관리자] 행사 생성", description = "행사를 생성합니다.")
@Operation(
summary = "[관리자] 행사 생성",
description =
"""
multipart/form-data 로 요청합니다.

- request: 행사 정보(JSON)
- thumbnail: 대표 사진(파일)
""")
ResponseEntity<BaseResponse<EventDetailResponse>> createEvent(
@Parameter(
description = "행사 정보",
description = "행사 정보(JSON 파트)",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE))
@RequestPart("request")
@Valid
EventInfoRequest request,
@Parameter(
description = "대표 사진(썸네일)",
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 = "행사를 수정합니다.")
@Operation(
summary = "[관리자] 행사 수정",
description =
"""
multipart/form-data 로 요청합니다.

- request: 행사 정보(JSON)
- thumbnail: 대표 사진(파일, 선택)
""")
ResponseEntity<BaseResponse<EventDetailResponse>> updateEvent(
@PathVariable Long id,
@RequestPart("request") @Valid EventInfoRequest request,
@RequestPart(value = "thumbnail", required = false) MultipartFile thumbnail);
@Parameter(description = "행사 ID", example = "1") @PathVariable Long id,
@Parameter(
description = "행사 정보(JSON 파트)",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE))
@RequestPart("request")
@Valid
EventInfoRequest request,
@Parameter(
description = "대표 사진(썸네일 파일 파트, 선택)",
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
@RequestPart(value = "thumbnail", required = false)
MultipartFile thumbnail);

@DeleteMapping("/admin/{id}")
@Operation(summary = "[관리자] 행사 삭제", description = "행사 뿐만 아니라 대표사진 및 이미지들까지 모두 삭제합니다.")
ResponseEntity<BaseResponse<Void>> deleteEvent(@PathVariable Long id);
@Operation(summary = "[관리자] 행사 삭제", description = "행사 뿐만 아니라 대표사진 및 예약 이미지들까지 모두 삭제합니다.")
ResponseEntity<BaseResponse<Void>> deleteEvent(
@Parameter(description = "행사 ID", example = "1") @PathVariable Long id);

@GetMapping("/admin")
@Operation(
summary = "[관리자] 행사 리스트 조회",
description =
"""
페이지네이션 기반으로 행사 목록을 조회합니다.

■ 조회 옵션
- status 생략: 전체 조회
- status=UPCOMING: 예정
- status=ONGOING: 진행중
- status=ENDED: 완료
- q: 검색어(행사명/장소 부분일치)

■ 상태 판정 기준 (today 기준)
- UPCOMING: startDate > today
- ONGOING: startDate <= today <= endDate
- ENDED: endDate < today

■ 정렬
- status 미지정(전체): ONGOING → UPCOMING → ENDED (우선순위) + 각 그룹 내 정렬은 아래 기준 적용
- ONGOING: startDate desc (최근 시작한 진행중부터)
- UPCOMING: startDate asc (곧 시작하는 행사부터)
- ENDED: endDate desc (최근 종료된 것부터)
""")
ResponseEntity<BaseResponse<EventPagedResponse>> getEvents(
@Parameter(description = "페이지 번호(0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "10") @RequestParam(defaultValue = "10")
int size,
@Parameter(description = "행사 상태 필터(미지정 시 전체). UPCOMING/ONGOING/ENDED", example = "ONGOING")
@RequestParam(required = false)
EventStatus status,
@Parameter(description = "검색어(행사명/장소 부분일치)", example = "서울") @RequestParam(required = false)
String q);

/* =========================
* Public List
* ========================= */

@GetMapping("/upcoming")
@Operation(summary = "예정된 행사 리스트", description = "예정된 행사만 조회합니다. D-day가 가까운 순(오름차순)으로 정렬합니다.")
@Operation(
summary = "예정된 행사 리스트",
description =
"""
예정된 행사만 조회합니다.

- UPCOMING: startDate > today
- 정렬: startDate asc (곧 시작하는 행사부터)
""")
ResponseEntity<BaseResponse<List<EventCardResponse>>> getUpcomingEvents();

@GetMapping("/end")
@Operation(summary = "종료된 행사 리스트", description = "종료된 행사만 조회합니다. 최근 종료 순(내림차순)으로 정렬합니다.")
@GetMapping("/ended")
@Operation(
summary = "종료된 행사 리스트",
description =
"""
종료된 행사만 조회합니다.

- ENDED: endDate < today
- 정렬: endDate desc (최근 종료된 행사부터)
""")
ResponseEntity<BaseResponse<List<EventSimpleResponse>>> getEndedEvents();

@GetMapping
@GetMapping("/group")
@Operation(
summary = "다가오는/예정된/종료된 행사 조회",
description = "다가오는(가장 가까운 D-day 1개) / 예정된(그 다음 1개) / 종료된(가장 최근 종료 1개)로 반환합니다.")
summary = "행사 그룹 조회 (다가오는/예정/종료 행사 1개씩)",
description =
"""
목록(list)이 아니라 그룹 응답입니다.

- upcoming: 가장 가까운 D-day 1개
- scheduled: 그 다음 예정 1개
- ended: 가장 최근 종료 1개
""")
ResponseEntity<BaseResponse<EventGroupResponse>> getEventGroups();

/* =========================
Expand All @@ -80,22 +173,40 @@ ResponseEntity<BaseResponse<EventDetailResponse>> updateEvent(
@GetMapping("/{id}")
@Operation(
summary = "행사 상세 조회",
description = "행사 예약 여부 + 행사 정보 + 최근 예약 이미지 4장 + 4장 제외 의류수를 반환합니다.")
ResponseEntity<BaseResponse<EventDetailResponse>> getEventDetail(@PathVariable Long id);
description = "행사 예약 여부 + 행사 정보 + 최근 예약 이미지 4장 + 4장 제외 의류 수를 반환합니다.")
ResponseEntity<BaseResponse<EventDetailResponse>> getEventDetail(
@Parameter(description = "행사 ID", example = "1") @PathVariable Long id);

@GetMapping("/{id}/img")
@Operation(summary = "행사 더보기 이미지 조회", description = "해당 행사의 예약에서 업로드된 모든 옷 사진을 최신 등록순으로 반환합니다.")
ResponseEntity<BaseResponse<List<EventImageResponse>>> getEventAllReservationImages(
@PathVariable Long id);
@Parameter(description = "행사 ID", example = "1") @PathVariable Long id);

/* =========================
* Reservation
* ========================= */

@PostMapping(value = "/{id}/rsv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "행사 예약", description = "예약 정보를 저장하고 업로드 이미지는 WebP로 저장합니다.")
@Operation(
summary = "행사 예약",
description =
"""
multipart/form-data 로 요청합니다.

- request: 예약 정보(JSON)
- clothImageList: 의류 이미지 리스트(파일, 선택)
""")
ResponseEntity<BaseResponse<EventReservationResponse>> reserveEvent(
@PathVariable Long id,
@RequestPart("request") @Valid EventRsvRequest request,
@RequestPart(value = "clothImageList", required = false) List<MultipartFile> clothImageList);
@Parameter(description = "행사 ID", example = "1") @PathVariable Long id,
@Parameter(
description = "예약 정보(JSON 파트)",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE))
@RequestPart("request")
@Valid
EventRsvRequest request,
@Parameter(
description = "의류 이미지 리스트(파일 파트, 선택)",
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
@RequestPart(value = "clothImageList", required = false)
List<MultipartFile> clothImageList);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
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.EventPagedResponse;
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.entity.EventStatus;
import com.sku.refit.domain.event.service.EventService;
import com.sku.refit.global.response.BaseResponse;

Expand Down Expand Up @@ -94,4 +96,11 @@ public ResponseEntity<BaseResponse<EventReservationResponse>> reserveEvent(
return ResponseEntity.ok(
BaseResponse.success(eventService.reserveEvent(id, request, clothImageList)));
}

@Override
public ResponseEntity<BaseResponse<EventPagedResponse>> getEvents(
int page, int size, EventStatus status, String q) {

return ResponseEntity.ok(BaseResponse.success(eventService.getEvents(page, size, status, q)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ public static class EventInfoRequest {
@Schema(description = "행사 설명", example = "파티에 대한 짧은 설명입니다.")
private String description;

@NotNull(message = "행사 날짜는 필수입니다.") @Schema(description = "행사 날짜", example = "2025-12-24")
private LocalDate date;
@NotNull(message = "행사 시작 날짜는 필수입니다.") @Schema(description = "시작 날짜", example = "2025-12-24")
private LocalDate startDate;

@NotNull(message = "행사 종료 날짜는 필수입니다.") @Schema(description = "종료 날짜", example = "2025-12-26")
private LocalDate endDate;

@Schema(description = "예약 정원", example = "100")
private Integer capacity;
Comment on lines +30 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

날짜 범위 및 capacity 유효성 검증 누락

  1. startDateendDate 간의 관계 검증이 없습니다. endDatestartDate보다 이전일 경우 유효하지 않은 데이터가 저장될 수 있습니다.
  2. capacity 필드에 최소값 제약이 없어 0 또는 음수가 입력될 수 있습니다.

날짜 범위 검증은 커스텀 validator나 서비스 레이어에서 처리할 수 있으며, capacity에는 기본 제약을 추가하는 것을 권장합니다:

  @Schema(description = "예약 정원", example = "100")
+ @Positive(message = "예약 정원은 1 이상이어야 합니다.")
  private Integer capacity;

날짜 범위 검증이 서비스 레이어에서 처리되고 있는지 확인이 필요합니다:

#!/bin/bash
# 서비스에서 날짜 검증 로직 확인
rg -n "startDate.*endDate|endDate.*startDate|isBefore|isAfter" --type java -C 3


@NotBlank(message = "행사 장소는 필수입니다.")
@Schema(description = "행사 장소", example = "서울 성동구")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.time.LocalDate;
import java.util.List;

import com.sku.refit.domain.event.entity.EventStatus;

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

Expand All @@ -25,7 +27,7 @@ public static class EventDetailResponse {
@Schema(description = "행사 식별자", example = "1")
private Long eventId;

@Schema(description = "누적 예약 인원(옷 수량 기준)", example = "25")
@Schema(description = "누적 예약 인원", example = "25")
private Integer totalReservedCount;

@Schema(description = "행사 대표 이미지 URL")
Expand All @@ -42,12 +44,18 @@ public static class EventDetailResponse {

/* ===== 중간 ===== */

@Schema(description = "행사 날짜", example = "2025-12-24")
private LocalDate date;
@Schema(description = "시작 날짜", example = "2025-12-24")
private LocalDate startDate;

@Schema(description = "종료 날짜", example = "2025-12-26")
private LocalDate endDate;

@Schema(description = "행사 장소", example = "서울 성동구")
private String location;

@Schema(description = "예약 정원", example = "100")
private Integer capacity;

/* ===== 하단 ===== */

@Schema(description = "최근 등록된 예약 이미지 4장 URL 리스트")
Expand Down Expand Up @@ -106,9 +114,6 @@ public static class EventCardResponse {
@Schema(description = "행사 설명", example = "따뜻한 겨울을 위한 의류 기부 행사입니다.")
private String description;

@Schema(description = "행사 날짜", example = "2025-12-24")
private LocalDate date;

@Schema(description = "행사 장소", example = "서울 성동구")
private String location;
}
Expand All @@ -127,8 +132,8 @@ public static class EventSimpleResponse {
@Schema(description = "행사명", example = "겨울 의류 나눔 행사")
private String name;

@Schema(description = "행사 날짜", example = "2025-12-24")
private LocalDate date;
@Schema(description = "시작 날짜", example = "2025-12-24")
private LocalDate startDate;

@Schema(description = "행사 장소", example = "서울 성동구")
private String location;
Expand All @@ -148,4 +153,41 @@ public static class EventGroupResponse {
@Schema(description = "종료된 행사")
private EventSimpleResponse ended;
}

@Getter
@Builder
public static class EventPagedResponse {
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean hasNext;
private List<EventListItem> items;
}

@Getter
@Builder
public static class EventListItem {

@Schema(description = "행사 식별자", example = "1")
private Long eventId;

@Schema(description = "행사명", example = "겨울 의류 나눔 행사")
private String name;

@Schema(description = "시작 날짜", example = "2025-12-24")
private LocalDate startDate;

@Schema(description = "행사 장소", example = "서울 성동구")
private String location;

@Schema(description = "누적 예약 인원", example = "25")
private Integer reservedCount;

@Schema(description = "예약 정원", example = "100")
private Integer capacity;

@Schema(description = "행사 상태", example = "진행중")
private EventStatus status;
}
}
Loading