From ab03446b0e8c8bc6320b12b969d80c03dc30279e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 14 Apr 2026 12:10:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=96=89=EC=82=AC=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 미니 이벤트 다음 단계에서 콘텐츠 조회만 추가해 stacked PR의 의존 순서를 유지한다 - event_content 스키마, 모델, repository, DTO, API wiring을 함께 올려 콘텐츠 응답 계약을 한 번에 검토할 수 있게 맞춘다 - 홈 집계 로직은 다시 섞지 않고 콘텐츠 목록 조회만 남겨 이번 PR의 리뷰 범위를 고정한다 --- .../domain/event/controller/EventApi.java | 10 ++++ .../event/controller/EventController.java | 7 +++ .../dto/EventContentSummaryResponse.java | 13 ++++ .../event/dto/EventContentsResponse.java | 12 ++++ .../domain/event/enums/EventContentType.java | 7 +++ .../domain/event/model/EventContent.java | 59 +++++++++++++++++++ .../repository/EventContentRepository.java | 14 +++++ .../domain/event/service/EventService.java | 42 +++++++++++++ .../db/migration/V70__add_event_tables.sql | 17 ++++++ 9 files changed, 181 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/event/dto/EventContentSummaryResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/event/dto/EventContentsResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/event/enums/EventContentType.java create mode 100644 src/main/java/gg/agit/konect/domain/event/model/EventContent.java create mode 100644 src/main/java/gg/agit/konect/domain/event/repository/EventContentRepository.java 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 index 4f8c81630..3064e720a 100644 --- a/src/main/java/gg/agit/konect/domain/event/controller/EventApi.java +++ b/src/main/java/gg/agit/konect/domain/event/controller/EventApi.java @@ -8,6 +8,7 @@ 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.EventMiniEventsResponse; import gg.agit.konect.domain.event.dto.EventProgramsResponse; import gg.agit.konect.domain.event.enums.EventProgramType; @@ -54,4 +55,13 @@ ResponseEntity getEventMiniEvents( @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 index 74a841141..f7ecb7b40 100644 --- a/src/main/java/gg/agit/konect/domain/event/controller/EventController.java +++ b/src/main/java/gg/agit/konect/domain/event/controller/EventController.java @@ -6,6 +6,7 @@ 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.EventMiniEventsResponse; import gg.agit.konect.domain.event.dto.EventProgramsResponse; import gg.agit.konect.domain.event.enums.EventProgramType; @@ -42,4 +43,10 @@ public ResponseEntity getEventMiniEvents(Integer eventI 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/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/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/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/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/service/EventService.java b/src/main/java/gg/agit/konect/domain/event/service/EventService.java index f0f68d8dd..49682899c 100644 --- a/src/main/java/gg/agit/konect/domain/event/service/EventService.java +++ b/src/main/java/gg/agit/konect/domain/event/service/EventService.java @@ -10,6 +10,8 @@ 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.EventMiniEventSummaryResponse; import gg.agit.konect.domain.event.dto.EventMiniEventsResponse; import gg.agit.konect.domain.event.dto.EventProgramSummaryResponse; @@ -18,11 +20,13 @@ 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; @@ -40,6 +44,7 @@ public class EventService { private final EventBoothMapRepository eventBoothMapRepository; private final EventBoothMapItemRepository eventBoothMapItemRepository; private final EventMiniEventRepository eventMiniEventRepository; + private final EventContentRepository eventContentRepository; public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType type, Integer page, Integer limit, Integer userId) { @@ -99,6 +104,7 @@ public EventBoothMapResponse getEventBoothMap(Integer eventId) { .map(this::toEventBoothMapItemResponse) .toList(); + // 부스 목록 응답과 같은 구역 값을 맵에서도 그대로 내려 프론트가 별도 매핑 없이 재사용할 수 있게 맞춘다. List zones = booths.stream() .map(EventBoothMapResponse.BoothMapItemResponse::zone) .filter(zone -> zone != null && !zone.isBlank()) @@ -131,6 +137,31 @@ public EventMiniEventsResponse getEventMiniEvents(Integer eventId, Integer page, ); } + 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 void getEvent(Integer eventId) { eventRepository.findById(eventId) .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); @@ -196,6 +227,17 @@ private EventMiniEventSummaryResponse toEventMiniEventSummaryResponse(EventMiniE ); } + 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/resources/db/migration/V70__add_event_tables.sql b/src/main/resources/db/migration/V70__add_event_tables.sql index caa894d5a..908eb9e35 100644 --- a/src/main/resources/db/migration/V70__add_event_tables.sql +++ b/src/main/resources/db/migration/V70__add_event_tables.sql @@ -94,3 +94,20 @@ CREATE TABLE IF NOT EXISTS event_mini_event 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 +);