1- // AnimationFactory.js - DV-Flow v1.3 완전 대응 (key 수정)
1+ // AnimationFactory.js - 강화된 자동 감지 시스템
22import React from 'react' ;
33
4- // 실제 애니메이션 컴포넌트들 import
54import SortAnimation from './animations/SortAnimation' ;
65import BinaryTreeAnimation from './animations/BinaryTreeAnimation' ;
76import GraphAnimation from './animations/GraphAnimation' ;
@@ -11,19 +10,19 @@ import RecursionAnimation from './animations/RecursionAnimation';
1110import PlaceholderAnimation from './animations/PlaceholderAnimation' ;
1211
1312/**
14- * 🏭 AnimationFactory - DV-Flow v1.3 지원
13+ * 🏭 AnimationFactory - DV-Flow v1.3 지원 + 강화된 자동 감지
1514 */
1615export 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 - z 0 - 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 = () =>
210348export const detectAnimationType = ( events ) =>
211349 AnimationFactory . detectAnimationType ( events ) ;
212350
213- // 기본 export
214351export default AnimationFactory ;
215352
216- console . log ( '🏭 AnimationFactory (DV-Flow v1.3 ) 로드 완료:' , AnimationFactory . getFactoryInfo ( ) ) ;
353+ console . log ( '🏭 AnimationFactory (Enhanced ) 로드 완료:' , AnimationFactory . getFactoryInfo ( ) ) ;
0 commit comments