Files
invyone/frontend/lib/utils/templateAdapter.ts
T
2026-04-24 04:56:30 +09:00

205 lines
6.7 KiB
TypeScript

/**
* INVYONE 스튜디오 ↔ templates 테이블 어댑터
*
* ScreenDesigner 의 layout 객체는 screens 테이블(layout-v2 JSON)을 기준으로 설계되어 있음.
* INVYONE 스튜디오는 templates 테이블(fields/views/connections 3-jsonb 분리) 을 사용하므로
* 두 포맷 사이 왕복 변환을 이 어댑터가 담당한다.
*
* - saveTemplate: 현재 layout + 3뷰 캐시 → templates.views JSON 으로 저장
* - loadTemplateAsLayout: templates.views JSON → ScreenDesigner 가 기대하는 layout + 3뷰 캐시
*
* 규칙: Record<string, any> 원칙 유지 (invyone-component.ts 의 확정 타입 외 금지).
*/
import {
getTemplateInfo,
insertTemplate,
updateTemplate,
} from "@/lib/api/template";
import { convertLegacyToV2, convertV2ToLegacy } from "./layoutV2Converter";
export interface TemplateSavePayload {
templateId: string;
name: string;
category?: string;
description?: string;
primaryTable?: string;
/** 현재 list 뷰 layout (components, gridSettings, screenResolution 포함) */
layout: Record<string, any>;
/** v2 포맷 컴포넌트 배열 — 등록/수정 뷰 */
v2Views?: {
create?: Record<string, any>[];
edit?: Record<string, any>[];
};
/** 뷰별 해상도 */
viewScreenResolutions?: {
list?: Record<string, any>;
create?: Record<string, any>;
edit?: Record<string, any>;
};
/** 템플릿 수준 필드 규격 (아직 미사용, 확장용) */
fields?: Record<string, any>[];
/** DataPort 연결 (아직 미사용, 확장용) */
connections?: Record<string, any>[];
}
/**
* 현재 layout + 3뷰 캐시를 templates.views JSON 으로 직렬화해서 updateTemplate 호출.
* 등록/수정/수정용 뷰는 v2 포맷으로, list 뷰는 convertLegacyToV2 를 거친 후 함께 묶어서 저장.
*/
export async function saveTemplate(p: TemplateSavePayload): Promise<void> {
const v2Layout = convertLegacyToV2(p.layout as any);
// screenResolutions 병합을 key-level 폴백으로 강화.
// 기존: `p.viewScreenResolutions ?? 전체폴백` → {} 나 일부 키 누락 시 정보 유실
// 개선: 항상 3뷰 모두 값이 보장되게 key 별로 폴백 처리
const fallbackSr = p.layout.screenResolution;
const vsrIn = (p.viewScreenResolutions ?? {}) as Record<string, any>;
const screenResolutions = {
list: vsrIn.list ?? fallbackSr,
create: vsrIn.create ?? fallbackSr,
edit: vsrIn.edit ?? fallbackSr,
};
const viewsJson = {
list: (v2Layout as any)?.components ?? [],
create: p.v2Views?.create ?? [],
edit: p.v2Views?.edit ?? [],
gridSettings: p.layout.gridSettings,
screenResolution: fallbackSr,
screenResolutions,
mainTableName: p.primaryTable,
};
// 진단 로그 — 저장 payload 확인용. 문제 재현 시 여기서 확인 가능.
/* eslint-disable no-console */
console.log("[saveTemplate] payload", {
templateId: p.templateId,
viewsKeys: Object.keys(viewsJson),
screenResolutions,
listCount: viewsJson.list.length,
createCount: viewsJson.create.length,
editCount: viewsJson.edit.length,
});
/* eslint-enable no-console */
const payload: Record<string, any> = {
name: p.name,
category: p.category ?? "custom",
description: p.description ?? "",
primary_table: p.primaryTable ?? "",
fields: JSON.stringify(p.fields ?? []),
views: JSON.stringify(viewsJson),
connections: JSON.stringify(p.connections ?? []),
};
await updateTemplate(p.templateId, payload);
}
/**
* 신규 template 레코드 생성 — 빈 views/fields/connections 로 시작.
* 반환된 template_id 는 URL 쿼리 등에 사용.
*/
export async function createTemplate(data: {
name: string;
category?: string;
description?: string;
primaryTable?: string;
}): Promise<Record<string, any>> {
const payload: Record<string, any> = {
name: data.name,
category: data.category ?? "custom",
description: data.description ?? "",
primary_table: data.primaryTable ?? "",
fields: JSON.stringify([]),
views: JSON.stringify({ list: [], create: [], edit: [] }),
connections: JSON.stringify([]),
};
const res = await insertTemplate(payload);
return res ?? {};
}
export interface LoadedTemplate {
templateInfo: Record<string, any>;
/** ScreenDesigner 의 layout 상태에 그대로 setLayout 으로 주입 가능 */
layout: Record<string, any>;
/** viewLayoutsRef.current.create / edit 에 주입할 legacy 컴포넌트 배열 */
viewLayouts: {
create: any[];
edit: any[];
};
primaryTable: string;
screenResolution?: Record<string, any>;
viewScreenResolutions?: {
list?: Record<string, any>;
create?: Record<string, any>;
edit?: Record<string, any>;
};
}
function parseJsonMaybe(raw: any): any {
if (raw == null) return null;
if (typeof raw === "object") return raw;
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
return null;
}
/**
* templates.views JSON → ScreenDesigner layout + 3뷰 캐시로 역직렬화.
* 목록/등록/수정 3뷰가 모두 저장되어 있으면 각각 Legacy 포맷으로 변환.
*/
export async function loadTemplateAsLayout(
templateId: string,
): Promise<LoadedTemplate | null> {
const template = await getTemplateInfo(templateId);
if (!template) return null;
const viewsObj =
parseJsonMaybe(template.views) ?? parseJsonMaybe(template.VIEWS) ?? {};
const listV2 = Array.isArray(viewsObj.list) ? viewsObj.list : [];
const createV2 = Array.isArray(viewsObj.create) ? viewsObj.create : [];
const editV2 = Array.isArray(viewsObj.edit) ? viewsObj.edit : [];
const viewScreenResolutions = viewsObj.screenResolutions ?? {
list: viewsObj.screenResolution,
create: viewsObj.screenResolution,
edit: viewsObj.screenResolution,
};
const legacyListLayout = convertV2ToLegacy({
components: listV2,
gridSettings: viewsObj.gridSettings,
screenResolution: viewsObj.screenResolution,
} as any) ?? { components: [] };
const createLegacy = createV2.length
? convertV2ToLegacy({ components: createV2 } as any)?.components ?? []
: [];
const editLegacy = editV2.length
? convertV2ToLegacy({ components: editV2 } as any)?.components ?? []
: [];
return {
templateInfo: template,
layout: {
...legacyListLayout,
gridSettings: viewsObj.gridSettings ?? (legacyListLayout as any).gridSettings,
screenResolution:
viewsObj.screenResolution ?? (legacyListLayout as any).screenResolution,
},
viewLayouts: {
create: createLegacy,
edit: editLegacy,
},
primaryTable:
template.primary_table ?? template.PRIMARY_TABLE ?? viewsObj.mainTableName ?? "",
screenResolution: viewsObj.screenResolution,
viewScreenResolutions,
};
}