@@ -26,6 +26,20 @@ TaskEditor는 계층구조를 가진 데이터를 UI로 렌더링하는 컴포
2626 - InputModel 객체의 값을 표시
2727 - v-model을 통한 양방향 바인딩
2828
29+ ### 4. RecursiveFormField.vue
30+ - ** 역할** : Body Parameters를 재귀적으로 렌더링하는 컴포넌트
31+ - ** 기능** :
32+ - Simple Types (string, number, boolean) 입력 필드 렌더링
33+ - Array 타입 필드 렌더링 (Add entity, Remove 버튼 포함)
34+ - Object 타입 필드 렌더링 (collapse/expand 기능)
35+ - 재귀적 중첩 구조 지원
36+ - ** 특징** :
37+ - ** 모든 depth의 object 필드에서 collapse/expand 가능** (2024-11-06 개선)
38+ - Array는 'Add entity', 'Remove' 버튼으로 항목 관리
39+ - Object는 collapse 버튼(▶/▼)으로 접기/펼치기만 가능
40+ - depth 기반 시각적 인디케이터 (색상별 좌측 바)
41+ - maxAutoExpandDepth 설정으로 자동 펼침 깊이 제어
42+
2943## 계층적 데이터 처리 로직
3044
3145### Depth 0 (최상위)
@@ -231,6 +245,32 @@ console.log('nestedField.context.values:', nestedField.context.values);
231245- 각 depth별로 적절한 라벨 추가 (` [depth-X-type] ` )
232246- 중첩 구조에서 올바른 변환 로직 적용
233247
248+ ### 4. RecursiveFormField Object Collapse
249+ - ** 모든 depth의 object에서 collapse 가능** (기존: depth > 0만 가능)
250+ - Object 필드는 collapse/expand만 가능하며 Add/Remove 버튼 없음
251+ - Array 필드는 'Add entity', 'Remove' 버튼으로 항목 관리 가능
252+ - depth 0의 object는 기본적으로 펼쳐진 상태로 시작 (사용자 편의성)
253+
254+ ``` vue
255+ <!-- Object Type - 모든 depth에서 collapse 버튼 표시 -->
256+ <div v-else-if="fieldSchema.type === 'object'" class="object-field">
257+ <div class="object-header">
258+ <div class="header-left">
259+ <button @click="toggleObjectCollapse" class="btn-collapse">
260+ {{ isObjectCollapsed ? '▶' : '▼' }}
261+ </button>
262+ <label class="field-label">
263+ {{ fieldName }}<span v-if="isRequired" class="required-mark">*</span>
264+ <span class="field-type">({{ Object.keys(fieldSchema.properties || {}).length }} properties)</span>
265+ </label>
266+ </div>
267+ </div>
268+ <div v-if="!isObjectCollapsed" class="object-properties">
269+ <!-- nested properties -->
270+ </div>
271+ </div>
272+ ```
273+
234274## 확장 방법
235275
236276### 1. 새로운 Depth 추가
@@ -286,6 +326,300 @@ if (field.type === 'newType') {
286326- 다양한 depth와 타입의 데이터로 UI 확인
287327- 반응형 레이아웃 테스트
288328
329+ ### 4. RecursiveFormField 테스트
330+ - Object 필드의 collapse/expand 동작 확인 (모든 depth)
331+ - Array 필드의 'Add entity', 'Remove' 버튼 동작 확인
332+ - 중첩된 구조에서 데이터 바인딩 정상 작동 확인
333+ - 예시: BeetleTaskEditor의 Body Parameters (targetSshKey, targetCloud 등)
334+
335+ ## Property 순서 정렬 기능
336+
337+ ### 개요
338+ Task Editor의 Body Parameters 영역에서 표시되는 property들의 순서를 제어할 수 있는 기능입니다. Task별로 중요한 property를 먼저 표시하거나, 논리적 순서로 정렬하여 사용자 경험을 개선합니다.
339+
340+ ### 핵심 특징
341+ - ✅ ** Task별 개별 설정** : 각 Task Component마다 다른 정렬 규칙 적용 가능
342+ - ✅ ** 경로 기반 정렬** : 중첩된 객체/배열 내부 property도 정렬 가능
343+ - ✅ ** 부분 정렬 지원** : order에 지정된 property만 먼저 정렬, 나머지는 원래 순서 유지
344+ - ✅ ** 안전한 fallback** : 잘못된 설정이 있어도 모든 property는 반드시 표시됨
345+
346+ ### 구현 구조
347+
348+ #### 1. taskPropertyOrderConfig.ts
349+ Property 순서 설정을 관리하는 중앙 설정 파일입니다.
350+
351+ ``` typescript
352+ // 위치: front/src/features/sequential/designer/editor/config/taskPropertyOrderConfig.ts
353+
354+ export interface PropertyOrderRule {
355+ path: string ; // 'body_params', 'body_params.targetVmInfra' 등
356+ order: string []; // 순서대로 나열할 property 이름들
357+ }
358+
359+ export const TASK_PROPERTY_ORDER_CONFIG: Record <string , PropertyOrderRule []> = {
360+ ' beetle_task_infra_migration' : [
361+ {
362+ path: ' body_params' ,
363+ order: [' targetVmInfra' , ' targetSecurityGroupList' , ' targetSshKey' ]
364+ },
365+ {
366+ path: ' body_params.targetVmInfra' ,
367+ order: [' name' , ' description' , ' subGroups' ]
368+ }
369+ ]
370+ };
371+ ```
372+
373+ #### 2. RecursiveFormField.vue
374+ 재귀적으로 렌더링되는 모든 필드에 정렬 로직을 적용합니다.
375+
376+ ** 주요 변경사항:**
377+ - ` taskName ` , ` currentPath ` props 추가
378+ - ` sortedPropertyNames ` computed property: Object 타입 필드의 property 정렬
379+ - ` sortedArrayItemPropertyNames ` computed property: Array 아이템 내부 property 정렬
380+ - ` computedChildPath() ` : 중첩된 경로 자동 계산
381+
382+ ``` vue
383+ <template>
384+ <!-- Object 타입: sortedPropertyNames 사용 -->
385+ <recursive-form-field
386+ v-for="propName in sortedPropertyNames"
387+ :task-name="taskName"
388+ :current-path="computedChildPath(propName)"
389+ />
390+
391+ <!-- Array 타입: sortedArrayItemPropertyNames 사용 -->
392+ <recursive-form-field
393+ v-for="propName in sortedArrayItemPropertyNames"
394+ :task-name="taskName"
395+ :current-path="`${currentPath}[]`"
396+ />
397+ </template>
398+ ```
399+
400+ #### 3. TaskComponentEditor.vue
401+ 최상위 Body Parameters 섹션에 정렬 기능을 통합합니다.
402+
403+ ** 주요 변경사항:**
404+ - ` getCurrentTaskComponentName() ` : 현재 task 이름 추출 (step.name 또는 step.type 사용)
405+ - ` sortedBodyParamPropertyNames ` computed property: 최상위 body params property 정렬
406+ - ` hasBodyParams ` 를 computed property로 변경하여 reactive하게 동작
407+
408+ ### 경로(Path) 체계
409+
410+ 경로는 점(` . ` )으로 구분하며, 배열은 ` [] ` 로 표시합니다:
411+
412+ ``` typescript
413+ // 기본 경로
414+ ' body_params' // 최상위
415+ ' body_params.targetVmInfra' // 1단계 객체
416+ ' body_params.targetVmInfra.subGroups' // 2단계 객체
417+
418+ // 배열 경로
419+ ' body_params.servers[]' // 1단계 배열의 각 아이템
420+ ' body_params.servers[].migration_list' // 배열 아이템 내부 객체
421+ ' body_params.servers[].packages[]' // 중첩 배열
422+ ```
423+
424+ ### 정렬 로직
425+
426+ #### sortPropertiesByOrder() 함수
427+ ``` typescript
428+ export function sortPropertiesByOrder(
429+ properties : string [], // 실제 존재하는 property들
430+ order : string [] // 설정된 순서
431+ ): string [] {
432+ // 1. order에 명시된 property들 중 실제 존재하는 것만 추출
433+ const ordered = order .filter (key => properties .includes (key ));
434+
435+ // 2. order에 없는 나머지 property들 (원래 순서 유지)
436+ const remaining = properties .filter (key => ! order .includes (key ));
437+
438+ // 3. 순서대로 병합
439+ return [... ordered , ... remaining ];
440+ }
441+ ```
442+
443+ ** 동작 예시:**
444+ ``` typescript
445+ // 실제 properties
446+ const actual = [' name' , ' description' , ' label' , ' subGroups' , ' installMonAgent' ];
447+
448+ // 설정된 order
449+ const order = [' name' , ' description' , ' subGroups' ];
450+
451+ // 결과
452+ // → ['name', 'description', 'subGroups', 'label', 'installMonAgent']
453+ // ✅ name, description, subGroups는 앞에 순서대로
454+ // ✅ 나머지는 원래 순서로 뒤에 배치
455+ ```
456+
457+ ### 안전성 보장
458+
459+ #### 1. 잘못된 Property 이름
460+ ``` typescript
461+ // order에 존재하지 않는 property 포함
462+ order : [' xyz' , ' name' , ' abc' , ' description' ]
463+
464+ // 결과: xyz, abc는 자동으로 무시됨
465+ // → ['name', 'description', ...나머지]
466+ ```
467+
468+ #### 2. 경로 불일치
469+ ``` typescript
470+ // path가 틀렸거나 매칭되지 않는 경우
471+ const order = getPropertyOrder (' task_name' , ' wrong_path' );
472+ // → null 반환
473+
474+ // sortedPropertyNames에서
475+ return order ? sortPropertiesByOrder (keys , order ) : keys ;
476+ // → 원래 순서 그대로 표시
477+ ```
478+
479+ #### 3. Task 설정 없음
480+ ``` typescript
481+ // TASK_PROPERTY_ORDER_CONFIG에 없는 task
482+ const rules = TASK_PROPERTY_ORDER_CONFIG [' unknown_task' ];
483+ // → undefined
484+
485+ // getPropertyOrder에서
486+ if (! rules ) return null ;
487+ // → 원래 순서 그대로 표시
488+ ```
489+
490+ ### 사용 예시
491+
492+ #### 예시 1: beetle_task_infra_migration
493+ ``` typescript
494+ ' beetle_task_infra_migration' : [
495+ {
496+ // 최상위 Body Parameters 정렬
497+ path: ' body_params' ,
498+ order: [
499+ ' targetVmInfra' , // 1순위: VM 인프라 설정
500+ ' targetSecurityGroupList' , // 2순위: 보안 그룹
501+ ' targetSshKey' , // 3순위: SSH 키
502+ ' targetVNet' // 4순위: 가상 네트워크
503+ ]
504+ },
505+ {
506+ // targetVmInfra 내부 property 정렬
507+ path: ' body_params.targetVmInfra' ,
508+ order: [
509+ ' name' , // 1순위: 이름
510+ ' description' , // 2순위: 설명
511+ ' subGroups' , // 3순위: 서브 그룹
512+ ' installMonAgent' // 4순위: 모니터링 에이전트
513+ ]
514+ }
515+ ]
516+ ```
517+
518+ #### 예시 2: grasshopper_task_software_migration
519+ ``` typescript
520+ ' grasshopper_task_software_migration' : [
521+ {
522+ // servers 배열의 각 아이템 내부 정렬
523+ path: ' body_params.targetSoftwareModel.servers[]' ,
524+ order: [
525+ ' source_connection_info_id' , // 1순위: 소스 연결 정보
526+ ' migration_list' , // 2순위: 마이그레이션 목록
527+ ' errors' // 3순위: 에러 정보
528+ ]
529+ },
530+ {
531+ // 중첩 배열 내부 정렬
532+ path: ' body_params.targetSoftwareModel.servers[].migration_list.packages[]' ,
533+ order: [
534+ ' name' , // 1순위: 패키지 이름
535+ ' version' , // 2순위: 버전
536+ ' repo_url' // 3순위: 저장소 URL
537+ ]
538+ }
539+ ]
540+ ```
541+
542+ ### 새로운 Task 추가 방법
543+
544+ 1 . ** taskPropertyOrderConfig.ts 수정**
545+ ``` typescript
546+ export const TASK_PROPERTY_ORDER_CONFIG: Record <string , PropertyOrderRule []> = {
547+ // ... 기존 설정 ...
548+
549+ ' new_task_name' : [
550+ {
551+ path: ' body_params' ,
552+ order: [' important_field1' , ' important_field2' ]
553+ }
554+ ]
555+ };
556+ ```
557+
558+ 2 . ** 브라우저에서 확인**
559+ - 설정이 없어도 모든 property는 정상 표시됨 (기본 순서)
560+ - 설정을 추가하면 즉시 정렬이 적용됨
561+
562+ ### 디버깅
563+
564+ Property 정렬 과정을 디버깅하려면 브라우저 콘솔에서 다음 로그를 확인하세요:
565+
566+ ``` javascript
567+ // TaskComponentEditor.vue - 최상위 정렬
568+ 🔍 Body Params Property Sorting: {
569+ taskName: " beetle_task_infra_migration" ,
570+ originalKeys: [... ],
571+ order: [... ],
572+ sortedKeys: [... ]
573+ }
574+
575+ // RecursiveFormField.vue - 중첩 정렬
576+ ⭐ sortedPropertyNames computed called!
577+ 📋 Properties keys: [... ]
578+ 📋 Task name: " beetle_task_infra_migration"
579+ 📋 Order from config: [... ]
580+ ✅ Final sorted keys: [... ]
581+ ```
582+
583+ ### 성능 고려사항
584+
585+ - ** Computed Properties 사용** : Vue의 반응성 시스템을 활용하여 필요할 때만 재계산
586+ - ** 경량 알고리즘** : ` filter() ` 연산만 사용하여 O(n) 시간 복잡도
587+ - ** 메모리 효율** : 원본 배열 변경 없이 새 배열 반환
588+
589+ ### 제약사항 및 주의사항
590+
591+ 1 . ** Path 정확성** : 경로는 대소문자를 구분하며 정확히 일치해야 함
592+ 2 . ** 배열 표기** : 배열은 반드시 ` [] ` 로 표시 (예: ` servers[] ` )
593+ 3 . ** 부분 정렬** : order에 없는 property는 자동으로 뒤에 추가됨
594+ 4 . ** Reactive 동작** : ` hasBodyParams ` 는 computed property여야 정상 작동
595+
596+ ## 최근 변경 이력
597+
598+ ### 2024-11-06: Property 순서 정렬 기능 추가
599+ - ** 변경 내용** : Task Editor의 Body Parameters 영역에서 property 표시 순서를 제어하는 기능 추가
600+ - ** 추가된 파일** :
601+ - ` taskPropertyOrderConfig.ts ` : 중앙 설정 관리
602+ - ** 수정된 파일** :
603+ - ` RecursiveFormField.vue ` : 정렬 로직 통합, taskName/currentPath props 추가
604+ - ` TaskComponentEditor.vue ` : 최상위 정렬 지원, hasBodyParams를 computed로 변경
605+ - ** 주요 기능** :
606+ - Task별 개별 정렬 규칙 설정
607+ - 중첩된 객체/배열 내부 property 정렬 지원
608+ - 경로 기반 정렬 (` body_params.targetVmInfra ` , ` servers[] ` 등)
609+ - 안전한 fallback (잘못된 설정이 있어도 모든 property 표시)
610+ - ** 사용 예시** :
611+ - ` beetle_task_infra_migration ` : targetVmInfra를 최상단에 배치
612+ - ` grasshopper_task_software_migration ` : servers 배열 내부 정렬
613+
614+ ### 2024-11-06: RecursiveFormField Object Collapse 개선
615+ - ** 변경 내용** : Object 타입 필드의 collapse 기능을 모든 depth에서 사용 가능하도록 개선
616+ - ** 이전** : depth > 0에서만 collapse 버튼 표시
617+ - ** 개선 후** : 모든 depth (depth 0 포함)에서 collapse 버튼 표시
618+ - ** 영향 범위** :
619+ - ` RecursiveFormField.vue ` Line 124: ` v-if="depth > 0" ` 조건 제거
620+ - ` RecursiveFormField.vue ` Line 137: ` v-if="depth === 0 || !isObjectCollapsed" ` → ` v-if="!isObjectCollapsed" ` 로 변경
621+ - ** 사용 예시** : Body Parameters의 targetSshKey(9 properties), targetCloud(2 properties) 등의 object 필드에서 collapse/expand 가능
622+
289623---
290624
291625이 가이드는 TaskEditor의 핵심 구현 방법을 설명합니다. 추가 질문이나 개선사항이 있으면 언제든 문의하세요.
0 commit comments