website: 모집 스위치를 켜기 전에, 지원폼 없이도 안전하게 여는 법
관리자 페이지에서 모집을 켜고 끄는 화면을 만드는 작업이었다. 기능 자체는 토글 버튼 하나 놓는 정도로 단순해 보였는데, 막상 손을 대보니 "지원폼이 아직 없다"는 조건 하나 때문에 신경 쓸 게 꽤 늘어났다.
원래 계획은 /api/admin/recruitment/status를 PATCH로 열고 닫는 엔드포인트를 만들고, 프론트에서 버튼 눌러 호출하는 정도였다. RecruitmentManagement.tsx를 만들면서 세션 갱신(refreshSession) → 상태 조회 → 렌더링 흐름을 잡고, 토글할 때는 window.confirm으로 한 번 확인받게 했다. 특히 모집을 켤 때는 구독자 수를 보여주면서 "구독자 N명에게 안내 메일이 발송됩니다"라고 명시했는데, 이건 실수로 버튼 눌렀다가 메일이 나가버리는 사고를 막기 위한 최소한의 안전장치였다.
문제는 이 프로젝트에서 지원폼(#152)이 아직 완성되지 않았다는 점이었다. 모집을 켰는데 방문자가 /recruit 페이지에 들어가면 지원할 방법이 없는 빈 페이지를 보게 되는 상황이 생길 수 있었다. 이슈 #154에서 이 부분을 어떻게 처리할지 논의가 있었고, 결론(B안)은 "지원폼이 준비되지 않은 운영 환경에서는 모집을 여는 행위 자체를 서버에서 막는다"는 것이었다. 그래서 application-form-ready라는 설정값을 두고, 이게 false인 환경(운영 기본값)에서 open 요청이 오면 409와 함께 RECRUITMENT_PRODUCTION_HOLD 에러 코드를 반환하도록 RecruitmentProductionHoldException을 추가했다.
여기서 하나 더 고민한 지점은 "닫기까지 막으면 안 된다"는 것이었다. 만약 실수로 모집이 열린 상태가 됐는데 이 스위치 때문에 닫지도 못하면 더 큰 사고가 된다. 그래서 open만 이 조건에 걸리게 하고 close는 무조건 동작하도록 분리했다. 이 부분은 RecruitmentManagementControllerProductionHoldTest에 두 개의 테스트로 명시해뒀다 — 하나는 application-form-ready=false일 때 open이 409를 반환하고 메일도 안 나가는 걸 확인하는 테스트, 다른 하나는 같은 조건에서도 close는 정상적으로 200을 반환하는 걸 확인하는 테스트다. 테스트 클래스 이름과 주석에 왜 이렇게 나눴는지(#154 결정) 적어뒀는데, 나중에 이 로직을 다시 볼 사람(아마도 나)이 "왜 여는 것만 막았지?"라고 헷갈리지 않게 하려는 목적이었다.
공개 쪽도 손볼 게 있었다. 랜딩의 Recruit 섹션과 /recruit 페이지가 각자 모집 상태를 어떻게 표시할지 정하는 로직을 따로 두면 나중에 어긋날 게 뻔해서, useRecruitmentStatus라는 훅으로 뽑아냈다. /api/recruitment/status를 호출해서 열림/닫힘만 받아오고, 구독자 수 같은 내부 정보는 이 공개 엔드포인트에 안 실리도록 PublicRecruitmentStatusResponse를 관리자용 응답과 분리했다. 이 훅에는 skip 옵션도 넣었는데, 관리자가 "켰을 때 미리보기" 버튼으로 /recruit?preview=1을 열어볼 때는 실제 상태 조회 없이 바로 모집중 화면을 보여주기 위한 용도다. 조회가 실패했을 때는 열림으로 잘못 표시되면 안 되니 기본값을 "평소(닫힘)" 쪽으로 fail-safe하게 잡았다.
/recruit 페이지 자체도 원래는 "준비 중이에요" 한 줄짜리 정적 페이지였는데, 이번에 useSearchParams를 쓰면서 클라이언트 컴포넌트로 바꾸고 Suspense로 감쌌다. Next.js에서 useSearchParams를 쓰는 컴포넌트는 정적 렌더링 시 Suspense 경계가 없으면 빌드 경고가 나거나 폴백 없이 깨지는 경우가 있어서, 로딩 상태를 보여줄 fallback을 같이 넣었다.
테스트는 관리자 기능(RecruitmentManagementServiceTest, production hold 테스트)과 공개 엔드포인트(RecruitmentControllerTest)를 나눠서 작성했다. 공개 쪽 테스트에서는 비인증으로 상태를 조회할 수 있는지, 그리고 subscriberCount가 응답에 절대 노출되지 않는지를 명시적으로 확인했다 — 이건 나중에 실수로 관리자 DTO를 재사용하는 방향으로 리팩터링할 때 사고를 막아줄 안전망이다.
작업하면서 든 생각은, "지원폼 미완성"이라는 임시 상태를 코드 레벨에서 명시적인 설정값과 예외로 표현해둔 게 나중에 지원폼이 완성됐을 때 되돌리기 편할 것 같다는 점이다. 설정 하나 뒤집으면 production hold가 풀리는 구조라, /recruit 페이지의 임시 안내 문구만 실제 폼으로 교체하면 될 것 같다. 코드 안에도 "#152 완성 후 교체 예정"이라고 주석을 남겨뒀으니 나중에 잊어버릴 일은 없을 듯하다.