관리 패널

이 페이지

EmDash 관리 패널은 Astro 사이트에 포함된 React 단일 페이지 애플리케이션입니다. 편집자와 관리자를 위한 콘텐츠 관리 UI를 제공합니다.

아키텍처 개요

┌────────────────────────────────────────────────────────────────┐
│                    Astro 셸                                    │
│  /_emdash/admin/[...path].astro                              │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    React SPA                             │  │
│  │                                                          │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │  │
│  │  │  TanStack   │  │  TanStack   │  │     Kumo        │   │  │
│  │  │   Router    │  │   Query     │  │  컴포넌트        │   │  │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘   │  │
│  │                                                          │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │           REST API 클라이언트                     │  │  │
│  │  │           /_emdash/api/*                           │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘

관리 패널은 React «큰 아일랜드»입니다. Astro가 셸과 인증을 담당하고, 패널 안의 내비게이션과 렌더링은 모두 클라이언트에서 이뤄집니다.

기술 스택

계층기술역할
라우팅TanStack Router타입 안전 클라이언트 라우팅
데이터TanStack Query서버 상태, 캐시, 뮤테이션
UIKumo접근 가능한 컴포넌트(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

관리 패널은 컬렉션이나 플러그인을 하드코딩하지 않습니다. 서버에서 매니페스트를 가져옵니다.

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": "Audit Log",
			"adminPages": [{ "path": "history", "label": "Audit History" }],
			"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"version": "abc123"
}

관리 패널은 이 매니페스트만으로 내비게이션, 폼, 에디터를 구성합니다. 장점은 다음과 같습니다.

  • 스키마 변경이 즉시 반영 — 관리 패널 재빌드 불필요
  • 플러그인 UI 자동 통합 — 매니페스트의 페이지와 위젯
  • 경계에서 타입 안전 — Zod 스키마는 서버에 유지

데이터 흐름

  1. 관리 SPA 로드 — TanStack Router 초기화
  2. 매니페스트 가져오기 — TanStack Query가 컬렉션/플러그인 메타데이터 캐시
  3. 내비게이션 구성 — 사이드바는 매니페스트에서 생성
  4. 사용자 이동 — 클라이언트 라우팅, 페이지 새로고침 없음
  5. 데이터 가져오기 — TanStack Query가 REST API로 콘텐츠 요청
  6. 폼 렌더 — 매니페스트 필드 설명으로 필드 에디터 생성
  7. 변경 제출 — TanStack Query 뮤테이션, 낙관적 업데이트
  8. 서버 검증 — 서버의 Zod, 오류는 JSON으로 반환

REST API 엔드포인트

관리 패널은 REST 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

메서드엔드포인트목적
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

메서드엔드포인트목적
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/settings사이트 설정(GET/POST)
/api/menus/*내비게이션 메뉴
/api/widget-areas/*위젯 관리
/api/taxonomies/*분류 용어
/api/admin/plugins/*플러그인 상태

페이지네이션

목록 엔드포인트는 모두 커서 기반 페이지네이션을 사용합니다.

{
  "items": [...],
  "nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}

다음 페이지 가져오기:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

플러그인 관리 UI

플러그인은 페이지와 대시보드 위젯으로 관리 패널을 확장할 수 있습니다. 통합이 정적 import가 있는 가상 모듈을 생성합니다.

// virtual:emdash/plugin-admins(생성)
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.tsx
export const pages = [
	{
		path: "settings",
		component: SEOSettingsPage,
		label: "SEO Settings",
	},
];

렌더 URL: /_emdash/admin/plugins/seo/settings

대시보드 위젯

플러그인은 대시보드에 위젯을 추가할 수 있습니다.

export const widgets = [
	{
		id: "seo-overview",
		component: SEOWidget,
		title: "SEO Overview",
		size: "half", // "full" | "half" | "third"
	},
];

인증

관리 셸 라우트는 Astro 미들웨어로 인증을 강제합니다.

// 미들웨어 단순화 예
export 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 페이지입니다.

역할 기반 접근

역할마다 보이는 관리 패널 구역이 다릅니다.

역할보이는 구역
Editor대시보드, 할당된 컬렉션, 미디어
Admin+콘텐츠 타입, 모든 컬렉션, 설정
Developer+CLI 접근, 생성된 타입

매니페스트 엔드포인트는 요청 사용자 역할에 따라 컬렉션과 기능을 필터링합니다.

콘텐츠 에디터

콘텐츠 에디터는 필드 정의에 따라 폼을 동적으로 만듭니다.

// 에디터 렌더 단순 예
function 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다중 선택
portableTextTipTap 에디터
image미디어 선택기
reference항목 선택기

리치 텍스트 에디터

Portable Text 필드 편집에는 TipTap(ProseMirror)을 사용합니다.

사용자 입력 → TipTap(ProseMirror JSON) → 저장 → Portable Text(DB)
로드 → Portable Text(DB) → TipTap(ProseMirror JSON) → 표시

변환은 로드/저장 경계에서 portableTextToProsemirror()prosemirrorToPortableText()로 이뤄집니다.

지원 블록:

  • 단락, 제목(H1–H6)
  • 글머리·번호 목록
  • 인용, 코드 블록
  • 이미지(미디어 라이브러리)
  • 링크

플러그인이나 가져오기에서 온 알 수 없는 블록은 읽기 전용 플레이스홀더로 유지됩니다.

미디어 라이브러리

미디어 라이브러리는 다음을 제공합니다.

  • 그리드·목록 보기
  • 유형·날짜로 검색·필터
  • 드래그 앤 드롭 업로드
  • 메타데이터가 있는 이미지 미리보기
  • 일괄 선택·삭제

업로드는 서명 URL로 클라이언트에서 스토리지로 직접 전송합니다.

  1. 업로드 URL 요청POST /api/media/upload-url
  2. 직접 업로드 — 클라이언트가 서명 URL(R2/S3)로 파일 PUT
  3. 업로드 확인POST /api/media/:id/confirm
  4. 서버가 메타데이터 추출 — 크기, MIME 타입 등

이 방식은 Workers 본문 크기 제한을 피하고 실제 업로드 진행률도 얻을 수 있습니다.

다음 단계