dev #46

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

View File

@ -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")
}

View File

@ -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}`,

1434
control.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
/**
*
* .
*/
import { commonCodeApi } from "@/lib/api/commonCode";
interface CacheEntry {
data: any;
timestamp: number;
expiry: number;
}
class CodeCache {
private cache = new Map<string, CacheEntry>();
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<void> {
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<any[]> {
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 };

View File

@ -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;

View File

@ -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<DynamicComponentProps> = ({
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 <ComponentByName {...props} {...finalProps} />;
// const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
// console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
// return <ComponentByName {...props} {...finalProps} />;
console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화`);
return <div> ...</div>;
} catch (error) {
console.error(`DB 지정 컴포넌트 "${dbWebType.component_name}" 렌더링 실패:`, error);
}
@ -89,8 +91,10 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
// 3순위: 웹타입명으로 자동 매핑 (폴백)
try {
console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`);
const FallbackComponent = getWidgetComponentByWebType(webType);
return <FallbackComponent {...props} />;
// const FallbackComponent = getWidgetComponentByWebType(webType);
// return <FallbackComponent {...props} />;
console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`);
return <div> ...</div>;
} catch (error) {
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
return (

View File

@ -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";

View File

@ -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"),
},
};