dev #46

Merged
kjs merged 344 commits from dev into main 2025-09-22 18:17:24 +09:00
13 changed files with 117 additions and 64 deletions
Showing only changes of commit 8e2bb1d9a0 - Show all commits

View File

@ -282,15 +282,13 @@ export async function getTableLabels(
const tableLabels = await tableManagementService.getTableLabels(tableName);
if (!tableLabels) {
const response: ApiResponse<null> = {
success: false,
message: "테이블 라벨 정보를 찾을 수 없습니다.",
error: {
code: "TABLE_LABELS_NOT_FOUND",
details: `테이블 ${tableName}의 라벨 정보가 존재하지 않습니다.`,
},
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response: ApiResponse<{}> = {
success: true,
message: "테이블 라벨 정보를 조회했습니다.",
data: {},
};
res.status(404).json(response);
res.status(200).json(response);
return;
}
@ -350,15 +348,13 @@ export async function getColumnLabels(
);
if (!columnLabels) {
const response: ApiResponse<null> = {
success: false,
message: "컬럼 라벨 정보를 찾을 수 없습니다.",
error: {
code: "COLUMN_LABELS_NOT_FOUND",
details: `컬럼 ${tableName}.${columnName}의 라벨 정보가 존재하지 않습니다.`,
},
// 라벨이 없으면 빈 객체를 성공으로 반환 (404 에러 대신)
const response: ApiResponse<{}> = {
success: true,
message: "컬럼 라벨 정보를 조회했습니다.",
data: {},
};
res.status(404).json(response);
res.status(200).json(response);
return;
}

View File

@ -185,6 +185,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 모달이 열릴 때 기본값 설정
useEffect(() => {
if (isOpen && connection) {
// 모달이 열릴 때마다 캐시 초기화 (라벨 업데이트 반영)
setTableColumnsCache({});
const fromTableName = connection.fromNode.tableName;
const toTableName = connection.toNode.tableName;
const fromDisplayName = connection.fromNode.displayName;
@ -283,8 +286,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}, [selectedFromColumns, selectedToColumns]);
// 테이블 컬럼 로드 함수 (캐시 활용)
const loadTableColumns = async (tableName: string): Promise<ColumnInfo[]> => {
if (tableColumnsCache[tableName]) {
const loadTableColumns = async (tableName: string, forceReload = false): Promise<ColumnInfo[]> => {
if (tableColumnsCache[tableName] && !forceReload) {
return tableColumnsCache[tableName];
}

View File

@ -284,8 +284,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
description: "", // 새로 추가된 노드는 description 없이 통일
columns: Array.isArray(table.columns)
? table.columns.map((col) => ({
name: col.columnName || "unknown",
type: col.dataType || "varchar", // 기존과 동일한 기본값 사용
columnName: col.columnName || "unknown",
name: col.columnName || "unknown", // 호환성을 위해 유지
displayName: col.displayName, // 한국어 라벨
columnLabel: col.columnLabel, // 한국어 라벨
type: col.dataType || "varchar",
dataType: col.dataType || "varchar",
description: col.description || "",
}))
: [],

View File

@ -131,7 +131,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
<div className="flex flex-wrap gap-2">
{connectedTables.map((table) => (
<Badge key={table} variant="outline" className="text-xs">
📋 {table}
{table}
</Badge>
))}
</div>
@ -162,7 +162,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
</span>
</div>
<div className="mt-1 text-xs text-gray-600">
{relationship.fromColumns.join(", ")} {relationship.toColumns.join(", ")}
{relationship.fromTable} {relationship.toTable}
</div>
</div>
<Badge variant="outline" className="text-xs">

View File

@ -4,9 +4,13 @@ import React from "react";
import { Handle, Position } from "@xyflow/react";
interface TableColumn {
name: string;
type: string;
description: string;
columnName: string;
name?: string; // 호환성을 위해 유지
columnLabel?: string;
displayName?: string;
dataType?: string;
type?: string; // 호환성을 위해 유지
description?: string;
}
interface Table {
@ -43,21 +47,24 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
<div className="flex-1 overflow-hidden p-2" onMouseEnter={onScrollAreaEnter} onMouseLeave={onScrollAreaLeave}>
<div className="space-y-1">
{table.columns.map((column) => {
const isSelected = selectedColumns.includes(column.name);
const columnKey = column.columnName || column.name || "";
const columnDisplayName = column.displayName || column.columnLabel || column.name || column.columnName;
const columnType = column.dataType || column.type || "";
const isSelected = selectedColumns.includes(columnKey);
return (
<div
key={column.name}
key={columnKey}
className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
isSelected ? "bg-blue-100 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100"
}`}
onClick={() => onColumnClick(table.tableName, column.name)}
onClick={() => onColumnClick(table.tableName, columnKey)}
>
{/* 핸들 제거됨 - 컬럼 클릭으로만 연결 생성 */}
<div className="flex items-center justify-between">
<span className="font-mono font-medium">{column.name}</span>
<span className="text-gray-500">{column.type}</span>
<span className="font-mono font-medium">{columnDisplayName}</span>
<span className="text-gray-500">{columnType}</span>
</div>
{column.description && <div className="mt-0.5 text-gray-500">{column.description}</div>}
</div>

View File

@ -172,7 +172,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
<SelectContent>
{fromTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</SelectItem>
))}
</SelectContent>

View File

@ -214,13 +214,13 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
{condition.tableType === "from" &&
fromTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</SelectItem>
))}
{condition.tableType === "to" &&
toTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</SelectItem>
))}
</SelectContent>

View File

@ -154,7 +154,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
tableColumnsCache[mapping.sourceTable]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
<div className="truncate" title={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</div>
</SelectItem>
))}
@ -200,7 +200,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
tableColumnsCache[mapping.targetTable]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
<div className="truncate" title={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</div>
</SelectItem>
))}

View File

@ -90,7 +90,7 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<SelectContent>
{fromTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</SelectItem>
))}
</SelectContent>
@ -117,7 +117,7 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<SelectContent>
{toTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
{column.displayName || column.columnLabel || column.columnName}
</SelectItem>
))}
</SelectContent>

View File

@ -198,7 +198,9 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">{column.columnName}</span>
<span className="truncate font-medium">
{column.displayName || column.columnLabel || column.columnName}
</span>
{isSelected && <span className="flex-shrink-0 text-blue-500"></span>}
{isMapped && <span className="flex-shrink-0 text-green-500"></span>}
{oppositeSelectedColumn && !isTypeCompatible && (
@ -264,7 +266,9 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
>
<div className="flex min-w-0 flex-col justify-between">
<div className="flex items-center gap-2">
<span className="truncate font-medium">{column.columnName}</span>
<span className="truncate font-medium">
{column.displayName || column.columnLabel || column.columnName}
</span>
{isSelected && <span className="flex-shrink-0 text-green-500"></span>}
{oppositeSelectedColumn && !isTypeCompatible && (
<span className="flex-shrink-0 text-red-500" title="데이터 타입이 호환되지 않음">

View File

@ -236,14 +236,10 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
return (
<div className="mt-4">
{/* 헤더 섹션 */}
<Card className="mb-6 from-blue-50 to-green-50 py-2">
<CardContent className="pt-0">
<p className="text-sm leading-relaxed text-gray-700">
, .
. FROM TO .
</p>
</CardContent>
</Card>
<p className="mb-4 text-sm leading-relaxed text-gray-700">
, .
. FROM TO .
</p>
<div className="grid grid-cols-2 gap-6">
<ColumnTableSection
@ -290,7 +286,7 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
</div>
{/* 빠른 필터 액션 */}
<Card className="mt-6 py-2">
<Card className="mt-6 border-none py-2 shadow-none">
<CardContent className="flex flex-wrap gap-2 p-3">
<span className="self-center text-sm font-medium text-gray-700"> :</span>
<Button

View File

@ -83,7 +83,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span>{column.displayName || column.columnLabel || column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
@ -112,7 +112,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span>{column.displayName || column.columnLabel || column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
@ -132,11 +132,15 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
<Label className="text-xs font-medium text-gray-600"> From </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedFromColumns.length > 0 ? (
selectedFromColumns.map((column) => (
<Badge key={column} variant="outline" className="text-xs">
{column}
</Badge>
))
selectedFromColumns.map((column) => {
const columnInfo = fromTableColumns.find((col) => col.columnName === column);
const displayName = columnInfo?.displayName || columnInfo?.columnLabel || column;
return (
<Badge key={column} variant="outline" className="text-xs">
{displayName}
</Badge>
);
})
) : (
<span className="text-xs text-gray-400"> </span>
)}
@ -147,11 +151,15 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
<Label className="text-xs font-medium text-gray-600"> To </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedToColumns.length > 0 ? (
selectedToColumns.map((column) => (
<Badge key={column} variant="secondary" className="text-xs">
{column}
</Badge>
))
selectedToColumns.map((column) => {
const columnInfo = toTableColumns.find((col) => col.columnName === column);
const displayName = columnInfo?.displayName || columnInfo?.columnLabel || column;
return (
<Badge key={column} variant="secondary" className="text-xs">
{displayName}
</Badge>
);
})
) : (
<span className="text-xs text-gray-400"> </span>
)}

View File

@ -376,7 +376,13 @@ export class DataFlowAPI {
}
// 페이지네이션된 응답에서 columns 배열만 추출
return response.data.data?.columns || [];
const columns = response.data.data?.columns || [];
// 이미 displayName에 라벨이 포함되어 있으므로 추가 처리 불필요
return columns.map((column) => ({
...column,
columnLabel: column.displayName || column.columnName, // displayName을 columnLabel로도 설정
}));
} catch (error) {
console.error("컬럼 정보 조회 오류:", error);
throw error;
@ -390,11 +396,40 @@ export class DataFlowAPI {
try {
const columns = await this.getTableColumns(tableName);
// 테이블 라벨 정보 조회
let tableLabel = tableName;
let tableDescription = `${tableName} 테이블`;
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/labels`);
if (response.data.success && response.data.data) {
tableLabel = response.data.data.tableLabel || tableName;
tableDescription = response.data.data.description || `${tableName} 테이블`;
}
} catch (error) {
// 라벨 정보가 없으면 기본값 사용 (404 등의 에러는 무시)
const axiosError = error as { response?: { status?: number } };
if (axiosError?.response?.status !== 404) {
console.warn(`테이블 라벨 조회 중 예상치 못한 오류: ${tableName}`, error);
}
}
// TableNode가 기대하는 컬럼 구조로 변환
const formattedColumns = columns.map((column) => ({
columnName: column.columnName,
name: column.columnName, // TableNode에서 사용하는 필드
displayName: column.displayName, // 한국어 라벨
columnLabel: column.displayName, // 동일한 값으로 설정
type: column.dataType, // TableNode에서 사용하는 필드
dataType: column.dataType,
description: column.description || "",
}));
return {
tableName,
displayName: tableName,
description: `${tableName} 테이블`,
columns,
displayName: tableLabel,
description: tableDescription,
columns: formattedColumns,
};
} catch (error) {
console.error("테이블 및 컬럼 정보 조회 오류:", error);