콘텐츠로 이동

관리자 패널

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서버 상태, 캐싱, 변이
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/*플러그인 페이지

관리자는 컬렉션이나 플러그인에 대한 지식을 하드코딩하지 않습니다. 대신 서버에서 매니페스트를 가져옵니다:

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 스키마는 서버에 유지
  1. 관리자 SPA 로드 — TanStack Router 초기화 2. 매니페스트 가져오기 — TanStack Query가 컬렉션/플러그인 메타데이터 캐싱
  2. 탐색 구축 — 매니페스트에서 사이드바 생성 4. 사용자 탐색 — 클라이언트 측 라우팅, 페이지 재로드 없음 5. 데이터 가져오기 — TanStack Query가 REST API에서 콘텐츠 요청 6. 폼 렌더링 — 매니페스트 필드 설명자에서 필드 편집기 생성 7. 변경 사항 제출 — TanStack Query를 통한 변이, 낙관적 업데이트 8. 서버 유효성 검사 — 서버의 Zod 스키마, 오류가 JSON으로 반환

관리자는 REST 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 생성
메서드엔드포인트목적
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필드 삭제
메서드엔드포인트목적
GET/api/media미디어 항목 목록
POST/api/media/upload-url서명된 업로드 URL 가져오기
POST/api/media/:id/confirm업로드 완료 확인
DELETE/api/media/:id미디어 항목 삭제
GET/api/media/file/:key미디어 파일 제공
엔드포인트목적
/api/settings사이트 설정 (GET/POST)
/api/menus/*네비게이션 메뉴
/api/widget-areas/*위젯 관리
/api/taxonomies/*분류 체계 용어
/api/admin/plugins/*플러그인 상태

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

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

다음 페이지 가져오기:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

플러그인은 페이지와 대시보드 위젯으로 관리자를 확장할 수 있습니다. 통합은 정적 임포트를 가진 가상 모듈을 생성합니다:

// 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.tsx
export 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 logic
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 페이지가 담당합니다.

다른 역할은 관리자의 다른 부분을 볼 수 있습니다:

역할표시되는 섹션
편집자대시보드, 할당된 컬렉션, 미디어
관리자+ 콘텐츠 유형, 모든 컬렉션, 설정
개발자+ CLI 접근, 생성된 타입

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

콘텐츠 편집기는 필드 정의를 기반으로 동적으로 폼을 생성합니다:

// Simplified editor rendering
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)을 사용합니다:

User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)
Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display

변환은 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 본문 크기 제한을 우회하고 실제 업로드 진행 상황을 제공합니다.