반복 데이터 입력컴포넌트 통합중
This commit is contained in:
parent
2513b89ca2
commit
97675458d7
|
|
@ -92,6 +92,14 @@ export function ComponentsPanel({
|
|||
tags: ["tree", "org", "bom", "cascading", "unified"],
|
||||
defaultSize: { width: 400, height: 300 },
|
||||
},
|
||||
{
|
||||
id: "unified-repeater",
|
||||
name: "통합 반복 데이터",
|
||||
description: "반복 데이터 관리 (인라인/모달/버튼 모드)",
|
||||
category: "data" as ComponentCategory,
|
||||
tags: ["repeater", "table", "modal", "button", "unified"],
|
||||
defaultSize: { width: 600, height: 300 },
|
||||
},
|
||||
], []);
|
||||
|
||||
// 카테고리별 컴포넌트 그룹화
|
||||
|
|
|
|||
|
|
@ -3,22 +3,20 @@
|
|||
/**
|
||||
* UnifiedRepeater 컴포넌트
|
||||
*
|
||||
* 기존 컴포넌트 통합:
|
||||
* - simple-repeater-table: 인라인 모드
|
||||
* - modal-repeater-table: 모달 모드
|
||||
* - repeat-screen-modal: 화면 기반 모달 모드
|
||||
* - related-data-buttons: 버튼 모드
|
||||
*
|
||||
* 모든 하드코딩을 제거하고 설정 기반으로 동작합니다.
|
||||
* 렌더링 모드:
|
||||
* - inline: 현재 테이블 컬럼 직접 입력
|
||||
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
|
||||
* - button: 버튼으로 관련 화면/모달 열기
|
||||
* - mixed: inline + modal 혼합
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Plus, Trash2, Edit, Eye, GripVertical } from "lucide-react";
|
||||
import { Plus, Trash2, GripVertical, Search, X, Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
UnifiedRepeaterConfig,
|
||||
|
|
@ -61,42 +59,83 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
[propConfig],
|
||||
);
|
||||
|
||||
// 상태
|
||||
// 상태 - 메인 데이터
|
||||
const [data, setData] = useState<any[]>(initialData || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [editingRow, setEditingRow] = useState<number | null>(null);
|
||||
const [editedData, setEditedData] = useState<Record<string, any>>({});
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalRow, setModalRow] = useState<any>(null);
|
||||
|
||||
// 상태 - 검색 모달 (modal 모드)
|
||||
const [searchModalOpen, setSearchModalOpen] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [selectedSearchItems, setSelectedSearchItems] = useState<Set<number>>(new Set());
|
||||
|
||||
// 상태 - 버튼 모드
|
||||
const [codeButtons, setCodeButtons] = useState<{ label: string; value: string; variant?: string }[]>([]);
|
||||
|
||||
// 데이터 로드
|
||||
const loadData = useCallback(async () => {
|
||||
if (!config.dataSource?.tableName || !parentId) return;
|
||||
// 상태 - 엔티티 표시 정보 캐시 (FK값 → 표시 데이터)
|
||||
const [entityDisplayCache, setEntityDisplayCache] = useState<Record<string | number, Record<string, any>>>({});
|
||||
|
||||
// 상태 - 소스 테이블 컬럼 정보 (라벨 매핑용)
|
||||
const [sourceTableColumnMap, setSourceTableColumnMap] = useState<Record<string, string>>({});
|
||||
|
||||
// 외부 데이터 변경 시 동기화
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setData(initialData);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// 소스 테이블 컬럼 정보 로드 (라벨 매핑용)
|
||||
useEffect(() => {
|
||||
const loadSourceTableColumns = async () => {
|
||||
const sourceTable = config.dataSource?.sourceTable;
|
||||
if (!sourceTable) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(`/dynamic-form/${config.dataSource.tableName}`, {
|
||||
params: {
|
||||
[config.dataSource.foreignKey]: parentId,
|
||||
},
|
||||
const response = await apiClient.get(`/table-management/tables/${sourceTable}/columns`);
|
||||
console.log("소스 테이블 컬럼 API 응답:", response.data);
|
||||
|
||||
const colMap: Record<string, string> = {};
|
||||
|
||||
// 응답 구조에 따라 데이터 추출 - data.data.columns 형태 지원
|
||||
let columns: any[] = [];
|
||||
if (response.data?.success && response.data?.data) {
|
||||
// { success: true, data: { columns: [...] } } 구조
|
||||
if (Array.isArray(response.data.data.columns)) {
|
||||
columns = response.data.data.columns;
|
||||
} else if (Array.isArray(response.data.data)) {
|
||||
columns = response.data.data;
|
||||
}
|
||||
} else if (Array.isArray(response.data)) {
|
||||
columns = response.data;
|
||||
}
|
||||
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
const colLabel = col.displayName || col.columnLabel || col.column_label || colName;
|
||||
if (colName) {
|
||||
colMap[colName] = colLabel;
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data?.success && response.data?.data) {
|
||||
const items = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
|
||||
setData(items);
|
||||
onDataChange?.(items);
|
||||
}
|
||||
console.log("sourceTableColumnMap:", colMap);
|
||||
setSourceTableColumnMap(colMap);
|
||||
} catch (error) {
|
||||
console.error("UnifiedRepeater 데이터 로드 실패:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}, [config.dataSource?.tableName, config.dataSource?.foreignKey, parentId, onDataChange]);
|
||||
};
|
||||
|
||||
if (config.renderMode === "modal" || config.renderMode === "mixed") {
|
||||
loadSourceTableColumns();
|
||||
}
|
||||
}, [config.dataSource?.sourceTable, config.renderMode]);
|
||||
|
||||
// 공통코드 버튼 로드
|
||||
const loadCodeButtons = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
const loadCodeButtons = async () => {
|
||||
if (config.button?.sourceType !== "commonCode" || !config.button?.commonCode?.categoryCode) return;
|
||||
|
||||
try {
|
||||
|
|
@ -114,18 +153,51 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
} catch (error) {
|
||||
console.error("공통코드 버튼 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadCodeButtons();
|
||||
}, [config.button?.sourceType, config.button?.commonCode]);
|
||||
|
||||
// 초기 로드
|
||||
useEffect(() => {
|
||||
if (!initialData) {
|
||||
loadData();
|
||||
}
|
||||
}, [loadData, initialData]);
|
||||
// 소스 테이블 검색 (modal 모드)
|
||||
const searchSourceTable = useCallback(async () => {
|
||||
const sourceTable = config.dataSource?.sourceTable;
|
||||
if (!sourceTable) return;
|
||||
|
||||
setSearchLoading(true);
|
||||
try {
|
||||
const searchParams: any = {};
|
||||
|
||||
// 검색어가 있고, 검색 필드가 설정되어 있으면 검색
|
||||
if (searchKeyword && config.modal?.searchFields?.length) {
|
||||
// 검색 필드들에 검색어 적용
|
||||
config.modal.searchFields.forEach((field) => {
|
||||
searchParams[field] = searchKeyword;
|
||||
});
|
||||
}
|
||||
|
||||
const response = await apiClient.post(`/table-management/tables/${sourceTable}/data`, {
|
||||
search: Object.keys(searchParams).length > 0 ? searchParams : undefined,
|
||||
page: 1,
|
||||
size: 50,
|
||||
});
|
||||
|
||||
if (response.data?.success && response.data?.data) {
|
||||
const items = Array.isArray(response.data.data) ? response.data.data : response.data.data.data || [];
|
||||
setSearchResults(items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("검색 실패:", error);
|
||||
setSearchResults([]);
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
}, [config.dataSource?.sourceTable, config.modal?.searchFields, searchKeyword]);
|
||||
|
||||
// 검색 모달 열릴 때 자동 검색
|
||||
useEffect(() => {
|
||||
loadCodeButtons();
|
||||
}, [loadCodeButtons]);
|
||||
if (searchModalOpen) {
|
||||
searchSourceTable();
|
||||
}
|
||||
}, [searchModalOpen, searchSourceTable]);
|
||||
|
||||
// 행 선택 토글
|
||||
const toggleRowSelection = (index: number) => {
|
||||
|
|
@ -145,42 +217,128 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
// 검색 결과 항목 선택 토글 (검색 모달에서는 항상 다중 선택 허용)
|
||||
const toggleSearchItemSelection = (index: number) => {
|
||||
setSelectedSearchItems((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
} else {
|
||||
newSet.add(index);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 검색 항목 선택 확인 - FK만 저장하고 추가 입력 컬럼은 빈 값으로
|
||||
const confirmSearchSelection = () => {
|
||||
const selectedItems = Array.from(selectedSearchItems).map((index) => searchResults[index]).filter(Boolean);
|
||||
|
||||
console.log("confirmSearchSelection 호출:", {
|
||||
selectedSearchItems: Array.from(selectedSearchItems),
|
||||
selectedItems,
|
||||
searchResults: searchResults.length,
|
||||
foreignKey: config.dataSource?.foreignKey,
|
||||
referenceKey: config.dataSource?.referenceKey,
|
||||
});
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
console.warn("선택된 항목이 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
const newRows = selectedItems.map((sourceItem) => {
|
||||
const newRow: any = {};
|
||||
|
||||
// 1. FK 저장 (엔티티 참조 ID만!)
|
||||
if (config.dataSource?.foreignKey && config.dataSource?.referenceKey) {
|
||||
newRow[config.dataSource.foreignKey] = sourceItem[config.dataSource.referenceKey];
|
||||
console.log("FK 저장:", config.dataSource.foreignKey, "=", sourceItem[config.dataSource.referenceKey]);
|
||||
}
|
||||
|
||||
// 2. 추가 입력 컬럼은 빈 값으로 초기화
|
||||
config.columns.forEach((col) => {
|
||||
if (newRow[col.key] === undefined) {
|
||||
newRow[col.key] = "";
|
||||
}
|
||||
});
|
||||
|
||||
return newRow;
|
||||
});
|
||||
|
||||
// 선택한 항목의 표시 정보를 캐시에 저장 (렌더링용)
|
||||
const newDisplayCache = { ...entityDisplayCache };
|
||||
selectedItems.forEach((sourceItem) => {
|
||||
const fkValue = sourceItem[config.dataSource?.referenceKey || "id"];
|
||||
if (fkValue) {
|
||||
const displayData: Record<string, any> = {};
|
||||
// 모달 표시 컬럼들의 값을 캐시 - sourceDisplayColumns (useMemo로 변환된 것) 사용
|
||||
sourceDisplayColumns.forEach((col) => {
|
||||
displayData[col.key] = sourceItem[col.key];
|
||||
});
|
||||
// displayColumn도 캐시
|
||||
if (config.dataSource?.displayColumn) {
|
||||
displayData[config.dataSource.displayColumn] = sourceItem[config.dataSource.displayColumn];
|
||||
}
|
||||
newDisplayCache[fkValue] = displayData;
|
||||
console.log("캐시 저장:", fkValue, displayData);
|
||||
}
|
||||
});
|
||||
setEntityDisplayCache(newDisplayCache);
|
||||
|
||||
const newData = [...data, ...newRows];
|
||||
const firstNewRowIndex = data.length; // 기존 data 길이가 첫 새 행의 인덱스
|
||||
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
|
||||
// 모달 닫기 및 초기화
|
||||
setSearchModalOpen(false);
|
||||
setSelectedSearchItems(new Set());
|
||||
setSearchKeyword("");
|
||||
|
||||
// 마지막 추가된 행을 편집 모드로 (setTimeout으로 state 업데이트 후 실행)
|
||||
const hasEditableColumns = config.columns.length > 0;
|
||||
console.log("편집 모드 전환 체크:", {
|
||||
newRowsLength: newRows.length,
|
||||
inlineEdit: config.features?.inlineEdit,
|
||||
hasEditableColumns,
|
||||
columnsLength: config.columns.length,
|
||||
columns: config.columns,
|
||||
});
|
||||
|
||||
// 추가 입력 컬럼이 있으면 편집 모드 시작
|
||||
if (newRows.length > 0 && hasEditableColumns) {
|
||||
setTimeout(() => {
|
||||
console.log("편집 모드 시작:", firstNewRowIndex, newRows[0]);
|
||||
setEditingRow(firstNewRowIndex);
|
||||
setEditedData({ ...newRows[0] });
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 행 추가
|
||||
const handleAddRow = async () => {
|
||||
const handleAddRow = () => {
|
||||
if (config.renderMode === "modal" || config.renderMode === "mixed") {
|
||||
setModalRow(null);
|
||||
setModalOpen(true);
|
||||
// 검색 모달 열기
|
||||
setSearchModalOpen(true);
|
||||
} else {
|
||||
// 인라인 추가
|
||||
const newRow: any = {};
|
||||
config.columns.forEach((col) => {
|
||||
newRow[col.key] = "";
|
||||
});
|
||||
if (config.dataSource?.foreignKey && parentId) {
|
||||
newRow[config.dataSource.foreignKey] = parentId;
|
||||
}
|
||||
|
||||
const newData = [...data, newRow];
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
setEditingRow(newData.length - 1);
|
||||
setEditedData(newRow);
|
||||
}
|
||||
};
|
||||
|
||||
// 행 삭제
|
||||
const handleDeleteRow = async (index: number) => {
|
||||
const row = data[index];
|
||||
const rowId = row?.id || row?.objid;
|
||||
|
||||
if (rowId && config.dataSource?.tableName) {
|
||||
try {
|
||||
await apiClient.delete(`/dynamic-form/${config.dataSource.tableName}/${rowId}`);
|
||||
} catch (error) {
|
||||
console.error("행 삭제 실패:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 행 삭제 (로컬 상태만)
|
||||
const handleDeleteRow = (index: number) => {
|
||||
const newData = data.filter((_, i) => i !== index);
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
|
|
@ -189,49 +347,38 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
newSet.delete(index);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// 편집 중이던 행이 삭제되면 편집 취소
|
||||
if (editingRow === index) {
|
||||
setEditingRow(null);
|
||||
setEditedData({});
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 행 일괄 삭제
|
||||
const handleDeleteSelected = async () => {
|
||||
const handleDeleteSelected = () => {
|
||||
if (selectedRows.size === 0) return;
|
||||
|
||||
const indices = Array.from(selectedRows).sort((a, b) => b - a); // 역순 정렬
|
||||
const indices = Array.from(selectedRows).sort((a, b) => b - a);
|
||||
let newData = [...data];
|
||||
for (const index of indices) {
|
||||
await handleDeleteRow(index);
|
||||
newData = newData.filter((_, i) => i !== index);
|
||||
}
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
setSelectedRows(new Set());
|
||||
setEditingRow(null);
|
||||
setEditedData({});
|
||||
};
|
||||
|
||||
// 인라인 편집 시작
|
||||
const handleEditRow = (index: number) => {
|
||||
if (!config.features?.inlineEdit) return;
|
||||
setEditingRow(index);
|
||||
setEditedData({ ...data[index] });
|
||||
};
|
||||
|
||||
// 인라인 편집 저장
|
||||
const handleSaveEdit = async () => {
|
||||
// 인라인 편집 확인
|
||||
const handleConfirmEdit = () => {
|
||||
if (editingRow === null) return;
|
||||
|
||||
const rowId = editedData?.id || editedData?.objid;
|
||||
|
||||
try {
|
||||
if (rowId && config.dataSource?.tableName) {
|
||||
await apiClient.put(`/dynamic-form/${config.dataSource.tableName}/${rowId}`, editedData);
|
||||
} else if (config.dataSource?.tableName) {
|
||||
const response = await apiClient.post(`/dynamic-form/${config.dataSource.tableName}`, editedData);
|
||||
if (response.data?.data?.id) {
|
||||
editedData.id = response.data.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
const newData = [...data];
|
||||
newData[editingRow] = editedData;
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
}
|
||||
|
||||
setEditingRow(null);
|
||||
setEditedData({});
|
||||
|
|
@ -245,61 +392,51 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
|
||||
// 행 클릭
|
||||
const handleRowClick = (row: any, index: number) => {
|
||||
console.log("handleRowClick 호출:", {
|
||||
index,
|
||||
row,
|
||||
currentEditingRow: editingRow,
|
||||
inlineEdit: config.features?.inlineEdit,
|
||||
columns: config.columns
|
||||
});
|
||||
|
||||
if (editingRow === index) return;
|
||||
|
||||
if (editingRow !== null) {
|
||||
handleConfirmEdit();
|
||||
}
|
||||
|
||||
if (config.features?.selectable) {
|
||||
toggleRowSelection(index);
|
||||
}
|
||||
|
||||
onRowClick?.(row);
|
||||
|
||||
if (config.renderMode === "modal" || config.renderMode === "mixed") {
|
||||
setModalRow(row);
|
||||
setModalOpen(true);
|
||||
// 인라인 편집 모드면 편집 시작 (모달 모드에서도 추가 입력 컬럼이 있으면 편집 가능)
|
||||
const hasEditableColumns = config.columns.length > 0;
|
||||
const isModalRenderMode = config.renderMode === "modal" || config.renderMode === "mixed";
|
||||
if (config.features?.inlineEdit || (isModalRenderMode && hasEditableColumns)) {
|
||||
console.log("편집 모드 활성화:", { index, row });
|
||||
setEditingRow(index);
|
||||
setEditedData({ ...row });
|
||||
}
|
||||
};
|
||||
|
||||
// 버튼 클릭 핸들러
|
||||
const handleButtonAction = (action: ButtonActionType, row?: any, buttonConfig?: RepeaterButtonConfig) => {
|
||||
onButtonClick?.(action, row, buttonConfig);
|
||||
|
||||
if (action === "view" && row) {
|
||||
setModalRow(row);
|
||||
setModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 공통코드 버튼 클릭
|
||||
const handleCodeButtonClick = async (codeValue: string, row: any, index: number) => {
|
||||
const handleCodeButtonClick = (codeValue: string, row: any, index: number) => {
|
||||
const valueField = config.button?.commonCode?.valueField;
|
||||
if (!valueField) return;
|
||||
|
||||
const updatedRow = { ...row, [valueField]: codeValue };
|
||||
const rowId = row?.id || row?.objid;
|
||||
|
||||
try {
|
||||
if (rowId && config.dataSource?.tableName) {
|
||||
await apiClient.put(`/dynamic-form/${config.dataSource.tableName}/${rowId}`, { [valueField]: codeValue });
|
||||
}
|
||||
|
||||
const newData = [...data];
|
||||
newData[index] = updatedRow;
|
||||
setData(newData);
|
||||
onDataChange?.(newData);
|
||||
} catch (error) {
|
||||
console.error("상태 변경 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 모달 제목 생성
|
||||
const getModalTitle = (row?: any) => {
|
||||
const template = config.modal?.titleTemplate;
|
||||
if (!template) return row ? "상세 보기" : "새 항목";
|
||||
|
||||
let title = template.prefix || "";
|
||||
if (template.columnKey && row?.[template.columnKey]) {
|
||||
title += row[template.columnKey];
|
||||
}
|
||||
title += template.suffix || "";
|
||||
|
||||
return title || (row ? "상세 보기" : "새 항목");
|
||||
};
|
||||
|
||||
// 버튼 렌더링
|
||||
|
|
@ -328,7 +465,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
// 수동 버튼
|
||||
return (
|
||||
<div className={cn("flex gap-1", isVertical && "flex-col")}>
|
||||
{(config.button?.manualButtons || []).map((btn) => (
|
||||
|
|
@ -349,27 +485,60 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// 테이블 렌더링 (inline, mixed 모드)
|
||||
// FK 값으로 캐시에서 표시 정보 가져오기
|
||||
const getEntityDisplayInfo = (fkValue: any): Record<string, any> | null => {
|
||||
if (!fkValue) return null;
|
||||
return entityDisplayCache[fkValue] || null;
|
||||
};
|
||||
|
||||
// 테이블 렌더링
|
||||
const renderTable = () => {
|
||||
if (config.renderMode === "button") return null;
|
||||
|
||||
const fkColumn = config.dataSource?.foreignKey;
|
||||
const isModalMode = config.renderMode === "modal" || config.renderMode === "mixed";
|
||||
|
||||
// 모달 표시 컬럼들 (호환 처리된 sourceDisplayColumns 사용)
|
||||
const displayColumns = sourceDisplayColumns;
|
||||
|
||||
// 전체 컬럼 수 계산
|
||||
const totalColumns =
|
||||
(config.features?.selectable ? 1 : 0) +
|
||||
(config.features?.showRowNumber ? 1 : 0) +
|
||||
(isModalMode ? displayColumns.length : 0) +
|
||||
config.columns.length +
|
||||
(config.features?.showDeleteButton ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="overflow-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
{config.features?.dragSort && <TableHead className="w-8" />}
|
||||
{config.features?.selectable && <TableHead className="w-8" />}
|
||||
{config.features?.showRowNumber && <TableHead className="w-12 text-center">#</TableHead>}
|
||||
|
||||
{/* 모달 표시 컬럼들 (선택한 엔티티 정보 - 읽기 전용) */}
|
||||
{isModalMode && displayColumns.map((col) => (
|
||||
<TableHead key={`display_${col.key}`} className="bg-blue-50/50 min-w-[80px]">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
{/* 추가 입력 컬럼 */}
|
||||
{config.columns
|
||||
.filter((col) => col.visible !== false)
|
||||
.map((col) => (
|
||||
<TableHead key={col.key} style={{ width: col.width !== "auto" ? col.width : undefined }}>
|
||||
<TableHead
|
||||
key={col.key}
|
||||
className="min-w-[100px]"
|
||||
style={{ width: col.width !== "auto" ? col.width : undefined }}
|
||||
>
|
||||
{col.title}
|
||||
</TableHead>
|
||||
))}
|
||||
{(config.features?.inlineEdit || config.features?.showDeleteButton || config.renderMode === "mixed") && (
|
||||
<TableHead className="w-24 text-center">액션</TableHead>
|
||||
|
||||
{config.features?.showDeleteButton && (
|
||||
<TableHead className="w-20 text-center">액션</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -377,20 +546,21 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
config.columns.length +
|
||||
(config.features?.dragSort ? 1 : 0) +
|
||||
(config.features?.selectable ? 1 : 0) +
|
||||
(config.features?.showRowNumber ? 1 : 0) +
|
||||
1
|
||||
}
|
||||
colSpan={totalColumns || 1}
|
||||
className="text-muted-foreground py-8 text-center"
|
||||
>
|
||||
데이터가 없습니다
|
||||
{isModalMode
|
||||
? `"${config.modal?.buttonText || "검색"}" 버튼을 클릭하여 항목을 추가하세요`
|
||||
: "\"추가\" 버튼을 클릭하여 데이터를 입력하세요"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
data.map((row, index) => {
|
||||
// FK 값으로 캐시된 표시 정보 가져오기
|
||||
const fkValue = fkColumn ? row[fkColumn] : null;
|
||||
const displayInfo = getEntityDisplayInfo(fkValue);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id || row.objid || index}
|
||||
className={cn(
|
||||
|
|
@ -400,11 +570,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
)}
|
||||
onClick={() => handleRowClick(row, index)}
|
||||
>
|
||||
{config.features?.dragSort && (
|
||||
<TableCell className="w-8 p-1">
|
||||
<GripVertical className="text-muted-foreground h-4 w-4 cursor-grab" />
|
||||
</TableCell>
|
||||
)}
|
||||
{config.features?.selectable && (
|
||||
<TableCell className="w-8 p-1">
|
||||
<Checkbox
|
||||
|
|
@ -417,11 +582,22 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
{config.features?.showRowNumber && (
|
||||
<TableCell className="text-muted-foreground w-12 text-center text-xs">{index + 1}</TableCell>
|
||||
)}
|
||||
|
||||
{/* 모달 표시 컬럼들 (읽기 전용 - 캐시에서 조회) */}
|
||||
{isModalMode && displayColumns.map((col) => (
|
||||
<TableCell key={`display_${col.key}`} className="py-2 bg-blue-50/30">
|
||||
<span className="text-sm text-blue-700">
|
||||
{displayInfo?.[col.key] || "-"}
|
||||
</span>
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
{/* 추가 입력 컬럼 */}
|
||||
{config.columns
|
||||
.filter((col) => col.visible !== false)
|
||||
.map((col) => (
|
||||
<TableCell key={col.key} className="py-2">
|
||||
{editingRow === index ? (
|
||||
<TableCell key={col.key} className="py-2 min-w-[100px]">
|
||||
{editingRow === index && col.editable !== false ? (
|
||||
<Input
|
||||
value={editedData[col.key] || ""}
|
||||
onChange={(e) =>
|
||||
|
|
@ -430,71 +606,45 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
[col.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
className="h-7 text-xs min-w-[80px] w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm">{row[col.key]}</span>
|
||||
<span className="text-sm">{row[col.key] || "-"}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell className="w-24 p-1 text-center">
|
||||
|
||||
{config.features?.showDeleteButton && (
|
||||
<TableCell className="w-20 p-1 text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{editingRow === index ? (
|
||||
{editingRow === index && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="h-6 px-2 text-xs"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-green-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSaveEdit();
|
||||
handleConfirmEdit();
|
||||
}}
|
||||
>
|
||||
저장
|
||||
<Check className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 px-2 text-xs"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCancelEdit();
|
||||
}}
|
||||
>
|
||||
취소
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{config.features?.inlineEdit && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditRow(index);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{config.renderMode === "mixed" && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setModalRow(row);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{config.features?.showDeleteButton && (
|
||||
{editingRow !== index && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -507,12 +657,12 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
@ -537,16 +687,47 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// 소스 테이블 표시 컬럼 (라벨 포함) - 이전/새 형식 호환 + 동적 라벨 매핑
|
||||
const sourceDisplayColumns = useMemo(() => {
|
||||
const rawCols = config.modal?.sourceDisplayColumns || [];
|
||||
|
||||
// string[] 형식과 {key, label}[] 형식 모두 지원
|
||||
const result = rawCols
|
||||
.map((col: any, idx: number) => {
|
||||
if (typeof col === "string") {
|
||||
// 컬럼명만 있는 경우: sourceTableColumnMap에서 라벨 조회
|
||||
return {
|
||||
key: col,
|
||||
label: sourceTableColumnMap[col] || col
|
||||
};
|
||||
}
|
||||
// 라벨이 컬럼명과 같으면 sourceTableColumnMap에서 더 좋은 라벨 찾기
|
||||
const dynamicLabel = sourceTableColumnMap[col.key];
|
||||
return {
|
||||
key: col.key || `col_${idx}`,
|
||||
label: (col.label && col.label !== col.key) ? col.label : (dynamicLabel || col.label || col.key || `컬럼${idx + 1}`)
|
||||
};
|
||||
})
|
||||
// 잘못된 컬럼 필터링 (none, 빈값, undefined 등)
|
||||
.filter((col) => col.key && col.key !== "none" && col.key !== "undefined");
|
||||
|
||||
return result;
|
||||
}, [config.modal?.sourceDisplayColumns, sourceTableColumnMap]);
|
||||
|
||||
const displayColumn = config.dataSource?.displayColumn;
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
{/* 헤더 (추가/삭제 버튼) */}
|
||||
{/* 헤더 */}
|
||||
{(config.features?.showAddButton || (config.features?.selectable && selectedRows.size > 0)) && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{config.features?.showAddButton && (
|
||||
<Button size="sm" variant="outline" onClick={handleAddRow} className="h-7 text-xs">
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
{config.renderMode === "modal" || config.renderMode === "mixed"
|
||||
? config.modal?.buttonText || "검색"
|
||||
: "추가"}
|
||||
</Button>
|
||||
)}
|
||||
{config.features?.selectable && selectedRows.size > 0 && config.features?.showDeleteButton && (
|
||||
|
|
@ -560,62 +741,106 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 로딩 */}
|
||||
{loading && <div className="text-muted-foreground py-4 text-center text-sm">로딩 중...</div>}
|
||||
|
||||
{/* 메인 컨텐츠 */}
|
||||
{!loading && (
|
||||
<>
|
||||
{renderTable()}
|
||||
{renderButtonMode()}
|
||||
|
||||
{/* mixed 모드에서 버튼도 표시 */}
|
||||
{config.renderMode === "mixed" && data.length > 0 && (
|
||||
<div className="border-t pt-2">
|
||||
{data.map((row, index) => (
|
||||
<div key={row.id || row.objid || index} className="mb-1">
|
||||
{renderButtons(row, index)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 모달 */}
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className={cn(MODAL_SIZE_MAP[config.modal?.size || "md"])}>
|
||||
{/* 검색 모달 (modal 모드) */}
|
||||
<Dialog open={searchModalOpen} onOpenChange={setSearchModalOpen}>
|
||||
<DialogContent className={cn(MODAL_SIZE_MAP[config.modal?.size || "lg"])}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{getModalTitle(modalRow)}</DialogTitle>
|
||||
<DialogTitle>{config.modal?.title || "항목 검색"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
{config.modal?.screenId ? (
|
||||
// 화면 기반 모달 - 동적 화면 로드
|
||||
<div className="text-muted-foreground text-center text-sm">
|
||||
화면 ID: {config.modal.screenId}
|
||||
{/* TODO: DynamicScreen 컴포넌트로 교체 */}
|
||||
</div>
|
||||
) : (
|
||||
// 기본 폼 표시
|
||||
<div className="space-y-3">
|
||||
{config.columns.map((col) => (
|
||||
<div key={col.key} className="space-y-1">
|
||||
<label className="text-sm font-medium">{col.title}</label>
|
||||
|
||||
{/* 검색 입력 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={modalRow?.[col.key] || ""}
|
||||
onChange={(e) =>
|
||||
setModalRow((prev: any) => ({
|
||||
...prev,
|
||||
[col.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-9"
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
placeholder="검색어 입력..."
|
||||
className="flex-1"
|
||||
onKeyDown={(e) => e.key === "Enter" && searchSourceTable()}
|
||||
/>
|
||||
<Button onClick={searchSourceTable} disabled={searchLoading}>
|
||||
<Search className="mr-1 h-4 w-4" />
|
||||
검색
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 검색 결과 테이블 */}
|
||||
<div className="max-h-80 overflow-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-8" />
|
||||
{sourceDisplayColumns.length > 0 ? (
|
||||
sourceDisplayColumns.map((col) => (
|
||||
<TableHead key={col.key}>{col.label}</TableHead>
|
||||
))
|
||||
) : displayColumn ? (
|
||||
<TableHead>{displayColumn}</TableHead>
|
||||
) : (
|
||||
<TableHead>항목</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{searchLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={sourceDisplayColumns.length + 1 || 2} className="py-4 text-center">
|
||||
검색 중...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : searchResults.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={sourceDisplayColumns.length + 1 || 2} className="py-4 text-center text-muted-foreground">
|
||||
검색 결과가 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
searchResults.map((item, index) => (
|
||||
<TableRow
|
||||
key={item.id || item.objid || index}
|
||||
className={cn(
|
||||
"cursor-pointer hover:bg-muted/50",
|
||||
selectedSearchItems.has(index) && "bg-primary/10"
|
||||
)}
|
||||
onClick={() => toggleSearchItemSelection(index)}
|
||||
>
|
||||
<TableCell className="w-8 p-1">
|
||||
<Checkbox
|
||||
checked={selectedSearchItems.has(index)}
|
||||
onCheckedChange={() => toggleSearchItemSelection(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
{sourceDisplayColumns.length > 0 ? (
|
||||
sourceDisplayColumns.map((col) => (
|
||||
<TableCell key={col.key}>{item[col.key]}</TableCell>
|
||||
))
|
||||
) : displayColumn ? (
|
||||
<TableCell>{item[displayColumn]}</TableCell>
|
||||
) : (
|
||||
<TableCell>{JSON.stringify(item).substring(0, 50)}...</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSearchModalOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={confirmSearchSelection}
|
||||
disabled={selectedSearchItems.size === 0}
|
||||
>
|
||||
선택 ({selectedSearchItems.size}개)
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
@ -625,4 +850,3 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
UnifiedRepeater.displayName = "UnifiedRepeater";
|
||||
|
||||
export default UnifiedRepeater;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,6 +18,7 @@ import {
|
|||
UnifiedBiz,
|
||||
UnifiedHierarchy,
|
||||
} from "@/components/unified";
|
||||
import { UnifiedRepeater } from "@/components/unified/UnifiedRepeater";
|
||||
|
||||
// 컴포넌트 렌더러 인터페이스
|
||||
export interface ComponentRenderer {
|
||||
|
|
@ -392,6 +393,42 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
/>
|
||||
);
|
||||
|
||||
case "unified-repeater":
|
||||
return (
|
||||
<UnifiedRepeater
|
||||
config={{
|
||||
renderMode: config.renderMode || "inline",
|
||||
dataSource: config.dataSource || {
|
||||
tableName: "",
|
||||
foreignKey: "",
|
||||
referenceKey: "",
|
||||
},
|
||||
columns: config.columns || [],
|
||||
modal: config.modal,
|
||||
button: config.button,
|
||||
features: config.features || {
|
||||
showAddButton: true,
|
||||
showDeleteButton: true,
|
||||
inlineEdit: false,
|
||||
dragSort: false,
|
||||
showRowNumber: false,
|
||||
selectable: false,
|
||||
multiSelect: false,
|
||||
},
|
||||
}}
|
||||
parentId={props.formData?.[config.dataSource?.referenceKey] || props.formData?.id}
|
||||
onDataChange={(data) => {
|
||||
console.log("UnifiedRepeater data changed:", data);
|
||||
}}
|
||||
onRowClick={(row) => {
|
||||
console.log("UnifiedRepeater row clicked:", row);
|
||||
}}
|
||||
onButtonClick={(action, row, buttonConfig) => {
|
||||
console.log("UnifiedRepeater button clicked:", action, row, buttonConfig);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-amber-300 bg-amber-50 p-4">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* UnifiedRepeater 컴포넌트 타입 정의
|
||||
*
|
||||
* 기존 컴포넌트 통합:
|
||||
* - simple-repeater-table: 인라인 모드
|
||||
* - modal-repeater-table: 모달 모드
|
||||
* - repeat-screen-modal: 화면 기반 모달 모드
|
||||
* - related-data-buttons: 버튼 모드
|
||||
* 렌더링 모드:
|
||||
* - inline: 현재 테이블 컬럼 직접 입력 (simple-repeater-table)
|
||||
* - modal: 소스 테이블에서 검색/선택 후 복사 (modal-repeater-table)
|
||||
* - button: 버튼으로 관련 화면 열기 (related-data-buttons)
|
||||
* - mixed: inline + modal 혼합
|
||||
*/
|
||||
|
||||
// 렌더링 모드
|
||||
|
|
@ -35,6 +35,7 @@ export interface RepeaterColumnConfig {
|
|||
title: string;
|
||||
width: ColumnWidthOption;
|
||||
visible: boolean;
|
||||
editable?: boolean; // 편집 가능 여부 (inline 모드)
|
||||
isJoinColumn?: boolean;
|
||||
sourceTable?: string;
|
||||
}
|
||||
|
|
@ -62,10 +63,25 @@ export interface CommonCodeButtonConfig {
|
|||
variantMapping?: Record<string, ButtonVariant>; // 코드값별 색상 매핑
|
||||
}
|
||||
|
||||
// 모달 표시 컬럼 (라벨 포함)
|
||||
export interface ModalDisplayColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 모달 설정
|
||||
export interface RepeaterModalConfig {
|
||||
screenId?: number;
|
||||
// 기본 설정
|
||||
size: ModalSize;
|
||||
title?: string; // 모달 제목
|
||||
buttonText?: string; // 검색 버튼 텍스트
|
||||
|
||||
// 소스 테이블 표시 설정 (modal 모드)
|
||||
sourceDisplayColumns?: ModalDisplayColumn[]; // 모달에 표시할 소스 테이블 컬럼 (라벨 포함)
|
||||
searchFields?: string[]; // 검색에 사용할 필드
|
||||
|
||||
// 화면 기반 모달 (옵션)
|
||||
screenId?: number;
|
||||
titleTemplate?: {
|
||||
prefix?: string;
|
||||
columnKey?: string;
|
||||
|
|
@ -84,25 +100,55 @@ export interface RepeaterFeatureOptions {
|
|||
multiSelect: boolean;
|
||||
}
|
||||
|
||||
// 데이터 소스 설정
|
||||
export interface RepeaterDataSource {
|
||||
// inline 모드: 현재 테이블 설정은 필요 없음 (컬럼만 선택)
|
||||
|
||||
// modal 모드: 소스 테이블 설정
|
||||
sourceTable?: string; // 검색할 테이블 (엔티티 참조 테이블)
|
||||
foreignKey?: string; // 현재 테이블의 FK 컬럼 (part_objid 등)
|
||||
referenceKey?: string; // 소스 테이블의 PK 컬럼 (id 등)
|
||||
displayColumn?: string; // 표시할 컬럼 (item_name 등)
|
||||
|
||||
// 추가 필터
|
||||
filter?: {
|
||||
column: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 컬럼 매핑 (modal 모드에서 소스→타겟 복사용)
|
||||
export interface ColumnMapping {
|
||||
sourceColumn: string;
|
||||
targetColumn: string;
|
||||
transform?: "copy" | "reference"; // copy: 값 복사, reference: ID 참조
|
||||
}
|
||||
|
||||
// 계산 규칙
|
||||
export interface CalculationRule {
|
||||
id: string;
|
||||
targetColumn: string;
|
||||
formula: string; // 예: "quantity * unit_price"
|
||||
label?: string;
|
||||
}
|
||||
|
||||
// 메인 설정 타입
|
||||
export interface UnifiedRepeaterConfig {
|
||||
// 렌더링 모드
|
||||
renderMode: RepeaterRenderMode;
|
||||
|
||||
// 데이터 소스 설정
|
||||
dataSource: {
|
||||
tableName: string; // 데이터 테이블
|
||||
foreignKey: string; // 연결 키 (FK) - 데이터 테이블의 컬럼
|
||||
referenceKey: string; // 상위 키 - 현재 화면 테이블의 컬럼 (부모 ID)
|
||||
filter?: { // 추가 필터 조건
|
||||
column: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
dataSource: RepeaterDataSource;
|
||||
|
||||
// 컬럼 설정
|
||||
// 컬럼 설정 (inline: 입력 컬럼, modal: 표시 컬럼)
|
||||
columns: RepeaterColumnConfig[];
|
||||
|
||||
// 컬럼 매핑 (modal 모드)
|
||||
columnMappings?: ColumnMapping[];
|
||||
|
||||
// 계산 규칙 (modal 모드)
|
||||
calculationRules?: CalculationRule[];
|
||||
|
||||
// 모달 설정 (modal, mixed 모드)
|
||||
modal?: RepeaterModalConfig;
|
||||
|
||||
|
|
@ -141,14 +187,12 @@ export interface UnifiedRepeaterProps {
|
|||
// 기본 설정값
|
||||
export const DEFAULT_REPEATER_CONFIG: UnifiedRepeaterConfig = {
|
||||
renderMode: "inline",
|
||||
dataSource: {
|
||||
tableName: "",
|
||||
foreignKey: "",
|
||||
referenceKey: "",
|
||||
},
|
||||
dataSource: {},
|
||||
columns: [],
|
||||
modal: {
|
||||
size: "md",
|
||||
size: "lg",
|
||||
sourceDisplayColumns: [],
|
||||
searchFields: [],
|
||||
},
|
||||
button: {
|
||||
sourceType: "manual",
|
||||
|
|
@ -159,20 +203,20 @@ export const DEFAULT_REPEATER_CONFIG: UnifiedRepeaterConfig = {
|
|||
features: {
|
||||
showAddButton: true,
|
||||
showDeleteButton: true,
|
||||
inlineEdit: false,
|
||||
inlineEdit: true,
|
||||
dragSort: false,
|
||||
showRowNumber: false,
|
||||
selectable: false,
|
||||
multiSelect: false,
|
||||
multiSelect: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 고정 옵션들 (콤보박스용)
|
||||
export const RENDER_MODE_OPTIONS = [
|
||||
{ value: "inline", label: "인라인 (테이블)" },
|
||||
{ value: "modal", label: "모달" },
|
||||
{ value: "inline", label: "인라인 (직접 입력)" },
|
||||
{ value: "modal", label: "모달 (검색 선택)" },
|
||||
{ value: "button", label: "버튼" },
|
||||
{ value: "mixed", label: "혼합 (테이블 + 버튼)" },
|
||||
{ value: "mixed", label: "혼합 (입력 + 검색)" },
|
||||
] as const;
|
||||
|
||||
export const MODAL_SIZE_OPTIONS = [
|
||||
|
|
@ -227,4 +271,3 @@ export const LABEL_FIELD_OPTIONS = [
|
|||
{ value: "codeName", label: "코드명" },
|
||||
{ value: "codeValue", label: "코드값" },
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue