✨ [FEAT] 부스 관리 기능 구현 (WBS-067~077, WBS-186~190) - #21
Conversation
- BEFORE_OPEN에서만 전체 수정 허용, OPEN에서는 종료일 전용 API로만 연장/단축 가능 - 스케줄러의 시간 기반 자동 상태전환(BEFORE_OPEN→OPEN→CLOSED) 테스트 보강 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WBS-067~073: 개최자용 부스 CRUD API. 목록은 상태·층·구역 필터와 페이지네이션을 지원하고, 삭제는 AVAILABLE 상태의 미사용 부스만 허용한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
스케줄러의 상태 전환 로직이 REQUIRES_NEW로 별도 트랜잭션을 여는데, 테스트 클래스 전체를 @transactional로 감싸면 테스트에서 저장한 데이터가 아직 커밋되지 않아 스케줄러가 보지 못했다. @transactional을 제거하고 각 테스트 후 직접 데이터를 정리하도록 변경. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WBS-074~077: - 배정된 참가기업(조직 구성원)이 ASSIGNED 상태 부스의 소개(대표 이미지 등)를 수정할 수 있음 - 개최자가 부스 QR 토큰을 발급/재발급, 개최자와 배정된 조직 구성원이 조회 가능 - 부스 목록 API에 부스명·기업명·부스번호 키워드 검색 추가 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SecurityConfig에 /events/*/booths/** 경로가 빠져 있어 기본값(anyRequest permitAll)을 타면서 인증 없이 컨트롤러까지 들어와 member가 null이 되어 NPE(500)가 발생했음. authenticated() 규칙을 추가해 401로 정상 처리되도록 수정. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- BoothManagementPanel: 목록/필터/검색/페이지네이션, 단건·일괄 등록, 수정, 상태변경, 삭제, 배정 부스 소개 관리, QR 발급·조회를 한 화면에서 처리 - OrganizerAdmin의 "부스 관리" 탭에 연결(기존 목데이터 그리드 제거) - QR은 스캔 시 부스 상세로 이동하도록 URL을 인코딩해 qrcode로 실제 이미지 렌더링 - QR 발급을 멱등하게 변경: 이미 발급된 QR이 있으면 새로 만들지 않고 기존 것을 그대로 반환(분실 시에는 재조회로 충분하며, 실수로 기존 QR을 무효화하는 것을 방지) - "QR 보기" 버튼이 열려있는 부스에서는 "QR 닫기"로 토글 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
메인 화면 모집중 부스 공고 섹션과 모집 공고 상세 페이지를 실제 데이터로 연동하고, 비로그인 사용자도 조회 가능한 부스 공개 조회 API를 추가했다. 부스 카드를 클릭하면 규격/면적/설비 지원 여부 등 스펙을 팝업으로 확인할 수 있다.
# Conflicts: # BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java # BE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.java
행사 등록 화면이 없던 시절 임시로 쓰던 eventId 수동 입력을 제거하고, 개최자센터 상단에서 관리 중인 행사를 선택하면 부스 관리·모집 공고 패널이 자동으로 해당 행사 데이터를 불러오도록 연동했다. 또한 개최자센터 진입 시 로그인 계정이 속한 조직을 자동으로 조회해 organizationId 쿼리 파라미터 없이도 접근할 수 있도록 했다.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Walkthrough부스 CRUD, 공개 조회, 소개 관리, QR 발급 API와 관리자 화면을 추가했습니다. 공개 모집과 부스 상세 화면도 API 데이터를 사용합니다. Changes부스 관리 기능
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant 관리자
participant BoothManagementPanel
participant boothApi
participant BoothController
participant BoothService
participant BoothRepository
관리자->>BoothManagementPanel: 부스 생성 또는 QR 발급 실행
BoothManagementPanel->>boothApi: 부스 API 요청
boothApi->>BoothController: HTTP 요청 전송
BoothController->>BoothService: 인증 회원과 요청 데이터 전달
BoothService->>BoothRepository: 부스 조회 및 저장
BoothService-->>BoothController: 부스 응답 반환
BoothController-->>boothApi: JSON 응답 반환
boothApi-->>BoothManagementPanel: 결과 전달
BoothManagementPanel-->>관리자: 목록 또는 QR 정보 표시
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (11)
BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java (1)
340-361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win종료 시각 변경값을 검증하세요.
Line 359와 Line 360은 성공 상태와
OPEN상태만 검사합니다.updateEndAt이 종료 시각을 저장하지 않거나 이전 값을 반환해도 이 테스트는 통과합니다.응답의
recruitmentEndAt를newEnd와 비교하세요. 필요하면 영속성 컨텍스트를 비운 뒤 관리 조회로 저장값도 확인하세요.As per path instructions, "유지보수에 영향을 주는 문제를 우선적으로 알려주세요."를 적용했습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java` around lines 340 - 361, Update the test method OPEN_상태에서는_모집_종료일만_수정할_수_있다 to assert that the response recruitmentEndAt matches newEnd, in addition to the existing success and OPEN status checks. If the response is insufficient, clear the persistence context and verify the stored value through a managed retrieval.Source: Path instructions
BE/src/main/java/com/min/edu/booth/domain/Booth.java (1)
121-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win동일 타입 파라미터가 길게 이어져서 순서 실수를 잡을 수 없습니다.
create는 16개,updateDetails는 15개 파라미터를 위치 인자로 받습니다.floorName,zoneName,locationDescription은 모두String이고,widthMeter,depthMeter,areaSqm은 모두BigDecimal이며, 설비 4개는 모두boolean입니다. 호출부에서 순서를 바꿔 넘겨도 컴파일 오류가 나지 않고, 잘못된 값이 그대로 저장됩니다. 지금BoothService.create,createBulk,update세 곳에서 같은 순서를 반복해서 넘기고 있어서 앞으로 필드가 추가될 때 실수가 생기기 쉽습니다.이미
@Builder가 있으니 위치 인자 대신 값 객체(예: 위치 정보, 설비 정보, 규격 정보를 묶은 작은 record)를 파라미터로 받는 방식을 권합니다. 급하지 않으니 후속 작업으로 처리해도 됩니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/domain/Booth.java` around lines 121 - 191, Reduce positional-argument risk in Booth.create and Booth.updateDetails by grouping the related same-type fields into dedicated value objects, such as records for location, dimensions, and equipment, and update the BoothService.create, createBulk, and update callers to pass those objects. Preserve the existing field assignments and behavior while removing the long repeated parameter lists.BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java (1)
22-39: 🧹 Nitpick | 🔵 Trivial검색 쿼리가 인덱스를 타지 못합니다. 데이터가 늘어나면 느려집니다.
keyword는%...%형태로 들어오므로LOWER(b.boothCode) LIKE :keyword와LOWER(b.displayName) LIKE :keyword는 인덱스를 사용할 수 없습니다. 여기에Organization을 대상으로 하는EXISTS서브쿼리가 OR로 붙어 있어서, 부스 행마다 서브쿼리 평가가 발생합니다. 페이지 조회는 count 쿼리까지 같은 조건으로 두 번 실행됩니다.행사당 부스 수가 수백 건 수준이면 지금 형태로 충분합니다. 규모가 커질 계획이면 아래를 고려해 주세요.
booths(event_id, status),booths(event_id, floor_name, zone_name)복합 인덱스 추가- 조직명 검색은 OR 대신
LEFT JOIN Organization o ON o.id = b.assignedOrganizationId로 바꿔 서브쿼리 반복 평가 제거- 키워드 검색이 핵심 기능이 되면 PostgreSQL
pg_trgmGIN 인덱스 또는 별도 검색 컬럼 도입참고로 정적 분석이 이 부분을 LDAP 인젝션으로 표시했지만, 명명 파라미터 바인딩을 쓰고 있어서 오탐입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java` around lines 22 - 39, Update the BoothRepository.search query strategy for scalable keyword filtering: replace the organization-name EXISTS condition with a LEFT JOIN to Organization while preserving the existing filters and results, add the recommended composite indexes for event/status and event/floorName/zoneName, and use PostgreSQL pg_trgm GIN indexes or a dedicated searchable column if keyword search is a core requirement. Keep the named parameter binding unchanged.Source: Linters/SAST tools
BE/src/main/java/com/min/edu/booth/service/BoothService.java (2)
216-222: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win상세 조회 권한이 소개 수정 권한보다 좁습니다.
getDetail은requireEventManager만 통과시킵니다. 반면updateIntro(231행)와getQr(266행)은 배정 조직 멤버에게도 열려 있습니다. 결과적으로 배정 조직 멤버는 자기 부스의 소개를 수정할 수 있지만, 현재 등록된 소개 값을 읽을 수는 없습니다. 수정 화면에 기존 값을 채워 넣지 못합니다.
getDetail도requireAssignedOrganizationMemberOrEventManager를 쓰거나, 조직 멤버용 조회 API를 따로 두는 편이 자연스럽습니다. 단,toResponse에는qrToken이 포함되므로 권한을 넓힐 때 응답 필드 범위도 함께 정해 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/service/BoothService.java` around lines 216 - 222, Update BoothService.getDetail to allow assigned organization members as well as event managers, matching the authorization used by updateIntro and getQr, or provide an equivalent organization-member detail path. Because toResponse includes qrToken, ensure the expanded-access response excludes or safely handles that field according to the intended permission boundary.
462-475: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win파싱 실패 원인을 로그로 남겨 주세요.
fromJson이 잘못된basic_equipment값을 빈 리스트로 변환하면 응답이 실제 데이터 오류를 숨깁니다.@Slf4j를 추가하고catch블록에서log.warn(..., e)를 호출해 주세요.TypeReference를 사용할 경우 패키지는tools.jackson.core.type.TypeReference이며,objectMapper.readValue(json, new TypeReference<List<String>>() {})로 작성할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/service/BoothService.java` around lines 462 - 475, Update BoothService.fromJson to log parsing failures before returning an empty list: add the Lombok `@Slf4j` logger and call log.warn with descriptive context and the caught JacksonException in the catch block. Preserve the existing empty-list fallback for invalid JSON and blank or null input.BE/src/main/java/com/min/edu/common/config/SecurityConfig.java (1)
68-69: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winBroken Authentication (CWE-306): Missing Authentication for Critical Function
Reachability: External
서비스 계층에 인증 가드를 추가하세요.
현재
/events/*/booths/**규칙은BoothController의 모든 보호 경로를 포함하며, 공개 경로보다 먼저 적용됩니다. 다만anyRequest().permitAll()상태에서 경로가 규칙에서 누락되면BoothService의member역참조가NullPointerException을 발생시켜 500 응답을 반환합니다.
requireEventManager와requireAssignedOrganizationMemberOrEventManager의 시작부에서member == null이면BusinessException(GlobalErrorCode.UNAUTHORIZED)를 던지도록 공통 가드를 추가하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/common/config/SecurityConfig.java` around lines 68 - 69, In BoothService, add a shared guard at the start of requireEventManager and requireAssignedOrganizationMemberOrEventManager that throws BusinessException(GlobalErrorCode.UNAUTHORIZED) when member is null, before any member dereference or authorization checks.FE/src/api/boothApi.js (1)
9-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win쿼리 문자열 생성 로직이 두 함수에 중복되어 있습니다.
listBooths와listPublicBooths의 파라미터 필터링 코드가 완전히 같습니다. 나중에 필터 규칙(예:false값 처리)을 바꿀 때 한쪽만 수정될 수 있습니다. 헬퍼로 빼면 됩니다.♻️ 헬퍼 추출 예시
+const toQueryString = (params) => { + const query = new URLSearchParams( + Object.fromEntries(Object.entries(params).filter(([, value]) => value !== "" && value != null)) + ); + const queryString = query.toString(); + return queryString ? `?${queryString}` : ""; +}; + const listBooths = async (eventId, params = {}) => { - const query = new URLSearchParams( - Object.fromEntries(Object.entries(params).filter(([, value]) => value !== "" && value != null)) - ); - const queryString = query.toString(); - const response = await apiRequest(`/events/${eventId}/booths${queryString ? `?${queryString}` : ""}`); + const response = await apiRequest(`/events/${eventId}/booths${toQueryString(params)}`); return response.data; }; const listPublicBooths = async (eventId, params = {}) => { - const query = new URLSearchParams( - Object.fromEntries(Object.entries(params).filter(([, value]) => value !== "" && value != null)) - ); - const queryString = query.toString(); - const response = await apiRequest(`/events/${eventId}/booths/public${queryString ? `?${queryString}` : ""}`); + const response = await apiRequest(`/events/${eventId}/booths/public${toQueryString(params)}`); return response.data; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@FE/src/api/boothApi.js` around lines 9 - 25, Extract the shared parameter filtering and URLSearchParams construction from listBooths and listPublicBooths into a reusable helper, then use that helper in both functions while preserving the current handling of empty and nullish values.BE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.java (2)
164-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win벌크 UPDATE 전에
flush()를 명시해 주세요.MockMvc로 생성한 부스는 같은 트랜잭션의 영속성 컨텍스트에 있습니다. 여기서 JPQL 벌크 UPDATE는 Hibernate의 자동 flush 동작에 의존합니다. flush 시점이 바뀌면 아직 INSERT되지 않은 부스를 갱신하려 하고, 테스트가 간헐적으로 실패합니다. 명시적으로 flush하면 순서가 확정됩니다.
♻️ 명시적 flush 추가
private void assignToExhibitor(Long boothId) { + entityManager.flush(); entityManager.createQuery( "UPDATE Booth b SET b.assignedOrganizationId = :orgId WHERE b.id = :boothId")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.java` around lines 164 - 171, Update the assignToExhibitor method to explicitly flush the EntityManager before executing the JPQL bulk UPDATE, ensuring MockMvc-created Booth entities are inserted before the update runs. Keep the existing parameters, executeUpdate call, and entityManager.clear() behavior unchanged.
218-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win요청 JSON은 헬퍼로 만들고, 공개 조회 테스트도 추가하면 좋겠습니다.
new LinkedHashMap<String, Object>() {{ ... }}패턴은 호출마다 익명 내부 클래스를 만들고 테스트 인스턴스를 캡처합니다. 동작에는 문제가 없지만Map.of또는 작은 빌더 헬퍼로 바꾸면 의도가 더 분명합니다.추가로 프런트엔드가 사용하는 공개 목록 API(
/events/{eventId}/booths/public)와 페이지 파라미터 검증(잘못된page·size) 케이스는 이 테스트에 없습니다. 인증 없이 접근 가능한 경로이므로 테스트를 추가하는 편이 안전합니다.필요하면 해당 테스트 케이스를 작성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.java` around lines 218 - 222, Replace the double-brace LinkedHashMap construction in the bulk request test with Map.of or a focused request-body helper. Extend BoothControllerTest with unauthenticated coverage for /events/{eventId}/booths/public, including valid retrieval and validation failures for invalid page and size parameters.BE/src/main/java/com/min/edu/booth/dto/BoothIntroUpdateRequestDto.java (1)
18-20: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
description과exhibitionContent에도 길이 상한을 두는 것이 좋겠습니다.같은 DTO의
displayName과shortIntro에는 상한이 있는데 이 두 필드만 무제한입니다. 부스 소개는 공개 조회 응답에 포함되므로, 상한이 없으면 매우 긴 본문이 저장되고 목록 응답 크기도 함께 커집니다.♻️ 길이 상한 추가 예시
+ `@Size`(max = 5000) private String description; + `@Size`(max = 5000) private String exhibitionContent;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/dto/BoothIntroUpdateRequestDto.java` around lines 18 - 20, Update BoothIntroUpdateRequestDto by adding appropriate maximum-length validation constraints to description and exhibitionContent, matching the existing validation style and limits used for displayName and shortIntro. Keep the fields’ existing behavior unchanged apart from enforcing these length limits.BE/src/main/java/com/min/edu/booth/dto/BoothResponseDto.java (1)
41-42: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winSensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor
Reachability: Internal · Exploitability: Theoretical
QR 응답 DTO를 분리해 주세요. 현재 공개 API는
BoothPublicResponseDto를 사용하며,BoothResponseDto는 인증된 관리 API에서만 반환됩니다. 다만qrToken과qrIssuedAt을 공용 DTO에 포함하면 향후 재사용 시 민감한 값이 노출될 수 있으므로, QR 필드는BoothQrResponseDto에서만 반환하는 구조가 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/dto/BoothResponseDto.java` around lines 41 - 42, Remove qrToken and qrIssuedAt from BoothResponseDto, and introduce or reuse BoothQrResponseDto as the only DTO exposing those QR fields. Keep BoothPublicResponseDto free of QR data and update QR-specific response mappings or endpoints to return BoothQrResponseDto while preserving existing non-QR response behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@BE/src/main/java/com/min/edu/booth/dto/BoothBulkCreateRequestDto.java`:
- Around line 18-19: Update the boothCodes field in BoothBulkCreateRequestDto to
add a `@Size` upper bound alongside `@NotEmpty`, using the project’s established
maximum batch-size constant or validation limit so oversized requests are
rejected before controller processing.
In `@BE/src/main/java/com/min/edu/booth/dto/BoothCreateRequestDto.java`:
- Around line 17-21: 등록 DTO의 문자열 필드에 최대 길이 검증을 추가하세요. BoothCreateRequestDto의
boothCode는 30자, boothType·floorName·zoneName은 50자, locationDescription은 200자로
제한하고, BoothBulkCreateRequestDto의 boothCodes 각 요소에도 `@Size`(max = 30)을 적용하세요.
In `@BE/src/main/java/com/min/edu/booth/service/BoothService.java`:
- Around line 190-202: Validate status transitions in updateStatus or
Booth.changeStatus before persisting the requested status: reject ASSIGNED when
assignedOrganizationId is null, and reject AVAILABLE when assignedOrganizationId
is non-null. Preserve valid transitions and add a TODO if this is intentionally
temporary pending BoothApplication integration.
- Around line 378-384: Update normalizeKeyword to escape user-supplied LIKE
wildcard characters using the query’s escape character, including the escape
character itself, before trimming, lowercasing, and adding wildcards; use
Locale.ROOT for deterministic lowercasing. Update each keyword LIKE predicate in
the relevant JPQL query to include ESCAPE '!' so the escaped values are treated
literally.
- Around line 103-113: Update BoothService.createBulk to replace the per-code
existsByEventIdAndBoothCode loop with one repository query that retrieves
existing booth codes using eventId and an IN filter over boothCodes, then reject
when any conflict is found while preserving duplicate-request validation. Add an
appropriate `@Size`(max = ...) constraint to BoothBulkCreateRequestDto.boothCodes
to enforce the bulk request limit.
- Around line 300-318: Update BoothService.listPublic to apply an explicit
allowlist of publicly visible booth statuses through a dedicated repository
query or search condition, excluding APPLICATION_PENDING and UNAVAILABLE. Ensure
the public response path exposes only allowed statuses and does not return
price, description, or exhibitionContent; keep authenticated/internal listing
behavior unchanged.
- Around line 224-235: updateIntro에서 request의 representativeFileId를 저장하기 전에 해당
파일이 요청 주체 또는 배정 조직의 소유인지 검증하는 로직을 추가하고, 소유권 검증에 실패하면 기존 비즈니스 예외 방식으로 요청을 거부하세요.
파일 ID를 검증 없이 저장하는 경로를 제거하고, 파일 정책에 사용하는 기존 검증 메서드와 오류 코드를 재사용하세요.
In `@BE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.java`:
- Line 68: Update BOOTH_CODE_ALREADY_EXISTS in GlobalErrorCode to use
HttpStatus.CONFLICT and the matching BOOTH_409_001 code, keeping its
duplicate-booth message unchanged. Search frontend handling for BOOTH_400_001
and update that branch to recognize the new 409 code if it exists.
- Line 69: Update the BOOTH_DELETE_NOT_ALLOWED message in GlobalErrorCode to
state that only booths with AVAILABLE status can be deleted, replacing the
wording about application or assignment history while preserving the existing
error code and status.
In
`@BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java`:
- Around line 122-124: Update the end-time validation in BoothRecruitmentService
around validatePeriod and recruitment.updateEndAt so an OPEN recruitment accepts
only an end time strictly after the current time; reject past or equal-to-now
values before persisting, while leaving immediate closure to the existing close
API.
- Around line 114-116: In updateEndAt, call requireEventManager(eventId, member)
before getByEventIdOrThrow(eventId), so authorization is evaluated before
recruitment lookup and unauthorized callers receive the same response regardless
of event existence.
In `@FE/src/components/BoothManagementPanel.jsx`:
- Around line 50-57: Validate all numeric form fields in BoothManagementPanel
before constructing the submission payload, including widthMeter, depthMeter,
areaSqm, and price; reject non-empty values that convert to NaN or are otherwise
invalid, such as comma-formatted input. Display a user-facing message
identifying the invalid field and stop submission, while preserving null
handling for intentionally empty optional dimension fields.
- Around line 191-216: Update the event-change and booth-loading flows in
BoothManagementPanel so a failed load clears loadedEventId instead of retaining
the previous event, and reset editingBoothId, introBoothId, and qrBoothId
whenever eventId changes. Update the registration, search, pagination, edit,
delete, intro, and QR action paths to use the current eventId directly rather
than stale loadedEventId or prior-event booth state.
In `@FE/src/components/RecruitmentManagementPanel.jsx`:
- Around line 84-93: Update the RecruitmentManagementPanel request flow around
loadRecruitment and runAction to invalidate prior requests when eventId changes
or is cleared, using a useRef request version or AbortController. Only apply
success, failure, and completion state updates when the request still belongs to
the current event/version; when eventId is cleared, invalidate pending requests
and reset loading as well.
In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 46-58: Update the organization selection logic in the
managedOrganizations useEffect to validate the existing organizationId against
the loaded list, not only when it is empty. If the current ID is absent from the
user’s organizations and the list is non-empty, replace it with the first
organization’s ID; preserve valid selections and the existing empty-list
behavior.
In `@FE/src/pages/RecruitmentDetail.jsx`:
- Around line 75-77: Update the booth-loading flow in RecruitmentDetail around
listPublicBooths so it tracks the response page metadata (page and last or
totalPages) alongside content. When the response is not the final page, provide
pagination or a “load more” action that requests subsequent pages and appends
their booths to the existing list, while retaining the current page-size limit
instead of fetching an unlimited result.
---
Nitpick comments:
In `@BE/src/main/java/com/min/edu/booth/domain/Booth.java`:
- Around line 121-191: Reduce positional-argument risk in Booth.create and
Booth.updateDetails by grouping the related same-type fields into dedicated
value objects, such as records for location, dimensions, and equipment, and
update the BoothService.create, createBulk, and update callers to pass those
objects. Preserve the existing field assignments and behavior while removing the
long repeated parameter lists.
In `@BE/src/main/java/com/min/edu/booth/dto/BoothIntroUpdateRequestDto.java`:
- Around line 18-20: Update BoothIntroUpdateRequestDto by adding appropriate
maximum-length validation constraints to description and exhibitionContent,
matching the existing validation style and limits used for displayName and
shortIntro. Keep the fields’ existing behavior unchanged apart from enforcing
these length limits.
In `@BE/src/main/java/com/min/edu/booth/dto/BoothResponseDto.java`:
- Around line 41-42: Remove qrToken and qrIssuedAt from BoothResponseDto, and
introduce or reuse BoothQrResponseDto as the only DTO exposing those QR fields.
Keep BoothPublicResponseDto free of QR data and update QR-specific response
mappings or endpoints to return BoothQrResponseDto while preserving existing
non-QR response behavior.
In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java`:
- Around line 22-39: Update the BoothRepository.search query strategy for
scalable keyword filtering: replace the organization-name EXISTS condition with
a LEFT JOIN to Organization while preserving the existing filters and results,
add the recommended composite indexes for event/status and
event/floorName/zoneName, and use PostgreSQL pg_trgm GIN indexes or a dedicated
searchable column if keyword search is a core requirement. Keep the named
parameter binding unchanged.
In `@BE/src/main/java/com/min/edu/booth/service/BoothService.java`:
- Around line 216-222: Update BoothService.getDetail to allow assigned
organization members as well as event managers, matching the authorization used
by updateIntro and getQr, or provide an equivalent organization-member detail
path. Because toResponse includes qrToken, ensure the expanded-access response
excludes or safely handles that field according to the intended permission
boundary.
- Around line 462-475: Update BoothService.fromJson to log parsing failures
before returning an empty list: add the Lombok `@Slf4j` logger and call log.warn
with descriptive context and the caught JacksonException in the catch block.
Preserve the existing empty-list fallback for invalid JSON and blank or null
input.
In `@BE/src/main/java/com/min/edu/common/config/SecurityConfig.java`:
- Around line 68-69: In BoothService, add a shared guard at the start of
requireEventManager and requireAssignedOrganizationMemberOrEventManager that
throws BusinessException(GlobalErrorCode.UNAUTHORIZED) when member is null,
before any member dereference or authorization checks.
In `@BE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.java`:
- Around line 164-171: Update the assignToExhibitor method to explicitly flush
the EntityManager before executing the JPQL bulk UPDATE, ensuring
MockMvc-created Booth entities are inserted before the update runs. Keep the
existing parameters, executeUpdate call, and entityManager.clear() behavior
unchanged.
- Around line 218-222: Replace the double-brace LinkedHashMap construction in
the bulk request test with Map.of or a focused request-body helper. Extend
BoothControllerTest with unauthenticated coverage for
/events/{eventId}/booths/public, including valid retrieval and validation
failures for invalid page and size parameters.
In
`@BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java`:
- Around line 340-361: Update the test method OPEN_상태에서는_모집_종료일만_수정할_수_있다 to
assert that the response recruitmentEndAt matches newEnd, in addition to the
existing success and OPEN status checks. If the response is insufficient, clear
the persistence context and verify the stored value through a managed retrieval.
In `@FE/src/api/boothApi.js`:
- Around line 9-25: Extract the shared parameter filtering and URLSearchParams
construction from listBooths and listPublicBooths into a reusable helper, then
use that helper in both functions while preserving the current handling of empty
and nullish values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e76018e7-e9d8-4dc6-8610-c5fcc1fba53b
⛔ Files ignored due to path filters (1)
FE/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (33)
BE/src/main/java/com/min/edu/booth/controller/BoothController.javaBE/src/main/java/com/min/edu/booth/domain/Booth.javaBE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.javaBE/src/main/java/com/min/edu/booth/dto/BoothBulkCreateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothCreateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothIntroUpdateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothPageResponse.javaBE/src/main/java/com/min/edu/booth/dto/BoothPublicPageResponse.javaBE/src/main/java/com/min/edu/booth/dto/BoothPublicResponseDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothQrResponseDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothResponseDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothStatusUpdateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothUpdateRequestDto.javaBE/src/main/java/com/min/edu/booth/repository/BoothRepository.javaBE/src/main/java/com/min/edu/booth/service/BoothService.javaBE/src/main/java/com/min/edu/booth/support/BoothQrTokenGenerator.javaBE/src/main/java/com/min/edu/common/config/SecurityConfig.javaBE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.javaBE/src/main/java/com/min/edu/organization/repository/OrganizationMemberRepository.javaBE/src/main/java/com/min/edu/recruitment/controller/BoothRecruitmentController.javaBE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentEndAtUpdateRequestDto.javaBE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.javaBE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.javaBE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.javaBE/src/test/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusSchedulerTest.javaFE/package.jsonFE/src/api/boothApi.jsFE/src/api/recruitmentApi.jsFE/src/components/BoothManagementPanel.jsxFE/src/components/RecruitmentManagementPanel.jsxFE/src/pages/Home.jsxFE/src/pages/OrganizerAdmin.jsxFE/src/pages/RecruitmentDetail.jsx
| @NotBlank | ||
| private String boothCode; | ||
|
|
||
| @NotBlank | ||
| private String boothType; | ||
|
|
||
| private String floorName; | ||
|
|
||
| private String zoneName; | ||
|
|
||
| private String locationDescription; | ||
|
|
||
| @DecimalMin(value = "0", inclusive = true) | ||
| private BigDecimal widthMeter; | ||
|
|
||
| @DecimalMin(value = "0", inclusive = true) | ||
| private BigDecimal depthMeter; | ||
|
|
||
| @DecimalMin(value = "0", inclusive = true) | ||
| private BigDecimal areaSqm; | ||
|
|
||
| private List<String> basicEquipment; | ||
|
|
||
| private boolean electricityAvailable; | ||
|
|
||
| private boolean waterAvailable; | ||
|
|
||
| private boolean drainageAvailable; | ||
|
|
||
| private boolean internetAvailable; | ||
|
|
||
| @NotNull | ||
| @DecimalMin(value = "0", inclusive = true) | ||
| private BigDecimal price; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
DB 제약과 요청 검증을 일치시키세요.
@NotBlank와 @DecimalMin만으로는 문자열 길이와 소수 정밀도를 제한하지 않습니다. 예를 들어 31자 boothCode 또는 NUMERIC(6,2) 범위를 넘는 규격 값이 요청 검증을 통과합니다. DB 저장 시 값이 반올림되거나 실패할 수 있습니다.
또한 BoothService.update는 모든 DataIntegrityViolationException을 BOOTH_CODE_ALREADY_EXISTS로 변환합니다. 길이·정밀도 오류도 중복 코드 오류로 잘못 응답됩니다. DTO에 @Size와 @Digits를 추가하고, 서비스에서는 유일성 제약 위반만 중복 코드 오류로 변환하세요.
수정 예시
import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
`@NotBlank`
+ `@Size`(max = 30)
private String boothCode;
`@NotBlank`
+ `@Size`(max = 50)
private String boothType;
+ `@Size`(max = 50)
private String floorName;
+ `@Size`(max = 50)
private String zoneName;
+ `@Size`(max = 200)
private String locationDescription;
`@DecimalMin`(value = "0", inclusive = true)
+ `@Digits`(integer = 4, fraction = 2)
private BigDecimal widthMeter;
`@DecimalMin`(value = "0", inclusive = true)
+ `@Digits`(integer = 4, fraction = 2)
private BigDecimal depthMeter;
`@DecimalMin`(value = "0", inclusive = true)
+ `@Digits`(integer = 6, fraction = 2)
private BigDecimal areaSqm;
`@NotNull`
`@DecimalMin`(value = "0", inclusive = true)
+ `@Digits`(integer = 12, fraction = 0)
private BigDecimal price;근거: BE/src/main/resources/db/migration/V1__init_schema.sql의 열 제약과 BE/src/main/java/com/min/edu/booth/service/BoothService.java의 예외 변환 경로입니다.
- 부스 등록/수정 DTO에 문자열 길이·소수 정밀도 검증과 일괄등록 개수 상한 추가 - 일괄등록 중복검사를 N+1 쿼리에서 단일 IN 쿼리로 변경 - 부스 상태 전이 검증 추가 (배정 없이 ASSIGNED로, 배정된 부스를 AVAILABLE로 변경 차단) - 공개 부스 목록에 상태 allowlist 적용 (심사중/사용불가 부스 비공개) - LIKE 검색 와일드카드 이스케이프 및 로케일 고정 - 부스 번호 중복 에러를 409(Conflict)로, 삭제 제한 메시지를 실제 조건에 맞게 수정 - 모집 공고 서비스 전 메서드에서 권한 검사를 조회보다 먼저 수행하도록 순서 변경, 모집 종료일을 과거로 설정하는 것 차단 - 배정 부스 소개 수정 시 대표 이미지 파일 소유권 검증 추가 - BoothManagementPanel: 행사 전환 시 이전 행사에 요청이 나가던 버그 수정, 숫자 입력값 검증 추가 - RecruitmentManagementPanel: 행사 빠른 전환 시 늦게 도착한 이전 응답이 화면을 덮어쓰지 않도록 요청 버전 가드 추가 - OrganizerAdmin: localStorage에 남은 organizationId가 현재 계정 소속이 아니면 자동 교체 - RecruitmentDetail: 공개 부스 목록에 페이지네이션(더 보기) 추가
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
FE/src/pages/OrganizerAdmin.jsx (1)
257-260: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win조직 전환 뒤 이전 행사 목록 응답을 무시하세요.
조직 A의
organizationList요청이 진행 중일 때 조직 B를 선택하면, 늦게 도착한 A 응답이managedEvents를 다시 설정할 수 있습니다. 이전 effect는 캡처한 빈selectedEventId를 기준으로 A의 첫 행사를 자동 선택할 수도 있습니다.그 결과 선택 조직은 B인데
selectedEventId는 A가 됩니다.RecruitmentManagementPanel과BoothManagementPanel은 이 ID로 요청을 전송합니다.행사 목록 effect에 취소 플래그 또는 요청 버전을 추가하세요. 응답 반영과 자동 행사 선택은 최신
organizationId일 때만 실행하세요. 목록 조회 시작 시 이전 목록과 오류 상태도 초기화하세요.As per path instructions, "비즈니스 로직 오류"와 "중복 요청과 동시성 문제"를 확인했습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@FE/src/pages/OrganizerAdmin.jsx` around lines 257 - 260, Update the 행사 목록 effect that loads managedEvents for organizationId to cancel or version each request, and only apply its response or automatic first-event selection when it still belongs to the latest organizationId. When a new organization is selected in the onChange handler, clear the previous event list and error state before loading the new list, while preserving the existing selectedEventId reset.Source: Path instructions
FE/src/components/BoothManagementPanel.jsx (1)
220-246: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win이전 행사 요청의 결과를 현재 행사 화면에 반영하지 마세요.
Line 221에서 행사 A의 목록 요청이 진행 중일 때 행사 B를 선택하면, 늦게 도착한 A 응답이
pageResult를 B 화면에 저장할 수 있습니다. 이후 사용자는 A의 부스를 보지만 상태 변경·수정·삭제 요청은 현재eventId인 B로 전송합니다.
loadBooths에 요청 버전 또는AbortController를 적용하세요.setPageResult,setError,setLoading은 최신 행사 요청일 때만 실행하세요.runAction의 성공 후refresh()도 같은 버전을 확인해야 합니다.As per path instructions, "중복 요청과 동시성 문제"를 확인했습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@FE/src/components/BoothManagementPanel.jsx` around lines 220 - 246, loadBooths의 행사별 요청에 요청 버전 또는 AbortController를 적용해 이전 행사 요청이 완료되어도 현재 행사 상태를 갱신하지 않도록 수정하세요. setPageResult, setError, setLoading은 최신 요청일 때만 실행되게 하고, runAction의 성공 후 refresh()도 동일한 최신 요청 기준을 확인하도록 연결하세요.Source: Path instructions
♻️ Duplicate comments (1)
FE/src/components/RecruitmentManagementPanel.jsx (1)
90-99: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win행사 전환 시
submitting상태를 해제하세요.행사 A에서 작업을 시작한 뒤 행사 B로 전환하면
requestVersionRef.current가 증가합니다. 이전 작업의finally는 버전 불일치로setSubmitting(false)를 실행하지 않습니다. 현재 effect도submitting을 초기화하지 않습니다.그 결과 B 화면의 버튼이 계속 비활성화되고,
runAction은 Line 110의 조건에서 즉시 반환합니다. 행사 변경 effect에서 요청 버전을 증가시킨 직후setSubmitting(false)를 실행하세요.As per path instructions, "중복 요청과 동시성 문제"를 확인했습니다.
Also applies to: 112-126, 144-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@FE/src/components/RecruitmentManagementPanel.jsx` around lines 90 - 99, Update the event-change useEffect to call setSubmitting(false) immediately after incrementing requestVersionRef.current, ensuring switching events resets the submission state even when the previous request’s finally block is ignored due to a version mismatch.Source: Path instructions
🧹 Nitpick comments (1)
BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java (1)
34-42: 🚀 Performance & Scalability | 🔵 Trivial키워드 검색의 실행 계획을 확인해 주세요.
Line [34-42]는 세 컬럼에
LOWER(...) LIKE :keyword를 적용하고 조직명에는EXISTS서브쿼리를 사용합니다. 서비스가 contains 검색을 위해%...%를 전달하면 일반 인덱스 사용이 제한됩니다.Page조회는 콘텐츠 조회 외에 count 쿼리도 수행하므로 대규모 행사에서 전체 스캔 비용이 커질 수 있습니다. 콘텐츠 쿼리와 count 쿼리의 실행 계획을 확인하고, 필요하면 대소문자 무시 인덱스나 전용 검색 인덱스를 사용하세요.As per path instructions: 성능 저하와 불필요한 연산을 우선 확인했습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java` around lines 34 - 42, BoothRepository의 키워드 검색 쿼리에 대해 콘텐츠 조회와 Page count 쿼리 양쪽의 실행 계획을 확인하고, 대규모 데이터에서 전체 스캔이 발생하면 LOWER 대상 컬럼에 대소문자 무시 인덱스 또는 전용 검색 인덱스를 적용하세요. boothCode, displayName, Organization.name 검색과 assignedOrganizationId 연계를 각각 검토하고, contains 검색의 `%keyword%` 동작과 기존 필터 결과는 유지하세요.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@FE/src/pages/RecruitmentDetail.jsx`:
- Around line 104-119: Update the booth-loading flow around loadMoreBooths to
invalidate requests from previous events: reset booths, boothPage, boothHasMore,
and loadingMoreBooths when recruitment.eventId changes, track a request
generation for each event, and apply response or error state updates only when
the request generation still matches the current generation.
---
Outside diff comments:
In `@FE/src/components/BoothManagementPanel.jsx`:
- Around line 220-246: loadBooths의 행사별 요청에 요청 버전 또는 AbortController를 적용해 이전 행사
요청이 완료되어도 현재 행사 상태를 갱신하지 않도록 수정하세요. setPageResult, setError, setLoading은 최신 요청일
때만 실행되게 하고, runAction의 성공 후 refresh()도 동일한 최신 요청 기준을 확인하도록 연결하세요.
In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 257-260: Update the 행사 목록 effect that loads managedEvents for
organizationId to cancel or version each request, and only apply its response or
automatic first-event selection when it still belongs to the latest
organizationId. When a new organization is selected in the onChange handler,
clear the previous event list and error state before loading the new list, while
preserving the existing selectedEventId reset.
---
Duplicate comments:
In `@FE/src/components/RecruitmentManagementPanel.jsx`:
- Around line 90-99: Update the event-change useEffect to call
setSubmitting(false) immediately after incrementing requestVersionRef.current,
ensuring switching events resets the submission state even when the previous
request’s finally block is ignored due to a version mismatch.
---
Nitpick comments:
In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java`:
- Around line 34-42: BoothRepository의 키워드 검색 쿼리에 대해 콘텐츠 조회와 Page count 쿼리 양쪽의 실행
계획을 확인하고, 대규모 데이터에서 전체 스캔이 발생하면 LOWER 대상 컬럼에 대소문자 무시 인덱스 또는 전용 검색 인덱스를 적용하세요.
boothCode, displayName, Organization.name 검색과 assignedOrganizationId 연계를 각각
검토하고, contains 검색의 `%keyword%` 동작과 기존 필터 결과는 유지하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1aa764a9-18de-4935-b505-af28f4c537b8
📒 Files selected for processing (13)
BE/src/main/java/com/min/edu/booth/dto/BoothBulkCreateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothCreateRequestDto.javaBE/src/main/java/com/min/edu/booth/dto/BoothUpdateRequestDto.javaBE/src/main/java/com/min/edu/booth/repository/BoothRepository.javaBE/src/main/java/com/min/edu/booth/service/BoothService.javaBE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.javaBE/src/main/java/com/min/edu/file/service/FileService.javaBE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.javaBE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.javaFE/src/components/BoothManagementPanel.jsxFE/src/components/RecruitmentManagementPanel.jsxFE/src/pages/OrganizerAdmin.jsxFE/src/pages/RecruitmentDetail.jsx
🚧 Files skipped from review as they are similar to previous changes (5)
- BE/src/test/java/com/min/edu/booth/controller/BoothControllerTest.java
- BE/src/main/java/com/min/edu/booth/dto/BoothBulkCreateRequestDto.java
- BE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.java
- BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java
- BE/src/main/java/com/min/edu/booth/service/BoothService.java
행사를 빠르게 전환하면 이전 행사의 loadMoreBooths 응답이 새 행사 화면의 booths/boothPage/boothHasMore를 덮어쓸 수 있었다. 부스 목록 로딩과 더 보기 요청을 같은 세대(generation) 참조로 묶어, 오래된 응답은 무시하도록 했다.
📄 작업 내용
부스(Booth) 관리 기능을 백엔드/프론트엔드 전체 구간에 걸쳐 구현했습니다.
OPEN상태가 된 이후에는 모집 종료일만 수정 가능하도록 제한 (그 외 내용 수정 차단), 자동 상태 전환 스케줄러 테스트 보강🔗 관련 이슈
Closes #
☑️ 체크리스트
📸 스크린샷 (UI 변경 시)
개최자센터 부스 관리 화면, 모집 공고 상세 페이지의 부스 목록/스펙 팝업 등 UI 변경 포함
💬 리뷰어에게
dev를 두 차례 병합하며 최신화했습니다. 부스 신청(BoothApplication) 기능은 이번 범위에서 제외했습니다.Summary by CodeRabbit