fix(M02): 양식 가져오기 목록이 서버 묶음(층 · 사람 · 프로젝트)을 그대로 펴서 보임 (PLAN 10-1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148XFUpPfpiuTjxF1EK9c95
This commit is contained in:
2026-09-25 10:06:12 +09:00
co-authored by Claude Sonnet 5
parent 31c9fb6c61
commit 9ca31ef7e8
2 changed files with 36 additions and 26 deletions
@@ -101,16 +101,6 @@ export const saveProjectAs = (
이름: string,
): Promise<unknown> => post(project(id, "save-as"), { to, 종류, 이름 });
export interface SourceItem {
from: Layer;
label?: string;
user_id?: string;
project_id?: string;
종류?: Kind;
이름?: string;
[key: string]: unknown;
}
export const fetchSources = (id: string): Promise<unknown> => call(`/projects/${enc(id)}/sources`);
/** 설계값을 채운 표 문서 — 서버가 그때그때 채움 · 저장 안 함 */
@@ -91,17 +91,35 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
const failed = (error: unknown): void =>
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
/** 가져올 곳 목록 — 모양이 배열이든 묶음 객체든 한 줄씩 펴서 받음(길이 채워지면 맞춤) */
function flatten(raw: unknown): Record<string, unknown>[] {
if (Array.isArray(raw)) {
return raw.filter((x): x is Record<string, unknown> => !!x && typeof x === "object");
type ApplyFrom = Parameters<typeof applyToProject>[1];
interface Source {
label: string;
body: ApplyFrom;
}
/** 가져올 곳 — 서버 `sources` 는 층마다 묶음 · 개인·프로젝트는 사람·프로젝트 아래 양식 목록 */
function sourceItems(raw: unknown): Source[] {
const group = (raw ?? {}) as Record<string, Record<string, unknown>[] | undefined>;
const layerName: Record<string, string> = {
system: L("M02_LayerSystem"),
company: L("M02_LayerCompany"),
personal: L("M02_LayerPersonal"),
project: L("M02_LayerProject"),
};
const out: Source[] = [];
for (const from of ["system", "company", "personal", "project"] as const) {
for (const entry of group[from] ?? []) {
const templates = (Array.isArray(entry.양식) ? entry.양식 : [entry]) as TemplateInfo[];
const owner = entry.name ? ` · ${String(entry.name)}` : "";
for (const t of templates) {
const body: ApplyFrom = { from, 종류: t.종류, 이름: t.이름 };
if (entry.user_id != null) body.user_id = String(entry.user_id);
if (entry.project_id != null) body.project_id = String(entry.project_id);
out.push({ label: `${layerName[from]}${owner} · ${t.이름}`, body });
}
}
}
if (raw && typeof raw === "object") {
return Object.entries(raw).flatMap(([from, v]) =>
flatten(v).map((item) => ({ from: item.from ?? from, ...item })),
);
}
return [];
return out;
}
export function buildSide(opt: SideOptions): SideHandle {
@@ -337,9 +355,9 @@ export function buildSide(opt: SideOptions): SideHandle {
}
});
projectButton(L("M02_ProjectImport"), async () => {
let sources: Record<string, unknown>[] = [];
let sources: Source[] = [];
try {
sources = flatten(await fetchSources(projectId));
sources = sourceItems(await fetchSources(projectId));
} catch (error) {
return failed(error);
}
@@ -352,12 +370,14 @@ export function buildSide(opt: SideOptions): SideHandle {
body.append(el("p", { className: "m02-side__empty", text: L("M02_ImportNone") }));
}
for (const src of sources) {
const text = String(src.label ?? src.이름 ?? src.from ?? JSON.stringify(src));
const b = el("button", { className: "m02-side__item", text, attrs: { type: "button" } });
const b = el("button", {
className: "m02-side__item",
text: src.label,
attrs: { type: "button" },
});
b.addEventListener("click", () => {
close();
const { label: _label, ...rest } = src;
void act(() => applyToProject(projectId, rest as never));
void act(() => applyToProject(projectId, src.body));
});
body.append(b);
}