Skip to content

Commit 782bf85

Browse files
authored
Merge pull request #47 from MZC-CSC/feature/task-editor-object-collapse
Feature/task editor object collapse
2 parents 44d5548 + 37c0681 commit 782bf85

7 files changed

Lines changed: 715 additions & 18 deletions

File tree

TaskEditor-Development-Guide.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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의 핵심 구현 방법을 설명합니다. 추가 질문이나 개선사항이 있으면 언제든 문의하세요.

api/handler/http-util.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ func init() {
111111
log.Printf("DEBUG: - Service: %s -> %+v", serviceName, service)
112112
}
113113

114-
for serviceName, actions := range ApiYamlSet.ServiceActions {
115-
log.Printf("DEBUG: - ServiceActions: %s -> %d actions", serviceName, len(actions))
116-
for actionName, spec := range actions {
117-
log.Printf("DEBUG: - Action: %s -> %+v", actionName, spec)
118-
}
119-
}
114+
// for serviceName, actions := range ApiYamlSet.ServiceActions {
115+
// log.Printf("DEBUG: - ServiceActions: %s -> %d actions", serviceName, len(actions))
116+
// for actionName, spec := range actions {
117+
// log.Printf("DEBUG: - Action: %s -> %+v", actionName, spec)
118+
// }
119+
// }
120120
}
121121

122122
// AnyCaller는 buffalo.Context, operationId, commonRequest, auth유무 를 받아 conf/api.yaml 정보를 바탕으로 commonCaller를 호출합니다.

0 commit comments

Comments
 (0)