diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma
index 169adbf1..35a3578c 100644
--- a/backend-node/prisma/schema.prisma
+++ b/backend-node/prisma/schema.prisma
@@ -3952,7 +3952,7 @@ model layout_standards {
}
model table_relationships {
- relationship_id Int @id
+ relationship_id Int @id @default(autoincrement())
relationship_name String? @db.VarChar(200)
from_table_name String? @db.VarChar(100)
from_column_name String? @db.VarChar(100)
@@ -3968,6 +3968,9 @@ model table_relationships {
updated_date DateTime? @db.Timestamp(6)
updated_by String? @db.VarChar(50)
diagram_id Int?
+
+ // 역방향 관계
+ bridges data_relationship_bridge[]
}
model data_relationship_bridge {
@@ -3990,6 +3993,9 @@ model data_relationship_bridge {
to_key_value String? @db.VarChar(500)
to_record_id String? @db.VarChar(100)
+ // 관계 정의
+ relationship table_relationships? @relation(fields: [relationship_id], references: [relationship_id])
+
@@index([connection_type], map: "idx_data_bridge_connection_type")
@@index([company_code, is_active], map: "idx_data_bridge_company_active")
}
diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts
index 0f711335..5f41f18d 100644
--- a/backend-node/src/services/dataflowService.ts
+++ b/backend-node/src/services/dataflowService.ts
@@ -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}`,
diff --git a/control.html b/control.html
new file mode 100644
index 00000000..bcb76876
--- /dev/null
+++ b/control.html
@@ -0,0 +1,1434 @@
+
+
+
+
+
+ 테이블 관계 및 제어관리 시스템
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/lib/caching/codeCache.ts b/frontend/lib/caching/codeCache.ts
new file mode 100644
index 00000000..94616f53
--- /dev/null
+++ b/frontend/lib/caching/codeCache.ts
@@ -0,0 +1,198 @@
+/**
+ * 공통 코드 캐시 시스템
+ * 자주 사용되는 공통 코드들을 메모리에 캐싱하여 성능을 향상시킵니다.
+ */
+
+import { commonCodeApi } from "@/lib/api/commonCode";
+
+interface CacheEntry {
+ data: any;
+ timestamp: number;
+ expiry: number;
+}
+
+class CodeCache {
+ private cache = new Map();
+ private defaultTTL = 5 * 60 * 1000; // 5분
+
+ /**
+ * 캐시에 데이터 저장
+ */
+ set(key: string, data: any, ttl?: number): void {
+ const expiry = ttl || this.defaultTTL;
+ const entry: CacheEntry = {
+ data,
+ timestamp: Date.now(),
+ expiry,
+ };
+ this.cache.set(key, entry);
+ }
+
+ /**
+ * 캐시에서 데이터 조회
+ */
+ get(key: string): any | null {
+ const entry = this.cache.get(key);
+ if (!entry) {
+ return null;
+ }
+
+ // TTL 체크
+ if (Date.now() - entry.timestamp > entry.expiry) {
+ this.cache.delete(key);
+ return null;
+ }
+
+ return entry.data;
+ }
+
+ /**
+ * 캐시에서 데이터 삭제
+ */
+ delete(key: string): boolean {
+ return this.cache.delete(key);
+ }
+
+ /**
+ * 모든 캐시 삭제
+ */
+ clear(): void {
+ this.cache.clear();
+ }
+
+ /**
+ * 만료된 캐시 정리
+ */
+ cleanup(): void {
+ const now = Date.now();
+ for (const [key, entry] of this.cache.entries()) {
+ if (now - entry.timestamp > entry.expiry) {
+ this.cache.delete(key);
+ }
+ }
+ }
+
+ /**
+ * 캐시 상태 조회
+ */
+ getStats(): { size: number; keys: string[] } {
+ return {
+ size: this.cache.size,
+ keys: Array.from(this.cache.keys()),
+ };
+ }
+
+ /**
+ * 공통 코드 캐시 키 생성
+ */
+ createCodeKey(category: string, companyCode?: string): string {
+ return `code:${category}:${companyCode || "*"}`;
+ }
+
+ /**
+ * 여러 코드 카테고리를 배치로 미리 로딩
+ */
+ async preloadCodes(categories: string[]): Promise {
+ console.log(`🔄 코드 배치 로딩 시작: ${categories.join(", ")}`);
+
+ const promises = categories.map(async (category) => {
+ try {
+ const response = await commonCodeApi.codes.getList(category, { isActive: true });
+ if (response.success && response.data) {
+ const cacheKey = this.createCodeKey(category);
+ this.set(cacheKey, response.data, this.defaultTTL);
+ console.log(`✅ 코드 로딩 완료: ${category} (${response.data.length}개)`);
+ }
+ } catch (error) {
+ console.error(`❌ 코드 로딩 실패: ${category}`, error);
+ }
+ });
+
+ await Promise.all(promises);
+ console.log(`✅ 코드 배치 로딩 완료: ${categories.length}개 카테고리`);
+ }
+
+ /**
+ * 코드를 동기적으로 조회 (캐시에서만)
+ */
+ getCodeSync(category: string, companyCode?: string): any[] | null {
+ const cacheKey = this.createCodeKey(category, companyCode);
+ return this.get(cacheKey);
+ }
+
+ /**
+ * 코드를 비동기적으로 조회 (캐시 미스 시 API 호출)
+ */
+ async getCodeAsync(category: string, companyCode?: string): Promise {
+ const cached = this.getCodeSync(category, companyCode);
+ if (cached) {
+ return cached;
+ }
+
+ try {
+ const response = await commonCodeApi.codes.getList(category, { isActive: true });
+ if (response.success && response.data) {
+ const cacheKey = this.createCodeKey(category, companyCode);
+ this.set(cacheKey, response.data, this.defaultTTL);
+ return response.data;
+ }
+ } catch (error) {
+ console.error(`❌ 코드 조회 실패: ${category}`, error);
+ }
+
+ return [];
+ }
+
+ /**
+ * 캐시 정보 조회 (성능 메트릭용)
+ */
+ getCacheInfo(): {
+ size: number;
+ keys: string[];
+ totalMemoryUsage: number;
+ hitRate?: number;
+ } {
+ const stats = this.getStats();
+
+ // 메모리 사용량 추정 (대략적)
+ let totalMemoryUsage = 0;
+ for (const [key, entry] of this.cache.entries()) {
+ // 키 크기 + 데이터 크기 추정
+ totalMemoryUsage += key.length * 2; // 문자열은 UTF-16이므로 2바이트
+ if (Array.isArray(entry.data)) {
+ totalMemoryUsage += entry.data.length * 100; // 각 항목당 대략 100바이트로 추정
+ } else {
+ totalMemoryUsage += JSON.stringify(entry.data).length * 2;
+ }
+ }
+
+ return {
+ ...stats,
+ totalMemoryUsage,
+ };
+ }
+
+ /**
+ * 특정 카테고리의 캐시 무효화
+ */
+ invalidate(category: string, companyCode?: string): boolean {
+ const cacheKey = this.createCodeKey(category, companyCode);
+ return this.delete(cacheKey);
+ }
+}
+
+// 싱글톤 인스턴스 생성
+const codeCache = new CodeCache();
+
+// 주기적으로 만료된 캐시 정리 (10분마다)
+if (typeof window !== "undefined") {
+ setInterval(
+ () => {
+ codeCache.cleanup();
+ },
+ 10 * 60 * 1000,
+ );
+}
+
+export default codeCache;
+export { CodeCache, codeCache };
diff --git a/frontend/lib/hooks/useEntityJoinOptimization.ts b/frontend/lib/hooks/useEntityJoinOptimization.ts
index b124963a..3c3c7545 100644
--- a/frontend/lib/hooks/useEntityJoinOptimization.ts
+++ b/frontend/lib/hooks/useEntityJoinOptimization.ts
@@ -4,7 +4,7 @@
*/
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
-import { codeCache } from "@/lib/cache/codeCache";
+import { codeCache } from "@/lib/caching/codeCache";
interface ColumnMetaInfo {
webType?: string;
diff --git a/frontend/lib/registry/DynamicWebTypeRenderer.tsx b/frontend/lib/registry/DynamicWebTypeRenderer.tsx
index 96e9d3bf..47cb8dfd 100644
--- a/frontend/lib/registry/DynamicWebTypeRenderer.tsx
+++ b/frontend/lib/registry/DynamicWebTypeRenderer.tsx
@@ -3,7 +3,7 @@
import React, { useMemo } from "react";
import { WebTypeRegistry } from "./WebTypeRegistry";
import { DynamicComponentProps } from "./types";
-import { getWidgetComponentByWebType, getWidgetComponentByName } from "@/components/screen/widgets/types";
+// import { getWidgetComponentByWebType, getWidgetComponentByName } from "@/components/screen/widgets/types"; // 임시 비활성화
import { useWebTypes } from "@/hooks/admin/useWebTypes";
/**
@@ -53,9 +53,11 @@ export const DynamicWebTypeRenderer: React.FC = ({
console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
console.log(`DB 웹타입 정보:`, dbWebType);
console.log(`웹타입 데이터 배열:`, webTypes);
- const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
- console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
- return ;
+ // const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
+ // console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
+ // return ;
+ console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화`);
+ return 컴포넌트 로딩 중...
;
} catch (error) {
console.error(`DB 지정 컴포넌트 "${dbWebType.component_name}" 렌더링 실패:`, error);
}
@@ -89,8 +91,10 @@ export const DynamicWebTypeRenderer: React.FC = ({
// 3순위: 웹타입명으로 자동 매핑 (폴백)
try {
console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`);
- const FallbackComponent = getWidgetComponentByWebType(webType);
- return ;
+ // const FallbackComponent = getWidgetComponentByWebType(webType);
+ // return ;
+ console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`);
+ return 웹타입 로딩 중...
;
} catch (error) {
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
return (
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 21996dd9..3d8285fc 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useMemo } from "react";
import { TableListConfig, ColumnConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
-import { codeCache } from "@/lib/cache/codeCache";
+import { codeCache } from "@/lib/caching/codeCache";
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index 3b517a6b..6d01bfab 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -36,8 +36,10 @@ const nextConfig = {
// 환경 변수 (런타임에 읽기)
env: {
- // 개발 환경에서는 Next.js rewrites를 통해 /api로 프록시
- NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "/api",
+ // 프로덕션에서는 직접 백엔드 URL 사용, 개발환경에서는 프록시 사용
+ NEXT_PUBLIC_API_URL:
+ process.env.NEXT_PUBLIC_API_URL ||
+ (process.env.NODE_ENV === "production" ? "http://39.117.244.52:8080/api" : "/api"),
},
};