From 96327778bcbf3c2be111f9dfb893bc142fe522fb 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 11:41:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=96=89=EC=82=AC=20=EB=B6=80=EC=8A=A4?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로그램 조회 기반 위에 event_booth 스키마와 부스 목록 응답만 얹어 두 번째 stacked PR 범위를 190줄 수준으로 유지했다 - 부스맵 구현은 의존성은 남기되 리뷰 범위가 커지지 않도록 다음 PR로 미뤘다 - 프로그램 조회와 동일한 서비스/컨트롤러 흐름을 유지해 후속 PR이 같은 파일에 수직으로 쌓이도록 맞췄다 --- .../domain/event/controller/EventApi.java | 18 ++++++ .../event/controller/EventController.java | 13 +++++ .../event/dto/EventBoothSummaryResponse.java | 12 ++++ .../domain/event/dto/EventBoothsResponse.java | 12 ++++ .../konect/domain/event/model/EventBooth.java | 56 +++++++++++++++++++ .../repository/EventBoothRepository.java | 14 +++++ .../domain/event/service/EventService.java | 49 +++++++++++++++- .../db/migration/V70__add_event_tables.sql | 18 ++++++ 8 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/event/dto/EventBoothSummaryResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/event/dto/EventBoothsResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/event/model/EventBooth.java create mode 100644 src/main/java/gg/agit/konect/domain/event/repository/EventBoothRepository.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 1a215b36..b0c648a3 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 @@ -6,6 +6,8 @@ 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.EventProgramsResponse; import gg.agit.konect.domain.event.enums.EventProgramType; import gg.agit.konect.global.auth.annotation.UserId; @@ -26,4 +28,20 @@ ResponseEntity getEventPrograms( @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 + ); } 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 ce07b41e..603ad860 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 @@ -4,6 +4,8 @@ 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.EventProgramsResponse; import gg.agit.konect.domain.event.enums.EventProgramType; import gg.agit.konect.domain.event.service.EventService; @@ -22,4 +24,15 @@ public ResponseEntity getEventPrograms(Integer eventId, E 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)); + } } 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 00000000..95fb2b12 --- /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 00000000..5be449ed --- /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/model/EventBooth.java b/src/main/java/gg/agit/konect/domain/event/model/EventBooth.java new file mode 100644 index 00000000..ba38b15b --- /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/repository/EventBoothRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothRepository.java new file mode 100644 index 00000000..63795ec4 --- /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/service/EventService.java b/src/main/java/gg/agit/konect/domain/event/service/EventService.java index 5fc5cc0d..66584619 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 @@ -7,10 +7,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.event.dto.EventBoothSummaryResponse; +import gg.agit.konect.domain.event.dto.EventBoothsResponse; 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.EventBooth; import gg.agit.konect.domain.event.model.EventProgram; +import gg.agit.konect.domain.event.repository.EventBoothRepository; import gg.agit.konect.domain.event.repository.EventProgramRepository; import gg.agit.konect.domain.event.repository.EventRepository; import gg.agit.konect.global.exception.CustomException; @@ -23,11 +27,11 @@ public class EventService { private final EventRepository eventRepository; private final EventProgramRepository eventProgramRepository; + private final EventBoothRepository eventBoothRepository; public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType type, Integer page, Integer limit, Integer userId) { - eventRepository.findById(eventId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); + getEvent(eventId); List filteredPrograms = eventProgramRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc( eventId).stream() @@ -49,6 +53,35 @@ public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType ); } + 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 + ); + } + + private void getEvent(Integer eventId) { + 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); @@ -70,6 +103,18 @@ private EventProgramSummaryResponse toEventProgramSummaryResponse(EventProgram p ); } + 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 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 e9e35904..33d4e66d 100644 --- a/src/main/resources/db/migration/V70__add_event_tables.sql +++ b/src/main/resources/db/migration/V70__add_event_tables.sql @@ -28,3 +28,21 @@ CREATE TABLE IF NOT EXISTS event_program 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 +);