설정

이 페이지

샌드박스 플러그인은 플러그인별 KV 스토어에 설정을 저장하고 편집 UI를 Block Kit 페이지로 렌더링합니다: JSON으로 폼을 설명하고 라우트에서 제공합니다.

모든 것은 플러그인이 이미 훅과 라우트에 사용하는 동일한 메커니즘을 통해 발생합니다 – 추가로 배울 것이 없습니다.

KV 스토어

모든 플러그인은 모든 훅이나 라우트에서 ctx.kv로 접근 가능한 프라이빗 키-값 스토어를 얻습니다. 이것은 설정 및 기타 작은 영속 상태의 표준 위치입니다:

interface KVAccess {
	get<T>(key: string): Promise<T | null>;
	set(key: string, value: unknown): Promise<void>;
	delete(key: string): Promise<boolean>;
	list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}

KV는 플러그인별입니다 – 작성하는 키는 플러그인 ID 아래에 저장되며 다른 플러그인에는 표시되지 않습니다.

읽기 및 쓰기

// Read
const enabled = await ctx.kv.get<boolean>("settings:enabled");
const config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");

// Write
await ctx.kv.set("settings:lastSync", new Date().toISOString());
await ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000 });

// Delete
const deleted = await ctx.kv.delete("state:tempData");

// List by prefix
const allSettings = await ctx.kv.list("settings:");
// → [{ key: "settings:enabled", value: true }, ...]

키 명명 규칙

다양한 종류의 값을 분리하기 위해 접두사를 사용합니다. EmDash 플러그인 전체의 규칙:

접두사목적예시
settings:사용자 구성 가능한 설정settings:apiKey
state:내부 플러그인 상태state:lastSync
cache:캐시된 데이터cache:results
// Clear prefixes
await ctx.kv.set("settings:webhookUrl", url);
await ctx.kv.set("state:lastRun", timestamp);
await ctx.kv.set("cache:feed", feedData);

// Avoid bare keys
await ctx.kv.set("url", url);

Block Kit의 설정 UI

샌드박스 플러그인은 설정 페이지를 Block Kit으로 설명합니다. 관리자는 플러그인의 라우트(관례적으로 routes.admin)에 page_load 상호작용을 보내고, 플러그인은 폼의 JSON 설명을 반환합니다. 사용자가 저장을 클릭하면 관리자는 block_action 또는 form_submit 상호작용을 다시 보냅니다; 플러그인은 KV에 쓰고 업데이트된 블록을 반환합니다.

import type { PluginContext, SandboxedPlugin } from "emdash/plugin";

interface BlockInteraction {
	type: "page_load" | "block_action" | "form_submit";
	page?: string;
	action_id?: string;
	values?: Record<string, unknown>;
}

export default {
	routes: {
		admin: {
			handler: async (routeCtx, ctx) => {
				const interaction = routeCtx.input as BlockInteraction;

				if (interaction.type === "page_load" && interaction.page === "/settings") {
					return renderSettings(ctx);
				}

				if (interaction.type === "form_submit" && interaction.action_id === "save") {
					await saveSettings(ctx, interaction.values ?? {});
					return {
						...(await renderSettings(ctx)),
						toast: { message: "Settings saved", type: "success" },
					};
				}

				return { blocks: [] };
			},
		},
	},
} satisfies SandboxedPlugin;

async function renderSettings(ctx: PluginContext) {
	const apiKey = (await ctx.kv.get<string>("settings:apiKey")) ?? "";
	const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
	const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

	return {
		blocks: [
			{ type: "header", text: "Plugin settings" },
			{
				type: "form",
				block_id: "settings",
				fields: [
					{
						type: "secret_input",
						action_id: "apiKey",
						label: "API key",
						initial_value: apiKey,
					},
					{
						type: "toggle",
						action_id: "enabled",
						label: "Enabled",
						initial_value: enabled,
					},
					{
						type: "number_input",
						action_id: "maxItems",
						label: "Max items",
						min: 1,
						max: 1000,
						initial_value: maxItems,
					},
				],
				submit: { label: "Save", action_id: "save" },
			},
		],
	};
}

async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
	for (const [key, value] of Object.entries(values)) {
		if (value !== undefined) {
			await ctx.kv.set(`settings:${key}`, value);
		}
	}
}

설정 페이지를 관리 사이드바에 연결하려면 매니페스트에서 선언합니다:

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

EmDash는 해당 경로에 대한 page_load 상호작용을 자동으로 admin 라우트로 라우팅합니다.

블록 유형, 폼 필드, 조건부 필드, @emdash-cms/blocks 빌더 헬퍼의 전체 세트는 Block Kit을 참조하세요.

비밀 값

Block Kit의 secret_input 필드는 마스크된 입력으로 렌더링됩니다. 사용자가 입력하는 값을 신중하게 처리하세요:

{
	type: "secret_input",
	action_id: "apiKey",
	label: "API key",
	// Don't seed initial_value with the real secret — pass an empty string or a sentinel,
	// and only overwrite when the user enters a non-empty value.
	initial_value: "",
}

저장할 때 빈 문자열을 건너뛰어 매번 저장할 때 기존 비밀을 지우지 않도록 합니다:

async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
	if (typeof values.apiKey === "string" && values.apiKey.length > 0) {
		await ctx.kv.set("settings:apiKey", values.apiKey);
	}
	// ... other fields
}

기본값

KV 읽기는 작성되지 않은 키에 대해 null을 반환합니다. 읽기 사이트에서 기본값을 전달하세요:

const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

또는 설치 중에 기본값을 유지합니다:

hooks: {
	"plugin:install": async (_event, ctx) => {
		await ctx.kv.set("settings:enabled", true);
		await ctx.kv.set("settings:maxItems", 100);
	},
},

절충안은 plugin:install이 설치당 한 번 실행된다는 것입니다. 나중 버전에서 새 설정을 배송하면 새로운 설치만 기본값을 볼 수 있습니다 – 기존 설치는 plugin:activate에서 마이그레이션(멱등: 누락된 경우에만 쓰기)이 필요하거나 읽기 시 폴백을 계속 사용해야 합니다.

설정 vs. storage vs. KV

사용 사례메커니즘
관리자가 편집 가능한 설정settings: 접두사가 있는 ctx.kv + Block Kit 페이지
내부 플러그인 상태state: 접두사가 있는 ctx.kv
문서 컬렉션 (쿼리)ctx.storage

KV는 문자열로 키가 지정된 작은 값용입니다 – 설정, 동기화 커서, 캐시된 계산. 쿼리 없음, 인덱스 없음.

Storage는 인덱싱된 쿼리가 있는 문서 컬렉션용입니다 – 폼 제출, 감사 로그, 필터링, 페이징 또는 카운팅하려는 모든 것.

스토리지 레이아웃

KV 값은 플러그인 네임스페이스 키가 있는 _options 테이블에 있습니다. 코드는 settings:apiKey를 사용합니다; EmDash는 이것을 plugin:<your-plugin-id>:settings:apiKey로 저장합니다. 접두사는 자동으로 추가되며 한 플러그인이 다른 플러그인의 KV 데이터를 읽거나 덮어쓰는 것을 방지합니다.

네이티브 플러그인: settingsSchema

네이티브 플러그인을 작성하는 경우(React 관리 페이지 또는 PT 컴포넌트가 필요하기 때문에) definePlugin() 내부에서 직접 설정 스키마를 선언하고 EmDash가 폼을 자동 생성하도록 할 수 있습니다. 해당 경로는 네이티브 플러그인을 참조하세요.