diff --git a/src/main/java/gg/agit/konect/domain/event/controller/EventApi.java b/src/main/java/gg/agit/konect/domain/event/controller/EventApi.java new file mode 100644 index 000000000..fc24c77e7 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/controller/EventApi.java @@ -0,0 +1,75 @@ +package gg.agit.konect.domain.event.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import gg.agit.konect.domain.event.dto.EventBoothMapResponse; +import gg.agit.konect.domain.event.dto.EventBoothsResponse; +import gg.agit.konect.domain.event.dto.EventContentsResponse; +import gg.agit.konect.domain.event.dto.EventHomeResponse; +import gg.agit.konect.domain.event.dto.EventMiniEventsResponse; +import gg.agit.konect.domain.event.dto.EventProgramsResponse; +import gg.agit.konect.domain.event.enums.EventProgramType; +import gg.agit.konect.global.auth.annotation.UserId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Min; + +@Tag(name = "(Normal) Event: 행사", description = "행사 API") +@RequestMapping("/events") +public interface EventApi { + + @Operation(summary = "행사 홈 정보를 조회한다.") + @GetMapping("/{eventId}/home") + ResponseEntity getEventHome( + @PathVariable Integer eventId, + @UserId Integer userId + ); + + @Operation(summary = "행사 프로그램 목록을 조회한다.") + @GetMapping("/{eventId}/programs") + ResponseEntity getEventPrograms( + @PathVariable Integer eventId, + @RequestParam(defaultValue = "ALL") EventProgramType type, + @RequestParam(defaultValue = "1") @Min(1) Integer page, + @RequestParam(defaultValue = "20") @Min(1) Integer limit, + @UserId Integer userId + ); + + @Operation(summary = "행사 부스 목록을 조회한다.") + @GetMapping("/{eventId}/booths") + ResponseEntity getEventBooths( + @PathVariable Integer eventId, + @RequestParam(required = false) String category, + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "1") @Min(1) Integer page, + @RequestParam(defaultValue = "20") @Min(1) Integer limit + ); + + @Operation(summary = "행사 부스 맵을 조회한다.") + @GetMapping("/{eventId}/booth-map") + ResponseEntity getEventBoothMap( + @PathVariable Integer eventId + ); + + @Operation(summary = "행사 미니 이벤트 목록을 조회한다.") + @GetMapping("/{eventId}/mini-events") + ResponseEntity getEventMiniEvents( + @PathVariable Integer eventId, + @RequestParam(defaultValue = "1") @Min(1) Integer page, + @RequestParam(defaultValue = "20") @Min(1) Integer limit, + @UserId Integer userId + ); + + @Operation(summary = "행사 콘텐츠 목록을 조회한다.") + @GetMapping("/{eventId}/contents") + ResponseEntity getEventContents( + @PathVariable Integer eventId, + @RequestParam(required = false) String category, + @RequestParam(defaultValue = "1") @Min(1) Integer page, + @RequestParam(defaultValue = "20") @Min(1) Integer limit + ); +} diff --git a/src/main/java/gg/agit/konect/domain/event/controller/EventController.java b/src/main/java/gg/agit/konect/domain/event/controller/EventController.java new file mode 100644 index 000000000..278ce2b4e --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/controller/EventController.java @@ -0,0 +1,58 @@ +package gg.agit.konect.domain.event.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import gg.agit.konect.domain.event.dto.EventBoothMapResponse; +import gg.agit.konect.domain.event.dto.EventBoothsResponse; +import gg.agit.konect.domain.event.dto.EventContentsResponse; +import gg.agit.konect.domain.event.dto.EventHomeResponse; +import gg.agit.konect.domain.event.dto.EventMiniEventsResponse; +import gg.agit.konect.domain.event.dto.EventProgramsResponse; +import gg.agit.konect.domain.event.enums.EventProgramType; +import gg.agit.konect.domain.event.service.EventService; +import lombok.RequiredArgsConstructor; + +@RestController +@Validated +@RequiredArgsConstructor +public class EventController implements EventApi { + + private final EventService eventService; + + @Override + public ResponseEntity getEventHome(Integer eventId, Integer userId) { + return ResponseEntity.ok(eventService.getEventHome(eventId, userId)); + } + + @Override + public ResponseEntity getEventPrograms(Integer eventId, EventProgramType type, Integer page, + Integer limit, + Integer userId) { + return ResponseEntity.ok(eventService.getEventPrograms(eventId, type, page, limit, userId)); + } + + @Override + public ResponseEntity getEventBooths(Integer eventId, String category, String keyword, + Integer page, Integer limit) { + return ResponseEntity.ok(eventService.getEventBooths(eventId, category, keyword, page, limit)); + } + + @Override + public ResponseEntity getEventBoothMap(Integer eventId) { + return ResponseEntity.ok(eventService.getEventBoothMap(eventId)); + } + + @Override + public ResponseEntity getEventMiniEvents(Integer eventId, Integer page, Integer limit, + Integer userId) { + return ResponseEntity.ok(eventService.getEventMiniEvents(eventId, page, limit, userId)); + } + + @Override + public ResponseEntity getEventContents(Integer eventId, String category, Integer page, + Integer limit) { + return ResponseEntity.ok(eventService.getEventContents(eventId, category, page, limit)); + } +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventBoothMapResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothMapResponse.java new file mode 100644 index 000000000..cbd041ef0 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothMapResponse.java @@ -0,0 +1,28 @@ +package gg.agit.konect.domain.event.dto; + +import java.util.List; + +public record EventBoothMapResponse( + String mapImageUrl, + List zones, + List booths +) { + + public record ZoneResponse( + String code, + String label + ) { + } + + public record BoothMapItemResponse( + Integer boothId, + String name, + String zone, + Integer x, + Integer y, + Integer width, + Integer height, + String status + ) { + } +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventBoothSummaryResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothSummaryResponse.java new file mode 100644 index 000000000..95fb2b120 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothSummaryResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +public record EventBoothSummaryResponse( + Integer boothId, + String name, + String category, + String locationLabel, + String zone, + String thumbnailUrl, + boolean open +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventBoothsResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothsResponse.java new file mode 100644 index 000000000..5be449edc --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventBoothsResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +import java.util.List; + +public record EventBoothsResponse( + Long totalCount, + Integer currentCount, + Integer totalPage, + Integer currentPage, + List booths +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventContentSummaryResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventContentSummaryResponse.java new file mode 100644 index 000000000..c137ef301 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventContentSummaryResponse.java @@ -0,0 +1,13 @@ +package gg.agit.konect.domain.event.dto; + +import java.time.LocalDateTime; + +public record EventContentSummaryResponse( + Integer contentId, + String title, + String thumbnailUrl, + String type, + String summary, + LocalDateTime publishedAt +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventContentsResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventContentsResponse.java new file mode 100644 index 000000000..459293fa1 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventContentsResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +import java.util.List; + +public record EventContentsResponse( + Long totalCount, + Integer currentCount, + Integer totalPage, + Integer currentPage, + List contents +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventHomeResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventHomeResponse.java new file mode 100644 index 000000000..c9e4efb63 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventHomeResponse.java @@ -0,0 +1,30 @@ +package gg.agit.konect.domain.event.dto; + +import java.time.LocalDateTime; + +public record EventHomeResponse( + Integer eventId, + String title, + String subtitle, + String posterImageUrl, + LocalDateTime startAt, + LocalDateTime endAt, + String notice, + Summary summary, + UserStatus userStatus +) { + + public record Summary( + Integer programCount, + Integer boothCount, + Integer eventCount, + Integer contentCount + ) { + } + + public record UserStatus( + Integer point, + Integer participatedEventCount + ) { + } +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventSummaryResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventSummaryResponse.java new file mode 100644 index 000000000..0b9152635 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventSummaryResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +public record EventMiniEventSummaryResponse( + Integer miniEventId, + String title, + String thumbnailUrl, + String description, + String reward, + String status, + boolean joined +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventsResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventsResponse.java new file mode 100644 index 000000000..39ed37444 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventMiniEventsResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +import java.util.List; + +public record EventMiniEventsResponse( + Long totalCount, + Integer currentCount, + Integer totalPage, + Integer currentPage, + List miniEvents +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventProgramSummaryResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventProgramSummaryResponse.java new file mode 100644 index 000000000..3a77bdf66 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventProgramSummaryResponse.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.dto; + +public record EventProgramSummaryResponse( + Integer programId, + String title, + String description, + String thumbnailUrl, + Integer rewardPoint, + String status, + boolean participated +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/dto/EventProgramsResponse.java b/src/main/java/gg/agit/konect/domain/event/dto/EventProgramsResponse.java new file mode 100644 index 000000000..33242a892 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/dto/EventProgramsResponse.java @@ -0,0 +1,15 @@ +package gg.agit.konect.domain.event.dto; + +import java.util.List; + +import gg.agit.konect.domain.event.enums.EventProgramType; + +public record EventProgramsResponse( + Long totalCount, + Integer currentCount, + Integer totalPage, + Integer currentPage, + EventProgramType type, + List programs +) { +} diff --git a/src/main/java/gg/agit/konect/domain/event/enums/EventBoothMapItemStatus.java b/src/main/java/gg/agit/konect/domain/event/enums/EventBoothMapItemStatus.java new file mode 100644 index 000000000..d9b4198eb --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/enums/EventBoothMapItemStatus.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.event.enums; + +public enum EventBoothMapItemStatus { + OPEN, + CLOSED, + HIDDEN +} diff --git a/src/main/java/gg/agit/konect/domain/event/enums/EventContentType.java b/src/main/java/gg/agit/konect/domain/event/enums/EventContentType.java new file mode 100644 index 000000000..082fca99c --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/enums/EventContentType.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.event.enums; + +public enum EventContentType { + ARTICLE, + IMAGE, + VIDEO +} diff --git a/src/main/java/gg/agit/konect/domain/event/enums/EventProgramType.java b/src/main/java/gg/agit/konect/domain/event/enums/EventProgramType.java new file mode 100644 index 000000000..bd5388611 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/enums/EventProgramType.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.event.enums; + +public enum EventProgramType { + ALL, + POINT, + RESONANCE +} diff --git a/src/main/java/gg/agit/konect/domain/event/enums/EventProgressStatus.java b/src/main/java/gg/agit/konect/domain/event/enums/EventProgressStatus.java new file mode 100644 index 000000000..9c734667b --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/enums/EventProgressStatus.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.event.enums; + +public enum EventProgressStatus { + UPCOMING, + ONGOING, + ENDED +} diff --git a/src/main/java/gg/agit/konect/domain/event/enums/EventStatus.java b/src/main/java/gg/agit/konect/domain/event/enums/EventStatus.java new file mode 100644 index 000000000..7974b2b83 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/enums/EventStatus.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.event.enums; + +public enum EventStatus { + DRAFT, + PUBLISHED, + ENDED +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/Event.java b/src/main/java/gg/agit/konect/domain/event/model/Event.java new file mode 100644 index 000000000..3ad75db56 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/Event.java @@ -0,0 +1,52 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import java.time.LocalDateTime; + +import gg.agit.konect.domain.event.enums.EventStatus; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event") +@NoArgsConstructor(access = PROTECTED) +public class Event extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "subtitle", length = 255) + private String subtitle; + + @Column(name = "poster_image_url", length = 255) + private String posterImageUrl; + + @Column(name = "notice", columnDefinition = "TEXT") + private String notice; + + @Column(name = "start_at", nullable = false) + private LocalDateTime startAt; + + @Column(name = "end_at", nullable = false) + private LocalDateTime endAt; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private EventStatus status; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventBooth.java b/src/main/java/gg/agit/konect/domain/event/model/EventBooth.java new file mode 100644 index 000000000..ba38b15b2 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventBooth.java @@ -0,0 +1,56 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_booth") +@NoArgsConstructor(access = PROTECTED) +public class EventBooth extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "event_id", nullable = false, updatable = false) + private Event event; + + @Column(name = "name", nullable = false, length = 100) + private String name; + + @Column(name = "category", nullable = false, length = 50) + private String category; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Column(name = "location_label", length = 100) + private String locationLabel; + + @Column(name = "zone", length = 50) + private String zone; + + @Column(name = "thumbnail_url", length = 255) + private String thumbnailUrl; + + @Column(name = "is_open", nullable = false) + private Boolean isOpen; + + @Column(name = "display_order", nullable = false) + private Integer displayOrder; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventBoothMap.java b/src/main/java/gg/agit/konect/domain/event/model/EventBoothMap.java new file mode 100644 index 000000000..72d76cd1f --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventBoothMap.java @@ -0,0 +1,41 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_booth_map") +@NoArgsConstructor(access = PROTECTED) +public class EventBoothMap extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "event_id", nullable = false, updatable = false) + private Event event; + + @Column(name = "map_image_url", length = 255) + private String mapImageUrl; + + @Column(name = "width") + private Integer width; + + @Column(name = "height") + private Integer height; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventBoothMapItem.java b/src/main/java/gg/agit/konect/domain/event/model/EventBoothMapItem.java new file mode 100644 index 000000000..73f8699a6 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventBoothMapItem.java @@ -0,0 +1,56 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.domain.event.enums.EventBoothMapItemStatus; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_booth_map_item") +@NoArgsConstructor(access = PROTECTED) +public class EventBoothMapItem extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "event_booth_map_id", nullable = false, updatable = false) + private EventBoothMap eventBoothMap; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "event_booth_id", nullable = false, updatable = false) + private EventBooth eventBooth; + + @Column(name = "x", nullable = false) + private Integer x; + + @Column(name = "y", nullable = false) + private Integer y; + + @Column(name = "width", nullable = false) + private Integer width; + + @Column(name = "height", nullable = false) + private Integer height; + + @Enumerated(STRING) + @Column(name = "status", nullable = false, length = 20) + private EventBoothMapItemStatus status; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventContent.java b/src/main/java/gg/agit/konect/domain/event/model/EventContent.java new file mode 100644 index 000000000..0562797ab --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventContent.java @@ -0,0 +1,59 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import java.time.LocalDateTime; + +import gg.agit.konect.domain.event.enums.EventContentType; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_content") +@NoArgsConstructor(access = PROTECTED) +public class EventContent extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "event_id", nullable = false, updatable = false) + private Event event; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "summary", nullable = false, length = 255) + private String summary; + + @Column(name = "body", columnDefinition = "TEXT") + private String body; + + @Column(name = "thumbnail_url", length = 255) + private String thumbnailUrl; + + @Enumerated(STRING) + @Column(name = "type", nullable = false, length = 20) + private EventContentType type; + + @Column(name = "published_at") + private LocalDateTime publishedAt; + + @Column(name = "display_order", nullable = false) + private Integer displayOrder; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventMiniEvent.java b/src/main/java/gg/agit/konect/domain/event/model/EventMiniEvent.java new file mode 100644 index 000000000..682d9c1e8 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventMiniEvent.java @@ -0,0 +1,54 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.domain.event.enums.EventProgressStatus; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_mini_event") +@NoArgsConstructor(access = PROTECTED) +public class EventMiniEvent extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "event_id", nullable = false, updatable = false) + private Event event; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "description", nullable = false, length = 255) + private String description; + + @Column(name = "thumbnail_url", length = 255) + private String thumbnailUrl; + + @Column(name = "reward_label", length = 100) + private String rewardLabel; + + @Enumerated(STRING) + @Column(name = "status", nullable = false, length = 20) + private EventProgressStatus status; + + @Column(name = "display_order", nullable = false) + private Integer displayOrder; +} diff --git a/src/main/java/gg/agit/konect/domain/event/model/EventProgram.java b/src/main/java/gg/agit/konect/domain/event/model/EventProgram.java new file mode 100644 index 000000000..ffd11c6b7 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/model/EventProgram.java @@ -0,0 +1,59 @@ +package gg.agit.konect.domain.event.model; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.domain.event.enums.EventProgressStatus; +import gg.agit.konect.domain.event.enums.EventProgramType; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "event_program") +@NoArgsConstructor(access = PROTECTED) +public class EventProgram extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "event_id", nullable = false, updatable = false) + private Event event; + + @Enumerated(STRING) + @Column(name = "type", nullable = false, length = 20) + private EventProgramType type; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "description", nullable = false, length = 255) + private String description; + + @Column(name = "thumbnail_url", length = 255) + private String thumbnailUrl; + + @Column(name = "reward_point") + private Integer rewardPoint; + + @Enumerated(STRING) + @Column(name = "status", nullable = false, length = 20) + private EventProgressStatus status; + + @Column(name = "display_order", nullable = false) + private Integer displayOrder; +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapItemRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapItemRepository.java new file mode 100644 index 000000000..82d88e7a1 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapItemRepository.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventBoothMapItem; + +public interface EventBoothMapItemRepository extends Repository { + + List findAllByEventBoothMapIdOrderByIdAsc(Integer eventBoothMapId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapRepository.java new file mode 100644 index 000000000..bf57d6fb0 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapRepository.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventBoothMap; + +public interface EventBoothMapRepository extends Repository { + + Optional findByEventId(Integer eventId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventBoothRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothRepository.java new file mode 100644 index 000000000..63795ec42 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventBooth; + +public interface EventBoothRepository extends Repository { + + List findAllByEventIdOrderByDisplayOrderAscIdAsc(Integer eventId); + + int countByEventId(Integer eventId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventContentRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventContentRepository.java new file mode 100644 index 000000000..10afa98ee --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventContentRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventContent; + +public interface EventContentRepository extends Repository { + + List findAllByEventIdOrderByDisplayOrderAscIdAsc(Integer eventId); + + int countByEventId(Integer eventId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventMiniEventRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventMiniEventRepository.java new file mode 100644 index 000000000..441573261 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventMiniEventRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventMiniEvent; + +public interface EventMiniEventRepository extends Repository { + + List findAllByEventIdOrderByDisplayOrderAscIdAsc(Integer eventId); + + int countByEventId(Integer eventId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventProgramRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventProgramRepository.java new file mode 100644 index 000000000..4f6f0b457 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventProgramRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.EventProgram; + +public interface EventProgramRepository extends Repository { + + List findAllByEventIdOrderByDisplayOrderAscIdAsc(Integer eventId); + + int countByEventId(Integer eventId); +} diff --git a/src/main/java/gg/agit/konect/domain/event/repository/EventRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventRepository.java new file mode 100644 index 000000000..b61c91bf3 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/repository/EventRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.event.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.event.model.Event; + +public interface EventRepository extends Repository { + + Optional findById(Integer id); + + Event save(Event event); +} diff --git a/src/main/java/gg/agit/konect/domain/event/service/EventService.java b/src/main/java/gg/agit/konect/domain/event/service/EventService.java new file mode 100644 index 000000000..344f0f93e --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/event/service/EventService.java @@ -0,0 +1,266 @@ +package gg.agit.konect.domain.event.service; + +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_EVENT; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.event.dto.EventBoothMapResponse; +import gg.agit.konect.domain.event.dto.EventBoothSummaryResponse; +import gg.agit.konect.domain.event.dto.EventBoothsResponse; +import gg.agit.konect.domain.event.dto.EventContentSummaryResponse; +import gg.agit.konect.domain.event.dto.EventContentsResponse; +import gg.agit.konect.domain.event.dto.EventHomeResponse; +import gg.agit.konect.domain.event.dto.EventMiniEventSummaryResponse; +import gg.agit.konect.domain.event.dto.EventMiniEventsResponse; +import gg.agit.konect.domain.event.dto.EventProgramSummaryResponse; +import gg.agit.konect.domain.event.dto.EventProgramsResponse; +import gg.agit.konect.domain.event.enums.EventProgramType; +import gg.agit.konect.domain.event.model.Event; +import gg.agit.konect.domain.event.model.EventBooth; +import gg.agit.konect.domain.event.model.EventBoothMap; +import gg.agit.konect.domain.event.model.EventBoothMapItem; +import gg.agit.konect.domain.event.model.EventContent; +import gg.agit.konect.domain.event.model.EventMiniEvent; +import gg.agit.konect.domain.event.model.EventProgram; +import gg.agit.konect.domain.event.repository.EventBoothMapItemRepository; +import gg.agit.konect.domain.event.repository.EventBoothMapRepository; +import gg.agit.konect.domain.event.repository.EventBoothRepository; +import gg.agit.konect.domain.event.repository.EventContentRepository; +import gg.agit.konect.domain.event.repository.EventMiniEventRepository; +import gg.agit.konect.domain.event.repository.EventProgramRepository; +import gg.agit.konect.domain.event.repository.EventRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class EventService { + + private final EventRepository eventRepository; + private final EventProgramRepository eventProgramRepository; + private final EventBoothRepository eventBoothRepository; + private final EventBoothMapRepository eventBoothMapRepository; + private final EventBoothMapItemRepository eventBoothMapItemRepository; + private final EventMiniEventRepository eventMiniEventRepository; + private final EventContentRepository eventContentRepository; + + public EventHomeResponse getEventHome(Integer eventId, Integer userId) { + Event event = getEvent(eventId); + + return new EventHomeResponse( + event.getId(), + event.getTitle(), + event.getSubtitle(), + event.getPosterImageUrl(), + event.getStartAt(), + event.getEndAt(), + event.getNotice(), + new EventHomeResponse.Summary( + eventProgramRepository.countByEventId(eventId), + eventBoothRepository.countByEventId(eventId), + eventMiniEventRepository.countByEventId(eventId), + eventContentRepository.countByEventId(eventId) + ), + new EventHomeResponse.UserStatus(0, 0) + ); + } + + public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType type, Integer page, Integer limit, + Integer userId) { + getEvent(eventId); + + List filteredPrograms = eventProgramRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc( + eventId).stream() + .filter(program -> type == EventProgramType.ALL || program.getType() == type) + .toList(); + + PagedResult pagedPrograms = paginate(filteredPrograms, page, limit); + List programs = pagedPrograms.items().stream() + .map(this::toEventProgramSummaryResponse) + .toList(); + + return new EventProgramsResponse( + (long)pagedPrograms.totalCount(), + programs.size(), + pagedPrograms.totalPage(), + page, + type, + programs + ); + } + + public EventBoothsResponse getEventBooths(Integer eventId, String category, String keyword, Integer page, + Integer limit) { + getEvent(eventId); + + List filteredBooths = eventBoothRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc(eventId) + .stream() + .filter(booth -> category == null || category.isBlank() || booth.getCategory().equalsIgnoreCase(category)) + .filter(booth -> keyword == null || keyword.isBlank() || booth.getName().contains(keyword)) + .toList(); + + PagedResult pagedBooths = paginate(filteredBooths, page, limit); + List booths = pagedBooths.items().stream() + .map(this::toEventBoothSummaryResponse) + .toList(); + + return new EventBoothsResponse( + (long)pagedBooths.totalCount(), + booths.size(), + pagedBooths.totalPage(), + page, + booths + ); + } + + public EventBoothMapResponse getEventBoothMap(Integer eventId) { + EventBoothMap boothMap = eventBoothMapRepository.findByEventId(eventId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); + + List boothMapItems = eventBoothMapItemRepository.findAllByEventBoothMapIdOrderByIdAsc( + boothMap.getId()); + List booths = boothMapItems.stream() + .map(this::toEventBoothMapItemResponse) + .toList(); + + // 부스 목록 응답과 같은 구역 값을 맵에서도 그대로 내려 프론트가 별도 매핑 없이 재사용할 수 있게 맞춘다. + List zones = booths.stream() + .map(EventBoothMapResponse.BoothMapItemResponse::zone) + .filter(zone -> zone != null && !zone.isBlank()) + .distinct() + .map(zone -> new EventBoothMapResponse.ZoneResponse(zone, zone)) + .toList(); + + return new EventBoothMapResponse( + boothMap.getMapImageUrl(), + zones, + booths + ); + } + + public EventMiniEventsResponse getEventMiniEvents(Integer eventId, Integer page, Integer limit, Integer userId) { + getEvent(eventId); + + List miniEvents = eventMiniEventRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc(eventId); + PagedResult pagedMiniEvents = paginate(miniEvents, page, limit); + List miniEventResponses = pagedMiniEvents.items().stream() + .map(this::toEventMiniEventSummaryResponse) + .toList(); + + return new EventMiniEventsResponse( + (long)pagedMiniEvents.totalCount(), + miniEventResponses.size(), + pagedMiniEvents.totalPage(), + page, + miniEventResponses + ); + } + + public EventContentsResponse getEventContents(Integer eventId, String category, Integer page, Integer limit) { + getEvent(eventId); + + List filteredContents = eventContentRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc( + eventId).stream() + // 콘텐츠 타입 enum 이름과 동일한 문자열만 허용해 의도하지 않은 부분 일치를 막는다. + .filter(content -> category == null || category.isBlank() || content.getType() + .name() + .equalsIgnoreCase(category)) + .toList(); + + PagedResult pagedContents = paginate(filteredContents, page, limit); + List contents = pagedContents.items().stream() + .map(this::toEventContentSummaryResponse) + .toList(); + + return new EventContentsResponse( + (long)pagedContents.totalCount(), + contents.size(), + pagedContents.totalPage(), + page, + contents + ); + } + + private Event getEvent(Integer eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); + } + + private PagedResult paginate(List items, Integer page, Integer limit) { + int totalCount = items.size(); + int fromIndex = Math.max((page - 1) * limit, 0); + int toIndex = Math.min(fromIndex + limit, totalCount); + List pagedItems = fromIndex >= totalCount ? List.of() : items.subList(fromIndex, toIndex); + int totalPage = totalCount == 0 ? 0 : (int)Math.ceil((double)totalCount / limit); + return new PagedResult<>(pagedItems, totalCount, totalPage); + } + + private EventProgramSummaryResponse toEventProgramSummaryResponse(EventProgram program) { + return new EventProgramSummaryResponse( + program.getId(), + program.getTitle(), + program.getDescription(), + program.getThumbnailUrl(), + program.getRewardPoint(), + program.getStatus().name(), + false + ); + } + + private EventBoothSummaryResponse toEventBoothSummaryResponse(EventBooth booth) { + return new EventBoothSummaryResponse( + booth.getId(), + booth.getName(), + booth.getCategory(), + booth.getLocationLabel(), + booth.getZone(), + booth.getThumbnailUrl(), + Boolean.TRUE.equals(booth.getIsOpen()) + ); + } + + private EventBoothMapResponse.BoothMapItemResponse toEventBoothMapItemResponse(EventBoothMapItem boothMapItem) { + EventBooth booth = boothMapItem.getEventBooth(); + + return new EventBoothMapResponse.BoothMapItemResponse( + booth.getId(), + booth.getName(), + booth.getZone(), + boothMapItem.getX(), + boothMapItem.getY(), + boothMapItem.getWidth(), + boothMapItem.getHeight(), + boothMapItem.getStatus().name() + ); + } + + private EventMiniEventSummaryResponse toEventMiniEventSummaryResponse(EventMiniEvent miniEvent) { + return new EventMiniEventSummaryResponse( + miniEvent.getId(), + miniEvent.getTitle(), + miniEvent.getThumbnailUrl(), + miniEvent.getDescription(), + miniEvent.getRewardLabel(), + miniEvent.getStatus().name(), + false + ); + } + + private EventContentSummaryResponse toEventContentSummaryResponse(EventContent content) { + return new EventContentSummaryResponse( + content.getId(), + content.getTitle(), + content.getThumbnailUrl(), + content.getType().name(), + content.getSummary(), + content.getPublishedAt() + ); + } + + private record PagedResult(List items, int totalCount, int totalPage) { + } +} diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 47c916f93..3026484d4 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -104,6 +104,7 @@ public enum ApiResponseCode { NOT_FOUND_NOTIFICATION_TOKEN(HttpStatus.NOT_FOUND, "알림 토큰을 찾을 수 없습니다."), NOT_FOUND_NOTIFICATION_INBOX(HttpStatus.NOT_FOUND, "알림을 찾을 수 없습니다."), NOT_FOUND_ADVERTISEMENT(HttpStatus.NOT_FOUND, "광고를 찾을 수 없습니다."), + NOT_FOUND_EVENT(HttpStatus.NOT_FOUND, "행사를 찾을 수 없습니다."), NOT_FOUND_CLUB_SHEET_ID(HttpStatus.NOT_FOUND, "등록된 스프레드시트 ID가 없습니다. 먼저 스프레드시트 ID를 등록해 주세요."), NOT_FOUND_GOOGLE_DRIVE_AUTH(HttpStatus.NOT_FOUND, "Google Drive 권한이 연결되지 않았습니다. 먼저 Drive 권한을 연결해 주세요."), diff --git a/src/main/resources/db/migration/V70__add_event_tables.sql b/src/main/resources/db/migration/V70__add_event_tables.sql new file mode 100644 index 000000000..908eb9e35 --- /dev/null +++ b/src/main/resources/db/migration/V70__add_event_tables.sql @@ -0,0 +1,113 @@ +CREATE TABLE IF NOT EXISTS event +( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(100) NOT NULL, + subtitle VARCHAR(255), + poster_image_url VARCHAR(255), + notice TEXT, + start_at TIMESTAMP NOT NULL, + end_at TIMESTAMP NOT NULL, + status ENUM ('DRAFT', 'PUBLISHED', 'ENDED') NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS event_program +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL, + type ENUM ('POINT', 'RESONANCE') NOT NULL, + title VARCHAR(100) NOT NULL, + description VARCHAR(255) NOT NULL, + thumbnail_url VARCHAR(255), + reward_point INT, + status ENUM ('UPCOMING', 'ONGOING', 'ENDED') NOT NULL, + display_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS event_booth +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(50) NOT NULL, + description TEXT, + location_label VARCHAR(100), + zone VARCHAR(50), + thumbnail_url VARCHAR(255), + is_open BOOLEAN NOT NULL DEFAULT true, + display_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS event_booth_map +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL, + map_image_url VARCHAR(255), + width INT, + height INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE, + CONSTRAINT uq_event_booth_map_event_id UNIQUE (event_id) +); + +CREATE TABLE IF NOT EXISTS event_booth_map_item +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_booth_map_id INT NOT NULL, + event_booth_id INT NOT NULL, + x INT NOT NULL, + y INT NOT NULL, + width INT NOT NULL, + height INT NOT NULL, + status ENUM ('OPEN', 'CLOSED', 'HIDDEN') NOT NULL DEFAULT 'OPEN', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_booth_map_id) REFERENCES event_booth_map (id) ON DELETE CASCADE, + FOREIGN KEY (event_booth_id) REFERENCES event_booth (id) ON DELETE CASCADE, + CONSTRAINT uq_event_booth_map_item_booth_id UNIQUE (event_booth_id) +); + +CREATE TABLE IF NOT EXISTS event_mini_event +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL, + title VARCHAR(100) NOT NULL, + description VARCHAR(255) NOT NULL, + thumbnail_url VARCHAR(255), + reward_label VARCHAR(100), + status ENUM ('UPCOMING', 'ONGOING', 'ENDED') NOT NULL, + display_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS event_content +( + id INT AUTO_INCREMENT PRIMARY KEY, + event_id INT NOT NULL, + title VARCHAR(100) NOT NULL, + summary VARCHAR(255) NOT NULL, + body TEXT, + thumbnail_url VARCHAR(255), + type ENUM ('ARTICLE', 'IMAGE', 'VIDEO') NOT NULL, + published_at TIMESTAMP, + display_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE +); diff --git a/src/test/java/gg/agit/konect/integration/domain/event/EventApiTest.java b/src/test/java/gg/agit/konect/integration/domain/event/EventApiTest.java new file mode 100644 index 000000000..32cba2715 --- /dev/null +++ b/src/test/java/gg/agit/konect/integration/domain/event/EventApiTest.java @@ -0,0 +1,229 @@ +package gg.agit.konect.integration.domain.event; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.support.IntegrationTestSupport; + +class EventApiTest extends IntegrationTestSupport { + + @Nested + @DisplayName("GET /events/{eventId}/home - 행사 홈 조회") + class GetEventHome { + + @Test + @DisplayName("행사 기본 정보와 요약 카운트를 반환한다") + void getEventHomeSuccess() throws Exception { + // given + insertEvent(1, "대동제", "봄 축제", "https://poster", "공지사항", LocalDateTime.of(2026, 5, 1, 10, 0), + LocalDateTime.of(2026, 5, 3, 22, 0)); + insertEventProgram(11, 1, "POINT", "스탬프", 1); + insertEventProgram(12, 1, "RESONANCE", "공명", 2); + insertEventBooth(21, 1, "AI 부스", "체험", "A-1", "ZONE-A", true, 1); + insertEventMiniEvent(31, 1, "룰렛", "경품", "굿즈", "ONGOING", 1); + insertEventContent(41, 1, "기사", "요약", "ARTICLE", LocalDateTime.of(2026, 5, 1, 9, 0), 1); + clearPersistenceContext(); + mockLoginUser(10); + + // when & then + performGet("/events/1/home") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.eventId").value(1)) + .andExpect(jsonPath("$.title").value("대동제")) + .andExpect(jsonPath("$.summary.programCount").value(2)) + .andExpect(jsonPath("$.summary.boothCount").value(1)) + .andExpect(jsonPath("$.summary.eventCount").value(1)) + .andExpect(jsonPath("$.summary.contentCount").value(1)); + } + + @Test + @DisplayName("행사가 없으면 404를 반환한다") + void getEventHomeWhenMissing() throws Exception { + mockLoginUser(10); + + performGet("/events/999/home") + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ApiResponseCode.NOT_FOUND_EVENT.getCode())); + } + } + + @Nested + @DisplayName("GET /events/{eventId}/booth-map - 행사 부스 맵 조회") + class GetEventBoothMap { + + @Test + @DisplayName("맵 이미지와 부스 좌표 정보를 반환한다") + void getEventBoothMapSuccess() throws Exception { + // given + insertEvent(1, "대동제", "봄 축제", "https://poster", "공지사항", LocalDateTime.of(2026, 5, 1, 10, 0), + LocalDateTime.of(2026, 5, 3, 22, 0)); + insertEventBooth(21, 1, "AI 부스", "체험", "A-1", "ZONE-A", true, 1); + insertEventBoothMap(51, 1, "https://map", 1200, 800); + insertEventBoothMapItem(61, 51, 21, 10, 20, 30, 40, "OPEN"); + clearPersistenceContext(); + + // when & then + performGet("/events/1/booth-map") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.mapImageUrl").value("https://map")) + .andExpect(jsonPath("$.zones[0].code").value("ZONE-A")) + .andExpect(jsonPath("$.booths[0].boothId").value(21)) + .andExpect(jsonPath("$.booths[0].status").value("OPEN")); + } + } + + @Nested + @DisplayName("GET /events/{eventId}/contents - 행사 콘텐츠 목록 조회") + class GetEventContents { + + @Test + @DisplayName("카테고리 필터와 페이지 정보를 함께 반영한다") + void getEventContentsSuccess() throws Exception { + // given + insertEvent(1, "대동제", "봄 축제", "https://poster", "공지사항", LocalDateTime.of(2026, 5, 1, 10, 0), + LocalDateTime.of(2026, 5, 3, 22, 0)); + insertEventContent(41, 1, "기사", "요약1", "ARTICLE", LocalDateTime.of(2026, 5, 1, 9, 0), 1); + insertEventContent(42, 1, "포토", "요약2", "IMAGE", LocalDateTime.of(2026, 5, 1, 10, 0), 2); + insertEventContent(43, 1, "추가 포토", "요약3", "IMAGE", LocalDateTime.of(2026, 5, 1, 11, 0), 3); + clearPersistenceContext(); + + // when & then + performGet("/events/1/contents?category=IMAGE&page=1&limit=1") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalCount").value(2)) + .andExpect(jsonPath("$.currentCount").value(1)) + .andExpect(jsonPath("$.totalPage").value(2)) + .andExpect(jsonPath("$.contents[0].title").value("포토")) + .andExpect(jsonPath("$.contents[0].type").value("IMAGE")); + } + } + + private void insertEvent(Integer id, String title, String subtitle, String posterImageUrl, String notice, + LocalDateTime startAt, LocalDateTime endAt) { + entityManager.createNativeQuery(""" + insert into event (id, title, subtitle, poster_image_url, notice, start_at, end_at, status, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """) + .setParameter(1, id) + .setParameter(2, title) + .setParameter(3, subtitle) + .setParameter(4, posterImageUrl) + .setParameter(5, notice) + .setParameter(6, startAt) + .setParameter(7, endAt) + .setParameter(8, "PUBLISHED") + .setParameter(9, startAt) + .setParameter(10, startAt) + .executeUpdate(); + } + + private void insertEventProgram(Integer id, Integer eventId, String type, String title, Integer displayOrder) { + entityManager.createNativeQuery(""" + insert into event_program (id, event_id, type, title, description, thumbnail_url, reward_point, status, display_order, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventId) + .setParameter(3, type) + .setParameter(4, title) + .setParameter(5, title + " 설명") + .setParameter(6, "https://program/" + id) + .setParameter(7, 10) + .setParameter(8, "ONGOING") + .setParameter(9, displayOrder) + .executeUpdate(); + } + + private void insertEventBooth(Integer id, Integer eventId, String name, String category, String locationLabel, + String zone, + boolean isOpen, Integer displayOrder) { + entityManager.createNativeQuery(""" + insert into event_booth (id, event_id, name, category, description, location_label, zone, thumbnail_url, is_open, display_order, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventId) + .setParameter(3, name) + .setParameter(4, category) + .setParameter(5, name + " 설명") + .setParameter(6, locationLabel) + .setParameter(7, zone) + .setParameter(8, "https://booth/" + id) + .setParameter(9, isOpen) + .setParameter(10, displayOrder) + .executeUpdate(); + } + + private void insertEventBoothMap(Integer id, Integer eventId, String mapImageUrl, Integer width, Integer height) { + entityManager.createNativeQuery(""" + insert into event_booth_map (id, event_id, map_image_url, width, height, created_at, updated_at) + values (?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventId) + .setParameter(3, mapImageUrl) + .setParameter(4, width) + .setParameter(5, height) + .executeUpdate(); + } + + private void insertEventBoothMapItem(Integer id, Integer eventBoothMapId, Integer eventBoothId, Integer x, + Integer y, + Integer width, Integer height, String status) { + entityManager.createNativeQuery(""" + insert into event_booth_map_item (id, event_booth_map_id, event_booth_id, x, y, width, height, status, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventBoothMapId) + .setParameter(3, eventBoothId) + .setParameter(4, x) + .setParameter(5, y) + .setParameter(6, width) + .setParameter(7, height) + .setParameter(8, status) + .executeUpdate(); + } + + private void insertEventMiniEvent(Integer id, Integer eventId, String title, String description, String rewardLabel, + String status, Integer displayOrder) { + entityManager.createNativeQuery(""" + insert into event_mini_event (id, event_id, title, description, thumbnail_url, reward_label, status, display_order, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventId) + .setParameter(3, title) + .setParameter(4, description) + .setParameter(5, "https://mini/" + id) + .setParameter(6, rewardLabel) + .setParameter(7, status) + .setParameter(8, displayOrder) + .executeUpdate(); + } + + private void insertEventContent(Integer id, Integer eventId, String title, String summary, String type, + LocalDateTime publishedAt, Integer displayOrder) { + entityManager.createNativeQuery(""" + insert into event_content (id, event_id, title, summary, body, thumbnail_url, type, published_at, display_order, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now()) + """) + .setParameter(1, id) + .setParameter(2, eventId) + .setParameter(3, title) + .setParameter(4, summary) + .setParameter(5, title + " 본문") + .setParameter(6, "https://content/" + id) + .setParameter(7, type) + .setParameter(8, publishedAt) + .setParameter(9, displayOrder) + .executeUpdate(); + } +} diff --git a/src/test/java/gg/agit/konect/unit/domain/event/service/EventServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/event/service/EventServiceTest.java new file mode 100644 index 000000000..417688430 --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/domain/event/service/EventServiceTest.java @@ -0,0 +1,299 @@ +package gg.agit.konect.unit.domain.event.service; + +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_EVENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import java.lang.reflect.Constructor; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.util.ReflectionTestUtils; + +import gg.agit.konect.domain.event.dto.EventBoothMapResponse; +import gg.agit.konect.domain.event.dto.EventContentsResponse; +import gg.agit.konect.domain.event.dto.EventHomeResponse; +import gg.agit.konect.domain.event.dto.EventMiniEventsResponse; +import gg.agit.konect.domain.event.enums.EventBoothMapItemStatus; +import gg.agit.konect.domain.event.enums.EventContentType; +import gg.agit.konect.domain.event.enums.EventProgressStatus; +import gg.agit.konect.domain.event.enums.EventStatus; +import gg.agit.konect.domain.event.model.Event; +import gg.agit.konect.domain.event.model.EventBooth; +import gg.agit.konect.domain.event.model.EventBoothMap; +import gg.agit.konect.domain.event.model.EventBoothMapItem; +import gg.agit.konect.domain.event.model.EventContent; +import gg.agit.konect.domain.event.model.EventMiniEvent; +import gg.agit.konect.domain.event.repository.EventBoothMapItemRepository; +import gg.agit.konect.domain.event.repository.EventBoothMapRepository; +import gg.agit.konect.domain.event.repository.EventBoothRepository; +import gg.agit.konect.domain.event.repository.EventContentRepository; +import gg.agit.konect.domain.event.repository.EventMiniEventRepository; +import gg.agit.konect.domain.event.repository.EventProgramRepository; +import gg.agit.konect.domain.event.repository.EventRepository; +import gg.agit.konect.domain.event.service.EventService; +import gg.agit.konect.global.exception.CustomException; +import gg.agit.konect.support.ServiceTestSupport; + +class EventServiceTest extends ServiceTestSupport { + + @Mock + private EventRepository eventRepository; + + @Mock + private EventProgramRepository eventProgramRepository; + + @Mock + private EventBoothRepository eventBoothRepository; + + @Mock + private EventBoothMapRepository eventBoothMapRepository; + + @Mock + private EventBoothMapItemRepository eventBoothMapItemRepository; + + @Mock + private EventMiniEventRepository eventMiniEventRepository; + + @Mock + private EventContentRepository eventContentRepository; + + @InjectMocks + private EventService eventService; + + @Test + @DisplayName("getEventHome은 행사 요약 카운트를 응답에 담는다") + void getEventHomeReturnsSummaryCounts() { + // given + Event event = createEvent(1, "대동제", "봄 축제", "https://poster", "공지"); + given(eventRepository.findById(1)).willReturn(Optional.of(event)); + given(eventProgramRepository.countByEventId(1)).willReturn(3); + given(eventBoothRepository.countByEventId(1)).willReturn(5); + given(eventMiniEventRepository.countByEventId(1)).willReturn(2); + given(eventContentRepository.countByEventId(1)).willReturn(4); + + // when + EventHomeResponse response = eventService.getEventHome(1, 10); + + // then + assertThat(response.eventId()).isEqualTo(1); + assertThat(response.title()).isEqualTo("대동제"); + assertThat(response.summary().programCount()).isEqualTo(3); + assertThat(response.summary().boothCount()).isEqualTo(5); + assertThat(response.summary().eventCount()).isEqualTo(2); + assertThat(response.summary().contentCount()).isEqualTo(4); + assertThat(response.userStatus().point()).isZero(); + assertThat(response.userStatus().participatedEventCount()).isZero(); + } + + @Test + @DisplayName("getEventBoothMap은 맵과 부스 좌표 정보를 응답으로 변환한다") + void getEventBoothMapReturnsMappedBoothsAndZones() { + // given + Event event = createEvent(1, "대동제", "봄 축제", "https://poster", "공지"); + EventBoothMap boothMap = createBoothMap(11, event, "https://map", 1200, 800); + EventBooth scienceBooth = createBooth(101, event, "AI 부스", "체험", "A-1", "ZONE-A", true); + EventBooth artBooth = createBooth(102, event, "전시 부스", "전시", "B-2", "ZONE-B", false); + EventBoothMapItem scienceItem = createBoothMapItem(1001, boothMap, scienceBooth, 10, 20, 30, 40, + EventBoothMapItemStatus.OPEN); + EventBoothMapItem artItem = createBoothMapItem(1002, boothMap, artBooth, 50, 60, 70, 80, + EventBoothMapItemStatus.CLOSED); + + given(eventBoothMapRepository.findByEventId(1)).willReturn(Optional.of(boothMap)); + given(eventBoothMapItemRepository.findAllByEventBoothMapIdOrderByIdAsc(11)) + .willReturn(List.of(scienceItem, artItem)); + + // when + EventBoothMapResponse response = eventService.getEventBoothMap(1); + + // then + assertThat(response.mapImageUrl()).isEqualTo("https://map"); + assertThat(response.zones()) + .extracting(EventBoothMapResponse.ZoneResponse::code) + .containsExactly("ZONE-A", "ZONE-B"); + assertThat(response.booths()) + .extracting( + EventBoothMapResponse.BoothMapItemResponse::boothId, + EventBoothMapResponse.BoothMapItemResponse::name, + EventBoothMapResponse.BoothMapItemResponse::zone, + EventBoothMapResponse.BoothMapItemResponse::status + ) + .containsExactly( + org.assertj.core.groups.Tuple.tuple(101, "AI 부스", "ZONE-A", "OPEN"), + org.assertj.core.groups.Tuple.tuple(102, "전시 부스", "ZONE-B", "CLOSED") + ); + } + + @Test + @DisplayName("getEventMiniEvents는 페이지 범위만 잘라 응답한다") + void getEventMiniEventsReturnsPagedItems() { + // given + Event event = createEvent(1, "대동제", "봄 축제", "https://poster", "공지"); + EventMiniEvent first = createMiniEvent(201, event, "스탬프 투어", "캠퍼스를 돌며 참여", "10P", + EventProgressStatus.ONGOING); + EventMiniEvent second = createMiniEvent(202, event, "룰렛", "즉석 경품 이벤트", "굿즈", + EventProgressStatus.UPCOMING); + EventMiniEvent third = createMiniEvent(203, event, "퀴즈", "상식 퀴즈", "5P", + EventProgressStatus.ENDED); + + given(eventRepository.findById(1)).willReturn(Optional.of(event)); + given(eventMiniEventRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc(1)) + .willReturn(List.of(first, second, third)); + + // when + EventMiniEventsResponse response = eventService.getEventMiniEvents(1, 2, 2, 10); + + // then + assertThat(response.totalCount()).isEqualTo(3); + assertThat(response.currentCount()).isEqualTo(1); + assertThat(response.totalPage()).isEqualTo(2); + assertThat(response.currentPage()).isEqualTo(2); + assertThat(response.miniEvents()) + .extracting(item -> item.miniEventId(), item -> item.title(), item -> item.status()) + .containsExactly(org.assertj.core.groups.Tuple.tuple(203, "퀴즈", "ENDED")); + } + + @Test + @DisplayName("getEventContents는 category와 페이지를 함께 반영한다") + void getEventContentsFiltersByCategoryAndPaginates() { + // given + Event event = createEvent(1, "대동제", "봄 축제", "https://poster", "공지"); + EventContent article = createContent(301, event, "기사", "기사 요약", EventContentType.ARTICLE, + LocalDateTime.of(2026, 4, 12, 9, 0)); + EventContent image = createContent(302, event, "포토", "사진 요약", EventContentType.IMAGE, + LocalDateTime.of(2026, 4, 12, 10, 0)); + EventContent video = createContent(303, event, "영상", "영상 요약", EventContentType.VIDEO, + LocalDateTime.of(2026, 4, 12, 11, 0)); + + given(eventRepository.findById(1)).willReturn(Optional.of(event)); + given(eventContentRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc(1)) + .willReturn(List.of(article, image, video)); + + // when + EventContentsResponse response = eventService.getEventContents(1, "image", 1, 1); + + // then + assertThat(response.totalCount()).isEqualTo(1); + assertThat(response.currentCount()).isEqualTo(1); + assertThat(response.totalPage()).isEqualTo(1); + assertThat(response.currentPage()).isEqualTo(1); + assertThat(response.contents()) + .extracting(item -> item.contentId(), item -> item.title(), item -> item.type()) + .containsExactly(org.assertj.core.groups.Tuple.tuple(302, "포토", "IMAGE")); + } + + @Test + @DisplayName("getEventHome은 행사가 없으면 NOT_FOUND_EVENT를 던진다") + void getEventHomeThrowsWhenEventMissing() { + // given + given(eventRepository.findById(1)).willReturn(Optional.empty()); + + // when & then + assertErrorCode(() -> eventService.getEventHome(1, 10), NOT_FOUND_EVENT); + } + + private Event createEvent(Integer id, String title, String subtitle, String posterImageUrl, String notice) { + Event event = instantiate(Event.class); + ReflectionTestUtils.setField(event, "id", id); + ReflectionTestUtils.setField(event, "title", title); + ReflectionTestUtils.setField(event, "subtitle", subtitle); + ReflectionTestUtils.setField(event, "posterImageUrl", posterImageUrl); + ReflectionTestUtils.setField(event, "notice", notice); + ReflectionTestUtils.setField(event, "startAt", LocalDateTime.of(2026, 4, 12, 10, 0)); + ReflectionTestUtils.setField(event, "endAt", LocalDateTime.of(2026, 4, 12, 22, 0)); + ReflectionTestUtils.setField(event, "status", EventStatus.PUBLISHED); + return event; + } + + private EventBoothMap createBoothMap(Integer id, Event event, String mapImageUrl, Integer width, Integer height) { + EventBoothMap boothMap = instantiate(EventBoothMap.class); + ReflectionTestUtils.setField(boothMap, "id", id); + ReflectionTestUtils.setField(boothMap, "event", event); + ReflectionTestUtils.setField(boothMap, "mapImageUrl", mapImageUrl); + ReflectionTestUtils.setField(boothMap, "width", width); + ReflectionTestUtils.setField(boothMap, "height", height); + return boothMap; + } + + private EventBooth createBooth(Integer id, Event event, String name, String category, String locationLabel, + String zone, + Boolean isOpen) { + EventBooth booth = instantiate(EventBooth.class); + ReflectionTestUtils.setField(booth, "id", id); + ReflectionTestUtils.setField(booth, "event", event); + ReflectionTestUtils.setField(booth, "name", name); + ReflectionTestUtils.setField(booth, "category", category); + ReflectionTestUtils.setField(booth, "locationLabel", locationLabel); + ReflectionTestUtils.setField(booth, "zone", zone); + ReflectionTestUtils.setField(booth, "thumbnailUrl", "https://thumb/" + id); + ReflectionTestUtils.setField(booth, "isOpen", isOpen); + ReflectionTestUtils.setField(booth, "displayOrder", 1); + return booth; + } + + private EventBoothMapItem createBoothMapItem(Integer id, EventBoothMap boothMap, EventBooth booth, Integer x, + Integer y, + Integer width, Integer height, EventBoothMapItemStatus status) { + EventBoothMapItem boothMapItem = instantiate(EventBoothMapItem.class); + ReflectionTestUtils.setField(boothMapItem, "id", id); + ReflectionTestUtils.setField(boothMapItem, "eventBoothMap", boothMap); + ReflectionTestUtils.setField(boothMapItem, "eventBooth", booth); + ReflectionTestUtils.setField(boothMapItem, "x", x); + ReflectionTestUtils.setField(boothMapItem, "y", y); + ReflectionTestUtils.setField(boothMapItem, "width", width); + ReflectionTestUtils.setField(boothMapItem, "height", height); + ReflectionTestUtils.setField(boothMapItem, "status", status); + return boothMapItem; + } + + private EventMiniEvent createMiniEvent(Integer id, Event event, String title, String description, + String rewardLabel, + EventProgressStatus status) { + EventMiniEvent miniEvent = instantiate(EventMiniEvent.class); + ReflectionTestUtils.setField(miniEvent, "id", id); + ReflectionTestUtils.setField(miniEvent, "event", event); + ReflectionTestUtils.setField(miniEvent, "title", title); + ReflectionTestUtils.setField(miniEvent, "description", description); + ReflectionTestUtils.setField(miniEvent, "thumbnailUrl", "https://mini/" + id); + ReflectionTestUtils.setField(miniEvent, "rewardLabel", rewardLabel); + ReflectionTestUtils.setField(miniEvent, "status", status); + ReflectionTestUtils.setField(miniEvent, "displayOrder", 1); + return miniEvent; + } + + private EventContent createContent(Integer id, Event event, String title, String summary, EventContentType type, + LocalDateTime publishedAt) { + EventContent content = instantiate(EventContent.class); + ReflectionTestUtils.setField(content, "id", id); + ReflectionTestUtils.setField(content, "event", event); + ReflectionTestUtils.setField(content, "title", title); + ReflectionTestUtils.setField(content, "summary", summary); + ReflectionTestUtils.setField(content, "thumbnailUrl", "https://content/" + id); + ReflectionTestUtils.setField(content, "type", type); + ReflectionTestUtils.setField(content, "publishedAt", publishedAt); + ReflectionTestUtils.setField(content, "displayOrder", 1); + return content; + } + + private void assertErrorCode(org.assertj.core.api.ThrowableAssert.ThrowingCallable callable, + gg.agit.konect.global.code.ApiResponseCode expectedCode) { + org.assertj.core.api.Assertions.assertThatThrownBy(callable) + .isInstanceOf(CustomException.class) + .satisfies(exception -> assertThat(((CustomException)exception).getErrorCode()).isEqualTo(expectedCode)); + } + + private T instantiate(Class type) { + try { + Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception exception) { + throw new IllegalStateException(type.getSimpleName() + " test fixture 생성에 실패했습니다.", exception); + } + } +}