# 액션 핸들러 가이드 > **관련 문서**: [레이아웃 JSON](layout-json.md) | [데이터 바인딩](data-binding.md) | [상태 관리](state-management.md) --- ## TL;DR (5초 요약) ```text 1. 구조: type 또는 event(이벤트), handler(핸들러명), params(옵션) 2. type: 표준 DOM 이벤트 (click, change) → $event.target.value 3. event: 커스텀 콜백 (CodeEditor, DataGrid) → $args[0] 4. 핸들러명 정확히: apiCall (❌ api), navigate (❌ goto) 5. 조합: sequence(순차), parallel(병렬), onSuccess/onError ``` --- ## 분리된 문서 이 문서는 가독성을 위해 다음과 같이 분리되었습니다: | 문서 | 내용 | |------|------| | **actions.md** (현재) | 개요, 액션 정의 구조, 내장 핸들러 목록, 주의사항 | | [actions-handlers.md](actions-handlers.md) | 핸들러별 상세 사용법, onSuccess/onError, errorHandling, 실전 예시 | | [actions-g7core-api.md](actions-g7core-api.md) | G7Core.dispatch, 편의 API, 이벤트 헬퍼 | --- ## 목차 1. [개요](#개요) 2. [액션 정의 구조](#액션-정의-구조) 3. [내장 핸들러 목록](#내장-핸들러-목록) 4. [주의사항](#주의사항) --- ## 개요 **액션 핸들러**는 레이아웃 JSON에서 사용자 인터랙션(클릭, 폼 제출 등)을 처리하는 시스템입니다. ### 핵심 원칙 ```text 중요: 핸들러 이름은 ActionDispatcher에 정의된 이름과 정확히 일치해야 함 ✅ 필수: apiCall (❌ api 아님) ✅ 필수: navigate, setState, openModal, closeModal, toast 등 ``` --- ## 액션 정의 구조 ```json { "actions": [ { "type": "click", "handler": "navigate", "target": "/path", "params": { "key": "value" }, "onSuccess": [], "onError": [], "confirm": "확인 메시지", "key": "Enter" } ] } ``` ### 필드 설명 | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `type` | string | | 표준 DOM 이벤트 타입 (click, change, submit 등). `event` 필드와 상호 배타적 | | `event` | string | | 커스텀 콜백 이벤트명 (onSortChange, onChange 등). `type` 필드와 상호 배타적 | | `handler` | string | ✅ | 액션 핸들러 이름 | | `target` | string | ❌ | 핸들러별 타겟 값 (URL, 모달 ID 등) | | `params` | object | ❌ | 핸들러에 전달할 파라미터 | | `onSuccess` | array | ❌ | 성공 시 실행할 후속 액션 (단일 또는 배열) | | `onError` | array | ❌ | 실패 시 실행할 후속 액션 (단일 또는 배열) | | `confirm` | string | ❌ | 실행 전 확인 대화상자 메시지 | | `key` | string | ❌ | 키보드 이벤트 필터 (Enter, Escape 등) | | `cases` | object | ❌ | switch 핸들러용 케이스별 액션 정의 | | `if` | string | ❌ | 조건부 실행 (`{{expression}}` 형식, truthy일 때만 실행) | | `debounce` | number \| object | ❌ | 연속 호출 제어 (300 또는 { delay, leading, trailing }) | | `render` | boolean | ❌ | `false`이면 상태 값만 저장하고 React 리렌더를 건너뜀 (engine-v1.42.0+). 기본: `true` | | `resultTo` | object | ❌ | 핸들러 실행 결과를 상태에 저장 (engine-v1.11.0+). `{ target, key, merge }` | | `actionRef` | string | ❌ | `named_actions`에 정의된 액션을 참조 (engine-v1.19.0+). handler/params 대신 사용 | > **참고**: `type`과 `event` 중 하나는 반드시 필요. `type`은 표준 DOM 이벤트용, `event`는 커스텀 콜백용 ### actionRef로 named_actions 참조 (engine-v1.19.0+) `named_actions`에 정의된 재사용 가능한 액션을 `actionRef`로 참조합니다. 동일한 액션이 여러 컴포넌트에서 반복될 때 중복을 제거합니다. ```json // 레이아웃 최상위에 named_actions 정의 { "named_actions": { "searchProducts": { "handler": "navigate", "params": { "path": "/admin/products", "replace": true, "mergeQuery": true, "query": { "page": 1, "search_keyword": "{{$computed.keyword || null}}" } } } }, "components": [ { "name": "Button", "actions": [{ "type": "click", "actionRef": "searchProducts" }] }, { "name": "Input", "actions": [{ "type": "keypress", "key": "Enter", "actionRef": "searchProducts" }] } ] } ``` **규칙**: - `actionRef` 사용 시 `handler`/`params`/`target`은 생략 가능 (named_actions 정의에서 가져옴) - `type`, `key`, `event` 등 이벤트 바인딩 속성은 인라인에서 지정 - 인라인에 `handler`/`params`를 함께 지정하면 named_actions 값을 **오버라이드** - `named_actions`는 Partial 파일이 아닌 **부모 레이아웃**에 정의 - 상속(extends) 시 자식 레이아웃의 `named_actions`가 부모를 오버라이드 ### resultTo로 핸들러 결과 저장 (engine-v1.11.0+) 핸들러 실행 결과를 상태(`_local`, `_global`, `_isolated`)에 저장합니다. 커스텀 핸들러가 반환한 값을 레이아웃에서 활용할 때 사용합니다. ```json { "handler": "sirsoft-ecommerce.buildProductColumns", "params": { "baseColumns": [], "currencies": "{{ecommerceSettings?.data?.language_currency?.currencies}}" }, "resultTo": { "target": "_local", "key": "productColumns" } } ``` #### resultTo 필드 | 필드 | 타입 | 필수 | 기본값 | 설명 | | ---- | ---- | ---- | ------ | ---- | | `target` | string | ✅ | - | 저장 대상: `"_local"`, `"_global"`, `"_isolated"` | | `key` | string | ✅ | - | 상태 키 (dot notation 지원, `{{}}` 동적 경로 지원) | | `merge` | string | ❌ | `"deep"` | 병합 모드: `"replace"`, `"shallow"`, `"deep"` | #### 병합 모드 | 모드 | 설명 | | ---- | ---- | | `replace` | 기존 값을 완전히 교체 | | `shallow` | 1단계만 병합 (`{ ...existing, ...new }`) | | `deep` | 중첩 객체까지 재귀 병합 (기본값) | #### 동적 key 예시 ```json { "handler": "sirsoft-ecommerce.calculatePrice", "params": { "optionId": "{{option.id}}" }, "resultTo": { "target": "_local", "key": "calculatedPrices.{{option.id}}", "merge": "shallow" } } ``` #### 주의사항 ```text resultTo는 핸들러가 값을 반환(return)해야 동작 — undefined 반환 시 무시됨 init_actions에서 _local 저장 시 globalStateUpdater 경유 (componentContext 없는 환경) ✅ onSuccess/onError와 독립적으로 동작 — resultTo 저장 후 onSuccess 실행 ``` ### 이벤트 타입 | type | 설명 | 사용 컴포넌트 | |------|------|--------------| | `click` | 클릭 이벤트 | Button, Link, 모든 요소 | | `change` | 값 변경 이벤트 | Input, Select, Checkbox | | `submit` | 폼 제출 이벤트 | Form | | `keydown` | 키보드 누름 이벤트 | Input, 모든 요소 | | `keyup` | 키보드 뗌 이벤트 | Input, 모든 요소 | | `focus` | 포커스 획득 | Input, Select | | `blur` | 포커스 해제 | Input, Select | | `scroll` | 스크롤 이벤트 | 스크롤 가능한 요소 (Div 등) | | `mount` | 컴포넌트 마운트 | 모든 요소 | | `unmount` | 컴포넌트 언마운트 | 모든 요소 | | `onSortEnd` | sortable 정렬 완료 | sortable 컴포넌트 | | `onSortStart` | sortable 드래그 시작 | sortable 컴포넌트 | > **참고**: HTML5 네이티브 드래그 이벤트(`dragstart`, `dragover`, `drop` 등)도 지원되지만, > 목록 정렬에는 `sortable` 속성 + `onSortEnd` 이벤트 사용을 권장합니다. > 상세: [actions-handlers-state.md](actions-handlers-state.md#sortable-드래그앤드롭-정렬) ### 커스텀 이벤트 (event 필드) 표준 DOM 이벤트가 아닌 **컴포넌트 콜백**을 처리할 때 사용합니다. ```text 중요: 컴포넌트가 DOM Event 객체가 아닌 값(문자열, 배열 등)을 직접 전달하는 경우 type 필드 대신 event 필드를 사용하고, $args로 인자에 접근해야 함 ``` | 방식 | 필드 | 데이터 접근 | 사용 상황 | |------|------|-------------|----------| | 표준 DOM 이벤트 | `type: "change"` | `{{$event.target.value}}` | Input, Select, Checkbox 등 | | 커스텀 콜백 | `event: "onChange"` | `{{$args[0]}}` | CodeEditor, DataGrid 등 | #### 예시: CodeEditor onChange 처리 ```json // ✅ 올바름: CodeEditor는 string을 직접 전달 { "type": "composite", "name": "CodeEditor", "props": { "value": "{{_global.editorContent}}" }, "actions": [ { "event": "onChange", "handler": "setState", "params": { "target": "global", "editorContent": "{{$args[0]}}" } } ] } // ❌ 잘못됨: type: "change"는 DOM Event 객체를 기대함 { "actions": [ { "type": "change", "handler": "setState", "params": { "editorContent": "{{$event.target.value}}" } } ] } ``` #### 예시: DataGrid onRowAction 처리 ```json { "type": "composite", "name": "DataGrid", "actions": [ { "event": "onRowAction", "handler": "switch", "params": { "value": "{{$args[0].actionId}}" }, "cases": { "delete": { "handler": "apiCall", "target": "/api/items/{{$args[0].row.id}}", "params": { "method": "DELETE" } } } } ] } ``` #### 예시: DataGrid onSelectionChange 처리 ```json // ✅ 올바름: onSelectionChange는 선택된 ID 배열을 직접 전달 { "type": "composite", "name": "DataGrid", "props": { "selectable": true, "selectedIds": "{{_local.selectedIds ?? []}}" }, "actions": [ { "event": "onSelectionChange", "handler": "setState", "params": { "target": "local", "selectedIds": "{{$args[0]}}" } } ] } // ❌ 잘못됨: $event는 DOM Event 객체 → DataGrid가 깨짐 (컴포넌트 로드 실패) { "actions": [ { "event": "onSelectionChange", "handler": "setState", "params": { "selectedIds": "{{$event}}" } } ] } ``` #### 주요 composite 컴포넌트 콜백 목록 ```text 필수: 아래 콜백은 모두 event 필드 + $args[0] 패턴 사용 필수 $event 사용 시 DOM Event 객체가 전달되어 컴포넌트 오류 발생 ``` | 컴포넌트 | 콜백 이벤트 | $args[0] 값 | |----------|------------|-------------| | DataGrid | `onSelectionChange` | 선택된 ID 배열 `number[]` | | DataGrid | `onRowAction` | `{ actionId, row }` 객체 | | DataGrid | `onSortChange` | `{ key, direction }` 객체 | | DataGrid | `onPageChange` | 페이지 번호 `number` | | CategoryTree | `onNodeSelect` | 선택된 노드 객체 | | CodeEditor | `onChange` | 에디터 내용 `string` | | HtmlEditor | `onChange` | HTML 내용 `string` | | FileUploader | `onUploadComplete` | 업로드 결과 객체 | | TagInput | `onChange` | 태그 배열 `string[]` | #### $args 배열 접근 `event` 필드 사용 시 콜백의 모든 인자가 `$args` 배열에 저장됩니다: | 바인딩 | 설명 | |--------|------| | `{{$args[0]}}` | 첫 번째 인자 | | `{{$args[1]}}` | 두 번째 인자 | | `{{$args[0].field}}` | 첫 번째 인자의 field 속성 | --- ## 내장 핸들러 목록 | 핸들러 | 설명 | target 용도 | params 용도 | |--------|------|-------------|-------------| | `navigate` | 페이지 이동 | - | path, mergeQuery, query | | `navigateBack` | 브라우저 뒤로가기 | - | - | | `navigateForward` | 브라우저 앞으로가기 | - | - | | `apiCall` | API 호출 | 엔드포인트 URL | method, body, headers | | `login` | 로그인 (토큰 저장) | 인증 타입 (admin/user) | body (email, password) | | `logout` | 로그아웃 | - | - | | `setState` | 상태 변경 | - | target (global/local), 상태 값들 | | `setError` | 에러 상태 설정 | 에러 메시지 | - | | `openModal` | 모달 열기 | 모달 ID | - | | `closeModal` | 모달 닫기 | - | - | | `showAlert` | 알림 표시 (alert) | 메시지 | - | | `toast` | 토스트 알림 표시 | - | type, message | | `switch` | 조건 분기 처리 | - | params.value, cases (케이스별 액션 정의, default 지원) | | `sequence` | 순차 액션 실행 | - | actions (액션 배열) | | `parallel` | 병렬 액션 실행 | - | actions (액션 배열) | | `refetchDataSource` | 데이터 소스 다시 fetch | - | dataSourceId | | `remount` | 컴포넌트 강제 리마운트 | - | componentId | | `reloadExtensions` ⭐ NEW | 확장 상태 원자 재동기화 (routes+translations+layouts) | - | moduleInfo, pluginInfo, action | | `reloadRoutes` (deprecated) | 라우트 다시 로드 — `reloadExtensions` 위임 | - | - | | `reloadTranslations` (deprecated) | 다국어 파일 다시 로드 — `reloadExtensions` 위임 | - | - | | `refresh` | 현재 페이지 전체 새로고침 | - | - | | `stopPropagation` | 이벤트 전파 중지 | - | - | | `preventDefault` | 기본 동작 방지 | - | - | | `showErrorPage` | 에러 페이지 렌더링 | - | errorCode, target, containerId, layout | > **상세 문서**: 각 핸들러별 상세 사용법은 [actions-handlers.md](actions-handlers.md) 참조 --- ## Debounce 옵션 연속 호출되는 액션의 실행을 제어하여 성능을 최적화합니다. 텍스트 입력 필드의 실시간 업데이트 등에 유용합니다. ### 기본 사용법 ```json { "type": "change", "handler": "sirsoft-ecommerce.updateOptionField", "params": { "productId": "{{row.id}}", "field": "selling_price", "value": "{{$event.target.value}}" }, "debounce": 300 } ``` ### 상세 설정 ```json { "type": "change", "handler": "myHandler", "debounce": { "delay": 500, "leading": false, "trailing": true } } ``` ### 옵션 설명 | 옵션 | 타입 | 기본값 | 설명 | |------|------|--------|------| | `delay` | number | 300 | 대기 시간 (ms) | | `leading` | boolean | false | 첫 호출 즉시 실행 | | `trailing` | boolean | true | 마지막 호출 후 실행 | ### 주의사항 ```text debounce 사용 시 이벤트 객체는 비동기 접근을 위해 추출됩니다 ✅ 정상 접근 가능: $event.target.value, $event.target.checked ❌ 사용 불가: $event.preventDefault(), $event.stopPropagation() ``` ### 사용 시나리오 | 시나리오 | debounce 권장 | |----------|---------------| | 가격 입력 필드 | ✅ 300ms | | 검색 입력 필드 | ✅ 300~500ms | | 텍스트 입력 필드 | ✅ 300ms | | Toggle/Checkbox | ❌ 즉시 실행 | | Select 드롭다운 | ❌ 즉시 실행 | | 버튼 클릭 | ❌ 즉시 실행 | --- ## 주의사항 ### 1. 핸들러 이름 정확히 사용 ```json // ✅ 올바른 핸들러 이름 { "handler": "apiCall" } { "handler": "navigate" } { "handler": "setState" } // ❌ 잘못된 핸들러 이름 { "handler": "api" } { "handler": "nav" } { "handler": "set_state" } ``` ### 2. params vs target 구분 ```json // navigate: params.path 사용 { "handler": "navigate", "params": { "path": "/admin/users" } } // apiCall: target으로 URL 지정 { "handler": "apiCall", "target": "/api/admin/users" } // openModal: target으로 모달 ID 지정 { "handler": "openModal", "target": "confirm_modal" } ``` ### 3. 상태 변경 target 타입 ```json // 전역 상태 변경 { "handler": "setState", "params": { "target": "global", "selectedIds": [] } } // 로컬 상태 변경 { "handler": "setState", "params": { "target": "local", "isEditing": true } } ``` ### 4. 후속 액션 체인 ```json { "handler": "apiCall", "target": "/api/admin/users/{{row.id}}", "params": { "method": "DELETE" }, "onSuccess": [ { "handler": "toast", "params": { "type": "success", "message": "삭제되었습니다" } }, { "handler": "navigate", "params": { "path": "/admin/users" } } ], "onError": [ { "handler": "toast", "params": { "type": "error", "message": "삭제 실패" } } ] } ``` ### 5. 확인 다이얼로그 ```json { "handler": "apiCall", "target": "/api/admin/users/{{row.id}}", "params": { "method": "DELETE" }, "confirm": "정말 삭제하시겠습니까?" } ``` ### 6. 키보드 이벤트 필터링 ```json { "type": "keydown", "handler": "navigate", "params": { "path": "/admin/search" }, "key": "Enter" } ``` ### 7. $event 바인딩 시 값 추출 ```text 주의: {{$event}}를 직접 사용하면 Event 객체가 전달됩니다 Input/Select: "{{$event.target.value}}" 사용 Checkbox/Toggle: "{{$event.target.checked}}" 사용 ❌ 잘못됨: "{{$event}}" → NaN 오류 발생 ``` ```json // ✅ 올바른 예시 - Input/Select { "type": "change", "handler": "setState", "params": { "searchQuery": "{{$event.target.value}}" } } // ✅ 올바른 예시 - Checkbox/Toggle { "type": "change", "handler": "setState", "params": { "isActive": "{{$event.target.checked}}" } } // ❌ 잘못된 예시 - NaN 오류 발생 { "type": "change", "handler": "myHandler", "params": { "value": "{{$event}}" } } ``` **주의**: `{{$event}}`를 숫자 필드에 사용하면 `parseFloat(eventObject)`가 NaN을 반환하여 0으로 처리됩니다. --- ## 관련 문서 - [핸들러 상세 사용법](actions-handlers.md) - 각 핸들러별 파라미터, onSuccess/onError, errorHandling - [G7Core API](actions-g7core-api.md) - React 컴포넌트에서 액션 실행 - [레이아웃 JSON 스키마](layout-json.md) - 전체 레이아웃 구조 - [데이터 바인딩](data-binding.md) - `{{}}` 표현식, `$t:` 다국어 - [상태 관리](state-management.md) - `_global` 전역 상태