React 관리 페이지와 위젯

이 페이지

네이티브 플러그인은 커스텀 React 페이지와 대시보드 위젯으로 관리 패널을 확장할 수 있습니다. 샌드박스 플러그인은 대신 UI를 Block Kit으로 설명합니다. 플러그인 JavaScript를 관리 화면에 로드하면 샌드박스 격리가 깨지기 때문입니다.

플러그인에 설정 폼만 필요한 경우, 자동 생성되는 admin.settingsSchema 폼(첫 번째 네이티브 플러그인 참조)이 React를 작성하지 않고도 대부분의 경우를 처리합니다. settingsSchema가 제공하는 것보다 더 풍부한 UI가 필요한 경우에만 커스텀 컴포넌트를 사용하세요.

관리 엔트리 포인트

관리 UI가 있는 플러그인은 admin 엔트리 포인트에서 pageswidgets 객체를 내보냅니다:

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

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

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

package.json에서 엔트리 포인트를 구성합니다:

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

definePlugin()에서 참조합니다:

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" }],
	},
});

디스크립터에는 EmDash가 빌드 시 컴포넌트를 찾을 수 있도록 대응하는 adminEntry가 필요합니다:

adminEntry: "@my-org/plugin-seo/admin",

관리 페이지

관리 페이지는 /_emdash/admin/plugins/<plugin-id>/<path> 아래에 마운트되는 React 컴포넌트입니다.

페이지 정의

admin.pages 아래에 경로, 라벨, 아이콘을 지정하여 각 페이지를 선언합니다:

admin: {
	pages: [
		{
			path: "/settings",
			label: "Settings",
			icon: "settings",
		},
		{
			path: "/reports",
			label: "Reports",
			icon: "chart",
		},
	],
}

라벨은 영어로 선언하세요. 관리 화면은 사이드바와 명령 팔레트를 렌더링하기 전에 공유 Lingui 인스턴스를 통해 라벨을 처리합니다. 따라서 자체 메시지 카탈로그를 로드하는 플러그인(영어 라벨을 메시지 ID로 사용)은 무료로 현지화된 내비게이션을 얻습니다. 관리 화면 자체의 메시지(Settings, Dashboard 등)와 일치하는 라벨은 플러그인 카탈로그 없이도 관리 화면의 번역을 사용합니다. 카탈로그에 항목이 없는 라벨은 선언된 그대로 렌더링됩니다.

import { i18n } from "@lingui/core";

// 플러그인의 컴파일된 카탈로그를 관리 화면의 i18n 인스턴스에 병합합니다.
// 관리 화면의 로케일이 독일어인 경우 "Reports"가 "Berichte"로 렌더링됩니다.
const catalogs: Record<string, Record<string, string>> = {
	de: { Reports: "Berichte", Settings: "Einstellungen" },
};

function mergeCatalog() {
	const messages = catalogs[i18n.locale];
	if (messages && !("Reports" in i18n.messages)) i18n.load(i18n.locale, messages);
}

mergeCatalog();
// 관리 화면의 로케일 전환기는 변경 시 카탈로그를 *교체*하므로 다시 병합합니다.
// 위의 센티넬 검사가 재귀를 방지합니다(load()가 "change"를 발생시키므로).
i18n.on("change", mergeCatalog);

페이지 컴포넌트

다음 컴포넌트는 플러그인 API 훅을 통해 설정을 읽고 저장합니다:

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>플러그인 설정</h1>

			<label>
				사이트 제목
				<input
					type="text"
					value={(settings.siteTitle as string) || ""}
					onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
				/>
			</label>

			<button onClick={handleSave} disabled={saving}>
				{saving ? "저장 중..." : "설정 저장"}
			</button>
		</div>
	);
}

플러그인 API 훅

usePluginAPI()는 플러그인 ID 접두사와 X-EmDash-Request: 1 CSRF 헤더를 자동으로 추가하여 플러그인의 라우트를 호출합니다:

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

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

	const data = await api.get("status");                       // GET /_emdash/api/plugins/<id>/status
	await api.post("settings/save", { enabled: true });          // JSON 본문으로 POST
	const result = await api.get("history?limit=50");            // 쿼리 파라미터 지원
}

대시보드 위젯

위젯은 관리 대시보드에 표시되며 한눈에 정보를 제공합니다.

위젯 정의

admin.widgets 아래에 ID, 제목, 크기를 지정하여 각 위젯을 선언합니다:

admin: {
	widgets: [
		{
			id: "seo-overview",
			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 as { message: string }).message}</li>
				))}
			</ul>
		</div>
	);
}

위젯 크기

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

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

내보내기 구조

관리 엔트리 포인트는 두 개의 객체를 내보냅니다:

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,
};

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="설정">
			<Input label="API 키" type="password" />
			<Toggle label="활성화" defaultChecked />
			<Button variant="primary">저장</Button>
		</Card>
	);
}

자동 생성 설정 UI

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

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

EmDash가 자동으로 설정 페이지를 생성합니다. 기본 폼을 넘어서는 동작이 필요한 경우에만 커스텀 React 페이지를 사용하세요.

내비게이션

플러그인 페이지는 관리 사이드바에서 플러그인 이름 아래에 표시됩니다. 순서는 아래와 같이 admin.pages 배열과 일치합니다:

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // 첫 번째
		{ path: "/history", label: "History", icon: "history" },     // 두 번째
		{ path: "/reports", label: "Reports", icon: "chart" },        // 세 번째
	],
}

빌드 구성

관리 컴포넌트는 별도의 빌드 엔트리 포인트가 필요합니다. 다음 번들러 구성은 서버와 관리 엔트리 포인트를 모두 빌드합니다:

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 admin을 외부 종속성으로 유지하세요.

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

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

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

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

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

완전한 예제

다음 플러그인은 대시보드 페이지, 설정 페이지, 위젯을 정의하며, 런타임과 관리 엔트리 포인트를 별도의 파일에 배치합니다. src/index.ts 파일에는 디스크립터와 런타임이 포함되어 있습니다:

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

export function analyticsPlugin(): PluginDescriptor {
	return {
		id: "analytics",
		version: "1.0.0",
		format: "native",
		entrypoint: "@my-org/plugin-analytics",
		adminEntry: "@my-org/plugin-analytics/admin",
		adminPages: [
			{ path: "/dashboard", label: "Dashboard", icon: "chart" },
			{ path: "/settings", label: "Settings", icon: "settings" },
		],
		adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
	};
}

export function createPlugin() {
	return definePlugin({
		id: "analytics",
		version: "1.0.0",

		capabilities: ["network:request"],
		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 };
				},
			},
		},
	});
}

export default createPlugin;

src/admin.tsx 파일은 페이지 경로와 위젯 ID를 React 컴포넌트에 매핑합니다:

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,
};