기준 문서: 어드민 기능 명세 3.x, SEO 요구사항 4.x, 데이터 모델 5.x, MVP 범위 6.x 코드 스냅샷: 2026-05-12 (branch:
dev/admin) 범례: ✅ 완료 / 🔧 부분 구현 (UI만 또는 목업 연결) / ⬜ 미구현
apps/admin의 신규 페이지 / 리뉴얼 작업은 다음 HTML 목업을 단일 시각 레퍼런스로 사용한다.
- 파일:
~/Downloads/ddd-admin (1) (1).html(별도 보존; 외부 공유 금지) - 포함 페이지: 기수 / 사전 알림 / 지원자 / 프로젝트 / 블로그 + 각 영역 Drawer · Confirm Modal · Toast
- 개발 원칙
- 컬럼·필터·필드 구성은 HTML을 따르되, 데이터 형태·필드명은
@ddd/apiDTO에 정합되게 변환한다. - HTML에만 존재하지만 백엔드 DTO에 없는 필드(예: 프로젝트/블로그
status)는 노출하지 않는다. - HTML이 page-number 페이지네이션을 그리더라도 백엔드가 cursor 페이지네이션이면 "더 보기" / 무한스크롤로 대체한다.
- 토스트·확인 모달은 HeroUI v3 기본 컴포넌트로 구현한다 (별도 토스트 가이드는 docs/admin-toast.md 참조).
- 컬럼·필터·필드 구성은 HTML을 따르되, 데이터 형태·필드명은
- 세부 설계 문서: docs/superpowers/specs/2026-04-26-blog-projects-admin-design.md —
/projects,/blog-posts페이지의 11단계 구현 계획 포함.
| 영역 | 상태 | 핵심 갭 |
|---|---|---|
| 공통 인프라 (admin) | ✅ | 모든 도메인이 openapi-fetch 기반 api 싱글톤으로 통일 (commit 241ae4e). storage 보조 queries(listFiles·deleteFile·createSignedUrl·downloadFile) 는 후속 |
| 3.1 기수 관리 | ✅ | 목록/통계/등록/수정/상태변경/파트양식 저장 완료. 부분 실패(PartsSaveAfterCreateError) 시 edit 모드 자동 전환 — 브라우저 검증 미실시 |
| 3.2 사전 알림 | 🔧 | 일괄 발송·CSV·캠페인(PAUSED↔SCHEDULED 전환·편집) ✅, 개별 발송 액션 컬럼 부재 (BE 엔드포인트 없음) |
| 3.3 지원자 관리 | ✅ | 목록·필터·Drawer 상세·합격불합격 분기·면접일자 컬럼(슬롯 예약 join) 완료. 개인정보 동의 일자 표시 |
| 3.3.5 면접 슬롯 | ✅ | /interview-slots 신설 — 기수·파트 필터 + CRUD + Drawer + Dialog. INTERVIEW_SLOTS_NOT_READY → InterviewSlotsRequiredModal 로 페이지 navigate. 예약자 목록·예약 취소는 ReservationsDrawer + CancelReservationDialog 로 완료 |
| 3.4 프로젝트 DB | ✅ | 코드 완료 — 파일 업로드는 저장 시점 업로드로 전환 (BE PR #100) |
| 3.5 블로그 DB | ✅ | 코드 완료 (브라우저 회귀 테스트 미실시) |
| 3.6 FAQ | ✅ | MVP 제외 결정 (FE 하드코딩) |
| 5. 데이터 모델 타입 반영 | 🔧 | @ddd/api 생성 타입 도입 진행 중 — pages/semesters/types.d.ts 임시 타입 잔존 |
완료
- Vite + React 19 + TypeScript
- Tailwind CSS 4
- HeroUI v3 (shadcn/ui primitives 제거 완료)
- React Router Data Mode (
createBrowserRouter) —apps/admin/src/pages/index.tsx - TanStack Query (
QueryProvider) - MSW 목업 환경 —
apps/admin/src/mocks/ - ESLint + Prettier + Lefthook
- pages/shared 2단 디렉터리 구조 (페이지 콜로케이션, FSD 폐기)
AdminLayout(SideBar + MobileHeader + Outlet)
API 레이어 연동 현황
commit 241ae4e (
orval → openapi-typescript + openapi-fetch마이그레이션) 이후 orval 시절의 generated 함수(cohortGetAdminList등)는 제거됨. 현재packages/api/src/generated/api.ts는paths타입 정의 한 파일만 보관하며, 모든 도메인은import { api } from "../fetchClient"→api.get/post/patch/put/delete(...)호출 패턴으로 통일되어 있다.
- ✅ 전 도메인 (
application·blog·project·early-notification·discord·interview·cohort·auth·storage·notification-campaign·users) —fetchClientapi싱글톤 사용, 패턴 일관 - ✅ notification-campaign —
packages/api/src/notification-campaign/SDK + 어드민 UI(/early-notification페이지 안 캠페인 섹션: 목록·편집 Drawer·PAUSED↔SCHEDULED 토글) 연결 완료 - ⬜ storage 보조 queries —
listFiles·deleteFile·createSignedUrl·downloadFile미구현 (BE 엔드포인트 추가 후 진행)
완료 (인프라)
- ✅ Google OAuth 실제 연결 —
LoginPage가/api/v1/auth/google로 top-level navigation (same-origin), 백엔드가CLIENT_REDIRECT_URL로 되돌림 (httpOnly 쿠키) - ✅ 인증 보호 라우트 (Minimal) — 별도 loader 가드 없이
client.ts401 인터셉터 +main.tsxonUnauthorized콜백에 위임. 401 발생 시paths.login으로 자동 redirect - ✅ 사이드바 사용자 메뉴 드롭다운 + 로그아웃 흐름 —
shared/ui/AdminLayout/UserMenuDropdown.tsx+shared/hooks/useLogoutFlow.ts(@ddd/apiuseLogoutmutation → 토스트 →paths.loginredirect) - ✅ same-origin 배포 환경 코드 준비 —
client.tsbuildUrl()이window.location.origin자동 결합, Vite dev proxy (/api → localhost:3000),.github/workflows/deploy-admin.yml(SCP + atomic swap). 머지 전 GitHub Secrets 등록 + BE 측 Caddy/compose 부트스트랩 필요 — 단일 출처: docs/admin-deploy.md
비-목표 (별도 라운드)
- 회원가입 — 별도 흐름 없음 (Google OAuth 첫 로그인이 곧 가입)
- 로그인 사용자 컨텍스트 (me 표시) — 백엔드
GET /api/v1/users/me추가 합의됨 (별도 PR 에서 옵션 A 가드 도입 예정) - 회원 탈퇴 UI — 별도 라운드
- 권한(roles) 기반 접근 제어 — 별도 라운드
- ✅ 상태 Enum 표기 (모집예정 / 모집 중 / 활동 중 / 활동종료) — 테이블 컬럼 노출
- ⬜ 홈페이지 버튼 동기화 (사전 알림 신청 / 지원 신청 / 모집 종료) — 웹 측 책임
- ✅ 기수 목록 조회 —
useSemestersTableData(useCohorts+ 기수별 지원자/멤버 집계) 연동 - ✅ 상태별 필터 / 기수 검색 (클라이언트)
- ✅ 통계 카드 —
useSemestersTableData.summary로 동적 집계 (전체 기수 / 현재 상태 / 누적 지원자 / 누적 멤버) - ✅ 새 기수 등록 폼 —
SemesterRegisterDrawerRHF +useCreateOrUpdateCohortFlow(useCreateCohort/useUpdateCohort) 연동 - ✅ 새 기수 등록 버튼 —
SemestersPage.tsx:77-85onPress연결, Drawer 정상 오픈 - 🔧 프로세스 일정 등록/수정 — ProcessSection DateRangePicker/DatePicker RHF 연결, API 직렬화 브라우저 검증 미실시
- 🔧 커리큘럼 등록/수정 — CurriculumSection (9주차 고정) RHF 연결, 브라우저 검증 미실시
- ✅ 파트별 지원서 양식 관리 (PM/PD/Server/Web/iOS/Android Tabs) —
ApplicationFormSection에 label TextArea + required Switch + isOpen Switch (key Input 제거). 저장 직전serializeFormToPartsPayload가 빈 key 를slugify(label)결과로 자동 생성(한글 그대로 허용, 예:지원_동기). 저장된 question 은 label 이isReadOnly, 카드에저장됨: <key>caption 노출. DraweronSubmit게이트가validateFormParts로 빈 label·part 내부 중복 key 를 클라에서 차단 +toast.danger+ 위반 카드border-danger강조.useCreateOrUpdateCohortFlow가 create/update 후updateCohortParts호출, 부분 실패 시PartsSaveAfterCreateErrorthrow → 호출부가 edit 모드로 전환.cohort.applicationForm은 dead 필드로 제거 — 단일 source 는PUT /admin/cohorts/{id}/parts(정책) - ✅ 수동 상태 변경 버튼 ("모집중 전환") —
useTransitionCohortStatusFlow연동. RECRUITING 전환 시validateCohortPartsForRecruiting가드(isOpen 파트 0개 또는 양식 비어있는 파트가 있으면 mutation 호출 없이TransitionBlockedDialog노출 → "수정 화면 열기" 시 해당 cohort edit Drawer 자동 오픈). 위반 파트 자동 스크롤·강조는 후속 PR - ✅ 기수 수정 버튼 —
editTargetstate +isDrawerOpen, Drawermode="edit"분기 완성 - ✅
SemesterRegisterDrawerreact-hook-form + FormProvider 도입 - ⬜ 모집 종료일 경과 시 자동 "활동중" 전환 (백엔드/스케줄러 책임)
- ✅ 신청자 목록 조회 (이메일 / 기수 / 신청일 / 상태 / 발송 일시) —
useAdminEarlyNotifications연동 - ✅ 상태별 필터 (전체 / 대기 / 발송완료) — 클라이언트 predicate
- ✅ 이메일 검색 (클라이언트, 부분 일치)
- ✅ 통계 카드 — 동적 집계 (
EarlyNotificationStatsSection.tsx:22statsuseMemo) - ✅ 기수별 필터 —
useCohorts()매핑 + 최신 모집기수 자동 선택 (pickActiveCohortId) - ✅ 전체 일괄 발송 —
RemindersBulkSendDrawer.tsx:62useSendBulkEarlyNotificationmutation 연동 - ⬜ 개별 발송 버튼 —
RemindersTable.tsx:28-34헤더에 액션 컬럼 자체가 없음 (HTML 목업에는 있음) - ✅ 이메일 목록 CSV 다운로드 —
useDownloadEarlyNotificationsCsv훅 +earlyNotificationQueries팩토리 경유.EarlyNotificationToolbar버튼 연동 - ⬜ 개별 발송 — 백엔드 generated에 단건 발송 엔드포인트 없음. 백엔드 추가 후 구현 가능
- ✅ 캠페인(예약 발송) 관리 —
NotificationCampaignSection(목록·상태 배지·편집 Drawer·PAUSED↔SCHEDULED 토글). 기수 생성 시 자동 생성된 캠페인을 운영자가 본문·시각 수정 후 SCHEDULED 로 풀면 백엔드 스케줄러가 자동 발송 - ⬜ 기수 상태 "모집중" 전환 시 즉시 자동 발송 (Phase 2 — 현재는 캠페인 scheduledAt 기반)
- 🔧 상태 값 일부 노출 — 명세 전체 enum(서류대기 / 서류합격 / 서류불합격 / 면접합격 / 최종합격 / 최종불합격 / 활동중 / 활동완료 / 활동중단) 정합성 재검수 필요
- ✅ 목록 조회 (이름 / 연락처 / 파트 / 기수 / 지원일 / 상태) —
useAdminApplications연동 - ✅ 상태별 필터, 이름·연락처 검색 (클라이언트), 파트별/기수별 필터
- ✅ 통계 카드 —
ApplicationsPage.tsx:76-83에서 cardList 기반 동적 집계 - ✅ 지원자 행 클릭 →
ApplicationDetailDrawer오픈 —ApplicationsPageonRowPress+selectedApplicationIdstate 연결 - ⬜ 지원자 이름 행 hover 하이라이트 스타일 없음 (
cursor-pointer만 적용)
- ✅
ApplicationDetailDrawer—/applications내에서 슬라이드 패널로 진입 (별도 라우트 불필요) - ✅ 지원 파트 / 이름 / 휴대폰번호(가운데번호 마스킹) / 생년월일 / 거주지역 / 제출일 표시
- ✅ 파트별 질문+답변 —
AnswerList.tsx(answers Record 렌더링) - ✅ 개인정보 동의 여부 —
privacyAgreedboolean 표시 - ✅ 개인정보 동의 일시 —
ApplicationDetailDrawer/index.tsxInfoRow로privacyAgreedAt렌더링 (BE 미응답 시formatDate폴백"-") - ✅ 상세에서 합격/불합격 분기 상태 변경 —
STATUS_BRANCH+StatusChangeModal
- ⬜ 합격 발표 후 6개월 자동 파기 스케줄러 (Cron)
- ⬜ 개인정보 필드 null 처리 또는 레코드 삭제 로직
- ⬜ 감사 로그 / 관리자 알림 (필요 시)
- ✅ 별도 사이드바 라우트
/interview-slots—apps/admin/src/pages/interview-slots/ - ✅ 기수 + 파트 필터 (
InterviewSlotsToolbar) — 파트는 "전체" 또는 단일 선택 - ✅ 슬롯 테이블 (
InterviewSlotsTable) — 날짜/시간/파트/예약-정원/장소/액션 - ✅ 슬롯 등록·수정 통합 Drawer (
InterviewSlotRegisterDrawer) — RHF + Zod- DatePicker 1개 + 시작/종료 TimeField 2개 (같은 날짜 시작·종료)
- capacity / location / description
- 수정 모드 시 cohortId/cohortPartId Select 는 isReadOnly + 안내 caption (BE PATCH DTO 가 두 필드를 미지원)
- ✅ 슬롯 삭제 Dialog (
DeleteInterviewSlotDialog) —DeleteCohortDialog패턴 미러링 - ✅ 흐름 훅 —
pages/interview-slots/hooks/useCreateOrUpdateSlotFlow+useDeleteSlotFlow+serialize - ✅ Phase B —
StatusChangeModal의INTERVIEW_SLOTS_NOT_READY분기에서InterviewSlotsRequiredModal(HeroUI Modal) 노출 → "슬롯 등록하러 가기" 시/interview-slots?cohortId=X&cohortPartId=Y로 navigate (필터 prefill) - ✅ 예약자 목록 표시 — 슬롯 행 "예약/정원" 셀 클릭 →
ReservationsDrawer오픈 (InterviewSlotResponseDto.reservationsnested 활용). 지원자명은applicationQueries.getAdminApplications({ cohortId })useSuspenseQuery로applicationFormId → applicantName매핑 - ✅ 예약 취소 어드민 UI —
CancelReservationDialog(AlertDialog) +useCancelReservationFlow(cancelInterviewReservationmutation → toast +slotLists()invalidate). Drawer 안 예약 행 [취소] 버튼에서 confirm 후 호출 - ✅ 슬롯 반복 등록 — 등록 Drawer 의 "여러 개 생성" 모드. 운영 시간대 + 슬롯 길이(30/60분 프리셋·직접 입력)로 후보를 생성해 체크리스트 미리보기(
BulkSlotCandidateList) → 단건 POST 병렬 호출(useCreateSlotsBulkFlow, BE bulk API 없음). 같은 기수·파트 기존 슬롯과 겹치는 후보는useBulkSlotCandidates가 잠가 등록 불가. 부분 실패 시 Drawer 유지 + 실패분만 재전송. 순수 로직(generateSlotCandidates·markConflictingCandidates·직렬화·스키마)은 vitest 단위 테스트. 다중 날짜 그리드는 후속 - ✅ 지원자 테이블 면접일자 컬럼 —
useApplicationsBoard가getInterviewSlots응답의reservations[].applicationFormId → slot.startAt로 Map 빌드해ApplicationTable에 전달 (commit 197834b)
화면 레퍼런스: HTML 목업
#page-projects영역. 컬럼/필터/Drawer 구성은 HTML을 따르되status필드는 백엔드 DTO에 없으므로 제거한다. 세부 계획은 설계 문서 참조.
- ✅ 목록 조회 (썸네일 / 서비스명 / 플랫폼 / 기수 / 한줄설명 / 참여자수) —
useInfiniteProjects연동 - ✅ 플랫폼 필터 (서버) + 기수 필터 (클라이언트,
useCohorts매핑) + 서비스명 검색 - ✅ "더 보기" 페이지네이션 (cursor 기반,
useInfiniteQuery) - ✅ 새 프로젝트 등록/수정 Drawer (
ProjectFormDrawer.tsx) — 썸네일 · 플랫폼 다중 · 서비스명 · 한줄설명 · 기수 · 참여자 N명, react-hook-form + Zod - ✅ 썸네일 이미지 업로드 — 선택 시 업로드하지 않고 저장 시
POST /admin/projects/:id/thumbnail(projectMutations.uploadProjectThumbnail) - ✅ 참여자 입력 (
useFieldArray— 이름/파트/후기) - ✅ 삭제 확인 (
DeleteProjectDialog— HeroUIAlertDialog) +useDeleteProject - ✅ 저장/삭제 토스트 (HeroUI v3
toast,docs/admin-toast.md표준) - ✅ PDF 업로드 — 저장 시
POST /admin/projects/:id/pdf(projectMutations.uploadProjectPdf). 파일 실패 시 프로젝트 재생성 없이 실패한 파일만 재시도 - ⬜ 등록 시
/projects/[id]URL 자동 생성 (웹 연동) — 후속 스코프 - ⬜ 브라우저 회귀 테스트 (실제 백엔드/MSW 연동 후)
화면 레퍼런스: HTML 목업
#page-blog영역. 백엔드 DTO에 없는status/author/category필드는 제거한다. 세부 계획은 설계 문서 참조.
- ✅ 목록 조회 (썸네일 / 제목 / 본문일부 / 외부 링크 / 등록일) —
useInfiniteBlogPosts연동 - ✅ 제목 검색 (클라이언트, 부분 일치)
- ✅ "더 보기" 페이지네이션 (cursor 기반,
useInfiniteQuery) - ✅ 새 블로그 등록/수정 Drawer (
BlogPostFormDrawer.tsx) — 썸네일 · 제목 · 본문일부 · 외부 URL, react-hook-form + Zod - ✅ 썸네일 이미지 업로드 (
useUploadFile({ category: 'blog-thumbnail' })) - ✅ 삭제 확인 (
DeleteBlogPostDialog— HeroUIAlertDialog) +useDeleteBlogPost - ✅ 저장/삭제 토스트 (HeroUI v3
toast,docs/admin-toast.md표준) - ⬜ 등록 시
/blog/[id]URL 자동 생성 (웹 연동) — 후속 스코프 - ⬜ 브라우저 회귀 테스트 (실제 백엔드/MSW 연동 후)
- ✅ MVP: 프론트엔드 하드코딩 (어드민 구현 제외 결정)
- ⬜ Phase 2: 어드민 Q&A 등록/수정/삭제, 표시 순서 드래그 정렬
백엔드 스키마이지만 프론트 타입 반영 여부 추적 목적.
- ✅
cohortENUM —CreateCohortRequestDtoStatus,CohortStatus,CohortPartConfigDtoName사용 중 - ✅
project.platformENUM[] —ProjectPlatform타입 사용 중 - ✅
early_notification—EarlyNotificationDto사용 중 - 🔧
applicant—ApplicationDto사용 중. 개인정보 필드(privacy_agreed_at, delete_scheduled_at 등) 상세 페이지 미구현으로 미검증 - ✅
pages/semesters/types.d.ts의SemesterRegisterForm/CohortPartFormState는 폼 입력 전용 (UI state).serialize.ts가 DTO(CohortPartConfigDto등) 와 매핑 — 의도된 분리, 중복 아님
| 영역 | 상태 | 비고 |
|---|---|---|
| 홈페이지 — 홈 (소개/CTA/수치/미리보기/후원사/FAQ/지원 유도) | ⬜ | apps/web 스켈레톤 |
| 홈페이지 — 모집 안내 (파트/프로세스/커리큘럼/지원 신청) | ⬜ | |
| 홈페이지 — 지원 (사전 알림 / 지원서 + 개인정보 동의) | ⬜ | |
| 홈페이지 — 프로젝트 (목록+필터 / 상세 / PDF) | ⬜ | project/[id] 라우트만 존재 |
| 홈페이지 — 블로그 (외부 아티클 링크 목록) | ⬜ | |
| 어드민 — 기수 관리 (상태 + 수동 변경) | ✅ | 목록/통계/등록/수정/상태변경/파트양식 저장 완료. 부분 실패 시 edit 모드 자동 전환 |
| 어드민 — 지원자 목록/상세/상태 변경 | ✅ | 목록·Drawer 상세·합격불합격 분기·면접일자 컬럼·개인정보 동의 일자 표시 완료 |
| 어드민 — 사전 알림 DB + 수동 이메일 발송 | 🔧 | 목록/통계/일괄발송/CSV/캠페인(예약 발송 관리) 완료. 개별발송(백엔드 엔드포인트 없음) 대기 중 |
| 어드민 — 프로젝트 DB 등록/수정 | ✅ | 목록·필터·등록·수정·삭제 코드 완료 (브라우저 검증 미실시) |
| 어드민 — 블로그 DB 등록/수정 | ✅ | 목록·검색·등록·수정·삭제 코드 완료 (브라우저 검증 미실시) |
- ⬜ 프로젝트 참여자 후기
- ⬜ 블로그 아티클 내부 페이지화
- ⬜ 어드민 FAQ 관리
HTML 목업 대비 현재 코드의 하드코딩 / API 미연동 / 미구현 인터랙션 을 file:line 단위로 정리. 작업 우선순위는 ▲ 표시.
241ae4e 이전에 남아있던 "generated 미사용 / 패턴 불일치" 갭(cohort·auth·storage)은 openapi-fetch 마이그레이션으로 일괄 해소됨. 모든 도메인이
api.get/post/...패턴으로 통일.
| 도메인 | 문제 | 영향 |
|---|---|---|
| storage | listFiles·deleteFile·createSignedUrl·downloadFile queries 미구현 (BE 엔드포인트 추가 후 진행) |
파일 관리 기능 확장 불가 |
✅ 241ae4e 이후 api 싱글톤 사용 — 다른 도메인과 패턴 일치 |
갭 해소 | |
✅ 241ae4e 이후 api 싱글톤 사용 — 다른 도메인과 패턴 일치 |
갭 해소 | |
✅ SDK + 어드민 UI(NotificationCampaignSection / 편집 Drawer / pause·resume 토글) 연결 완료 |
갭 해소 | |
✅ cancelInterviewReservation mutation + 어드민 UI(ReservationsDrawer / CancelReservationDialog) 연결 완료 |
갭 해소 | |
✅ subscribeGeneralEarlyNotification (POST /api/v1/early-notifications/general) SDK·queries 추가, apps/web/lib/api/early-notification.ts 에서 활성 기수 없을 때 자동 폴백 |
갭 해소 | |
✅ getCohortPart (GET /api/v1/cohorts/parts/{id}) SDK·queries 추가 |
갭 해소 | |
webApi 단일 객체 |
✅ packages/api/src/web.ts 폐기. apps/web/lib/api/{project,blog,cohort,application,early-notification}.ts 도메인별 파일 + lib/mappers/* 매퍼 분리로 정리 |
갭 해소 |
apps/admin/src/pages/early-notification/components/EarlyNotificationTable.tsx— 개별 발송 액션 컬럼 자체 부재. 단건 발송 엔드포인트가 BE generated에 없어 구현 대기
- ⬜
/projects— 실제 백엔드 또는 MSW 연동 후 등록·수정·삭제·필터·"더 보기" 시나리오 브라우저 검증 - ⬜
/blog-posts— 동일
— ✅ 처리됨 (useUpdateCohortParts연결useCreateOrUpdateCohortFlow안에updateCohortPartsmutation 추가, 부분 실패는PartsSaveAfterCreateErrorthrow → 호출부 instanceof 분기. 흐름 훅에서 토스트/콜백 제거하고 호출부 onSubmit 으로 이관)— ✅ 처리됨 (StatusChangeModalINTERVIEW_SLOTS_NOT_READY에러 분기ApiError.is("INTERVIEW_SLOTS_NOT_READY")분기 + 강조 토스트, 모달 유지)— ✅ 처리됨 (개인정보 동의 일자표시ApplicationDetailDrawer에동의 일자InfoRow추가, BE 미응답 시"-"폴백)