시작하기
관리자 패널
EmDash 관리자 패널은 Astro 사이트에 내장된 React 단일 페이지 애플리케이션입니다. 편집자와 관리자를 위한 완전한 콘텐츠 관리 인터페이스를 제공합니다.
아키텍처 개요
섹션 제목: “아키텍처 개요”┌────────────────────────────────────────────────────────────────┐│ Astro Shell ││ /_emdash/admin/[...path].astro ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ React SPA │ ││ │ │ ││ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ ││ │ │ TanStack │ │ TanStack │ │ Kumo │ │ ││ │ │ Router │ │ Query │ │ Components │ │ ││ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ ││ │ │ ││ │ ┌────────────────────────────────────────────────────┐ │ ││ │ │ REST API Client │ │ ││ │ │ /_emdash/api/* │ │ ││ │ └────────────────────────────────────────────────────┘ │ ││ └──────────────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────┘관리자는 “큰 섬” React 앱입니다. Astro는 셸과 인증을 처리하며, 관리자 내부의 모든 탐색 및 렌더링은 클라이언트 측에서 이루어집니다.
기술 스택
섹션 제목: “기술 스택”| 계층 | 기술 | 목적 |
|---|---|---|
| 라우팅 | TanStack Router | 타입 안전 클라이언트 측 라우팅 |
| 데이터 | TanStack Query | 서버 상태, 캐싱, 변이 |
| UI | Kumo | 접근성 컴포넌트 (Base UI + Tailwind) |
| 테이블 | TanStack Table | 정렬, 필터링, 페이지네이션 |
| 폼 | React Hook Form + Zod | 서버 스키마와 일치하는 유효성 검사 |
| 아이콘 | Phosphor | 일관된 아이콘 시스템 |
| 에디터 | TipTap | 리치 텍스트 편집 (Portable Text) |
라우트 구조
섹션 제목: “라우트 구조”관리자는 /_emdash/admin/에 마운트되며 클라이언트 측 라우팅을 사용합니다:
| 경로 | 화면 |
|---|---|
/ | 대시보드 |
/content/:collection | 콘텐츠 목록 |
/content/:collection/:id | 콘텐츠 편집기 |
/content/:collection/new | 새 항목 |
/media | 미디어 라이브러리 |
/content-types | 스키마 빌더 (관리자 전용) |
/menus | 네비게이션 메뉴 |
/widgets | 위젯 영역 |
/taxonomies | 카테고리/태그 관리 |
/settings | 사이트 설정 |
/plugins/:pluginId/* | 플러그인 페이지 |
매니페스트 기반 UI
섹션 제목: “매니페스트 기반 UI”관리자는 컬렉션이나 플러그인에 대한 지식을 하드코딩하지 않습니다. 대신 서버에서 매니페스트를 가져옵니다:
GET /_emdash/api/manifest응답:
{ "collections": [ { "slug": "posts", "label": "Blog Posts", "labelSingular": "Post", "icon": "file-text", "supports": ["drafts", "revisions", "preview"], "fields": [ { "slug": "title", "type": "string", "required": true }, { "slug": "content", "type": "portableText" } ] } ], "plugins": [ { "id": "audit-log", "label": "감사 로그", "adminPages": [{ "path": "history", "label": "감사 기록" }], "widgets": [{ "id": "recent-activity", "title": "최근 활동" }] } ], "taxonomies": [{ "name": "category", "label": "카테고리", "hierarchical": true }], "version": "abc123"}관리자는 이 매니페스트를 기반으로 탐색, 폼 및 편집기를 완전히 구축합니다. 장점:
- 스키마 변경이 즉시 반영됨 — 관리자 재빌드 불필요
- 플러그인 UI가 자동으로 통합됨 — 매니페스트의 페이지 및 위젯
- 경계에서의 타입 안전성 — Zod 스키마는 서버에 유지
데이터 흐름
섹션 제목: “데이터 흐름”- 관리자 SPA 로드 — TanStack Router 초기화 2. 매니페스트 가져오기 — TanStack Query가 컬렉션/플러그인 메타데이터 캐싱
- 탐색 구축 — 매니페스트에서 사이드바 생성 4. 사용자 탐색 — 클라이언트 측 라우팅, 페이지 재로드 없음 5. 데이터 가져오기 — TanStack Query가 REST API에서 콘텐츠 요청 6. 폼 렌더링 — 매니페스트 필드 설명자에서 필드 편집기 생성 7. 변경 사항 제출 — TanStack Query를 통한 변이, 낙관적 업데이트 8. 서버 유효성 검사 — 서버의 Zod 스키마, 오류가 JSON으로 반환
REST API 엔드포인트
섹션 제목: “REST API 엔드포인트”관리자는 REST API를 통해서만 통신합니다:
콘텐츠 API
섹션 제목: “콘텐츠 API”| 메서드 | 엔드포인트 | 목적 |
|---|---|---|
GET | /api/content/:collection | 항목 목록 |
POST | /api/content/:collection | 항목 생성 |
GET | /api/content/:collection/:id | 항목 가져오기 |
PUT | /api/content/:collection/:id | 항목 업데이트 |
DELETE | /api/content/:collection/:id | 항목 소프트 삭제 |
GET | /api/content/:collection/:id/revisions | 개정 목록 |
POST | /api/content/:collection/:id/preview-url | 미리보기 URL 생성 |
스키마 API
섹션 제목: “스키마 API”| 메서드 | 엔드포인트 | 목적 |
|---|---|---|
GET | /api/schema | 전체 스키마 내보내기 |
GET | /api/schema/collections | 컬렉션 목록 |
POST | /api/schema/collections | 컬렉션 생성 |
PUT | /api/schema/collections/:slug | 컬렉션 업데이트 |
DELETE | /api/schema/collections/:slug | 컬렉션 삭제 |
POST | /api/schema/collections/:slug/fields | 필드 추가 |
PUT | /api/schema/collections/:slug/fields/:field | 필드 업데이트 |
DELETE | /api/schema/collections/:slug/fields/:field | 필드 삭제 |
미디어 API
섹션 제목: “미디어 API”| 메서드 | 엔드포인트 | 목적 |
|---|---|---|
GET | /api/media | 미디어 항목 목록 |
POST | /api/media/upload-url | 서명된 업로드 URL 가져오기 |
POST | /api/media/:id/confirm | 업로드 완료 확인 |
DELETE | /api/media/:id | 미디어 항목 삭제 |
GET | /api/media/file/:key | 미디어 파일 제공 |
기타 API
섹션 제목: “기타 API”| 엔드포인트 | 목적 |
|---|---|
/api/settings | 사이트 설정 (GET/POST) |
/api/menus/* | 네비게이션 메뉴 |
/api/widget-areas/* | 위젯 관리 |
/api/taxonomies/* | 분류 체계 용어 |
/api/admin/plugins/* | 플러그인 상태 |
페이지네이션
섹션 제목: “페이지네이션”모든 목록 엔드포인트는 커서 기반 페이지네이션을 사용합니다:
{ "items": [...], "nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"}다음 페이지 가져오기:
GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9플러그인 관리자 UI
섹션 제목: “플러그인 관리자 UI”플러그인은 페이지와 대시보드 위젯으로 관리자를 확장할 수 있습니다. 통합은 정적 임포트를 가진 가상 모듈을 생성합니다:
// virtual:emdash/plugin-admins (generated)import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";
export const pluginAdmins = { seo: pluginAdmin0, analytics: pluginAdmin1,};플러그인 페이지
섹션 제목: “플러그인 페이지”플러그인 페이지는 /_emdash/admin/plugins/:pluginId/* 아래에 마운트됩니다:
// @emdash-cms/plugin-seo/src/admin.tsxexport const pages = [ { path: "settings", component: SEOSettingsPage, label: "SEO Settings", },];렌더링 위치: /_emdash/admin/plugins/seo/settings
대시보드 위젯
섹션 제목: “대시보드 위젯”플러그인은 대시보드에 위젯을 추가할 수 있습니다:
export const widgets = [ { id: "seo-overview", component: SEOWidget, title: "SEO Overview", size: "half", // "full" | "half" | "third" },];관리자 셸 경로는 Astro 미들웨어를 통해 인증을 강제합니다:
// Simplified middleware logicexport async function onRequest({ request, locals }, next) { const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) { if (!session?.user) { return redirect("/_emdash/admin/login"); } locals.user = session.user; }
return next();}관리자 SPA 자체는 로그인을 처리하지 않습니다 — 세션 쿠키를 설정하는 Astro 페이지가 담당합니다.
역할 기반 접근
섹션 제목: “역할 기반 접근”다른 역할은 관리자의 다른 부분을 볼 수 있습니다:
| 역할 | 표시되는 섹션 |
|---|---|
| 편집자 | 대시보드, 할당된 컬렉션, 미디어 |
| 관리자 | + 콘텐츠 유형, 모든 컬렉션, 설정 |
| 개발자 | + CLI 접근, 생성된 타입 |
매니페스트 엔드포인트는 요청하는 사용자의 역할에 따라 컬렉션과 기능을 필터링합니다.
콘텐츠 편집기
섹션 제목: “콘텐츠 편집기”콘텐츠 편집기는 필드 정의를 기반으로 동적으로 폼을 생성합니다:
// Simplified editor renderingfunction ContentEditor({ collection, fields }) { return ( <form> {fields.map((field) => ( <FieldWidget key={field.slug} type={field.type} label={field.label} required={field.required} options={field.options} /> ))} </form> );}각 필드 유형에는 해당하는 위젯이 있습니다:
| 필드 유형 | 위젯 |
|---|---|
string | 텍스트 입력 |
text | 텍스트 영역 |
number | 숫자 입력 |
boolean | 토글 스위치 |
datetime | 날짜/시간 선택기 |
select | 드롭다운 |
multiSelect | 다중 선택 |
portableText | TipTap 편집기 |
image | 미디어 선택기 |
reference | 항목 선택기 |
리치 텍스트 편집기
섹션 제목: “리치 텍스트 편집기”Portable Text 필드는 편집을 위해 TipTap(ProseMirror)을 사용합니다:
User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display변환은 portableTextToProsemirror() 및 prosemirrorToPortableText()를 통해 로드/저장 경계에서 발생합니다.
지원되는 블록:
- 단락, 제목 (H1-H6)
- 글머리 기호 및 번호 매기기 목록
- 인용문, 코드 블록
- 이미지 (미디어 라이브러리에서)
- 링크
플러그인 또는 가져오기에서 알 수 없는 블록은 읽기 전용 자리 표시자로 보존됩니다.
미디어 라이브러리
섹션 제목: “미디어 라이브러리”미디어 라이브러리는 다음을 제공합니다:
- 그리드 및 목록 보기
- 유형, 날짜별 검색 및 필터링
- 드래그 앤 드롭 업로드
- 메타데이터가 포함된 이미지 미리보기
- 대량 선택 및 삭제
업로드는 직접 클라이언트-스토리지 업로드를 위해 서명된 URL을 사용합니다:
- 업로드 URL 요청 —
POST /api/media/upload-url2. 직접 업로드 — 클라이언트가 파일을 서명된 URL(R2/S3)에 PUT 3. 업로드 확인 —POST /api/media/:id/confirm4. 서버가 메타데이터 추출 — 크기, MIME 유형 등
이 접근 방식은 Workers 본문 크기 제한을 우회하고 실제 업로드 진행 상황을 제공합니다.