관리 UI 확장

이 페이지

플러그인은 사용자 정의 페이지와 대시보드 위젯으로 관리 패널을 확장할 수 있습니다. 핵심 관리 기능과 함께 렌더링되는 React 컴포넌트입니다.

관리 진입점

관리 UI가 있는 플러그인은 admin 진입점에서 컴포넌트를 내보냅니다:

import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";

// Dashboard widgets
export const widgets = {
	"seo-overview": SEODashboardWidget,
};

// Admin pages
export const pages = {
	"/settings": SEOSettingsPage,
};

package.json에서 진입점을 구성합니다:

{
	"exports": {
		".": "./dist/index.js",
		"./admin": "./dist/admin.js"
	}
}

플러그인 정의에서 참조합니다:

definePlugin({
	id: "seo",
	version: "1.0.0",

	admin: {
		entry: "@my-org/plugin-seo/admin",
		pages: [{ path: "/settings", label: "SEO Settings", icon: "settings" }],
		widgets: [{ id: "seo-overview", title: "SEO Overview", size: "half" }],
	},
});

관리 페이지

관리 페이지는 훅을 통해 플러그인 컨텍스트를 받는 React 컴포넌트입니다.

페이지 정의

admin.pages에서 페이지를 정의합니다:

admin: {
	pages: [
		{
			path: "/settings", // URL 경로 (플러그인 기본 경로에 상대적)
			label: "Settings", // 사이드바 라벨
			icon: "settings", // 아이콘 이름 (선택 사항)
		},
		{
			path: "/reports",
			label: "Reports",
			icon: "chart",
		},
	];
}

페이지는 /_emdash/admin/plugins/<plugin-id>/<path>에 마운트됩니다.

페이지 컴포넌트

import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";

export function SettingsPage() {
  const api = usePluginAPI();
  const [settings, setSettings] = useState<Record<string, unknown>>({});
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    api.get("settings").then(setSettings);
  }, []);

  const handleSave = async () => {
    setSaving(true);
    await api.post("settings/save", settings);
    setSaving(false);
  };

  return (
    <div>
      <h1>Plugin Settings</h1>

      <label>
        Site Title
        <input
          type="text"
          value={settings.siteTitle || ""}
          onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
        />
      </label>

      <label>
        <input
          type="checkbox"
          checked={settings.enabled ?? true}
          onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })}
        />
        Enabled
      </label>

      <button onClick={handleSave} disabled={saving}>
        {saving ? "Saving..." : "Save Settings"}
      </button>
    </div>
  );
}

플러그인 API 훅

usePluginAPI()를 사용하여 플러그인 라우트를 호출합니다:

import { usePluginAPI } from "@emdash-cms/admin";

function MyComponent() {
	const api = usePluginAPI();

	// 플러그인 라우트에 GET 요청
	const data = await api.get("status");

	// 본문을 포함한 POST 요청
	await api.post("settings/save", { enabled: true });

	// URL 매개변수 포함
	const result = await api.get("history?limit=50");
}

이 훅은 라우트 URL에 플러그인 ID 접두사를 자동으로 추가합니다.

대시보드 위젯

위젯은 관리 대시보드에 나타나며 한눈에 볼 수 있는 정보를 제공합니다.

위젯 정의

admin.widgets에서 위젯을 정의합니다:

admin: {
	widgets: [
		{
			id: "seo-overview", // 고유 위젯 ID
			title: "SEO Overview", // 위젯 제목 (선택 사항)
			size: "half", // "full" | "half" | "third"
		},
	];
}

위젯 컴포넌트

import { useState, useEffect } from "react";
import { usePluginAPI } from "@emdash-cms/admin";

export function SEOWidget() {
  const api = usePluginAPI();
  const [data, setData] = useState({ score: 0, issues: [] });

  useEffect(() => {
    api.get("analyze").then(setData);
  }, []);

  return (
    <div className="widget-content">
      <div className="score">{data.score}%</div>
      <ul>
        {data.issues.map((issue, i) => (
          <li key={i}>{issue.message}</li>
        ))}
      </ul>
    </div>
  );
}

위젯 크기

크기설명
full대시보드 전체 너비
half대시보드 절반 너비
third대시보드 1/3 너비

위젯은 화면 너비에 따라 자동으로 줄 바꿈됩니다.

내보내기 구조

관리 진입점은 두 개의 객체를 내보냅니다:

import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
import { OverviewWidget } from "./components/OverviewWidget";

// 경로별 페이지
export const pages = {
	"/settings": SettingsPage,
	"/reports": ReportsPage,
};

// ID별 위젯
export const widgets = {
	status: StatusWidget,
	overview: OverviewWidget,
};

관리 컴포넌트 사용

EmDash는 일반적인 패턴을 위한 사전 제작된 컴포넌트를 제공합니다:

import {
  Card,
  Button,
  Input,
  Select,
  Toggle,
  Table,
  Pagination,
  Alert,
  Loading
} from "@emdash-cms/admin";

function SettingsPage() {
  return (
    <Card title="Settings">
      <Input label="API Key" type="password" />
      <Toggle label="Enabled" defaultChecked />
      <Button variant="primary">Save</Button>
    </Card>
  );
}

자동 생성 설정 UI

플러그인이 설정 폼만 필요한 경우, 사용자 정의 컴포넌트 없이 admin.settingsSchema를 사용합니다:

admin: {
  settingsSchema: {
    apiKey: { type: "secret", label: "API Key" },
    enabled: { type: "boolean", label: "Enabled", default: true }
  }
}

EmDash는 설정 페이지를 자동으로 생성합니다. 기본 설정 이상의 기능이 필요한 경우에만 사용자 정의 페이지를 추가하세요.

내비게이션

플러그인 페이지는 플러그인 이름 아래 관리 사이드바에 나타납니다. 순서는 admin.pages 배열과 일치합니다.

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" }, // First
		{ path: "/history", label: "History", icon: "history" }, // Second
		{ path: "/reports", label: "Reports", icon: "chart" }, // Third
	];
}

빌드 구성

관리 컴포넌트는 별도의 빌드 진입점이 필요합니다. 번들러를 구성합니다:

tsdown

export default {
  entry: {
    index: "src/index.ts",
    admin: "src/admin.tsx"
  },
  format: "esm",
  dts: true,
  external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]
};

tsup

export default {
  entry: ["src/index.ts", "src/admin.tsx"],
  format: "esm",
  dts: true,
  external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]
};

중복 번들링을 방지하기 위해 React와 EmDash 관리를 외부 종속성으로 유지합니다.

플러그인 활성화/비활성화

관리에서 플러그인이 비활성화되면:

  • 사이드바 링크가 숨겨집니다
  • 대시보드 위젯이 렌더링되지 않습니다
  • 관리 페이지가 404를 반환합니다
  • 백엔드 훅은 계속 실행됩니다 (데이터 안전을 위해)

플러그인은 활성화 상태를 확인할 수 있습니다:

const enabled = await ctx.kv.get<boolean>("_emdash:enabled");

예시: 완전한 관리 UI

import { definePlugin } from "emdash";

export default definePlugin({
	id: "analytics",
	version: "1.0.0",

	capabilities: ["network:fetch"],
	allowedHosts: ["api.analytics.example.com"],

	storage: {
		events: { indexes: ["type", "createdAt"] },
	},

	admin: {
		entry: "@my-org/plugin-analytics/admin",
		settingsSchema: {
			trackingId: { type: "string", label: "Tracking ID" },
			enabled: { type: "boolean", label: "Enabled", default: true },
		},
		pages: [
			{ path: "/dashboard", label: "Dashboard", icon: "chart" },
			{ path: "/settings", label: "Settings", icon: "settings" },
		],
		widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
	},

	routes: {
		stats: {
			handler: async (ctx) => {
				const today = new Date().toISOString().split("T")[0];
				const count = await ctx.storage.events!.count({
					createdAt: { gte: today },
				});
				return { today: count };
			},
		},
	},
});
import { EventsWidget } from "./components/EventsWidget";
import { DashboardPage } from "./components/DashboardPage";
import { SettingsPage } from "./components/SettingsPage";

export const widgets = {
	"events-today": EventsWidget,
};

export const pages = {
	"/dashboard": DashboardPage,
	"/settings": SettingsPage,
};