Skip to content

Commit 27b4bc8

Browse files
authored
Merge pull request #108 from DMU-DebugVisual/dongjun1
알고리즘 감지 시스템 개선 및 예제 추가
2 parents 9e01e32 + e29bac0 commit 27b4bc8

File tree

9 files changed

+1550
-132
lines changed

9 files changed

+1550
-132
lines changed
Lines changed: 218 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
// AnimationFactory.js - DV-Flow v1.3 완전 대응 (key 수정)
1+
// AnimationFactory.js - 강화된 자동 감지 시스템
22
import React from 'react';
33

4-
// 실제 애니메이션 컴포넌트들 import
54
import SortAnimation from './animations/SortAnimation';
65
import BinaryTreeAnimation from './animations/BinaryTreeAnimation';
76
import GraphAnimation from './animations/GraphAnimation';
@@ -11,19 +10,19 @@ import RecursionAnimation from './animations/RecursionAnimation';
1110
import PlaceholderAnimation from './animations/PlaceholderAnimation';
1211

1312
/**
14-
* 🏭 AnimationFactory - DV-Flow v1.3 지원
13+
* 🏭 AnimationFactory - DV-Flow v1.3 지원 + 강화된 자동 감지
1514
*/
1615
export class AnimationFactory {
1716
static animations = {
18-
// ✅ 정렬 알고리즘 (SortAnimation 사용)
17+
// ✅ 정렬 알고리즘
1918
'bubble-sort': SortAnimation,
2019
'selection-sort': SortAnimation,
2120
'insertion-sort': SortAnimation,
2221
'merge-sort': SortAnimation,
2322
'quick-sort': SortAnimation,
2423
'sort': SortAnimation,
2524

26-
// ✅ 트리 구조
25+
// ✅ 트리
2726
'binary-tree': BinaryTreeAnimation,
2827
'tree': BinaryTreeAnimation,
2928

@@ -45,8 +44,9 @@ export class AnimationFactory {
4544
'recursion': RecursionAnimation,
4645
'fibonacci-recursion': RecursionAnimation,
4746
'fibonacci': RecursionAnimation,
47+
'factorial': RecursionAnimation,
4848

49-
// 🚧 기타 (Placeholder)
49+
// 🚧 기타
5050
'variables': PlaceholderAnimation,
5151
'default': PlaceholderAnimation
5252
};
@@ -58,22 +58,14 @@ export class AnimationFactory {
5858
console.log('🏭 AnimationFactory.createAnimation:', {
5959
type,
6060
hasData: !!props.data,
61-
eventsCount: props.data?.events?.length || 0,
62-
currentStep: props.currentStep,
63-
totalSteps: props.totalSteps
61+
eventsCount: props.data?.events?.length || 0
6462
});
6563

6664
const normalizedType = this.normalizeType(type);
6765
const AnimationComponent = this.animations[normalizedType] || PlaceholderAnimation;
6866

69-
// DV-Flow v1.3 검증
70-
if (props.data && !props.data.events) {
71-
console.warn('⚠️ 구버전 JSON 구조 감지! events 필드가 없습니다.');
72-
}
73-
74-
// ⚡ CRITICAL: Date.now() 제거! 고정된 key 사용
7567
return React.createElement(AnimationComponent, {
76-
key: `animation-${normalizedType}`, // 타입별 고정 key
68+
key: `animation-${normalizedType}`,
7769
animationType: normalizedType,
7870
...props
7971
});
@@ -83,12 +75,216 @@ export class AnimationFactory {
8375
* 🔧 타입 정규화
8476
*/
8577
static normalizeType(type) {
86-
if (!type || typeof type !== 'string') {
87-
return 'unknown';
88-
}
78+
if (!type || typeof type !== 'string') return 'unknown';
8979
return type.toLowerCase().trim().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '') || 'unknown';
9080
}
9181

82+
/**
83+
* 🎯 강화된 애니메이션 타입 자동 감지
84+
*/
85+
static detectAnimationType(events) {
86+
if (!events || events.length === 0) return 'variables';
87+
88+
// 1️⃣ viz.type 우선 확인 (명시적 지정)
89+
for (let event of events) {
90+
if (event.viz?.type) {
91+
const vizType = event.viz.type.toLowerCase();
92+
console.log('✅ viz.type 감지:', vizType);
93+
94+
// 정렬 타입들
95+
if (['bubble-sort', 'selection-sort', 'insertion-sort', 'merge-sort', 'quick-sort'].includes(vizType)) {
96+
return vizType;
97+
}
98+
99+
// 자료구조 타입들
100+
if (vizType === 'heap') return 'heap';
101+
if (vizType === 'bst' || vizType === 'tree') return 'binary-tree';
102+
if (vizType === 'graph') return 'graph';
103+
if (vizType === 'list' || vizType === 'linkedlist') return 'linked-list';
104+
if (vizType === 'recursion' || vizType === 'recursive') return 'recursion';
105+
}
106+
}
107+
108+
// 2️⃣ ds_op의 target으로 자료구조 감지
109+
const heapOp = events.find(e =>
110+
e.kind === 'ds_op' &&
111+
e.target &&
112+
e.target.toLowerCase().includes('heap')
113+
);
114+
if (heapOp) {
115+
console.log('✅ ds_op target으로 heap 감지');
116+
return 'heap';
117+
}
118+
119+
const listOp = events.find(e =>
120+
e.kind === 'ds_op' &&
121+
e.target &&
122+
(e.target.toLowerCase().includes('list') ||
123+
e.target.toLowerCase().includes('linkedlist') ||
124+
e.target.toLowerCase().includes('node'))
125+
);
126+
if (listOp) {
127+
console.log('✅ ds_op target으로 linked-list 감지');
128+
return 'linked-list';
129+
}
130+
131+
const graphOp = events.find(e =>
132+
e.kind === 'ds_op' &&
133+
e.target &&
134+
(e.target.toLowerCase().includes('graph') ||
135+
e.target.toLowerCase().includes('adj'))
136+
);
137+
if (graphOp) {
138+
console.log('✅ ds_op target으로 graph 감지');
139+
return 'graph';
140+
}
141+
142+
const treeOp = events.find(e =>
143+
e.kind === 'ds_op' &&
144+
e.target &&
145+
(e.target.toLowerCase().includes('tree') ||
146+
e.target.toLowerCase().includes('bst'))
147+
);
148+
if (treeOp) {
149+
console.log('✅ ds_op target으로 binary-tree 감지');
150+
return 'binary-tree';
151+
}
152+
153+
// 3️⃣ 재귀 패턴 감지 (call/return 분석) - 강화됨!
154+
const callEvents = events.filter(e => e.kind === 'call');
155+
const returnEvents = events.filter(e => e.kind === 'return');
156+
157+
if (callEvents.length >= 3 && returnEvents.length >= 3) {
158+
// 같은 함수가 여러 번 호출되는지 확인
159+
const functionCallCounts = {};
160+
const functionReturnCounts = {};
161+
162+
callEvents.forEach(e => {
163+
if (e.fn) {
164+
functionCallCounts[e.fn] = (functionCallCounts[e.fn] || 0) + 1;
165+
}
166+
});
167+
168+
returnEvents.forEach(e => {
169+
if (e.fn) {
170+
functionReturnCounts[e.fn] = (functionReturnCounts[e.fn] || 0) + 1;
171+
}
172+
});
173+
174+
// 같은 함수의 call/return이 각각 3번 이상이면 재귀
175+
for (const fn in functionCallCounts) {
176+
const callCount = functionCallCounts[fn];
177+
const returnCount = functionReturnCounts[fn] || 0;
178+
179+
if (callCount >= 3 && returnCount >= 3) {
180+
const ratio = returnCount / callCount;
181+
// call과 return 개수가 비슷하면 재귀
182+
if (ratio >= 0.8 && ratio <= 1.2) {
183+
console.log('✅ 재귀 패턴 감지 (call/return 균형)', {
184+
function: fn,
185+
calls: callCount,
186+
returns: returnCount,
187+
ratio: ratio
188+
});
189+
return 'recursion';
190+
}
191+
}
192+
}
193+
}
194+
195+
// 4️⃣ 정렬 알고리즘 패턴 감지 (강화!)
196+
const hasCompare = events.some(e => e.kind === 'compare');
197+
const hasSwap = events.some(e => e.kind === 'swap');
198+
199+
if (hasCompare && hasSwap) {
200+
// QuickSort: pivot 존재
201+
const hasPivot = events.some(e =>
202+
e.viz?.pivot !== undefined ||
203+
(e.kind === 'assign' && e.var && e.var.includes('pivot'))
204+
);
205+
if (hasPivot) {
206+
console.log('✅ QuickSort 감지 (pivot 발견)');
207+
return 'quick-sort';
208+
}
209+
210+
// MergeSort: merge 이벤트 존재
211+
const hasMerge = events.some(e => e.kind === 'merge');
212+
if (hasMerge) {
213+
console.log('✅ MergeSort 감지 (merge 이벤트)');
214+
return 'merge-sort';
215+
}
216+
217+
// SelectionSort vs InsertionSort vs BubbleSort 구분
218+
const sortType = this.detectSortPattern(events);
219+
console.log('✅ 정렬 패턴 감지:', sortType);
220+
return sortType;
221+
}
222+
223+
// 5️⃣ 기본값
224+
console.log('⚠️ 감지 실패 - variables로 처리');
225+
return 'variables';
226+
}
227+
228+
/**
229+
* 🔍 정렬 알고리즘 세부 패턴 분석
230+
*/
231+
static detectSortPattern(events) {
232+
const compares = events.filter(e => e.kind === 'compare');
233+
const swaps = events.filter(e => e.kind === 'swap');
234+
235+
if (compares.length === 0 || swaps.length === 0) {
236+
return 'bubble-sort'; // 기본값
237+
}
238+
239+
// SelectionSort 특징: 라운드당 swap이 적음 (보통 1번)
240+
// compare 횟수 대비 swap이 적으면 Selection
241+
const swapRatio = swaps.length / compares.length;
242+
243+
if (swapRatio < 0.3) {
244+
console.log('📊 SelectionSort 특징: swap 비율 낮음', swapRatio);
245+
return 'selection-sort';
246+
}
247+
248+
// InsertionSort 특징: 연속된 swap (삽입 위치까지 밀어내기)
249+
// swap이 연속으로 일어나는 패턴 확인
250+
let consecutiveSwaps = 0;
251+
let maxConsecutiveSwaps = 0;
252+
253+
for (let i = 1; i < events.length; i++) {
254+
if (events[i].kind === 'swap' && events[i-1].kind === 'swap') {
255+
consecutiveSwaps++;
256+
maxConsecutiveSwaps = Math.max(maxConsecutiveSwaps, consecutiveSwaps);
257+
} else if (events[i].kind === 'swap') {
258+
consecutiveSwaps = 1;
259+
} else {
260+
consecutiveSwaps = 0;
261+
}
262+
}
263+
264+
if (maxConsecutiveSwaps >= 2) {
265+
console.log('📊 InsertionSort 특징: 연속 swap 패턴', maxConsecutiveSwaps);
266+
return 'insertion-sort';
267+
}
268+
269+
// BubbleSort 특징: compare 직후 swap (인접 원소 교환)
270+
let immediateSwaps = 0;
271+
for (let i = 1; i < events.length; i++) {
272+
if (events[i].kind === 'swap' && events[i-1].kind === 'compare') {
273+
immediateSwaps++;
274+
}
275+
}
276+
277+
const immediateSwapRatio = immediateSwaps / swaps.length;
278+
if (immediateSwapRatio > 0.5) {
279+
console.log('📊 BubbleSort 특징: compare 직후 swap', immediateSwapRatio);
280+
return 'bubble-sort';
281+
}
282+
283+
// 기본값
284+
console.log('📊 정렬 패턴 불명확 - BubbleSort 기본 처리');
285+
return 'bubble-sort';
286+
}
287+
92288
/**
93289
* ✅ 구현 여부 확인
94290
*/
@@ -127,8 +323,8 @@ export class AnimationFactory {
127323
const available = this.getAvailableTypes();
128324

129325
return {
130-
version: '2.1.0-v1.3',
131-
mode: 'events-based',
326+
version: '2.3.0-recursion-enhanced',
327+
mode: 'smart-pattern-detection',
132328
jsonSchema: 'DV-Flow v1.3',
133329
totalAnimations: available.length,
134330
implementedCount: implemented.length,
@@ -137,64 +333,6 @@ export class AnimationFactory {
137333
availableTypes: available
138334
};
139335
}
140-
141-
/**
142-
* 🔍 애니메이션 타입 자동 감지
143-
*/
144-
static detectAnimationType(events) {
145-
if (!events || events.length === 0) return 'variables';
146-
147-
// 정렬 패턴
148-
const hasCompare = events.some(e => e.kind === 'compare');
149-
const hasSwap = events.some(e => e.kind === 'swap');
150-
if (hasCompare && hasSwap) {
151-
const hasPivot = events.some(e => e.viz?.pivot !== undefined);
152-
if (hasPivot) return 'quick-sort';
153-
154-
const hasMerge = events.some(e => e.kind === 'merge');
155-
if (hasMerge) return 'merge-sort';
156-
157-
return 'bubble-sort';
158-
}
159-
160-
// 그래프 패턴
161-
const hasGraphOp = events.some(e =>
162-
e.kind === 'ds_op' &&
163-
e.target &&
164-
(e.target.includes('adj_mat') || e.target.includes('graph'))
165-
);
166-
if (hasGraphOp) return 'graph';
167-
168-
// 트리 패턴
169-
const hasTreeOp = events.some(e =>
170-
e.kind === 'ds_op' &&
171-
e.target &&
172-
(e.target.includes('tree') || e.target.includes('node'))
173-
);
174-
if (hasTreeOp) return 'binary-tree';
175-
176-
// 힙 패턴
177-
const hasHeapOp = events.some(e =>
178-
e.kind === 'ds_op' &&
179-
e.target &&
180-
e.target.includes('heap')
181-
);
182-
if (hasHeapOp) return 'heap';
183-
184-
// 링크드 리스트 패턴
185-
const hasListOp = events.some(e =>
186-
e.kind === 'ds_op' &&
187-
e.target &&
188-
e.target.includes('list')
189-
);
190-
if (hasListOp) return 'linked-list';
191-
192-
// 재귀 패턴
193-
const hasRecursiveCall = events.filter(e => e.kind === 'call').length > 3;
194-
if (hasRecursiveCall) return 'recursion';
195-
196-
return 'variables';
197-
}
198336
}
199337

200338
// 편의 함수들
@@ -210,7 +348,6 @@ export const getFactoryInfo = () =>
210348
export const detectAnimationType = (events) =>
211349
AnimationFactory.detectAnimationType(events);
212350

213-
// 기본 export
214351
export default AnimationFactory;
215352

216-
console.log('🏭 AnimationFactory (DV-Flow v1.3) 로드 완료:', AnimationFactory.getFactoryInfo());
353+
console.log('🏭 AnimationFactory (Enhanced) 로드 완료:', AnimationFactory.getFactoryInfo());

0 commit comments

Comments
 (0)