샌드박스 플러그인은 플러그인별 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가 폼을 자동 생성하도록 할 수 있습니다. 해당 경로는 네이티브 플러그인을 참조하세요.