Skip to content

feat: 부스 모집 공고 API 추가 (recruitment) - #7

Closed
hyun326 wants to merge 6 commits into
devfrom
feature/recruitment-create
Closed

hyun326 wants to merge 6 commits into
devfrom
feature/recruitment-create

Conversation

@hyun326

@hyun326 hyun326 commented Aug 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • 부스 모집 공고 API 7개 구현 (REC-API-001~007, API 명세서 기준)
    • 공개 목록/상세 조회, 행사별 생성/관리조회/수정, 조기마감, 완료처리
  • 모집 시작/종료일 기준 상태 자동 전환 스케줄러 추가 (BEFORE_OPEN→OPEN→CLOSED)
  • (명세서엔 없지만) BEFORE_OPEN 상태 한정 삭제 API 추가 — 테스트/운영 편의
  • API 확인용 개발 페이지(/recruitment-check) 추가 — 정식 화면(WBS-184/185)은 별도 작업 예정

주요 변경

  • recruitment/ 패키지: controller, service, repository, dto, scheduler
  • event/repository/EventRepository.java, EventMemberRepository.java (신규, 권한 검증용)
  • SecurityConfig, GlobalErrorCode, BeApplication(@EnableScheduling) — 기존 코드 순수 추가만, 충돌 없음

Test plan

  • Testcontainers 기반 통합 테스트 11개 (생성/권한/중복/공개목록/상태전환/삭제) 전부 통과
  • 로컬 bootRun + 브라우저에서 전체 흐름 수동 확인 (생성→조기마감→상세조회→완료처리→수정→삭제)
  • 원격 배포 환경 확인은 merge 후 ArgoCD 반영 시 진행

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새로운 기능

    • 부스 모집 공고의 생성, 조회, 수정, 삭제, 조기 마감 및 완료 기능을 추가했습니다.
    • 모집 상태별 공개 목록과 상세 정보를 확인할 수 있습니다.
    • 모집 기간에 따라 공고 상태가 자동으로 변경됩니다.
    • 모집 관리 화면과 상태 필터, 오류 안내를 제공합니다.
    • 관련 기능의 접근 권한과 입력값 검증을 강화했습니다.
  • 테스트

    • 모집 공고의 주요 흐름과 권한별 동작을 검증하는 통합 테스트를 추가했습니다.

행사당 모집 공고 등록/수정/조기마감/완료처리 및 공개 목록·상세 조회
API 명세서(REC-API-001~007) 기준 경로/권한 구성
Testcontainers 기반으로 생성/권한/중복/공개목록/상태전환 시나리오 검증
TestcontainersConfiguration을 public으로 변경해 다른 패키지 테스트에서 재사용
BEFORE_OPEN 상태 상세조회 404, 공개 후 조회 성공
담당자 수정 성공/권한 없는 회원 수정 실패 시나리오 검증
recruitment API 7개(REC-API-001~007)를 실제로 호출해 동작을 확인하는 개발용 화면
공개 목록/상세, 행사별 관리(조회·생성·수정·조기마감·완료처리) 지원
FE dev 서버 실행용 .claude/launch.json 추가
recruitmentStartAt 도래 시 BEFORE_OPEN -> OPEN,
recruitmentEndAt 도래 시 OPEN -> CLOSED로 매분 자동 전환
지금까지는 수동 조기마감 없이는 공개 목록에 노출될 방법이 없었음
명세서(REC-API-001~007)엔 없는 기능이지만, 아직 시작 전인 초안 상태에서만
삭제를 허용해 부스 신청 등 하위 데이터와의 정합성 문제를 방지
프론트 확인 페이지에도 삭제 버튼 추가
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

부스 모집 공고 기능을 백엔드와 프런트엔드에 추가했습니다. 생성·수정·삭제·상태 변경 API, 관리자 권한 검증, 자동 상태 전환 스케줄러, 모집 관리 화면과 통합 테스트를 구현했습니다.

Changes

부스 모집 공고

Layer / File(s) Summary
모집 도메인과 데이터 계약
BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java, BE/src/main/java/com/min/edu/recruitment/dto/*, BE/src/main/java/com/min/edu/recruitment/repository/*, BE/src/main/java/com/min/edu/event/repository/*
모집 생성, 상세 수정, 상태 변경, 완료 처리를 추가했습니다. 요청·응답 DTO, 저장소 메서드와 모집 오류 코드를 정의했습니다.
모집 관리 서비스와 API
BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java, BE/src/main/java/com/min/edu/recruitment/controller/BoothRecruitmentController.java, BE/src/main/java/com/min/edu/common/config/SecurityConfig.java, BE/src/test/java/com/min/edu/recruitment/controller/*
공개 조회와 관리자 작업 API를 추가했습니다. 관리자 권한, 모집 기간, 중복 생성 및 상태를 검증합니다. 통합 테스트에서 주요 성공·실패 흐름을 검증합니다.
모집 상태 자동 전환
BE/src/main/java/com/min/edu/BeApplication.java, BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java
스케줄링을 활성화했습니다. 1분마다 BEFORE_OPEN을 OPEN으로, OPEN을 CLOSED로 전환합니다.
모집 관리 화면과 API 연동
FE/src/api/recruitmentApi.js, FE/src/pages/RecruitmentCheck.jsx, FE/src/main.jsx, .claude/launch.json
모집 API 모듈과 /recruitment-check 페이지를 추가했습니다. 공개 조회, 관리 조회, 생성, 수정, 마감, 완료, 삭제를 화면에 연결했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 관리자
  participant RecruitmentCheck
  participant recruitmentApi
  participant BoothRecruitmentController
  participant BoothRecruitmentService
  관리자->>RecruitmentCheck: 모집 관리 작업 실행
  RecruitmentCheck->>recruitmentApi: 생성·수정·상태 변경 요청
  recruitmentApi->>BoothRecruitmentController: HTTP 요청 전송
  BoothRecruitmentController->>BoothRecruitmentService: 인증 회원과 요청 데이터 전달
  BoothRecruitmentService-->>BoothRecruitmentController: 검증 및 처리 결과 반환
  BoothRecruitmentController-->>recruitmentApi: ApiResponse 반환
  recruitmentApi-->>RecruitmentCheck: 응답 데이터 반환
  RecruitmentCheck-->>관리자: 목록과 상태 갱신
Loading

Possibly related PRs

  • half-fifty/eventoday#2: SecurityConfig, JWT 쿠키와 인증 상태를 사용해 모집 관리 API의 인증 흐름을 구성합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 부스 모집 공고 API와 관련 기능 추가라는 PR의 주요 변경 사항을 명확하고 간결하게 설명합니다.
Description check ✅ Passed 주요 변경 사항과 테스트 계획을 구체적으로 설명하며, 관련 이슈와 일부 템플릿 항목은 누락되었습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/recruitment-create

Comment @coderabbitai help to get the list of available commands.

@hyun326 hyun326 closed this Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (10)
FE/src/api/recruitmentApi.js (1)

3-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

쿼리 파라미터와 경로 파라미터를 인코딩하는 편이 안전합니다.

지금은 화면에서 select 값과 숫자 입력만 들어오므로 실제 문제는 없습니다. 다만 이 모듈은 공개 API라서 다른 화면에서 임의 문자열을 넘길 수 있습니다. encodeURIComponent를 적용하면 #, &, 공백 같은 문자에서 URL이 깨지는 것을 막을 수 있습니다.

♻️ 제안 diff
 const listPublicRecruitments = async (status) => {
-  const query = status ? `?status=${status}` : "";
+  const query = status ? `?status=${encodeURIComponent(status)}` : "";
   const response = await apiRequest(`/booth-recruitments${query}`);
   return response.data;
 };
 
 const getPublicRecruitment = async (recruitmentId) => {
-  const response = await apiRequest(`/booth-recruitments/${recruitmentId}`);
+  const response = await apiRequest(`/booth-recruitments/${encodeURIComponent(recruitmentId)}`);
   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/recruitmentApi.js` around lines 3 - 12, Update
listPublicRecruitments and getPublicRecruitment to apply encodeURIComponent to
the status query value and recruitmentId path value before constructing the API
URLs, preserving the existing behavior when status is absent.

Source: Path instructions

FE/src/main.jsx (1)

24-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

개발용 확인 페이지가 운영 빌드에도 그대로 노출됩니다.

/recruitment-check는 API 확인용 화면입니다. 지금 설정은 누구나 접근할 수 있고, 운영 번들에도 포함됩니다. 서버가 권한을 검증하므로 데이터 유출 위험은 낮습니다. 다만 내부용 화면은 감추는 편이 좋습니다. 두 가지 방법이 있습니다.

  • import.meta.env.DEV 조건으로 라우트를 등록합니다.
  • 기존 /platform-admin처럼 ProtectedRoute로 감쌉니다.
♻️ 제안 diff (개발 환경 한정 등록)
-  { path: "/recruitment-check", element: <RecruitmentCheck /> },
+  ...(import.meta.env.DEV
+    ? [{ path: "/recruitment-check", element: <RecruitmentCheck /> }]
+    : []),
🤖 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/main.jsx` at line 24, Update the route registration containing
RecruitmentCheck so /recruitment-check is only registered when
import.meta.env.DEV is true, preventing the internal API verification page from
being exposed in production while preserving its development-only availability.

Source: Path instructions

FE/src/pages/RecruitmentCheck.jsx (1)

131-148: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

중복 클릭을 막는 처리가 없습니다.

runAction은 요청 진행 상태를 관리하지 않습니다. 생성 버튼을 연타하면 POST /events/{eventId}/booth-recruitment가 여러 번 나갑니다. 서버가 중복 생성을 막아 준다고 해도, 사용자에게는 원인을 알 수 없는 오류 메시지가 뜹니다. 조기 마감·완료 처리도 같습니다.

진행 중 플래그를 두고 버튼을 disabled 처리해 주세요.

♻️ 제안 diff
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
   const runAction = async (actionFn, successMessage) => {
+    if (isSubmitting) return;
+    setIsSubmitting(true);
     setManagementError("");
     setActionMessage("");
 
     try {
       const data = await actionFn();
       setManagementResult(data);
       setActionMessage(successMessage);
       loadPublicList();
     } catch (error) {
       setManagementError(error instanceof ApiError ? `${error.code}: ${error.message}` : "요청에 실패했습니다.");
+    } finally {
+      setIsSubmitting(false);
     }
   };
🤖 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/RecruitmentCheck.jsx` around lines 131 - 148, runAction과 연결된
Recruitment 액션에 요청 진행 상태를 추가해 중복 실행을 방지하세요. 요청 시작 시 진행 플래그를 설정하고 finally에서 반드시
해제한 뒤, 생성·수정·조기 마감·완료 버튼이 해당 플래그로 disabled되도록 연결하세요.

Source: Path instructions

BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java (3)

33-36: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

대량 데이터 환경에서는 벌크 업데이트를 고려해 주세요.

현재는 대상 엔티티를 전부 리스트로 조회한 뒤, 반복문으로 하나씩 changeStatus를 호출해 JPA dirty checking으로 flush합니다. 모집 공고 건수가 적은 지금 규모에서는 문제없지만, 데이터가 많아지면 @Modifying 벌크 업데이트 쿼리로 바꾸는 것이 메모리와 처리 시간 면에서 더 유리합니다. 지금 당장 고칠 필요는 없습니다.

Also applies to: 40-43

🤖 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/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`
around lines 33 - 36, 대량 데이터 처리 시 전체 엔티티를 조회해 반복 변경하는
BoothRecruitmentStatusScheduler의 상태 갱신 흐름을 벌크 업데이트 방식으로 전환하세요.
BoothRecruitmentRepository에 조건에 맞는 BEFORE_OPEN 모집 공고를 OPEN으로 변경하는 `@Modifying` 쿼리를
추가하고, 스케줄러에서 해당 메서드를 호출해 엔티티 목록 조회와 개별 changeStatus 반복을 제거하세요.

23-30: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

예외 발생 시 관측 가능성이 부족합니다.

openRecruitmentsThatHaveStarted(now)에서 예외가 발생하면 같은 트랜잭션이라 closeRecruitmentsThatHaveEnded(now)가 그 주기에 실행되지 않고 전체가 롤백됩니다. Spring 기본 에러 핸들러가 예외를 로그로 남기고 다음 주기에 재시도하긴 하지만, 이 클래스에는 실패를 감지할 로깅이나 메트릭이 전혀 없어서 운영 중 장시간 상태 전환이 멈춰도 알아차리기 어렵습니다.

각 단계를 개별 로깅하거나, 실패 시 알림이 가도록 에러 핸들러를 커스터마이즈하는 것을 권장합니다.

🔧 로깅 추가 예시
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 `@Component`
 `@RequiredArgsConstructor`
 public class BoothRecruitmentStatusScheduler {

     `@Scheduled`(fixedRate = FIXED_RATE_MILLIS)
     `@Transactional`
     public void transitionStatuses() {
         OffsetDateTime now = OffsetDateTime.now();

-        openRecruitmentsThatHaveStarted(now);
-        closeRecruitmentsThatHaveEnded(now);
+        try {
+            openRecruitmentsThatHaveStarted(now);
+            closeRecruitmentsThatHaveEnded(now);
+        } catch (Exception e) {
+            log.error("모집 상태 자동 전환 실패", e);
+            throw e;
+        }
     }
🤖 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/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`
around lines 23 - 30, Update BoothRecruitmentStatusScheduler.transitionStatuses
to add explicit failure observability around openRecruitmentsThatHaveStarted and
closeRecruitmentsThatHaveEnded, logging each failed transition with contextual
details and ensuring the failure is surfaced for the scheduler’s existing retry
behavior. Preserve the current transactional semantics unless separate handling
is required to let the later phase run after an independent failure.

15-30: 🩺 Stability & Availability | 🔵 Trivial

다중 인스턴스 배포 시 스케줄러 중복 실행 여부를 확인해 주세요.

이 스케줄러는 인스턴스별로 독립적으로 60초마다 동작합니다. 배포 환경이 단일 인스턴스라면 문제없지만, 서버를 여러 대 띄우는 구성(오토스케일링, 롤링 배포 등)이라면 각 인스턴스가 동시에 같은 대상 레코드를 조회해 상태를 갱신하려 시도합니다. 결과 값은 동일해도 불필요한 중복 UPDATE와, 위에서 언급한 lost update 위험이 인스턴스 수만큼 커집니다.

다중 인스턴스 운영 계획이 있다면 ShedLock 같은 분산 락 라이브러리 도입을 검토해 주세요. 단일 인스턴스로 운영한다면 현재 구조로도 충분합니다.

🤖 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/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`
around lines 15 - 30, Review the deployment model for
BoothRecruitmentStatusScheduler and ensure transitionStatuses is protected by a
distributed scheduler lock when multiple application instances can run
concurrently, using the project’s established ShedLock or equivalent
configuration. If deployment is guaranteed to remain single-instance, document
or preserve the current scheduling behavior instead of adding a lock.
BE/src/main/java/com/min/edu/recruitment/repository/BoothRecruitmentRepository.java (1)

21-29: 🚀 Performance & Scalability | 🔵 Trivial

스케줄러 조회용 인덱스도 같이 챙기면 좋을 것 같아요.

findAllByStatusAndRecruitmentStartAtLessThanEqual, findAllByStatusAndRecruitmentEndAtLessThanEqual 두 메서드는 스케줄러가 주기적으로 호출하는 쿼리입니다. 지금은 이벤트당 공고가 1건이라 데이터 양이 적겠지만, 나중에 데이터가 늘어나면 status와 recruitment_start_at/recruitment_end_at 조합에 인덱스가 없으면 매번 풀스캔이 발생할 수 있어요. 마이그레이션 파일에 복합 인덱스를 추가하는 걸 검토해보세요.

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/recruitment/repository/BoothRecruitmentRepository.java`
around lines 21 - 29, 스케줄러 조회 메서드인
findAllByStatusAndRecruitmentStartAtLessThanEqual과
findAllByStatusAndRecruitmentEndAtLessThanEqual에 맞춰 마이그레이션에 각각 status와
recruitment_start_at, status와 recruitment_end_at 조합의 복합 인덱스를 추가하세요.

Source: Path instructions

BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentUpdateRequestDto.java (1)

16-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

여기도 @Size 제약이 없습니다.

Create DTO와 같은 이유로 title, participantTarget, contactName, contactPhone, notice에 길이 제한이 없어요. 이 부분은 Create DTO의 코멘트와 같은 원인이라 아래 consolidated 코멘트에서 함께 정리했습니다.

🤖 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/recruitment/dto/BoothRecruitmentUpdateRequestDto.java`
around lines 16 - 44, BoothRecruitmentUpdateRequestDto의 title,
participantTarget, contactName, contactPhone, notice 필드에 Create DTO와 동일한 `@Size`
길이 제약을 추가하세요. 기존 검증 어노테이션은 유지하고 각 필드에 정의된 공통 길이 기준을 재사용하세요.

Source: Path instructions

BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java (1)

136-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

모집 기간 검증 실패(RECRUITMENT_PERIOD_INVALID) 테스트가 빠져있어요.

GlobalErrorCode에 RECRUITMENT_400_001(모집 종료일이 시작일 이전)이 추가됐는데, 이 케이스를 검증하는 테스트가 없습니다. 중복 생성 테스트처럼 recruitmentEndAt을 recruitmentStartAt보다 이전으로 보내서 400과 RECRUITMENT_400_001을 확인하는 테스트를 추가하면 좋을 것 같습니다.

🤖 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 136 - 171, BoothRecruitmentControllerTest에 모집 기간 역전 검증 테스트를 추가하세요.
담당자 권한으로 생성 요청을 보내되 recruitmentEndAt이 recruitmentStartAt보다 이전이 되도록 요청 데이터를 구성하고,
응답 상태가 400이며 오류 코드가 RECRUITMENT_400_001인지 검증하세요. 기존 담당자 인증과 요청 생성 흐름은
같은_행사에_중복_생성하면_실패한다 테스트 패턴을 따르세요.
BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentCreateRequestDto.java (1)

16-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

문자열 필드의 최대 길이를 검증하세요.

title, contactName, contactPhone, contactEmail은 데이터베이스 컬럼 길이가 각각 200, 50, 30, 255로 제한되어 있습니다. DTO에 @Size가 없으므로 초과 입력이 저장 단계에서 예외를 발생시키고 GlobalExceptionHandler에서 500 응답으로 처리될 수 있습니다. 컬럼 길이에 맞는 @Size(max = ...)를 생성 및 수정 DTO에 추가하세요.

🤖 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/recruitment/dto/BoothRecruitmentCreateRequestDto.java`
around lines 16 - 44, BoothRecruitmentCreateRequestDto의 title, contactName,
contactPhone, contactEmail 필드에 데이터베이스 컬럼 제한과 일치하는 `@Size`(max = 200), `@Size`(max =
50), `@Size`(max = 30), `@Size`(max = 255)를 추가하세요. 동일한 문자열 필드를 사용하는 수정 DTO에도 각각의 최대
길이 검증을 적용하고, 기존 `@NotBlank` 및 `@Email` 검증은 유지하세요.

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/recruitment/dto/BoothRecruitmentUpdateRequestDto.java`:
- Around line 1-45: BoothRecruitmentUpdateRequestDto의 필드와 검증 애노테이션이
BoothRecruitmentCreateRequestDto와 중복되므로 공통 부모 클래스 또는 공용 DTO로 추출하세요. 두 요청 DTO가 해당
공통 타입을 재사용하도록 변경하고, 기존 필드와 검증 동작은 그대로 유지하세요.

In
`@BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`:
- Around line 32-44: BoothRecruitment 상태 변경에 낙관적 락을 적용해 관리자 변경 사항이 스케줄러에 의해
덮어쓰이지 않도록 수정하세요. BoothRecruitment에 `@Version` 필드와 데이터베이스 마이그레이션을 추가하고,
openRecruitmentsThatHaveStarted 및 closeRecruitmentsThatHaveEnded에서
OptimisticLockException이 발생한 항목은 건너뛰어 이후 스케줄 실행에서 재시도되도록 처리하세요.

In
`@BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java`:
- Around line 70-96: Update BoothRecruitmentService.update to allow recruitment
detail changes only when the current recruitment status is BEFORE_OPEN or OPEN;
reject CLOSED and COMPLETED statuses before validating or applying the update,
while preserving the existing authorization and update flow for allowed states.
- Around line 44-46: Update the save flow in BoothRecruitmentService after
existsByEventId to flush immediately and catch DataIntegrityViolationException
only when it represents the event_id unique-constraint violation, converting
that case to BusinessException with GlobalErrorCode.RECRUITMENT_ALREADY_EXISTS;
preserve propagation of unrelated integrity errors.

In
`@BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java`:
- Around line 157-171: Map the database unique-constraint exception raised
during booth recruitment creation to RECRUITMENT_ALREADY_EXISTS
(RECRUITMENT_400_002) in GlobalExceptionHandler, including violations caused by
concurrent requests that pass existsByEventId before saving. Add a concurrent
creation test in BoothRecruitmentControllerTest asserting the losing request
receives bad request with the expected error code.

In `@FE/src/api/recruitmentApi.js`:
- Around line 43-55: Update closeRecruitment and completeRecruitment to await
apiRequest without accessing or returning response.data, so successful 204 No
Content responses with null parsed bodies are handled safely.

In `@FE/src/pages/RecruitmentCheck.jsx`:
- Around line 37-45: Update toDatetimeLocal to format ISO values in local time
rather than slicing UTC output from toISOString, so datetime-local round trips
preserve the original time. Also validate datetimeLocalValue in toIsoOffset
before calling toISOString and handle incomplete or invalid input without
allowing a RangeError, using the existing action validation/error flow.

---

Nitpick comments:
In
`@BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentCreateRequestDto.java`:
- Around line 16-44: BoothRecruitmentCreateRequestDto의 title, contactName,
contactPhone, contactEmail 필드에 데이터베이스 컬럼 제한과 일치하는 `@Size`(max = 200), `@Size`(max =
50), `@Size`(max = 30), `@Size`(max = 255)를 추가하세요. 동일한 문자열 필드를 사용하는 수정 DTO에도 각각의 최대
길이 검증을 적용하고, 기존 `@NotBlank` 및 `@Email` 검증은 유지하세요.

In
`@BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentUpdateRequestDto.java`:
- Around line 16-44: BoothRecruitmentUpdateRequestDto의 title, participantTarget,
contactName, contactPhone, notice 필드에 Create DTO와 동일한 `@Size` 길이 제약을 추가하세요. 기존 검증
어노테이션은 유지하고 각 필드에 정의된 공통 길이 기준을 재사용하세요.

In
`@BE/src/main/java/com/min/edu/recruitment/repository/BoothRecruitmentRepository.java`:
- Around line 21-29: 스케줄러 조회 메서드인
findAllByStatusAndRecruitmentStartAtLessThanEqual과
findAllByStatusAndRecruitmentEndAtLessThanEqual에 맞춰 마이그레이션에 각각 status와
recruitment_start_at, status와 recruitment_end_at 조합의 복합 인덱스를 추가하세요.

In
`@BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`:
- Around line 33-36: 대량 데이터 처리 시 전체 엔티티를 조회해 반복 변경하는
BoothRecruitmentStatusScheduler의 상태 갱신 흐름을 벌크 업데이트 방식으로 전환하세요.
BoothRecruitmentRepository에 조건에 맞는 BEFORE_OPEN 모집 공고를 OPEN으로 변경하는 `@Modifying` 쿼리를
추가하고, 스케줄러에서 해당 메서드를 호출해 엔티티 목록 조회와 개별 changeStatus 반복을 제거하세요.
- Around line 23-30: Update BoothRecruitmentStatusScheduler.transitionStatuses
to add explicit failure observability around openRecruitmentsThatHaveStarted and
closeRecruitmentsThatHaveEnded, logging each failed transition with contextual
details and ensuring the failure is surfaced for the scheduler’s existing retry
behavior. Preserve the current transactional semantics unless separate handling
is required to let the later phase run after an independent failure.
- Around line 15-30: Review the deployment model for
BoothRecruitmentStatusScheduler and ensure transitionStatuses is protected by a
distributed scheduler lock when multiple application instances can run
concurrently, using the project’s established ShedLock or equivalent
configuration. If deployment is guaranteed to remain single-instance, document
or preserve the current scheduling behavior instead of adding a lock.

In
`@BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java`:
- Around line 136-171: BoothRecruitmentControllerTest에 모집 기간 역전 검증 테스트를 추가하세요.
담당자 권한으로 생성 요청을 보내되 recruitmentEndAt이 recruitmentStartAt보다 이전이 되도록 요청 데이터를 구성하고,
응답 상태가 400이며 오류 코드가 RECRUITMENT_400_001인지 검증하세요. 기존 담당자 인증과 요청 생성 흐름은
같은_행사에_중복_생성하면_실패한다 테스트 패턴을 따르세요.

In `@FE/src/api/recruitmentApi.js`:
- Around line 3-12: Update listPublicRecruitments and getPublicRecruitment to
apply encodeURIComponent to the status query value and recruitmentId path value
before constructing the API URLs, preserving the existing behavior when status
is absent.

In `@FE/src/main.jsx`:
- Line 24: Update the route registration containing RecruitmentCheck so
/recruitment-check is only registered when import.meta.env.DEV is true,
preventing the internal API verification page from being exposed in production
while preserving its development-only availability.

In `@FE/src/pages/RecruitmentCheck.jsx`:
- Around line 131-148: runAction과 연결된 Recruitment 액션에 요청 진행 상태를 추가해 중복 실행을
방지하세요. 요청 시작 시 진행 플래그를 설정하고 finally에서 반드시 해제한 뒤, 생성·수정·조기 마감·완료 버튼이 해당 플래그로
disabled되도록 연결하세요.
🪄 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: 0fe1a6ee-0492-4ce2-be2a-e7c2f4dd1df3

📥 Commits

Reviewing files that changed from the base of the PR and between 8d85ef5 and 6a254c8.

📒 Files selected for processing (20)
  • .claude/launch.json
  • BE/src/main/java/com/min/edu/BeApplication.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java
  • BE/src/main/java/com/min/edu/common/config/SecurityConfig.java
  • BE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.java
  • BE/src/main/java/com/min/edu/event/repository/EventMemberRepository.java
  • BE/src/main/java/com/min/edu/event/repository/EventRepository.java
  • BE/src/main/java/com/min/edu/recruitment/controller/BoothRecruitmentController.java
  • BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentCreateRequestDto.java
  • BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentResponseDto.java
  • BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentUpdateRequestDto.java
  • BE/src/main/java/com/min/edu/recruitment/repository/BoothRecruitmentRepository.java
  • BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java
  • BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java
  • BE/src/test/java/com/min/edu/TestcontainersConfiguration.java
  • BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java
  • BE/src/test/resources/application.properties
  • FE/src/api/recruitmentApi.js
  • FE/src/main.jsx
  • FE/src/pages/RecruitmentCheck.jsx

Comment on lines +1 to +45
package com.min.edu.recruitment.dto;

import java.time.OffsetDateTime;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class BoothRecruitmentUpdateRequestDto {

@NotBlank
private String title;

@NotNull
private OffsetDateTime recruitmentStartAt;

@NotNull
private OffsetDateTime recruitmentEndAt;

@NotBlank
private String participantTarget;

private String qualification;

private String selectionMethod;

private OffsetDateTime expectedDecisionAt;

@NotBlank
private String contactName;

@NotBlank
@Email
private String contactEmail;

@NotBlank
private String contactPhone;

private String notice;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Create DTO와 필드가 완전히 같습니다.

BoothRecruitmentUpdateRequestDto가 BoothRecruitmentCreateRequestDto와 필드, 검증 애노테이션까지 전부 동일해요. 나중에 필드를 추가하거나 검증 규칙을 바꿀 때 두 파일을 항상 같이 고쳐야 해서 하나를 놓치기 쉽습니다. 공통 필드를 담는 부모 클래스나 공용 DTO로 묶는 걸 추천합니다.

♻️ 제안 방향
+public abstract class BoothRecruitmentRequestBase {
+    `@NotBlank`
+    private String title;
+    `@NotNull`
+    private OffsetDateTime recruitmentStartAt;
+    `@NotNull`
+    private OffsetDateTime recruitmentEndAt;
+    // ... 공통 필드
+}

-public class BoothRecruitmentCreateRequestDto {
+public class BoothRecruitmentCreateRequestDto extends BoothRecruitmentRequestBase {
     ...
 }

-public class BoothRecruitmentUpdateRequestDto {
+public class BoothRecruitmentUpdateRequestDto extends BoothRecruitmentRequestBase {
     ...
 }
🤖 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/recruitment/dto/BoothRecruitmentUpdateRequestDto.java`
around lines 1 - 45, BoothRecruitmentUpdateRequestDto의 필드와 검증 애노테이션이
BoothRecruitmentCreateRequestDto와 중복되므로 공통 부모 클래스 또는 공용 DTO로 추출하세요. 두 요청 DTO가 해당
공통 타입을 재사용하도록 변경하고, 기존 필드와 검증 동작은 그대로 유지하세요.

Comment on lines +32 to +44
private void openRecruitmentsThatHaveStarted(OffsetDateTime now) {
for (BoothRecruitment recruitment : boothRecruitmentRepository
.findAllByStatusAndRecruitmentStartAtLessThanEqual(BoothRecruitmentStatus.BEFORE_OPEN, now)) {
recruitment.changeStatus(BoothRecruitmentStatus.OPEN, now);
}
}

private void closeRecruitmentsThatHaveEnded(OffsetDateTime now) {
for (BoothRecruitment recruitment : boothRecruitmentRepository
.findAllByStatusAndRecruitmentEndAtLessThanEqual(BoothRecruitmentStatus.OPEN, now)) {
recruitment.changeStatus(BoothRecruitmentStatus.CLOSED, now);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: BoothRecruitment 엔티티의 낙관적 락(`@Version`) 존재 여부와
# 관리자 API의 상태 변경 트랜잭션 경계를 확인합니다.

fd BoothRecruitment.java -t f --exec cat -n {}

echo "---- controller status 변경 메서드 확인 ----"
fd BoothRecruitmentController.java -t f --exec rg -n -B3 -A15 'close|complete|Transactional' {}

Repository: half-fifty/eventoday

Length of output: 6881


🏁 Script executed:

#!/bin/bash
set -eu

echo "---- scheduler ----"
fd 'BoothRecruitmentStatusScheduler.java' -t f --exec cat -n {}

echo "---- service status 변경 및 트랜잭션 ----"
fd -i 'BoothRecruitmentService.java' -t f --exec rg -n -B8 -A30 'complete|close|`@Transactional`|findBy' {}

echo "---- repository 조회 정의 ----"
fd -i 'BoothRecruitmentRepository.java' -t f --exec cat -n {}

echo "---- 상태 변경 호출부와 트랜잭션 선언 ----"
rg -n -S -g '*.java' 'changeStatus|\.complete\(|findAllByStatusAndRecruitment(Start|End)AtLessThanEqual|`@Transactional`' BE/src/main/java

Repository: half-fifty/eventoday

Length of output: 13872


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

entity = Path("BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java").read_text()
scheduler = Path(
    "BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java"
).read_text()
service = Path(
    "BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java"
).read_text()

assert not re.search(r"`@Version`\b", entity), "BoothRecruitment에 `@Version이` 존재합니다."
assert re.search(
    r"`@Scheduled`\(fixedRate = FIXED_RATE_MILLIS\)\s+@Transactional\s+public void transitionStatuses",
    scheduler,
)
assert len(re.findall(r"`@Transactional`\s+public BoothRecruitmentResponseDto (?:close|complete)", service)) == 2

# 실제 상태 전이 메서드의 대체 동작을 단순화한 interleaving 검증
scheduler_snapshot = "BEFORE_OPEN"
admin_close = "CLOSED"
scheduler_write = "OPEN"
assert scheduler_snapshot == "BEFORE_OPEN"
assert admin_close == "CLOSED"
assert scheduler_write != admin_close
print("close interleaving: BEFORE_OPEN -> admin CLOSED -> scheduler OPEN (관리자 변경 덮어쓰기)")

scheduler_snapshot = "OPEN"
admin_close = "CLOSED"
admin_complete = "COMPLETED"
scheduler_write = "CLOSED"
assert scheduler_snapshot == "OPEN"
assert admin_close == "CLOSED"
assert admin_complete == "COMPLETED"
assert scheduler_write != admin_complete
print("complete interleaving: OPEN -> admin CLOSED -> admin COMPLETED -> scheduler CLOSED (완료 상태 덮어쓰기)")
PY

Repository: half-fifty/eventoday

Length of output: 334


BoothRecruitment 상태 전이에 낙관적 락을 적용해 주세요.

BoothRecruitment에 @Version이 없고, 스케줄러와 close·complete API가 별도 트랜잭션으로 실행됩니다. 스케줄러가 이전 상태를 읽은 뒤 관리자가 CLOSED 또는 COMPLETED로 변경하면, 스케줄러가 OPEN 또는 CLOSED를 다시 저장하여 관리자 상태를 덮어쓸 수 있습니다. @Version 필드와 마이그레이션을 추가하고, OptimisticLockException 발생 시 해당 항목을 건너뛰고 다음 실행에서 재시도하도록 처리해 주세요.

🤖 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/recruitment/scheduler/BoothRecruitmentStatusScheduler.java`
around lines 32 - 44, BoothRecruitment 상태 변경에 낙관적 락을 적용해 관리자 변경 사항이 스케줄러에 의해
덮어쓰이지 않도록 수정하세요. BoothRecruitment에 `@Version` 필드와 데이터베이스 마이그레이션을 추가하고,
openRecruitmentsThatHaveStarted 및 closeRecruitmentsThatHaveEnded에서
OptimisticLockException이 발생한 항목은 건너뛰어 이후 스케줄 실행에서 재시도되도록 처리하세요.

Comment on lines +44 to +46
if (boothRecruitmentRepository.existsByEventId(eventId)) {
throw new BusinessException(GlobalErrorCode.RECRUITMENT_ALREADY_EXISTS);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target service ---'
sed -n '1,130p' BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java
printf '%s\n' '--- related repository/entity/error/exception usages ---'
rg -n -C 3 'class BoothRecruitment|eventId|existsByEventId|DataIntegrityViolationException|RECRUITMENT_ALREADY_EXISTS|BusinessExceptionHandler|`@ExceptionHandler`' BE/src/main/java BE/src/test 2>/dev/null | head -n 500
printf '%s\n' '--- transaction annotations and persistence configuration ---'
rg -n -C 2 '`@Transactional`|spring.jpa|ddl-auto|unique|event_id' BE/src/main BE/src/test 2>/dev/null | head -n 400

Repository: half-fifty/eventoday

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- repository ---'
find BE/src/main -type f -name 'BoothRecruitmentRepository.java' -print -exec sed -n '1,120p' {} \;
printf '%s\n' '--- entity mapping ---'
sed -n '1,125p' BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java
printf '%s\n' '--- exception types and handler ---'
sed -n '1,180p' BE/src/main/java/com/min/edu/common/exception/GlobalErrorCode.java
sed -n '1,110p' BE/src/main/java/com/min/edu/common/exception/GlobalExceptionHandler.java
sed -n '1,100p' BE/src/main/java/com/min/edu/common/exception/BusinessException.java
printf '%s\n' '--- schema/migrations containing booth recruitment ---'
rg -l -i 'booth.?recruitment|booth_recruitment|event_id.*unique|unique.*event_id' BE/src/main BE/src/test 2>/dev/null | while read -r f; do echo "### $f"; rg -n -C 4 -i 'booth.?recruitment|booth_recruitment|event_id.*unique|unique.*event_id' "$f"; done
printf '%s\n' '--- persistence dependencies/config ---'
find BE -maxdepth 3 -type f \( -name 'pom.xml' -o -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'application*.yml' -o -name 'application*.yaml' -o -name 'application*.properties' \) -print
rg -n -C 3 'spring-boot-starter-data-jpa|hibernate|ddl-auto|jdbc|datasource' BE/pom.xml BE/build.gradle BE/build.gradle.kts BE/src/main/resources 2>/dev/null

Repository: half-fifty/eventoday

Length of output: 45421


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
service = Path("BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java").read_text()
entity = Path("BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java").read_text()
schema = Path("BE/src/main/resources/db/migration/V1__init_schema.sql").read_text()
handler = Path("BE/src/main/java/com/min/edu/common/exception/GlobalExceptionHandler.java").read_text()
dto = Path("BE/src/main/java/com/min/edu/recruitment/dto/BoothRecruitmentCreateRequestDto.java").read_text()

checks = {
    "create_is_transactional": "`@Transactional`" in service and "public BoothRecruitmentResponseDto create" in service,
    "precheck_exists_by_event": "existsByEventId(eventId)" in service,
    "save_is_not_explicitly_flushed": "saveAndFlush" not in service and "flush()" not in service,
    "identity_generation": "GenerationType.IDENTITY" in entity,
    "entity_event_id_unique": "`@Column`(name = \"event_id\", nullable = false, unique = true)" in entity,
    "database_event_id_unique": "event_id BIGINT NOT NULL UNIQUE" in schema,
    "generic_handler_returns_500": "`@ExceptionHandler`(Exception.class)" in handler
        and "GlobalErrorCode.INTERNAL_SERVER_ERROR" in handler,
    "data_integrity_handler_present": "DataIntegrityViolationException" in service or "DataIntegrityViolationException" in handler,
}
for name, value in checks.items():
    print(f"{name}={value}")

print("\n--- create method relevant lines ---")
for i, line in enumerate(service.splitlines(), 1):
    if 33 <= i <= 68:
        print(f"{i}: {line}")
print("\n--- create DTO constraints ---")
for i, line in enumerate(dto.splitlines(), 1):
    print(f"{i}: {line}")
PY
printf '%s\n' '--- concurrency-related recruitment tests ---'
rg -n -i 'concurr|parallel|Executor|CountDownLatch|RECRUITMENT_ALREADY_EXISTS|DataIntegrityViolationException' BE/src/test BE/src/main 2>/dev/null || true

Repository: half-fifty/eventoday

Length of output: 3386


동시 생성 시 중복 오류를 비즈니스 예외로 변환하세요.

existsByEventId와 save 사이에 경쟁 구간이 있습니다. 동시 요청은 모두 검사를 통과할 수 있습니다. 이후 event_id unique 제약 위반이 DataIntegrityViolationException으로 발생하면 일반 예외 처리로 500 응답이 반환됩니다. 실제 저장 시점의 해당 예외를 RECRUITMENT_ALREADY_EXISTS로 변환하세요. saveAndFlush 등을 사용하고, 변환 범위는 event_id unique 제약 위반으로 제한하세요.

🤖 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/recruitment/service/BoothRecruitmentService.java`
around lines 44 - 46, Update the save flow in BoothRecruitmentService after
existsByEventId to flush immediately and catch DataIntegrityViolationException
only when it represents the event_id unique-constraint violation, converting
that case to BusinessException with GlobalErrorCode.RECRUITMENT_ALREADY_EXISTS;
preserve propagation of unrelated integrity errors.

Source: Path instructions

Comment on lines +70 to +96
@Transactional
public BoothRecruitmentResponseDto update(
Long eventId,
BoothRecruitmentUpdateRequestDto request,
AuthenticatedMemberDto member) {
BoothRecruitment recruitment = getByEventIdOrThrow(eventId);

requireEventManager(eventId, member);
validatePeriod(request.getRecruitmentStartAt(), request.getRecruitmentEndAt());

recruitment.updateDetails(
request.getTitle(),
request.getRecruitmentStartAt(),
request.getRecruitmentEndAt(),
request.getParticipantTarget(),
request.getQualification(),
request.getSelectionMethod(),
request.getExpectedDecisionAt(),
request.getContactName(),
request.getContactEmail(),
request.getContactPhone(),
request.getNotice(),
OffsetDateTime.now()
);

return BoothRecruitmentResponseDto.from(recruitment);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- service outline ---'
ast-grep outline BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java --view expanded

printf '%s\n' '--- service implementation ---'
sed -n '1,180p' BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java

printf '%s\n' '--- related status and transition references ---'
rg -n -C 3 'BoothRecruitmentStatus|closeRecruitmentsThatHaveEnded|openRecruitmentsThatHaveStarted|RECRUITMENT_STATUS_TRANSITION_INVALID|class BoothRecruitment' BE/src/main/java BE/src/test 2>/dev/null || true

Repository: half-fifty/eventoday

Length of output: 34463


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- service helpers ---'
sed -n '175,225p' BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java

printf '%s\n' '--- recruitment domain ---'
sed -n '1,180p' BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java

printf '%s\n' '--- status enum and scheduler ---'
cat -n BE/src/main/java/com/min/edu/booth/domain/BoothRecruitmentStatus.java
cat -n BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java

printf '%s\n' '--- update-related tests and documentation ---'
rg -n -C 5 'update|수정|CLOSED|COMPLETED|BEFORE_OPEN|OPEN' \
  BE/src/test/java/com/min/edu/recruitment \
  README.md BE/docs docs 2>/dev/null || true

Repository: half-fifty/eventoday

Length of output: 39224


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

service = Path("BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java").read_text()
domain = Path("BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java").read_text()
scheduler = Path("BE/src/main/java/com/min/edu/recruitment/scheduler/BoothRecruitmentStatusScheduler.java").read_text()

update = re.search(
    r'public BoothRecruitmentResponseDto update\(.*?\n    \}\n\n    `@Transactional`',
    service,
    re.S,
).group(0)
update_body = update.split("{", 1)[1]
scheduler_statuses = re.findall(
    r'findAllByStatusAndRecruitment(?:Start|End)AtLessThanEqual\('
    r'\s*BoothRecruitmentStatus\.([A-Z_]+)',
    scheduler,
)

print("update_checks_status:",
      bool(re.search(r'getStatus\(\)|BoothRecruitmentStatus\.', update_body)))
print("update_calls_updateDetails:",
      "recruitment.updateDetails(" in update_body)
print("update_preserves_status:",
      "this.status" not in domain[domain.index("public void updateDetails"):domain.index("public void changeStatus")])
print("scheduler_transition_query_statuses:", scheduler_statuses)
print("scheduler_targets_closed_or_completed:",
      any(status in {"CLOSED", "COMPLETED"} for status in scheduler_statuses))
PY

Repository: half-fifty/eventoday

Length of output: 355


모집 공고 수정 시 상태를 확인하세요.

현재 update()는 CLOSED와 COMPLETED 상태에서도 기간을 변경하지만 상태는 유지합니다. CLOSED 상태의 recruitmentEndAt을 미래로 변경하면 스케줄러가 OPEN 상태만 처리하므로 상태와 종료일이 불일치합니다. BEFORE_OPEN 또는 OPEN 상태에서만 수정하도록 상태 검사를 추가하세요.

🤖 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/recruitment/service/BoothRecruitmentService.java`
around lines 70 - 96, Update BoothRecruitmentService.update to allow recruitment
detail changes only when the current recruitment status is BEFORE_OPEN or OPEN;
reject CLOSED and COMPLETED statuses before validating or applying the update,
while preserving the existing authorization and update flow for allowed states.

Source: Path instructions

Comment on lines +157 to +171
@Test
void 같은_행사에_중복_생성하면_실패한다() throws Exception {
mockMvc.perform(post("/events/{eventId}/booth-recruitment", eventId)
.header("Authorization", "Bearer " + eventManagerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(createRequestJson()))
.andExpect(status().isOk());

mockMvc.perform(post("/events/{eventId}/booth-recruitment", eventId)
.header("Authorization", "Bearer " + eventManagerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(createRequestJson()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("RECRUITMENT_400_002"));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# booth_recruitment 테이블/엔티티에 eventId 유니크 제약이 있는지 확인
fd -e sql | xargs -I{} rg -n -i 'booth_recruitment' -A5 {} 2>/dev/null
fd -i 'BoothRecruitment.java' --full-path BE/src/main/java/com/min/edu/booth/domain \
  | xargs -I{} rg -n -B2 -A5 '`@Table`|uniqueConstraints|eventId' {}

Repository: half-fifty/eventoday

Length of output: 2902


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- service and exception handling ---'
rg -n -C 8 'existsByEventId|save\(|RECRUITMENT_ALREADY_EXISTS|DataIntegrityViolationException|ConstraintViolationException|create\(' \
  BE/src/main/java BE/src/test/java 2>/dev/null | head -n 260
printf '%s\n' '--- repository and schema constraint context ---'
rg -n -C 4 'interface .*BoothRecruitment|findByEventId|existsByEventId|event_id BIGINT NOT NULL UNIQUE|unique = true' \
  BE/src/main/java BE/src/main/resources BE/src/test 2>/dev/null | head -n 220

Repository: half-fifty/eventoday

Length of output: 45418


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- BoothRecruitmentService.create ---'
sed -n '1,75p' BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java
printf '%s\n' '--- exception handlers ---'
rg -n -C 10 '`@ExceptionHandler`|DataIntegrityViolationException|ConstraintViolationException|BusinessException|GlobalException' \
  BE/src/main/java 2>/dev/null | head -n 320
printf '%s\n' '--- recruitment controller tests around duplicate creation ---'
sed -n '125,185p' BE/src/test/java/com/min/edu/recruitment/controller/BoothRecruitmentControllerTest.java

Repository: half-fifty/eventoday

Length of output: 39533


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exception-related files and handlers ---'
fd -i 'exception|handler|advice' BE/src/main/java
rg -n -C 12 'ControllerAdvice|RestControllerAdvice|ExceptionHandler|ErrorResponse|GlobalErrorCode' \
  BE/src/main/java/com/min/edu 2>/dev/null | head -n 300

Repository: half-fifty/eventoday

Length of output: 32872


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,240p' BE/src/main/java/com/min/edu/common/exception/GlobalExceptionHandler.java

Repository: half-fifty/eventoday

Length of output: 2264


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

entity = Path("BE/src/main/java/com/min/edu/booth/domain/BoothRecruitment.java").read_text()
migration = Path("BE/src/main/resources/db/migration/V1__init_schema.sql").read_text()
service = Path("BE/src/main/java/com/min/edu/recruitment/service/BoothRecruitmentService.java").read_text()
handler = Path("BE/src/main/java/com/min/edu/common/exception/GlobalExceptionHandler.java").read_text()

checks = {
    "entity event_id unique": '`@Column`(name = "event_id", nullable = false, unique = true)' in entity,
    "migration event_id unique": "event_id BIGINT NOT NULL UNIQUE" in migration,
    "check then act": "existsByEventId(eventId)" in service and "boothRecruitmentRepository.save(recruitment)" in service,
    "duplicate exception mapping": "DataIntegrityViolationException" in handler,
}
for name, passed in checks.items():
    print(f"{name}: {'present' if passed else 'absent'}")
PY

Repository: half-fifty/eventoday

Length of output: 285


동시 생성의 유니크 제약 위반을 RECRUITMENT_400_002로 매핑해주세요.

event_id에는 엔티티와 마이그레이션 모두 UNIQUE 제약이 있어 중복 레코드는 방지됩니다. 그러나 동시 요청에서 existsByEventId를 통과한 요청이 저장 시 제약 위반을 일으킬 수 있습니다. 현재 GlobalExceptionHandler는 이를 COMMON_500으로 반환하므로, 해당 예외를 RECRUITMENT_ALREADY_EXISTS로 변환하고 동시 요청 테스트를 추가해주세요.

🤖 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 157 - 171, Map the database unique-constraint exception raised
during booth recruitment creation to RECRUITMENT_ALREADY_EXISTS
(RECRUITMENT_400_002) in GlobalExceptionHandler, including violations caused by
concurrent requests that pass existsByEventId before saving. Add a concurrent
creation test in BoothRecruitmentControllerTest asserting the losing request
receives bad request with the expected error code.

Source: Path instructions

Comment on lines +43 to +55
const closeRecruitment = async (eventId) => {
const response = await apiRequest(`/events/${eventId}/booth-recruitment/closure`, {
method: "POST",
});
return response.data;
};

const completeRecruitment = async (eventId) => {
const response = await apiRequest(`/events/${eventId}/booth-recruitment/completion`, {
method: "POST",
});
return response.data;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# parseResponse 구현과 빈 본문 처리 확인
fd -t f 'apiClient.js' FE/src | xargs -r rg -n -C 15 'parseResponse'

Repository: half-fifty/eventoday

Length of output: 1358


🏁 Script executed:

#!/bin/bash
set -e
api_file="$(fd -t f 'apiClient.js' FE/src | head -n 1)"
printf '%s\n' "== apiClient.js =="
cat -n "$api_file" | sed -n '55,125p'
printf '%s\n' "== recruitmentApi.js =="
cat -n FE/src/api/recruitmentApi.js | sed -n '1,80p'
printf '%s\n' "== response.data usage and apiRequest calls =="
rg -n -C 2 'apiRequest|closeRecruitment|completeRecruitment|response\.data' FE/src

Repository: half-fifty/eventoday

Length of output: 10178


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
for path in Path("FE/src").rglob("*.js"):
    text = path.read_text()
    if "parseResponse" in text or "const apiRequest" in text or "function apiRequest" in text:
        print(f"== {path} ==")
        for i, line in enumerate(text.splitlines(), 1):
            if any(token in line for token in ("parseResponse", "apiRequest", "response.data")):
                start, end = max(1, i - 3), i + 5
                lines = text.splitlines()
                for n in range(start, min(end, len(lines)) + 1):
                    print(f"{n}: {lines[n-1]}")
                print()
PY

Repository: half-fifty/eventoday

Length of output: 798


본문이 없는 성공 응답을 안전하게 처리해 주세요.

parseResponse는 JSON 본문이 없으면 null을 반환합니다. 따라서 두 API가 204 No Content를 반환하면 response.data 접근에서 런타임 오류가 발생합니다. 반환값을 사용하지 않으므로 await apiRequest(...)만 호출하도록 수정해 주세요.

🤖 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/recruitmentApi.js` around lines 43 - 55, Update closeRecruitment
and completeRecruitment to await apiRequest without accessing or returning
response.data, so successful 204 No Content responses with null parsed bodies
are handled safely.

Comment on lines +37 to +45
const toIsoOffset = (datetimeLocalValue) => {
if (!datetimeLocalValue) return "";
return new Date(datetimeLocalValue).toISOString();
};

const toDatetimeLocal = (isoValue) => {
if (!isoValue) return "";
return new Date(isoValue).toISOString().slice(0, 16);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

toDatetimeLocal이 UTC 문자열을 반환해서 시간이 밀립니다.

toDatetimeLocal은 toISOString()의 앞 16자를 자릅니다. 이 값은 UTC 기준입니다. 반면 <input type="datetime-local">은 값을 로컬 시간으로 해석하고, toIsoOffset은 new Date()로 다시 로컬 시간으로 파싱합니다. 따라서 관리용 조회 후 수정 버튼을 누르면 모집 시작·종료 시각이 시간대 오프셋만큼(KST 기준 9시간) 과거로 저장됩니다. 왕복할 때마다 계속 밀립니다.

추가로 toIsoOffset은 사용자가 날짜만 부분 입력하면 Invalid Date가 되어 toISOString()이 RangeError를 던집니다. 이 예외는 runAction의 try 블록 밖(buildPayload 호출 시점)이 아니라 안쪽에서 발생하긴 하지만, 화면에는 "요청에 실패했습니다."만 뜨고 원인을 알 수 없습니다. 유효성 검사를 먼저 하는 편이 좋습니다.

🐛 제안 수정
 const toIsoOffset = (datetimeLocalValue) => {
   if (!datetimeLocalValue) return "";
-  return new Date(datetimeLocalValue).toISOString();
+  const parsed = new Date(datetimeLocalValue);
+  if (Number.isNaN(parsed.getTime())) return "";
+  return parsed.toISOString();
 };
 
 const toDatetimeLocal = (isoValue) => {
   if (!isoValue) return "";
-  return new Date(isoValue).toISOString().slice(0, 16);
+  const parsed = new Date(isoValue);
+  if (Number.isNaN(parsed.getTime())) return "";
+  const localMs = parsed.getTime() - parsed.getTimezoneOffset() * 60000;
+  return new Date(localMs).toISOString().slice(0, 16);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toIsoOffset = (datetimeLocalValue) => {
if (!datetimeLocalValue) return "";
return new Date(datetimeLocalValue).toISOString();
};
const toDatetimeLocal = (isoValue) => {
if (!isoValue) return "";
return new Date(isoValue).toISOString().slice(0, 16);
};
const toIsoOffset = (datetimeLocalValue) => {
if (!datetimeLocalValue) return "";
const parsed = new Date(datetimeLocalValue);
if (Number.isNaN(parsed.getTime())) return "";
return parsed.toISOString();
};
const toDatetimeLocal = (isoValue) => {
if (!isoValue) return "";
const parsed = new Date(isoValue);
if (Number.isNaN(parsed.getTime())) return "";
const localMs = parsed.getTime() - parsed.getTimezoneOffset() * 60000;
return new Date(localMs).toISOString().slice(0, 16);
};
🤖 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/RecruitmentCheck.jsx` around lines 37 - 45, Update
toDatetimeLocal to format ISO values in local time rather than slicing UTC
output from toISOString, so datetime-local round trips preserve the original
time. Also validate datetimeLocalValue in toIsoOffset before calling toISOString
and handle incomplete or invalid input without allowing a RangeError, using the
existing action validation/error flow.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants