feature/v2-unified-renewal #379
|
|
@ -275,13 +275,13 @@ export function ComponentsPanel({
|
||||||
|
|
||||||
{/* 테이블 컬럼 탭 */}
|
{/* 테이블 컬럼 탭 */}
|
||||||
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
||||||
<TablesPanel
|
<TablesPanel
|
||||||
tables={tables}
|
tables={tables}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
onSearchChange={onSearchChange || (() => {})}
|
onSearchChange={onSearchChange || (() => {})}
|
||||||
onDragStart={onTableDragStart || (() => {})}
|
onDragStart={onTableDragStart || (() => {})}
|
||||||
selectedTableName={selectedTableName}
|
selectedTableName={selectedTableName}
|
||||||
placedColumns={placedColumns}
|
placedColumns={placedColumns}
|
||||||
onTableSelect={onTableSelect}
|
onTableSelect={onTableSelect}
|
||||||
showTableSelector={showTableSelector}
|
showTableSelector={showTableSelector}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* 렌더링 모드:
|
* 렌더링 모드:
|
||||||
* - inline: 현재 테이블 컬럼 직접 입력
|
* - inline: 현재 테이블 컬럼 직접 입력
|
||||||
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
|
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
|
||||||
*
|
*
|
||||||
* RepeaterTable 및 ItemSelectionModal 재사용
|
* RepeaterTable 및 ItemSelectionModal 재사용
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
|
|
||||||
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거
|
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거
|
||||||
const [autoWidthTrigger, setAutoWidthTrigger] = useState(0);
|
const [autoWidthTrigger, setAutoWidthTrigger] = useState(0);
|
||||||
|
|
||||||
// 소스 테이블 컬럼 라벨 매핑
|
// 소스 테이블 컬럼 라벨 매핑
|
||||||
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
|
@ -71,10 +71,10 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
|
|
||||||
// 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용)
|
// 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용)
|
||||||
const [categoryLabelMap, setCategoryLabelMap] = useState<Record<string, string>>({});
|
const [categoryLabelMap, setCategoryLabelMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 현재 테이블 컬럼 정보 (inputType 매핑용)
|
// 현재 테이블 컬럼 정보 (inputType 매핑용)
|
||||||
const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, any>>({});
|
const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
// 동적 데이터 소스 상태
|
// 동적 데이터 소스 상태
|
||||||
const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});
|
const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
|
|
||||||
// 🆕 마스터 테이블에서 생성된 ID (FK 연결용)
|
// 🆕 마스터 테이블에서 생성된 ID (FK 연결용)
|
||||||
const masterRecordId = event.detail?.masterRecordId || mainFormData?.id;
|
const masterRecordId = event.detail?.masterRecordId || mainFormData?.id;
|
||||||
|
|
||||||
if (!tableName || data.length === 0) {
|
if (!tableName || data.length === 0) {
|
||||||
console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length });
|
console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length });
|
||||||
return;
|
return;
|
||||||
|
|
@ -139,10 +139,10 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("테이블 컬럼 정보 조회 실패");
|
console.warn("테이블 컬럼 정보 조회 실패");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const row = data[i];
|
const row = data[i];
|
||||||
|
|
||||||
// 내부 필드 제거
|
// 내부 필드 제거
|
||||||
const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_")));
|
const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_")));
|
||||||
|
|
||||||
|
|
@ -159,11 +159,11 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 기존 방식: 메인 폼 데이터 병합
|
// 기존 방식: 메인 폼 데이터 병합
|
||||||
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
|
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
|
||||||
mergedData = {
|
mergedData = {
|
||||||
...mainFormDataWithoutId,
|
...mainFormDataWithoutId,
|
||||||
...cleanRow,
|
...cleanRow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유효하지 않은 컬럼 제거
|
// 유효하지 않은 컬럼 제거
|
||||||
|
|
@ -173,7 +173,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
filteredData[key] = value;
|
filteredData[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
|
await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +199,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||||
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
||||||
|
|
||||||
const columnMap: Record<string, any> = {};
|
const columnMap: Record<string, any> = {};
|
||||||
columns.forEach((col: any) => {
|
columns.forEach((col: any) => {
|
||||||
const name = col.columnName || col.column_name || col.name;
|
const name = col.columnName || col.column_name || col.name;
|
||||||
|
|
@ -292,7 +292,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`);
|
const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`);
|
||||||
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
||||||
|
|
||||||
const labels: Record<string, string> = {};
|
const labels: Record<string, string> = {};
|
||||||
const categoryCols: string[] = [];
|
const categoryCols: string[] = [];
|
||||||
|
|
||||||
|
|
@ -336,13 +336,13 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
calculated: true,
|
calculated: true,
|
||||||
width: col.width === "auto" ? undefined : col.width,
|
width: col.width === "auto" ? undefined : col.width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 입력 컬럼
|
// 일반 입력 컬럼
|
||||||
let type: "text" | "number" | "date" | "select" | "category" = "text";
|
let type: "text" | "number" | "date" | "select" | "category" = "text";
|
||||||
if (inputType === "number" || inputType === "decimal") type = "number";
|
if (inputType === "number" || inputType === "decimal") type = "number";
|
||||||
else if (inputType === "date" || inputType === "datetime") type = "date";
|
else if (inputType === "date" || inputType === "datetime") type = "date";
|
||||||
else if (inputType === "code") type = "select";
|
else if (inputType === "code") type = "select";
|
||||||
else if (inputType === "category") type = "category"; // 🆕 카테고리 타입
|
else if (inputType === "category") type = "category"; // 🆕 카테고리 타입
|
||||||
|
|
||||||
// 🆕 카테고리 참조 ID 가져오기 (tableName.columnName 형식)
|
// 🆕 카테고리 참조 ID 가져오기 (tableName.columnName 형식)
|
||||||
|
|
@ -355,19 +355,19 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
categoryRef = `${tableName}.${col.key}`;
|
categoryRef = `${tableName}.${col.key}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
field: col.key,
|
field: col.key,
|
||||||
label: col.title || colInfo?.displayName || col.key,
|
label: col.title || colInfo?.displayName || col.key,
|
||||||
type,
|
type,
|
||||||
editable: col.editable !== false,
|
editable: col.editable !== false,
|
||||||
width: col.width === "auto" ? undefined : col.width,
|
width: col.width === "auto" ? undefined : col.width,
|
||||||
required: false,
|
required: false,
|
||||||
categoryRef, // 🆕 카테고리 참조 ID 전달
|
categoryRef, // 🆕 카테고리 참조 ID 전달
|
||||||
hidden: col.hidden, // 🆕 히든 처리
|
hidden: col.hidden, // 🆕 히든 처리
|
||||||
autoFill: col.autoFill, // 🆕 자동 입력 설정
|
autoFill: col.autoFill, // 🆕 자동 입력 설정
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]);
|
}, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]);
|
||||||
|
|
||||||
// 🆕 데이터 변경 시 카테고리 라벨 로드 (RepeaterTable 표시용)
|
// 🆕 데이터 변경 시 카테고리 라벨 로드 (RepeaterTable 표시용)
|
||||||
|
|
@ -423,8 +423,8 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
// 데이터 변경 핸들러
|
// 데이터 변경 핸들러
|
||||||
const handleDataChange = useCallback(
|
const handleDataChange = useCallback(
|
||||||
(newData: any[]) => {
|
(newData: any[]) => {
|
||||||
setData(newData);
|
setData(newData);
|
||||||
onDataChange?.(newData);
|
onDataChange?.(newData);
|
||||||
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정
|
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정
|
||||||
setAutoWidthTrigger((prev) => prev + 1);
|
setAutoWidthTrigger((prev) => prev + 1);
|
||||||
},
|
},
|
||||||
|
|
@ -434,11 +434,11 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
// 행 변경 핸들러
|
// 행 변경 핸들러
|
||||||
const handleRowChange = useCallback(
|
const handleRowChange = useCallback(
|
||||||
(index: number, newRow: any) => {
|
(index: number, newRow: any) => {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
newData[index] = newRow;
|
newData[index] = newRow;
|
||||||
// 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요)
|
// 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요)
|
||||||
setData(newData);
|
setData(newData);
|
||||||
onDataChange?.(newData);
|
onDataChange?.(newData);
|
||||||
},
|
},
|
||||||
[data, onDataChange],
|
[data, onDataChange],
|
||||||
);
|
);
|
||||||
|
|
@ -446,16 +446,16 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
// 행 삭제 핸들러
|
// 행 삭제 핸들러
|
||||||
const handleRowDelete = useCallback(
|
const handleRowDelete = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const newData = data.filter((_, i) => i !== index);
|
const newData = data.filter((_, i) => i !== index);
|
||||||
handleDataChange(newData); // 🆕 handleDataChange 사용
|
handleDataChange(newData); // 🆕 handleDataChange 사용
|
||||||
|
|
||||||
// 선택 상태 업데이트
|
// 선택 상태 업데이트
|
||||||
const newSelected = new Set<number>();
|
const newSelected = new Set<number>();
|
||||||
selectedRows.forEach((i) => {
|
selectedRows.forEach((i) => {
|
||||||
if (i < index) newSelected.add(i);
|
if (i < index) newSelected.add(i);
|
||||||
else if (i > index) newSelected.add(i - 1);
|
else if (i > index) newSelected.add(i - 1);
|
||||||
});
|
});
|
||||||
setSelectedRows(newSelected);
|
setSelectedRows(newSelected);
|
||||||
},
|
},
|
||||||
[data, selectedRows, handleDataChange],
|
[data, selectedRows, handleDataChange],
|
||||||
);
|
);
|
||||||
|
|
@ -537,7 +537,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
} else if (autoValue !== undefined) {
|
} else if (autoValue !== undefined) {
|
||||||
newRow[col.key] = autoValue;
|
newRow[col.key] = autoValue;
|
||||||
} else {
|
} else {
|
||||||
newRow[col.key] = "";
|
newRow[col.key] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,23 +549,23 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
// 모달에서 항목 선택 - 비동기로 변경
|
// 모달에서 항목 선택 - 비동기로 변경
|
||||||
const handleSelectItems = useCallback(
|
const handleSelectItems = useCallback(
|
||||||
async (items: Record<string, unknown>[]) => {
|
async (items: Record<string, unknown>[]) => {
|
||||||
const fkColumn = config.dataSource?.foreignKey;
|
const fkColumn = config.dataSource?.foreignKey;
|
||||||
const currentRowCount = data.length;
|
const currentRowCount = data.length;
|
||||||
|
|
||||||
// 채번이 필요한 컬럼 찾기
|
// 채번이 필요한 컬럼 찾기
|
||||||
const numberingColumns = config.columns.filter(
|
const numberingColumns = config.columns.filter(
|
||||||
(col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId
|
(col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId
|
||||||
);
|
);
|
||||||
|
|
||||||
const newRows = await Promise.all(
|
const newRows = await Promise.all(
|
||||||
items.map(async (item, index) => {
|
items.map(async (item, index) => {
|
||||||
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
|
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
|
||||||
|
|
||||||
// FK 값 저장 (resolvedReferenceKey 사용)
|
// FK 값 저장 (resolvedReferenceKey 사용)
|
||||||
if (fkColumn && item[resolvedReferenceKey]) {
|
if (fkColumn && item[resolvedReferenceKey]) {
|
||||||
row[fkColumn] = item[resolvedReferenceKey];
|
row[fkColumn] = item[resolvedReferenceKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 컬럼 처리 (순서대로)
|
// 모든 컬럼 처리 (순서대로)
|
||||||
for (const col of config.columns) {
|
for (const col of config.columns) {
|
||||||
if (col.isSourceDisplay) {
|
if (col.isSourceDisplay) {
|
||||||
|
|
@ -581,18 +581,18 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
row[col.key] = autoValue;
|
row[col.key] = autoValue;
|
||||||
} else if (row[col.key] === undefined) {
|
} else if (row[col.key] === undefined) {
|
||||||
// 입력 컬럼: 빈 값으로 초기화
|
// 입력 컬럼: 빈 값으로 초기화
|
||||||
row[col.key] = "";
|
row[col.key] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const newData = [...data, ...newRows];
|
const newData = [...data, ...newRows];
|
||||||
handleDataChange(newData);
|
handleDataChange(newData);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
},
|
},
|
||||||
[config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode],
|
[config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode],
|
||||||
);
|
);
|
||||||
|
|
@ -615,7 +615,7 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||||
const formData = customEvent.detail?.formData;
|
const formData = customEvent.detail?.formData;
|
||||||
|
|
||||||
if (!formData || !dataRef.current.length) return;
|
if (!formData || !dataRef.current.length) return;
|
||||||
|
|
||||||
// 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환
|
// 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환
|
||||||
const processedData = await Promise.all(
|
const processedData = await Promise.all(
|
||||||
dataRef.current.map(async (row) => {
|
dataRef.current.map(async (row) => {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
if (partialConfig.useCustomTable !== undefined) {
|
if (partialConfig.useCustomTable !== undefined) {
|
||||||
newConfig.useCustomTable = partialConfig.useCustomTable;
|
newConfig.useCustomTable = partialConfig.useCustomTable;
|
||||||
}
|
}
|
||||||
if (partialConfig.customTableName !== undefined) {
|
if (partialConfig.customTableName !== undefined) {
|
||||||
newConfig.customTableName = partialConfig.customTableName;
|
newConfig.customTableName = partialConfig.customTableName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -976,15 +976,15 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
{/* 컬럼 설정 탭 - 🆕 통합 컬럼 선택 */}
|
{/* 컬럼 설정 탭 - 🆕 통합 컬럼 선택 */}
|
||||||
<TabsContent value="columns" className="mt-4 space-y-4">
|
<TabsContent value="columns" className="mt-4 space-y-4">
|
||||||
{/* 통합 컬럼 선택 */}
|
{/* 통합 컬럼 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">컬럼 선택</Label>
|
<Label className="text-xs font-medium">컬럼 선택</Label>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
{isModalMode
|
{isModalMode
|
||||||
? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분"
|
? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분"
|
||||||
: "입력받을 컬럼을 선택하세요"
|
: "입력받을 컬럼을 선택하세요"
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
|
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
|
||||||
{isModalMode && config.dataSource?.sourceTable && (
|
{isModalMode && config.dataSource?.sourceTable && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1019,9 +1019,9 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 저장 테이블 컬럼 (입력용) */}
|
{/* 저장 테이블 컬럼 (입력용) */}
|
||||||
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
|
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
|
||||||
<Database className="h-3 w-3" />
|
<Database className="h-3 w-3" />
|
||||||
|
|
@ -1113,15 +1113,15 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
{col.isSourceDisplay ? (
|
{col.isSourceDisplay ? (
|
||||||
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
||||||
) : (
|
) : (
|
||||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={col.title}
|
value={col.title}
|
||||||
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
|
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
|
||||||
placeholder="제목"
|
placeholder="제목"
|
||||||
className="h-6 flex-1 text-xs"
|
className="h-6 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 히든 토글 (입력 컬럼만) */}
|
{/* 히든 토글 (입력 컬럼만) */}
|
||||||
{!col.isSourceDisplay && (
|
{!col.isSourceDisplay && (
|
||||||
|
|
@ -1133,7 +1133,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
col.hidden ? "text-gray-400" : "text-gray-600",
|
col.hidden ? "text-gray-400" : "text-gray-600",
|
||||||
)}
|
)}
|
||||||
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
|
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
|
||||||
>
|
>
|
||||||
{col.hidden ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
{col.hidden ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1145,17 +1145,17 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
|
|
||||||
{/* 편집 가능 체크박스 */}
|
{/* 편집 가능 체크박스 */}
|
||||||
{!col.isSourceDisplay && (
|
{!col.isSourceDisplay && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={col.editable ?? true}
|
checked={col.editable ?? true}
|
||||||
onCheckedChange={(checked) => updateColumnProp(col.key, "editable", !!checked)}
|
onCheckedChange={(checked) => updateColumnProp(col.key, "editable", !!checked)}
|
||||||
title="편집 가능"
|
title="편집 가능"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (col.isSourceDisplay) {
|
if (col.isSourceDisplay) {
|
||||||
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
||||||
|
|
@ -1163,11 +1163,11 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-destructive h-6 w-6 p-0"
|
className="text-destructive h-6 w-6 p-0"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
||||||
{!col.isSourceDisplay && expandedColumn === col.key && (
|
{!col.isSourceDisplay && expandedColumn === col.key && (
|
||||||
|
|
|
||||||
|
|
@ -231,8 +231,8 @@ export function RepeaterTable({
|
||||||
columns
|
columns
|
||||||
.filter((col) => !col.hidden)
|
.filter((col) => !col.hidden)
|
||||||
.forEach((col) => {
|
.forEach((col) => {
|
||||||
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
widths[col.field] = col.width ? parseInt(col.width) : 120;
|
||||||
});
|
});
|
||||||
return widths;
|
return widths;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -415,10 +415,10 @@ export function RepeaterTable({
|
||||||
// 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배
|
// 데이터가 있으면 데이터 기반 자동 맞춤, 없으면 균등 분배
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
applyAutoFitWidths();
|
applyAutoFitWidths();
|
||||||
} else {
|
} else {
|
||||||
applyEqualizeWidths();
|
applyEqualizeWidths();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
@ -799,7 +799,7 @@ export function RepeaterTable({
|
||||||
{/* 드래그 핸들 - 좌측 고정 */}
|
{/* 드래그 핸들 - 좌측 고정 */}
|
||||||
<td
|
<td
|
||||||
className={cn(
|
className={cn(
|
||||||
"sticky left-0 z-10 border-r border-b border-gray-200 px-1 py-1 text-center",
|
"sticky left-0 z-10 border-r border-b border-gray-200 px-1 py-1 text-center",
|
||||||
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -818,7 +818,7 @@ export function RepeaterTable({
|
||||||
{/* 체크박스 - 좌측 고정 */}
|
{/* 체크박스 - 좌측 고정 */}
|
||||||
<td
|
<td
|
||||||
className={cn(
|
className={cn(
|
||||||
"sticky left-8 z-10 border-r border-b border-gray-200 px-3 py-1 text-center",
|
"sticky left-8 z-10 border-r border-b border-gray-200 px-3 py-1 text-center",
|
||||||
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
selectedRows.has(rowIndex) ? "bg-blue-50" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export interface RepeaterColumnConfig {
|
||||||
fixedValue?: string | number; // fixed 타입일 때 고정값
|
fixedValue?: string | number; // fixed 타입일 때 고정값
|
||||||
format?: string; // 날짜 포맷 등
|
format?: string; // 날짜 포맷 등
|
||||||
};
|
};
|
||||||
|
|
||||||
// 컬럼 매핑 설정
|
// 컬럼 매핑 설정
|
||||||
mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정
|
mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1098,25 +1098,25 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
|
|
||||||
// 전체 테이블 목록 항상 로드 (좌측/우측 모두 사용)
|
// 전체 테이블 목록 항상 로드 (좌측/우측 모두 사용)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAllTables = async () => {
|
const loadAllTables = async () => {
|
||||||
try {
|
try {
|
||||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||||
const response = await tableManagementApi.getTableList();
|
const response = await tableManagementApi.getTableList();
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
console.log("✅ 분할패널: 전체 테이블 목록 로드", response.data.length, "개");
|
console.log("✅ 분할패널: 전체 테이블 목록 로드", response.data.length, "개");
|
||||||
setAllTables(response.data);
|
setAllTables(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 전체 테이블 목록 로드 실패:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("❌ 전체 테이블 목록 로드 실패:", error);
|
loadAllTables();
|
||||||
}
|
|
||||||
};
|
|
||||||
loadAllTables();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 초기 로드 시 좌측 패널 테이블이 없으면 화면 테이블로 설정
|
// 초기 로드 시 좌측 패널 테이블이 없으면 화면 테이블로 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (screenTableName && !config.leftPanel?.tableName) {
|
if (screenTableName && !config.leftPanel?.tableName) {
|
||||||
updateLeftPanel({ tableName: screenTableName });
|
updateLeftPanel({ tableName: screenTableName });
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [screenTableName]);
|
}, [screenTableName]);
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
const result = await tableManagementApi.getColumnList(targetTableName);
|
const result = await tableManagementApi.getColumnList(targetTableName);
|
||||||
console.log("🔧 tableManagementApi 응답:", result);
|
console.log("🔧 tableManagementApi 응답:", result);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// API 응답 구조: { columns: [...], total, page, ... }
|
// API 응답 구조: { columns: [...], total, page, ... }
|
||||||
const columns = Array.isArray(result.data) ? result.data : result.data.columns;
|
const columns = Array.isArray(result.data) ? result.data : result.data.columns;
|
||||||
console.log("🔧 컬럼 배열:", columns);
|
console.log("🔧 컬럼 배열:", columns);
|
||||||
|
|
@ -862,7 +862,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
"mr-2 h-3 w-3",
|
"mr-2 h-3 w-3",
|
||||||
config.customTableName === table.tableName ? "opacity-100" : "opacity-0",
|
config.customTableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Table2 className="mr-2 h-3 w-3 text-slate-400" />
|
<Table2 className="mr-2 h-3 w-3 text-slate-400" />
|
||||||
{table.displayName || table.tableName}
|
{table.displayName || table.tableName}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|
@ -1283,25 +1283,25 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-blue-200 bg-blue-50/30 p-2">
|
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-blue-200 bg-blue-50/30 p-2">
|
||||||
{joinTable.availableColumns.map((column, colIndex) => {
|
{joinTable.availableColumns.map((column, colIndex) => {
|
||||||
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
|
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
|
||||||
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAlreadyAdded = config.columns?.some(
|
const isAlreadyAdded = config.columns?.some(
|
||||||
(col) => col.columnName === matchingJoinColumn?.joinAlias,
|
(col) => col.columnName === matchingJoinColumn?.joinAlias,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!matchingJoinColumn) return null;
|
if (!matchingJoinColumn) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={colIndex}
|
key={colIndex}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-blue-100/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
"hover:bg-blue-100/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
||||||
isAlreadyAdded && "bg-blue-100",
|
isAlreadyAdded && "bg-blue-100",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAlreadyAdded) {
|
if (isAlreadyAdded) {
|
||||||
// 컬럼 제거
|
// 컬럼 제거
|
||||||
handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||||
|
|
@ -1311,28 +1311,28 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isAlreadyAdded}
|
checked={isAlreadyAdded}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
if (isAlreadyAdded) {
|
if (isAlreadyAdded) {
|
||||||
handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||||
} else {
|
} else {
|
||||||
addEntityColumn(matchingJoinColumn);
|
addEntityColumn(matchingJoinColumn);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="pointer-events-none h-3.5 w-3.5"
|
className="pointer-events-none h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" />
|
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" />
|
||||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||||
<span className="text-[10px] text-blue-400 ml-auto">{column.inputType || column.dataType}</span>
|
<span className="text-[10px] text-blue-400 ml-auto">{column.inputType || column.dataType}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* UnifiedRepeater 컴포넌트 타입 정의
|
* UnifiedRepeater 컴포넌트 타입 정의
|
||||||
*
|
*
|
||||||
* 렌더링 모드:
|
* 렌더링 모드:
|
||||||
* - inline: 현재 테이블 컬럼 직접 입력 (simple-repeater-table)
|
* - inline: 현재 테이블 컬럼 직접 입력 (simple-repeater-table)
|
||||||
* - modal: 소스 테이블에서 검색/선택 후 복사 (modal-repeater-table)
|
* - modal: 소스 테이블에서 검색/선택 후 복사 (modal-repeater-table)
|
||||||
|
|
@ -77,11 +77,11 @@ export interface RepeaterModalConfig {
|
||||||
size: ModalSize;
|
size: ModalSize;
|
||||||
title?: string; // 모달 제목
|
title?: string; // 모달 제목
|
||||||
buttonText?: string; // 검색 버튼 텍스트
|
buttonText?: string; // 검색 버튼 텍스트
|
||||||
|
|
||||||
// 소스 테이블 표시 설정 (modal 모드)
|
// 소스 테이블 표시 설정 (modal 모드)
|
||||||
sourceDisplayColumns?: ModalDisplayColumn[]; // 모달에 표시할 소스 테이블 컬럼 (라벨 포함)
|
sourceDisplayColumns?: ModalDisplayColumn[]; // 모달에 표시할 소스 테이블 컬럼 (라벨 포함)
|
||||||
searchFields?: string[]; // 검색에 사용할 필드
|
searchFields?: string[]; // 검색에 사용할 필드
|
||||||
|
|
||||||
// 화면 기반 모달 (옵션)
|
// 화면 기반 모달 (옵션)
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
titleTemplate?: {
|
titleTemplate?: {
|
||||||
|
|
@ -105,13 +105,13 @@ export interface RepeaterFeatureOptions {
|
||||||
// 데이터 소스 설정
|
// 데이터 소스 설정
|
||||||
export interface RepeaterDataSource {
|
export interface RepeaterDataSource {
|
||||||
// inline 모드: 현재 테이블 설정은 필요 없음 (컬럼만 선택)
|
// inline 모드: 현재 테이블 설정은 필요 없음 (컬럼만 선택)
|
||||||
|
|
||||||
// modal 모드: 소스 테이블 설정
|
// modal 모드: 소스 테이블 설정
|
||||||
sourceTable?: string; // 검색할 테이블 (엔티티 참조 테이블)
|
sourceTable?: string; // 검색할 테이블 (엔티티 참조 테이블)
|
||||||
foreignKey?: string; // 현재 테이블의 FK 컬럼 (part_objid 등)
|
foreignKey?: string; // 현재 테이블의 FK 컬럼 (part_objid 등)
|
||||||
referenceKey?: string; // 소스 테이블의 PK 컬럼 (id 등)
|
referenceKey?: string; // 소스 테이블의 PK 컬럼 (id 등)
|
||||||
displayColumn?: string; // 표시할 컬럼 (item_name 등)
|
displayColumn?: string; // 표시할 컬럼 (item_name 등)
|
||||||
|
|
||||||
// 추가 필터
|
// 추가 필터
|
||||||
filter?: {
|
filter?: {
|
||||||
column: string;
|
column: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue