246 lines
8.9 KiB
TypeScript
246 lines
8.9 KiB
TypeScript
/**
|
|
* FieldConfig Adapter — INVYONE FieldConfig[] 를 기존 v2-* 컴포넌트 내부 포맷
|
|
* (ColumnConfig / SearchField / FormField) 으로 변환한다.
|
|
*
|
|
* 역할:
|
|
* 화면 디자이너에서 Screen 수준으로 정의한 fields 배열을 각 컴포넌트가
|
|
* 소비할 수 있는 기존 포맷으로 풀어주는 브리지. 컴포넌트 자체를 FieldConfig
|
|
* 네이티브로 리팩토링하기 전까지의 호환 레이어.
|
|
*
|
|
* 원칙:
|
|
* - 호환성 우선. 기존 포맷에 없는 필드는 버리고 기본값은 보존.
|
|
* - 타입은 Record<string, any>. v2-* 포맷이 컴포넌트마다 달라 강타입 못 씀.
|
|
* - fields 가 없으면 호출부는 fallback 으로 기존 config.columns 사용 (이 파일
|
|
* 호출 자체를 건너뛰어야 함).
|
|
*/
|
|
import type { FieldConfig } from "@/types/invyone-component";
|
|
|
|
/**
|
|
* FieldConfig[] → snake_case ColumnConfig[] 호환 배열.
|
|
*
|
|
* (Phase F.2/F.8 이전 옛 table-list 본체가 소비하던 형태. 본체는 삭제됐고 현재는
|
|
* 외부 코드가 snake_case 컬럼을 기대할 때의 호환 변환에만 사용된다.)
|
|
*
|
|
* canonical `TableComponent` 는 `fieldsToCanonicalColumns` (camelCase 키 + 풀 옵션) 를
|
|
* 사용한다 (Phase C.2 신설).
|
|
*
|
|
* 공통 필드 (column_name / column_label / visible / display_order / width / align /
|
|
* sortable / data_type / format / pk / editable) 외에 Phase C.2 에서 칸 단위 옵션
|
|
* (`searchable` / `input_type` / `thousand_separator`) 도 함께 매핑. FieldConfig 에 없는
|
|
* legacy-only 옵션 (`fixed` / `hidden` / entity 조인 메타) 은 매핑하지 않으며 옛 본체의 자체
|
|
* ConfigPanel 에서 set 된 값이 그대로 column 객체에 머무른다.
|
|
*/
|
|
export function fieldsToColumns(
|
|
fields: FieldConfig[],
|
|
): Record<string, any>[] {
|
|
return [...fields]
|
|
.filter((f) => f.visible !== false && !f.system)
|
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
.map((f) => {
|
|
const isNumberFormat =
|
|
f.type === "number" || f.format === "number" || f.format === "currency";
|
|
// Phase D.10 — FieldConfig 자체엔 autoGeneration 이 없지만 legacy layout 의
|
|
// 원본 field 객체에 박혀들어올 수 있으므로 (any 캐스트로) 메타 보존.
|
|
const autoGen =
|
|
(f as any).autoGeneration ?? (f as any).auto_generation ?? undefined;
|
|
return {
|
|
column_name: f.column,
|
|
column_label: f.label,
|
|
label: f.label,
|
|
visible: f.visible !== false,
|
|
display_order: f.order ?? 0,
|
|
width: f.width,
|
|
align: f.align ?? (f.type === "number" ? "right" : "left"),
|
|
sortable: f.sortable ?? true,
|
|
searchable: f.searchable === true,
|
|
data_type: f.type,
|
|
input_type: f.type,
|
|
format: f.format,
|
|
pk: f.pk ?? false,
|
|
editable: f.editable ?? true,
|
|
thousand_separator: isNumberFormat ? true : undefined,
|
|
...(autoGen ? { autoGeneration: autoGen, auto_generation: autoGen } : {}),
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* FieldConfig[] → canonical `TableColumn[]` (camelCase, `frontend/lib/registry/components/table/types.ts`).
|
|
*
|
|
* 신설 (Phase C.2 2026-05-20). canonical `TableComponent` 의 fields → columns 변환 경로에서 사용.
|
|
* FieldConfig 에 표현되지 않는 옵션 (fixed / fixedOrder / hidden / entity 조인 메타) 은
|
|
* undefined 로 두고 ConfigPanel 에서 사용자가 set 한 값이 별도 보존되도록 한다.
|
|
*
|
|
* Phase D.1 부터 canonical table 은 source columns 와 render columns 를 분리하므로
|
|
* `visible === false` 도 source 로 보존한다. 실제 표시/숨김은 `TableComponent` 의
|
|
* renderColumns derive 단계에서 처리한다.
|
|
*/
|
|
export function fieldsToCanonicalColumns(
|
|
fields: FieldConfig[],
|
|
): Array<Record<string, any>> {
|
|
return [...fields]
|
|
.filter((f) => !f.system)
|
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
.map((f) => {
|
|
const isNumberFormat =
|
|
f.type === "number" || f.format === "number" || f.format === "currency";
|
|
// Phase D.10 — autoGeneration 메타 보존. FieldConfig 자체엔 없지만 legacy field 가
|
|
// 옵션 필드로 들고 들어올 수 있어 any 캐스트로 흡수. runtime 적용은 별도 phase.
|
|
const autoGen =
|
|
(f as any).autoGeneration ?? (f as any).auto_generation ?? undefined;
|
|
return {
|
|
key: f.column,
|
|
label: f.label,
|
|
visible: f.visible !== false,
|
|
order: f.order ?? 0,
|
|
width: f.width,
|
|
align: f.align ?? (f.type === "number" ? "right" : "left"),
|
|
sortable: f.sortable ?? true,
|
|
searchable: f.searchable === true,
|
|
editable: f.editable ?? true,
|
|
format: f.format,
|
|
// FieldConfig.type 을 inputType / dataType 양쪽으로 미러링 (D.3/D.5 wiring).
|
|
// 사용자가 ConfigPanel 에서 명시 inputType 을 set 했으면 그쪽이 우선 (ConfigPanel
|
|
// 출력 columns 가 fields 머지보다 후위로 들어가므로).
|
|
inputType: f.type,
|
|
dataType: f.type,
|
|
thousandSeparator: isNumberFormat ? true : undefined,
|
|
...(autoGen ? { autoGeneration: autoGen } : {}),
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* FieldConfig[] → 검색 위젯의 SearchField[] 호환 배열.
|
|
*
|
|
* searchable: true 인 것만 포함. 타입별 기본 검색 모드 매핑:
|
|
* date/datetime/number → 'range'
|
|
* select → 'multi'
|
|
* entity → 'single' (팝업)
|
|
* code → 'exact'
|
|
* text/textarea → 'partial'
|
|
* checkbox → 'tri' (전체/✓/✗)
|
|
*/
|
|
export function fieldsToSearchFields(
|
|
fields: FieldConfig[],
|
|
): Record<string, any>[] {
|
|
return fields
|
|
.filter((f) => f.searchable && !f.system)
|
|
.map((f) => ({
|
|
column_name: f.column,
|
|
column_label: f.label,
|
|
label: f.label,
|
|
data_type: f.type,
|
|
search_mode: getDefaultSearchMode(f.type),
|
|
options: f.options,
|
|
ref: f.ref,
|
|
default_value: f.defaultValue,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* FieldConfig[] → 폼 컴포넌트의 FormField[] 호환 배열.
|
|
*
|
|
* system 필드는 자동 제외. required / editable / options / ref / computed 는
|
|
* 그대로 전달. pk 이고 code 타입이면 readonly 자동 설정 (자동채번).
|
|
*/
|
|
export function fieldsToFormFields(
|
|
fields: FieldConfig[],
|
|
): Record<string, any>[] {
|
|
return [...fields]
|
|
.filter((f) => f.visible !== false && !f.system)
|
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
.map((f) => ({
|
|
column_name: f.column,
|
|
column_label: f.label,
|
|
label: f.label,
|
|
data_type: f.type,
|
|
required: f.required ?? false,
|
|
editable: f.editable ?? true,
|
|
readonly: f.editable === false || f.type === "code",
|
|
default_value: f.defaultValue,
|
|
placeholder: f.placeholder,
|
|
options: f.options,
|
|
ref: f.ref,
|
|
format: f.format,
|
|
computed: f.computed,
|
|
pk: f.pk ?? false,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* 역방향: 기존 컬럼 포맷 → FieldConfig[] 추정.
|
|
*
|
|
* 마이그레이션 도우미. 완벽하지 않음 (검색 모드 역추정 불가, computed 등 누락
|
|
* 가능). 화면 디자이너에서 기존 Screen 을 "FieldConfig 로 자동 채우기" 할 때
|
|
* 초기값 생성용.
|
|
*/
|
|
export function columnsToFields(
|
|
columns: Record<string, any>[],
|
|
): FieldConfig[] {
|
|
return columns.map((col, idx) => ({
|
|
column: col.column_name ?? col.column ?? `col_${idx}`,
|
|
label:
|
|
col.column_label ?? col.label ?? col.column_name ?? `컬럼 ${idx + 1}`,
|
|
type: normalizeType(col.data_type ?? col.type ?? "text"),
|
|
visible: col.visible !== false,
|
|
order: col.display_order ?? idx,
|
|
required: col.required === true,
|
|
editable: col.editable !== false,
|
|
width: col.width,
|
|
align: col.align,
|
|
sortable: col.sortable !== false,
|
|
searchable: col.searchable === true,
|
|
format: col.format,
|
|
options: col.options,
|
|
ref: col.ref,
|
|
pk: col.pk === true,
|
|
system: col.system === true,
|
|
}));
|
|
}
|
|
|
|
// ─── 내부 헬퍼 ──────────────────────────────────────────────────────────
|
|
|
|
type SearchMode = "exact" | "partial" | "range" | "multi" | "single" | "tri";
|
|
|
|
function getDefaultSearchMode(type: FieldConfig["type"]): SearchMode {
|
|
switch (type) {
|
|
case "date":
|
|
case "datetime":
|
|
case "number":
|
|
return "range";
|
|
case "select":
|
|
return "multi";
|
|
case "entity":
|
|
return "single";
|
|
case "code":
|
|
return "exact";
|
|
case "checkbox":
|
|
return "tri";
|
|
case "text":
|
|
case "textarea":
|
|
default:
|
|
return "partial";
|
|
}
|
|
}
|
|
|
|
function normalizeType(raw: string): FieldConfig["type"] {
|
|
const allowed: FieldConfig["type"][] = [
|
|
"text",
|
|
"number",
|
|
"date",
|
|
"datetime",
|
|
"time",
|
|
"daterange",
|
|
"select",
|
|
"entity",
|
|
"checkbox",
|
|
"textarea",
|
|
"file",
|
|
"code",
|
|
];
|
|
return (allowed as string[]).includes(raw)
|
|
? (raw as FieldConfig["type"])
|
|
: "text";
|
|
}
|