From 36406e174d417c8881b7d6b14cc4a0ec2a397f8f 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:01:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=96=89=EC=82=AC=20=EB=B6=80=EC=8A=A4?= =?UTF-8?q?=EB=A7=B5=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 - 부스 목록 PR 위에 event_booth_map과 item 스키마, 맵 응답 변환 로직만 올려 세 번째 stacked PR을 234줄로 맞췄다 - booth-map은 booth 엔티티에 의존하므로 직전 부스 목록 브랜치 위에 쌓아 리뷰 흐름을 자연스럽게 유지했다 - zone 계산과 좌표 응답만 추가해 이후 미니 이벤트와 콘텐츠 PR이 이 변경과 섞이지 않도록 분리했다 --- .../event/dto/EventBoothMapResponse.java | 28 ++++++++++ .../event/enums/EventBoothMapItemStatus.java | 7 +++ .../domain/event/model/EventBoothMap.java | 41 ++++++++++++++ .../domain/event/model/EventBoothMapItem.java | 56 +++++++++++++++++++ .../EventBoothMapItemRepository.java | 12 ++++ .../repository/EventBoothMapRepository.java | 12 ++++ .../domain/event/service/EventService.java | 46 +++++++++++++++ .../db/migration/V70__add_event_tables.sql | 32 +++++++++++ 8 files changed, 234 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/event/dto/EventBoothMapResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/event/enums/EventBoothMapItemStatus.java create mode 100644 src/main/java/gg/agit/konect/domain/event/model/EventBoothMap.java create mode 100644 src/main/java/gg/agit/konect/domain/event/model/EventBoothMapItem.java create mode 100644 src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapItemRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapRepository.java 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 00000000..cbd041ef --- /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/enums/EventBoothMapItemStatus.java b/src/main/java/gg/agit/konect/domain/event/enums/EventBoothMapItemStatus.java new file mode 100644 index 00000000..d9b4198e --- /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/model/EventBoothMap.java b/src/main/java/gg/agit/konect/domain/event/model/EventBoothMap.java new file mode 100644 index 00000000..72d76cd1 --- /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 00000000..73f8699a --- /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/repository/EventBoothMapItemRepository.java b/src/main/java/gg/agit/konect/domain/event/repository/EventBoothMapItemRepository.java new file mode 100644 index 00000000..82d88e7a --- /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 00000000..bf57d6fb --- /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/service/EventService.java b/src/main/java/gg/agit/konect/domain/event/service/EventService.java index 66584619..73f45382 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,13 +7,18 @@ 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.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.EventBoothMap; +import gg.agit.konect.domain.event.model.EventBoothMapItem; 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.EventProgramRepository; import gg.agit.konect.domain.event.repository.EventRepository; @@ -28,6 +33,8 @@ public class EventService { private final EventRepository eventRepository; private final EventProgramRepository eventProgramRepository; private final EventBoothRepository eventBoothRepository; + private final EventBoothMapRepository eventBoothMapRepository; + private final EventBoothMapItemRepository eventBoothMapItemRepository; public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType type, Integer page, Integer limit, Integer userId) { @@ -77,6 +84,30 @@ public EventBoothsResponse getEventBooths(Integer eventId, String category, Stri ); } + 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 + ); + } + private void getEvent(Integer eventId) { eventRepository.findById(eventId) .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); @@ -115,6 +146,21 @@ private EventBoothSummaryResponse toEventBoothSummaryResponse(EventBooth booth) ); } + 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 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 33d4e66d..aee317aa 100644 --- a/src/main/resources/db/migration/V70__add_event_tables.sql +++ b/src/main/resources/db/migration/V70__add_event_tables.sql @@ -46,3 +46,35 @@ CREATE TABLE IF NOT EXISTS event_booth 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) +);