Skip to content

TopologyChart

MSA (Microservices Architecture) 분석을 위한 Force 기반 노드-링크 다이어그램 컴포넌트입니다.

개요

TopologyChart는 마이크로서비스 간의 관계와 호출 패턴을 자유로운 형태의 네트워크 그래프로 시각화합니다. Force 시뮬레이션을 통해 자연스러운 노드 배치가 가능하며, 사용자가 직접 노드를 드래그하여 레이아웃을 조정할 수 있습니다.

주요 특징

  • 인터랙티브: 노드 드래그, 확대/축소, 팬 기능 지원
  • Force 시뮬레이션: 물리 기반 시뮬레이션으로 자연스러운 노드 배치
  • 자동 스케일: 데이터에 맞는 자동 스케일 조정
  • 노드 필터링: 고립된 노드 숨기기 기능
  • 시각적 피드백: 호버/클릭 시 연결된 노드/엣지 하이라이팅
  • 실시간 편집: 드래그로 노드 위치 조정 가능

Props Interface

typescript
interface TopologyChartProps {
    /** 토폴로지 데이터 */
    data?: TopologyData;
    /** 차트 너비 (기본값: 800) */
    width?: number;
    /** 차트 높이 (기본값: 600) */
    height?: number;
    /** Force 시뮬레이션 사용 여부 (기본값: false) */
    enableForceSimulation?: boolean;
    /** 웹 워커 객체 (undefined: 자동생성, Worker: 직접제공) */
    worker?: Worker;
    /** 노드 스케일 컨트롤 표시 여부 (기본값: true) */
    showScaleControls?: boolean;
    /** 자동 스케일 조정 활성화 여부 (기본값: true) */
    enableAutoScale?: boolean;
    /** 노드 라벨 숨김 여부 (기본값: false) */
    hideNodeLabels?: boolean;
    /** 실시간 모드 활성화 여부 (기본값: false) */
    realtimeMode?: boolean;
    /** 현재 선택된 엣지 - 상세 정보 표시용 */
    selectedEdge?: TopologyEdge;
    /** 엣지 상세 데이터 - API 호출 후 가져온 데이터 */
    edgeDetailData?: EdgeDetailData;
    /** 현재 선택된 노드 - 상세 정보 표시용 */
    selectedNode?: TopologyNode;
    /** 노드 상세 데이터 - API 호출 후 가져온 데이터 */
    nodeDetailData?: Record<string, number[]>;
    /** 활성화할 클러스터 ID 배열 */
    activeClusterIds?: string[];
    /** 애니메이션 효과를 적용할 클러스터 ID */
    animatedClusterId?: string;
}

Events

typescript
interface TopologyChartEmits {
    /** 노드 호버 시 발생 */
    'node-hover': [node: TopologyNode];
    /** 노드 클릭 시 발생 */
    'node-click': [node: TopologyNode];
    /** 노드 선택 시 발생 (상세 데이터 로딩 포함) */
    'node-select': [node: TopologyNode];
    /** 엣지 호버 시 발생 */
    'edge-hover': [edge: TopologyEdge];
    /** 엣지 클릭 시 발생 */
    'edge-click': [edge: TopologyEdge];
    /** 엣지 선택 시 발생 (상세 데이터 로딩 포함) */
    'edge-select': [edge: TopologyEdge];
    /** 호버 해제 시 발생 */
    'hover-out': [];
    /** 차트 렌더링 완료 시 발생 */
    'chart-ready': [];
    /** 레이아웃 오류 발생 시 발생 */
    'layout-error': [error: Error];
    /** 클러스터 감지 완료 이벤트 */
    'clusters-detected': [clusters: TopologyCluster[]];
    /** 개별 클러스터 렌더링 완료 이벤트 */
    'cluster-rendered': [clusterId: string, renderInfo: ClusterRenderInfo];
    /** 전체 클러스터 렌더링 완료 이벤트 */
    'all-clusters-rendered': [renderInfo: ClusterRenderInfo[]];
    /** 포스 시뮬레이션 종료 이벤트 */
    'force-simulation-end': [tickCount: number, finalAlpha: number];
}

기본 사용법

vue
<template>
    <TopologyChart
        :data="topologyData"
        :width="800"
        :height="600"
        :enable-force-simulation="false"
        :enable-auto-scale="true"
        :active-cluster-ids="activeClusterIds"
        :selected-edge="selectedEdge"
        :edge-detail-data="edgeDetailData"
        :selected-node="selectedNode"
        :node-detail-data="nodeDetailData"
        @node-select="handleNodeSelect"
        @edge-select="handleEdgeSelect"
        @clusters-detected="handleClustersDetected"
    />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { TopologyChart } from '@jennifersoft/apm-components';
import type {
    TopologyData,
    TopologyNode,
    TopologyEdge,
    EdgeDetailData,
    TopologyCluster,
} from '@jennifersoft/apm-apis';

const topologyData = ref<TopologyData>(sampleTopologyData);
const activeClusterIds = ref<string[]>([]);
const selectedEdge = ref<TopologyEdge | null>(null);
const edgeDetailData = ref<EdgeDetailData | null>(null);
const selectedNode = ref<TopologyNode | null>(null);
const nodeDetailData = ref<Record<string, number[]> | null>(null);

const handleNodeSelect = async (node: TopologyNode) => {
    selectedNode.value = node;
    // API를 통해 노드 상세 데이터 로딩
    nodeDetailData.value = await fetchNodeDetailData(node);
};

const handleEdgeSelect = async (edge: TopologyEdge) => {
    selectedEdge.value = edge;
    // API를 통해 엣지 상세 데이터 로딩
    edgeDetailData.value = await fetchEdgeDetailData(edge);
};

const handleClustersDetected = (clusters: TopologyCluster[]) => {
    // 기본적으로 일반 클러스터만 활성화
    const normalClusters = clusters.filter(c => c.type !== 'isolated');
    activeClusterIds.value = normalClusters.map(c => c.id);
};
</script>

인터랙티브 데모

토폴로지 통계

총 노드:8개
연결된 노드:6개
고립된 노드:2개
총 엣지:8개
정상 엣지:5개
실패 엣지:3개
TopologyChart 컴포넌트 로딩 중...
💡 사용법: 노드 드래그/클릭, 마우스 휠 확대/축소, 배경 드래그 이동, Force 시뮬레이션을 통한 물리 기반 자동 배치 등의 기능을 사용할 수 있습니다.

동적 크기 조절

vue
<template>
    <div class="chart-container">
        <!-- 크기 선택 컨트롤 -->
        <div class="size-controls">
            <label>차트 크기:</label>
            <select v-model="selectedSize" @change="updateChartSize">
                <option value="small">Small (600×400)</option>
                <option value="medium">Medium (800×600)</option>
                <option value="large">Large (1200×800)</option>
                <option value="custom">Custom</option>
            </select>

            <div v-if="selectedSize === 'custom'" class="custom-size">
                <input type="number" v-model="customWidth" placeholder="너비" />
                <span>×</span>
                <input
                    type="number"
                    v-model="customHeight"
                    placeholder="높이"
                />
            </div>
        </div>

        <TopologyChart
            :data="topologyData"
            :width="chartWidth"
            :height="chartHeight"
            v-bind="chartOptions"
        />
    </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

const selectedSize = ref('medium');
const customWidth = ref(800);
const customHeight = ref(600);

const chartWidth = computed(() => {
    switch (selectedSize.value) {
        case 'small':
            return 600;
        case 'large':
            return 1200;
        case 'custom':
            return customWidth.value;
        default:
            return 800; // medium
    }
});

const chartHeight = computed(() => {
    switch (selectedSize.value) {
        case 'small':
            return 400;
        case 'large':
            return 800;
        case 'custom':
            return customHeight.value;
        default:
            return 600; // medium
    }
});
</script>

이벤트 로깅 시스템

사용자 인터랙션을 추적하고 디버깅에 활용할 수 있는 이벤트 로깅 시스템:

vue
<template>
    <div class="topology-with-logs">
        <TopologyChart
            :data="topologyData"
            @node-click="logNodeClick"
            @edge-click="logEdgeClick"
            @node-hover="logNodeHover"
            @edge-hover="logEdgeHover"
            @chart-ready="logChartReady"
        />

        <!-- 이벤트 로그 패널 -->
        <div v-if="events.length > 0" class="event-logs">
            <h4>이벤트 로그</h4>
            <div class="event-list">
                <div
                    v-for="event in recentEvents"
                    :key="event.id"
                    class="event-item"
                >
                    <span class="event-time">{{ event.time }}</span>
                    <span class="event-type" :class="event.type">{{
                        event.type
                    }}</span>
                    <span class="event-detail">{{ event.detail }}</span>
                </div>
            </div>
            <button @click="clearEvents" class="clear-button">
                로그 지우기
            </button>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

interface EventLog {
    id: number;
    time: string;
    type: string;
    detail: string;
}

const events = ref<EventLog[]>([]);
let eventIdCounter = 0;

const recentEvents = computed(() => events.value.slice(-10).reverse());

const addEvent = (type: string, detail: string) => {
    events.value.push({
        id: ++eventIdCounter,
        time: new Date().toLocaleTimeString(),
        type,
        detail,
    });
};

const logNodeClick = (node: TopologyNode) => {
    addEvent('node-click', `${getNodeDisplayName(node)} 클릭`);
};

const logEdgeClick = (edge: TopologyEdge) => {
    const source = getNodeDisplayName(edge.relation.source);
    const target = getNodeDisplayName(edge.relation.target);
    addEvent('edge-click', `${source} → ${target} 엣지 클릭`);
};

const logNodeHover = (node: TopologyNode) => {
    addEvent('node-hover', `${getNodeDisplayName(node)} 호버`);
};

const logEdgeHover = (edge: TopologyEdge) => {
    const source = getNodeDisplayName(edge.relation.source);
    const target = getNodeDisplayName(edge.relation.target);
    addEvent('edge-hover', `${source} → ${target} 엣지 호버`);
};

const logChartReady = () => {
    addEvent('chart-ready', '차트 렌더링 완료');
};

const clearEvents = () => {
    events.value = [];
};

const getNodeDisplayName = (node: TopologyNode): string => {
    return node.props.shortName || node.props.longName || 'Unknown';
};
</script>

통계 정보 표시

토폴로지 데이터의 통계 정보를 실시간으로 계산하고 표시:

vue
<template>
    <div class="topology-with-stats">
        <TopologyChart :data="topologyData" />

        <!-- 통계 정보 패널 -->
        <div class="stats-panel">
            <h4>토폴로지 통계</h4>
            <div class="stats-grid">
                <div class="stat-item">
                    <span class="stat-label">총 노드 수:</span>
                    <span class="stat-value">{{ nodeStats.total }}개</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">연결된 노드:</span>
                    <span class="stat-value">{{ nodeStats.connected }}개</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">고립된 노드:</span>
                    <span class="stat-value">{{ nodeStats.isolated }}개</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">총 엣지 수:</span>
                    <span class="stat-value">{{ edgeStats.total }}개</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">정상 엣지:</span>
                    <span class="stat-value">{{ edgeStats.normal }}개</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">실패 엣지:</span>
                    <span class="stat-value error"
                        >{{ edgeStats.failed }}개</span
                    >
                </div>
                <div class="stat-item">
                    <span class="stat-label">총 실패 건수:</span>
                    <span class="stat-value error"
                        >{{ edgeStats.totalFailures }}건</span
                    >
                </div>
                <div class="stat-item">
                    <span class="stat-label">최고 실패 엣지:</span>
                    <span class="stat-value">{{
                        edgeStats.topFailureEdge
                    }}</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

const nodeStats = computed(() => {
    const total = topologyData.value.nodes.length;
    const connectedNodeIds = new Set<string>();

    topologyData.value.edges.forEach((edge) => {
        connectedNodeIds.add(getNodeId(edge.relation.source));
        connectedNodeIds.add(getNodeId(edge.relation.target));
    });

    const connected = connectedNodeIds.size;
    const isolated = total - connected;

    return { total, connected, isolated };
});

const edgeStats = computed(() => {
    const total = topologyData.value.edges.length;
    const failed = topologyData.value.edges.filter(
        (edge) => (edge.statistic.failureCount || 0) > 0
    ).length;
    const normal = total - failed;

    const totalFailures = topologyData.value.edges.reduce(
        (sum, edge) => sum + (edge.statistic.failureCount || 0),
        0
    );

    // 최고 실패 엣지 찾기
    const topFailureEdge = topologyData.value.edges
        .filter((edge) => (edge.statistic.failureCount || 0) > 0)
        .sort(
            (a, b) =>
                (b.statistic.failureCount || 0) -
                (a.statistic.failureCount || 0)
        )[0];

    const topFailureEdgeName = topFailureEdge
        ? `${getNodeDisplayName(
              topFailureEdge.relation.source
          )} → ${getNodeDisplayName(topFailureEdge.relation.target)} (${
              topFailureEdge.statistic.failureCount
          }건)`
        : '없음';

    return {
        total,
        normal,
        failed,
        totalFailures,
        topFailureEdge: topFailureEdgeName,
    };
});
</script>

<style scoped>
.stats-panel {
    margin-top: 20px;
    padding: 16px;
    border: 1px solid #e1e5e9;
    border-radius: 8px;
    background: #fafbfc;
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 12px;
    margin-top: 12px;
}

.stat-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
    background: white;
    border-radius: 4px;
    border: 1px solid #e5e7eb;
}

.stat-label {
    font-size: 14px;
    color: #6b7280;
}

.stat-value {
    font-weight: 600;
    color: #1f2937;
}

.stat-value.error {
    color: #dc2626;
}
</style>

## 데이터 구조 ### TopologyData ```typescript interface TopologyData { nodes:
TopologyNode[]; edges: TopologyEdge[]; }

TopologyNode

노드는 세 가지 타입으로 구분됩니다:

typescript
// 인스턴스 노드
interface TopologyInstanceNode {
    type: 'INSTANCE';
    props: {
        domainId: number;
        instId: number;
        oid: number;
        shortName: string;
        longName: string;
    };
}

// 도메인 노드
interface TopologyDomainNode {
    type: 'DOMAIN';
    props: {
        domainId: number;
        shortName: string;
        longName: string;
    };
}

// 원격 호출 노드
interface TopologyRemoteCallNode {
    type: 'REMOTE_CALL';
    props: {
        remoteCallType: RemoteCallTypeDef;
        languageTypeOrZero: number;
        customMethodDescHashOrZero: number;
        customMethodDescOrEmpty: string;
        ipAddressOrEmpty: string;
        portOrZero: number;
    };
}

type TopologyNode =
    | TopologyInstanceNode
    | TopologyDomainNode
    | TopologyRemoteCallNode;

TopologyEdge

typescript
interface TopologyEdge {
    relation: {
        source: TopologyNode;
        target: TopologyNode;
    };
    statistic: {
        count: number; // 호출 횟수
        timeSum: number; // 총 응답 시간 (ms)
        failureCount: number; // 실패 횟수
    };
}

샘플 데이터

자세한 샘플 데이터는 별도 파일에서 관리됩니다:

typescript
// j5-components/data/sample-topology-data.ts 에서 import
import {
    getCompleteTopologyData,
    sampleTopologyData,
} from './data/sample-topology-data';

// 완전한 샘플 데이터 사용
const topologyData = getCompleteTopologyData();

// 또는 기본 노드만 사용하고 엣지는 직접 생성
const basicData = sampleTopologyData;

샘플 데이터에는 다음 요소들이 포함되어 있습니다:

  • 8개 노드: 웹서버, API게이트웨이, 마이크로서비스들, 데이터베이스, 캐시, 고립된 서비스
  • 8개 엣지: 다양한 호출량과 에러 발생 시나리오
  • 실제적인 메트릭: count, timeSum, failureCount 값들
  • Force 시뮬레이션 테스트: 고립된 노드를 포함한 다양한 네트워크 구조

클러스터 기능

클러스터 자동 감지

TopologyChart는 노드 간의 연결 관계를 분석하여 자동으로 클러스터를 감지합니다:

vue
<template>
    <TopologyChart
        :data="topologyData"
        :active-cluster-ids="activeClusterIds"
        @clusters-detected="handleClustersDetected"
        @cluster-rendered="handleClusterRendered"
        @all-clusters-rendered="handleAllClustersRendered"
    />
</template>

<script setup>
const activeClusterIds = ref([]);
const detectedClusters = ref([]);

const handleClustersDetected = (clusters) => {
    detectedClusters.value = clusters;
    
    // 기본적으로 일반 클러스터만 활성화 (고립된 노드 제외)
    const normalClusters = clusters.filter(c => c.type !== 'isolated');
    activeClusterIds.value = normalClusters.map(c => c.id);
};

const handleClusterRendered = (clusterId, renderInfo) => {
    console.log(`클러스터 ${clusterId} 렌더링 완료:`, renderInfo);
};

const handleAllClustersRendered = (renderInfos) => {
    console.log('전체 클러스터 렌더링 완료:', renderInfos);
};
</script>

클러스터 유형

typescript
interface TopologyCluster {
    id: string;           // 클러스터 고유 ID
    nodeIds: string[];    // 클러스터에 포함된 노드 ID 배열
    type: 'normal' | 'isolated';  // 일반 클러스터 또는 고립된 노드
    nodeCount: number;    // 클러스터 내 노드 개수
}

interface ClusterRenderInfo {
    cluster: TopologyCluster;
    isRendered: boolean;
    nodeCount: number;
}

선택적 클러스터 표시

특정 클러스터만 선택적으로 표시할 수 있습니다:

vue
<template>
    <div>
        <!-- 클러스터 선택 UI -->
        <div class="cluster-controls">
            <label v-for="cluster in detectedClusters" :key="cluster.id">
                <input 
                    type="checkbox" 
                    :value="cluster.id"
                    v-model="activeClusterIds"
                >
                {{ cluster.type === 'isolated' ? '고립된 노드' : `클러스터 ${cluster.id}` }}
                ({{ cluster.nodeCount }}개 노드)
            </label>
        </div>
        
        <TopologyChart
            :data="topologyData"
            :active-cluster-ids="activeClusterIds"
            :animated-cluster-id="animatedClusterId"
        />
    </div>
</template>

<script setup>
const activeClusterIds = ref([]);
const animatedClusterId = ref(undefined);
const detectedClusters = ref([]);

// 특정 클러스터에 애니메이션 효과 적용
const highlightCluster = (clusterId) => {
    animatedClusterId.value = clusterId;
    setTimeout(() => {
        animatedClusterId.value = undefined;
    }, 2000);
};
</script>

API 통합 기능

노드 상세 데이터 로딩

노드 선택 시 자동으로 상세 메트릭 데이터를 로딩합니다:

vue
<template>
    <TopologyChart
        :data="topologyData"
        :selected-node="selectedNode"
        :node-detail-data="nodeDetailData"
        @node-select="handleNodeSelect"
    />
</template>

<script setup>
const selectedNode = ref(null);
const nodeDetailData = ref(null);

const handleNodeSelect = async (node) => {
    selectedNode.value = node;
    nodeDetailData.value = null; // 이전 데이터 초기화
    
    try {
        let apiUrl, metrics;
        
        if (node.type === 'DOMAIN') {
            apiUrl = `/api-v2/topology/metrics/${node.props.domainId}`;
            metrics = ['alive_inst_count', 'service_rate', 'active_service', 'concurrent_user'];
        } else if (node.type === 'INSTANCE') {
            apiUrl = `/api-v2/topology/metrics/${node.props.domainId}/${node.props.instId}`;
            metrics = ['service_rate', 'service_err_count', 'active_service', 'proc_cpu', 'proc_mem'];
        } else {
            // REMOTE_CALL 유형은 메트릭 조회 미지원
            return;
        }
        
        const now = Date.now();
        const response = await fetch(
            `${apiUrl}?startTime=${now - 60000}&endTime=${now}`,
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ metrics })
            }
        );
        
        nodeDetailData.value = await response.json();
    } catch (error) {
        console.error('노드 메트릭 조회 오류:', error);
    }
};
</script>

엣지 상세 데이터 로딩

엣지 선택 시 트랜잭션 상세 데이터를 로딩합니다:

vue
<template>
    <TopologyChart
        :data="topologyData"
        :selected-edge="selectedEdge"
        :edge-detail-data="edgeDetailData"
        @edge-select="handleEdgeSelect"
    />
</template>

<script setup>
const selectedEdge = ref(null);
const edgeDetailData = ref(null);

const handleEdgeSelect = async (edge) => {
    selectedEdge.value = edge;
    edgeDetailData.value = null;
    
    try {
        // 1. 엣지 상세 세션 생성
        const now = Date.now();
        const sessionResponse = await fetch(
            `/api-v2/topology/edge/session?startTime=${now - 60000}&endTime=${now}`,
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    source: edge.relation.source,
                    target: edge.relation.target
                })
            }
        );
        
        const { sessionId } = await sessionResponse.json();
        
        // 2. 엣지 상세 데이터 조회 (진행률 확인 반복)
        let hasNext = true;
        while (hasNext) {
            const dataResponse = await fetch(`/api-v2/topology/edge/data/${sessionId}`);
            const result = await dataResponse.json();
            
            hasNext = result.hasNext;
            if (!hasNext) {
                edgeDetailData.value = result.data;
                break;
            }
            
            await new Promise(resolve => setTimeout(resolve, 100));
        }
    } catch (error) {
        console.error('엣지 상세 데이터 조회 오류:', error);
    }
};
</script>

성능 테스트 시나리오

다양한 크기의 토폴로지 데이터로 성능 테스트를 수행할 수 있습니다:

vue
<template>
    <div>
        <select v-model="performanceScenario" @change="loadTopologyData">
            <option value="small">Small Scale (8개 노드)</option>
            <option value="medium">Medium Scale (25개 노드)</option>
            <option value="large">Large Scale (50개 노드)</option>
            <option value="extreme">Extreme Scale (100개 노드)</option>
        </select>
        
        <TopologyChart :data="topologyData" />
    </div>
</template>

<script setup>
const performanceScenario = ref('small');
const topologyData = ref({ nodes: [], edges: [] });

const loadTopologyData = async () => {
    const now = Date.now();
    const sessionResponse = await fetch(
        `/api-v2/topology/statistic/session/instance?startTime=${now - 60000}&endTime=${now}&performanceScenario=${performanceScenario.value}`,
        {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                instanceOidByDomain: { 1000: [77503, 77504, 88601] }
            })
        }
    );
    
    const { sessionId } = await sessionResponse.json();
    const dataResponse = await fetch(`/api-v2/topology/statistic/instance/${sessionId}`);
    const { data } = await dataResponse.json();
    
    topologyData.value = data;
};
</script>

고급 기능

Force 시뮬레이션

enableForceSimulation을 활성화하면 D3.js의 force 시뮬레이션을 사용하여 노드들이 물리 법칙에 따라 자동으로 배치됩니다:

  • Link Force: 연결된 노드들을 적절한 거리에 배치
  • Charge Force: 노드들 간의 반발력으로 겹치지 않게 배치
  • Center Force: 노드들을 중앙으로 끌어당김
  • Collision Force: 노드들이 겹치지 않도록 충돌 검사

Force 시뮬레이션 종료 감지

Force 시뮬레이션이 안정화되어 종료될 때 force-simulation-end 이벤트가 발생합니다:

vue
<template>
    <div>
        <TopologyChart
            :data="topologyData"
            :enable-force-simulation="true"
            @force-simulation-end="handleSimulationEnd"
        />
        <div v-if="simulationStatus" class="simulation-status">
            시뮬레이션 완료: {{ simulationStatus.tickCount }}틱, 
            최종 알파값: {{ simulationStatus.finalAlpha.toFixed(4) }}
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const simulationStatus = ref<{
    tickCount: number;
    finalAlpha: number;
} | null>(null);

const handleSimulationEnd = (tickCount: number, finalAlpha: number) => {
    console.log(`Force 시뮬레이션 종료: ${tickCount}틱 후 안정화`);
    simulationStatus.value = { tickCount, finalAlpha };
    
    // 시뮬레이션 종료 후 추가 작업 수행
    // 예: 레이아웃 저장, 통계 계산 등
};
</script>

시각적 상태

  • 정상 노드: 초록색 배경, 연결된 상태
  • 에러 노드: 빨간색 배경, 실패한 엣지가 있는 경우
  • 고립 노드: 회색 배경, 연결이 없는 경우
  • 선택/호버: 관련된 노드와 엣지가 하이라이팅됨

스케일 컨트롤

  • 확대/축소: 마우스 휠 또는 버튼으로 차트 확대/축소
  • 자동 스케일: 데이터에 맞는 최적의 스케일 자동 계산
  • 뷰포트 이동: 배경 드래그로 차트 이동 가능

활용 사례

기본 모니터링

  1. 마이크로서비스 아키텍처 시각화: 서비스 간 의존성과 호출 관계 파악
  2. 성능 병목 지점 탐지: 높은 응답 시간과 실패율을 가진 연결 식별
  3. 장애 전파 경로 추적: 실패 카운트 기반 장애 영향 범위 분석

고급 분석

  1. 클러스터 기반 서비스 그룹화: 자동 감지된 클러스터를 통한 마이크로서비스 도메인 분리
  2. 선택적 시스템 뷰: 특정 클러스터만 표시하여 복잡한 시스템의 관심 영역 집중 분석
  3. 대규모 인프라 최적화: 100개 이상 노드를 가진 복잡한 시스템의 성능 분석

실시간 운영

  1. 동적 모니터링: 노드 위치 보존을 통한 실시간 상태 변화 추적
  2. 자동화된 인시던트 대응: API 통합을 통한 자동 상세 데이터 로딩 및 분석
  3. 다단계 드릴다운 분석: 엣지 선택 시 자동 트랜잭션 내역 로딩

비즈니스 인사이트

  1. 용량 계획: 성능 테스트 시나리오를 통한 확장성 검증
  2. 비용 최적화: 고립된 노드 식별 및 미사용 리소스 발견
  3. 아키텍처 ꫀ버닝: 마이크로서비스 의존성 맵 기반 전체 시스템 이해도 향상

개발 서버에서 테스트

업데이트된 TopologyChart를 테스트하려면:

bash
# VitePress 개발 서버 실행
cd ../../clara-server/frontend/apps/design-docs
pnpm dev

# 브라우저에서 확인
open http://localhost:5173/j5-components/topology-chart

버전 업데이트 내역

v0.19.2 주요 업데이트

  • 클러스터 자동 감지: 노드 간 연결 관계 분석을 통한 스마트 클러스터링
  • API 통합 기능: 노드/엣지 선택 시 자동 상세 데이터 로딩
  • 성능 테스트 시나리오: Small~Extreme 스케일의 다양한 테스트 데이터 지원
  • 선택적 클러스터 표시: 특정 클러스터만 표시하여 대규모 시스템의 및이 대응
  • 실시간 모드: 노드 위치 보존을 통한 지속적인 모니터링 지원
  • 향상된 이벤트 시스템: node-select, edge-select, clusters-detected 등 새로운 이벤트 추가

업데이트된 TopologyChart는 이제 엔터프라이즈급 대규모 시스템 모니터링과 지능형 상태 분석을 지원하며, 실시간 API 통합을 통해 더욱 심층적인 인사이트를 제공합니다.