빌드 에러 수정
This commit is contained in:
parent
724ed51826
commit
2d8e33088e
|
|
@ -821,7 +821,7 @@ export class DataflowService {
|
|||
relationships.forEach((rel) => {
|
||||
const diagramId = rel.diagram_id;
|
||||
|
||||
if (!diagramMap.has(diagramId)) {
|
||||
if (diagramId && !diagramMap.has(diagramId)) {
|
||||
diagramMap.set(diagramId, {
|
||||
diagramId: diagramId,
|
||||
diagramName: rel.relationship_name, // 첫 번째 관계의 이름을 사용
|
||||
|
|
@ -837,15 +837,19 @@ export class DataflowService {
|
|||
});
|
||||
}
|
||||
|
||||
const diagram = diagramMap.get(diagramId);
|
||||
diagram.tableCount.add(rel.from_table_name);
|
||||
diagram.tableCount.add(rel.to_table_name);
|
||||
diagram.relationshipCount++;
|
||||
if (diagramId) {
|
||||
const diagram = diagramMap.get(diagramId);
|
||||
if (diagram) {
|
||||
diagram.tableCount.add(rel.from_table_name || "");
|
||||
diagram.tableCount.add(rel.to_table_name || "");
|
||||
}
|
||||
diagram.relationshipCount++;
|
||||
|
||||
// 최신 업데이트 시간 유지
|
||||
if (rel.updated_date && rel.updated_date > diagram.updatedAt) {
|
||||
diagram.updatedAt = rel.updated_date;
|
||||
diagram.updatedBy = rel.updated_by;
|
||||
// 최신 업데이트 시간 유지
|
||||
if (rel.updated_date && rel.updated_date > diagram.updatedAt) {
|
||||
diagram.updatedAt = rel.updated_date;
|
||||
diagram.updatedBy = rel.updated_by;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1110,10 +1114,14 @@ export class DataflowService {
|
|||
}
|
||||
|
||||
// diagram_id로 모든 관계 조회
|
||||
return this.getDiagramRelationshipsByDiagramId(
|
||||
companyCode,
|
||||
targetRelationship.diagram_id
|
||||
);
|
||||
if (targetRelationship.diagram_id) {
|
||||
return this.getDiagramRelationshipsByDiagramId(
|
||||
companyCode,
|
||||
targetRelationship.diagram_id
|
||||
);
|
||||
} else {
|
||||
throw new Error("관계에 diagram_id가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`DataflowService: relationship_id로 관계도 관계 조회 실패 - ${relationshipId}`,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import { useMemo } from "react";
|
||||
import codeCache from "@/lib/cache/codeCache";
|
||||
|
||||
/**
|
||||
* 엔티티 조인 최적화 훅
|
||||
* 테이블 간의 관계를 분석하여 최적화된 조인 전략을 제공합니다.
|
||||
*/
|
||||
|
||||
interface JoinOptimization {
|
||||
strategy: "eager" | "lazy" | "batch";
|
||||
priority: number;
|
||||
estimatedCost: number;
|
||||
}
|
||||
|
||||
interface EntityRelation {
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
joinType: "inner" | "left" | "right" | "full";
|
||||
cardinality: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
}
|
||||
|
||||
export const useEntityJoinOptimization = (
|
||||
relations: EntityRelation[],
|
||||
queryContext?: {
|
||||
expectedResultSize?: number;
|
||||
performanceProfile?: "fast" | "balanced" | "memory-efficient";
|
||||
},
|
||||
) => {
|
||||
const optimization = useMemo(() => {
|
||||
const cacheKey = `join-optimization:${JSON.stringify(relations)}:${JSON.stringify(queryContext)}`;
|
||||
|
||||
// 캐시에서 먼저 확인
|
||||
const cached = codeCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 최적화 전략 계산
|
||||
const optimizations: Record<string, JoinOptimization> = {};
|
||||
|
||||
relations.forEach((relation) => {
|
||||
const key = `${relation.fromTable}-${relation.toTable}`;
|
||||
|
||||
// 카디널리티에 따른 기본 전략
|
||||
let strategy: JoinOptimization["strategy"] = "eager";
|
||||
let priority = 1;
|
||||
let estimatedCost = 1;
|
||||
|
||||
switch (relation.cardinality) {
|
||||
case "one-to-one":
|
||||
strategy = "eager";
|
||||
priority = 3;
|
||||
estimatedCost = 1;
|
||||
break;
|
||||
case "one-to-many":
|
||||
strategy = "lazy";
|
||||
priority = 2;
|
||||
estimatedCost = 2;
|
||||
break;
|
||||
case "many-to-one":
|
||||
strategy = "eager";
|
||||
priority = 2;
|
||||
estimatedCost = 1.5;
|
||||
break;
|
||||
case "many-to-many":
|
||||
strategy = "batch";
|
||||
priority = 1;
|
||||
estimatedCost = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
// 성능 프로필에 따른 조정
|
||||
if (queryContext?.performanceProfile === "fast") {
|
||||
if (strategy === "lazy") strategy = "eager";
|
||||
priority += 1;
|
||||
} else if (queryContext?.performanceProfile === "memory-efficient") {
|
||||
if (strategy === "eager") strategy = "lazy";
|
||||
estimatedCost *= 0.8;
|
||||
}
|
||||
|
||||
// 예상 결과 크기에 따른 조정
|
||||
if (queryContext?.expectedResultSize && queryContext.expectedResultSize > 1000) {
|
||||
if (strategy === "eager") strategy = "batch";
|
||||
estimatedCost *= 1.2;
|
||||
}
|
||||
|
||||
optimizations[key] = {
|
||||
strategy,
|
||||
priority,
|
||||
estimatedCost,
|
||||
};
|
||||
});
|
||||
|
||||
// 결과를 캐시에 저장 (1분 TTL)
|
||||
codeCache.set(cacheKey, optimizations, 60 * 1000);
|
||||
|
||||
return optimizations;
|
||||
}, [relations, queryContext]);
|
||||
|
||||
const getOptimizationForRelation = (fromTable: string, toTable: string): JoinOptimization | null => {
|
||||
const key = `${fromTable}-${toTable}`;
|
||||
return optimization[key] || null;
|
||||
};
|
||||
|
||||
const getSortedRelations = (): Array<{ relation: EntityRelation; optimization: JoinOptimization }> => {
|
||||
return relations
|
||||
.map((relation) => ({
|
||||
relation,
|
||||
optimization: getOptimizationForRelation(relation.fromTable, relation.toTable)!,
|
||||
}))
|
||||
.filter((item) => item.optimization)
|
||||
.sort((a, b) => b.optimization.priority - a.optimization.priority);
|
||||
};
|
||||
|
||||
const getTotalEstimatedCost = (): number => {
|
||||
return Object.values(optimization).reduce((total, opt) => total + opt.estimatedCost, 0);
|
||||
};
|
||||
|
||||
return {
|
||||
optimization,
|
||||
getOptimizationForRelation,
|
||||
getSortedRelations,
|
||||
getTotalEstimatedCost,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||
import codeCache from "@/lib/cache/codeCache";
|
||||
|
||||
interface TableListProps {
|
||||
data: any[];
|
||||
columns: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "text" | "number" | "date" | "boolean";
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
}>;
|
||||
relations?: Array<{
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
joinType: "inner" | "left" | "right" | "full";
|
||||
cardinality: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
}>;
|
||||
onRowClick?: (row: any) => void;
|
||||
onSort?: (column: string, direction: "asc" | "desc") => void;
|
||||
onFilter?: (column: string, value: any) => void;
|
||||
loading?: boolean;
|
||||
pagination?: {
|
||||
current: number;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
onChange: (page: number, size: number) => void;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TableListComponent: React.FC<TableListProps> = ({
|
||||
data,
|
||||
columns,
|
||||
relations = [],
|
||||
onRowClick,
|
||||
onSort,
|
||||
onFilter,
|
||||
loading = false,
|
||||
pagination,
|
||||
className = "",
|
||||
}) => {
|
||||
// 조인 최적화 적용
|
||||
const { optimization, getSortedRelations } = useEntityJoinOptimization(relations, {
|
||||
expectedResultSize: data.length,
|
||||
performanceProfile: "balanced",
|
||||
});
|
||||
|
||||
// 최적화된 데이터 처리
|
||||
const processedData = useMemo(() => {
|
||||
if (!relations.length) return data;
|
||||
|
||||
const cacheKey = `table-list-processed:${JSON.stringify(data.slice(0, 5))}:${JSON.stringify(relations)}`;
|
||||
const cached = codeCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
// 관계 기반 데이터 처리 로직
|
||||
const sortedRelations = getSortedRelations();
|
||||
let processedResult = [...data];
|
||||
|
||||
// 여기에서 실제 조인 로직을 구현할 수 있습니다
|
||||
// 현재는 기본 데이터를 반환
|
||||
|
||||
codeCache.set(cacheKey, processedResult, 30 * 1000); // 30초 캐시
|
||||
return processedResult;
|
||||
}, [data, relations, getSortedRelations]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`animate-pulse ${className}`}>
|
||||
<div className="mb-2 h-4 w-full rounded bg-gray-200"></div>
|
||||
<div className="mb-2 h-4 w-3/4 rounded bg-gray-200"></div>
|
||||
<div className="h-4 w-1/2 rounded bg-gray-200"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`table-list-component ${className}`}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className="cursor-pointer px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:bg-gray-100"
|
||||
onClick={() => onSort?.(column.key, "asc")}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{column.label}</span>
|
||||
{column.sortable && (
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{processedData.map((row, index) => (
|
||||
<tr key={index} className="cursor-pointer hover:bg-gray-50" onClick={() => onRowClick?.(row)}>
|
||||
{columns.map((column) => (
|
||||
<td key={column.key} className="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||
{formatCellValue(row[column.key], column.type)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-6 py-3">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-700">
|
||||
총 {pagination.total}개 중{" "}
|
||||
{Math.min((pagination.current - 1) * pagination.pageSize + 1, pagination.total)}-
|
||||
{Math.min(pagination.current * pagination.pageSize, pagination.total)}개 표시
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => pagination.onChange(pagination.current - 1, pagination.pageSize)}
|
||||
disabled={pagination.current <= 1}
|
||||
className="rounded border px-3 py-1 text-sm disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<span className="text-sm">
|
||||
{pagination.current} / {Math.ceil(pagination.total / pagination.pageSize)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => pagination.onChange(pagination.current + 1, pagination.pageSize)}
|
||||
disabled={pagination.current >= Math.ceil(pagination.total / pagination.pageSize)}
|
||||
className="rounded border px-3 py-1 text-sm disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 셀 값 포맷팅 유틸리티
|
||||
function formatCellValue(value: any, type?: string): string {
|
||||
if (value === null || value === undefined) return "-";
|
||||
|
||||
switch (type) {
|
||||
case "date":
|
||||
return new Date(value).toLocaleDateString();
|
||||
case "number":
|
||||
return typeof value === "number" ? value.toLocaleString() : value;
|
||||
case "boolean":
|
||||
return value ? "예" : "아니오";
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableListComponent;
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import React from "react";
|
||||
import { TableListComponent } from "./TableListComponent";
|
||||
import codeCache from "@/lib/cache/codeCache";
|
||||
|
||||
interface TableListRendererProps {
|
||||
config: {
|
||||
columns?: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "text" | "number" | "date" | "boolean";
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
}>;
|
||||
relations?: Array<{
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
joinType: "inner" | "left" | "right" | "full";
|
||||
cardinality: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
}>;
|
||||
dataSource?: string;
|
||||
pagination?: {
|
||||
enabled: boolean;
|
||||
pageSize: number;
|
||||
};
|
||||
sorting?: {
|
||||
enabled: boolean;
|
||||
defaultColumn?: string;
|
||||
defaultDirection?: "asc" | "desc";
|
||||
};
|
||||
filtering?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
styling?: {
|
||||
className?: string;
|
||||
theme?: "default" | "compact" | "striped";
|
||||
};
|
||||
};
|
||||
data?: any[];
|
||||
onAction?: (action: string, payload: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TableListRenderer: React.FC<TableListRendererProps> = ({
|
||||
config,
|
||||
data = [],
|
||||
onAction,
|
||||
className = "",
|
||||
}) => {
|
||||
const { columns = [], relations = [], pagination, sorting, filtering, styling } = config;
|
||||
|
||||
// 기본 컬럼 설정
|
||||
const defaultColumns = React.useMemo(() => {
|
||||
if (columns.length > 0) return columns;
|
||||
|
||||
// 데이터에서 자동으로 컬럼 추출
|
||||
if (data.length > 0) {
|
||||
const sampleRow = data[0];
|
||||
return Object.keys(sampleRow).map((key) => ({
|
||||
key,
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
type: inferColumnType(sampleRow[key]),
|
||||
sortable: sorting?.enabled ?? true,
|
||||
filterable: filtering?.enabled ?? true,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [columns, data, sorting?.enabled, filtering?.enabled]);
|
||||
|
||||
// 페이지네이션 상태
|
||||
const [currentPage, setCurrentPage] = React.useState(1);
|
||||
const [pageSize, setPageSize] = React.useState(pagination?.pageSize || 10);
|
||||
|
||||
// 정렬 상태
|
||||
const [sortColumn, setSortColumn] = React.useState(sorting?.defaultColumn || "");
|
||||
const [sortDirection, setSortDirection] = React.useState<"asc" | "desc">(sorting?.defaultDirection || "asc");
|
||||
|
||||
// 필터 상태
|
||||
const [filters, setFilters] = React.useState<Record<string, any>>({});
|
||||
|
||||
// 데이터 처리
|
||||
const processedData = React.useMemo(() => {
|
||||
let result = [...data];
|
||||
|
||||
// 필터링 적용
|
||||
if (filtering?.enabled && Object.keys(filters).length > 0) {
|
||||
result = result.filter((row) => {
|
||||
return Object.entries(filters).every(([column, value]) => {
|
||||
if (!value) return true;
|
||||
const rowValue = String(row[column]).toLowerCase();
|
||||
const filterValue = String(value).toLowerCase();
|
||||
return rowValue.includes(filterValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬 적용
|
||||
if (sorting?.enabled && sortColumn) {
|
||||
result.sort((a, b) => {
|
||||
const aVal = a[sortColumn];
|
||||
const bVal = b[sortColumn];
|
||||
|
||||
let comparison = 0;
|
||||
if (aVal < bVal) comparison = -1;
|
||||
if (aVal > bVal) comparison = 1;
|
||||
|
||||
return sortDirection === "desc" ? -comparison : comparison;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [data, filters, sortColumn, sortDirection, filtering?.enabled, sorting?.enabled]);
|
||||
|
||||
// 페이지네이션 데이터
|
||||
const paginatedData = React.useMemo(() => {
|
||||
if (!pagination?.enabled) return processedData;
|
||||
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return processedData.slice(startIndex, endIndex);
|
||||
}, [processedData, currentPage, pageSize, pagination?.enabled]);
|
||||
|
||||
// 이벤트 핸들러
|
||||
const handleRowClick = (row: any) => {
|
||||
onAction?.("rowClick", { row });
|
||||
};
|
||||
|
||||
const handleSort = (column: string, direction: "asc" | "desc") => {
|
||||
setSortColumn(column);
|
||||
setSortDirection(direction);
|
||||
onAction?.("sort", { column, direction });
|
||||
};
|
||||
|
||||
const handleFilter = (column: string, value: any) => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
[column]: value,
|
||||
}));
|
||||
onAction?.("filter", { column, value });
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
onAction?.("pageChange", { page, size });
|
||||
};
|
||||
|
||||
// 테마 클래스
|
||||
const themeClass = React.useMemo(() => {
|
||||
switch (styling?.theme) {
|
||||
case "compact":
|
||||
return "table-compact";
|
||||
case "striped":
|
||||
return "table-striped";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}, [styling?.theme]);
|
||||
|
||||
return (
|
||||
<TableListComponent
|
||||
data={paginatedData}
|
||||
columns={defaultColumns}
|
||||
relations={relations}
|
||||
onRowClick={handleRowClick}
|
||||
onSort={handleSort}
|
||||
onFilter={handleFilter}
|
||||
pagination={
|
||||
pagination?.enabled
|
||||
? {
|
||||
current: currentPage,
|
||||
total: processedData.length,
|
||||
pageSize,
|
||||
onChange: handlePageChange,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={`${className} ${styling?.className || ""} ${themeClass}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 컬럼 타입 추론 유틸리티
|
||||
function inferColumnType(value: any): "text" | "number" | "date" | "boolean" {
|
||||
if (typeof value === "boolean") return "boolean";
|
||||
if (typeof value === "number") return "number";
|
||||
if (value instanceof Date || (typeof value === "string" && !isNaN(Date.parse(value)))) {
|
||||
return "date";
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
export default TableListRenderer;
|
||||
Loading…
Reference in New Issue