Files
Gnuboard7/docs/frontend/actions-handlers-state.md
T
2026-07-01 10:30:32 +09:00

28 KiB

액션 핸들러 - 상태 관리

메인 문서: actions-handlers.md


목차

  1. apiCall
  2. setState
  3. setError
  4. refetchDataSource
  5. updateDataSource
  6. appendDataSource
  7. remount
  8. sortable (드래그앤드롭 정렬)
  9. onSuccess/onError 후속 액션
  10. API 데이터 바인딩 규칙
  11. 에러 핸들링 시스템

apiCall

API를 호출합니다. 주의: api가 아닌 apiCall을 사용해야 합니다.

{
  "type": "click",
  "handler": "apiCall",
  "auth_required": true,
  "target": "/api/admin/users/bulk-status",
  "params": {
    "method": "PATCH",
    "body": {
      "ids": "{{_global.selectedIds}}",
      "status": "active"
    }
  },
  "onSuccess": [
    { "handler": "closeModal" },
    { "handler": "toast", "params": { "type": "success", "message": "$t:common.success" } }
  ]
}

액션 레벨 속성

필드 타입 기본값 설명
auth_required boolean false true이면 Bearer 토큰을 Authorization 헤더에 포함 (401 에러 발생 시 로그인 페이지 리다이렉트)
auth_mode string "required" 인증 모드: "required" (토큰 없으면 에러), "optional" (토큰 있으면 포함, 없어도 진행)
identity_target object — 본인인증(IDV) 대상. 이 apiCall이 HTTP 428(본인인증 필요)을 받으면 인증 코드/링크를 보낼 { email?, phone? } 을 흐름이 직접 선언 (engine-v1.51.0+)
중요: auth_required, auth_mode, identity_target은 params 안이 아닌 액션 정의 최상위에 선언해야 합니다.

identity_target 사용 예시

본인인증 정책이 켜진 상태에서 비로그인(게스트) 사용자가 호출하는 API는, 인증 코드를 보낼 대상(이메일·전화)을 화면 입력값에서 선언해야 합니다. 서버는 428을 던지는 시점에 사용자가 방금 화면에 입력한 값을 알 수 없으므로, 대상 수집은 레이아웃의 책임입니다.

{
  "handler": "apiCall",
  "target": "/api/modules/sirsoft-ecommerce/user/orders",
  "auth_mode": "optional",
  "identity_target": {
    "email": "{{_local.orderer?.email || ''}}",
    "phone": "{{_local.orderer?.phone || _local.shipping?.recipient_phone || ''}}"
  },
  "params": { "method": "POST", "body": { } }
}
  • email / phone 둘 중 하나만 있어도 충분합니다 (서버가 둘 다 허용).
  • 우선순위는 표현식 자체가 결정합니다 (예: 주문자 정보 우선 → 수취인 정보 폴백).
  • 표현식은 || '' fallback으로 빈 문자열을 보장합니다 (undefined 방지).
  • 로그인 사용자는 빈 값이어도 서버가 세션에서 대상을 도출하므로 무방합니다.
  • G7Core.api(axios) 직접 호출 경로에서는 호출 config에 identity_target 을 실어 동일하게 동작합니다.

auth_mode 사용 예시

비회원도 접근 가능하지만, 로그인 시 추가 기능을 제공하는 API:

{
  "handler": "apiCall",
  "target": "/api/cart",
  "auth_mode": "optional",
  "params": { "method": "GET" }
}
auth_mode 토큰 있음 토큰 없음
"required" (기본) Bearer 토큰 전송 에러 발생
"optional" Bearer 토큰 전송 토큰 없이 요청 진행
// ✅ 올바른 사용
{
  "handler": "apiCall",
  "auth_required": true,
  "target": "/api/admin/users",
  "params": { "method": "POST" }
}

// ❌ 잘못된 사용 (Bearer 토큰이 전송되지 않음)
{
  "handler": "apiCall",
  "target": "/api/admin/users",
  "params": { "method": "POST", "auth_required": true }
}

apiCall params 구조

필드 타입 기본값 설명
method string "GET" HTTP 메서드 (GET, POST, PUT, PATCH, DELETE)
body object - 요청 본문 (JSON 또는 FormData)
headers object - 추가 헤더
contentType string "application/json" 요청 Content-Type ("multipart/form-data" 지정 시 FormData 자동 변환)

multipart/form-data 지원 (파일 업로드)

버전: engine-v1.19.0+

contentType: "multipart/form-data"를 지정하면 body가 자동으로 FormData로 변환됩니다. Content-Type 헤더는 설정하지 않으며 (브라우저가 boundary를 포함하여 자동 설정), File/Blob 객체는 원본 그대로 전송됩니다.

{
  "handler": "apiCall",
  "auth_required": true,
  "target": "/api/admin/modules/manual-install",
  "params": {
    "method": "POST",
    "contentType": "multipart/form-data",
    "body": {
      "file": "{{_global.moduleUploadFile}}",
      "source": "file_upload"
    }
  }
}

FormData 변환 규칙

body 값 타입 FormData 처리
File / Blob formData.append(key, value) (원본 유지)
string / number / boolean formData.append(key, String(value))
object / array formData.append(key, JSON.stringify(value))
null / undefined 제외 (전송하지 않음)

주의사항

주의: contentType: "multipart/form-data" 시 Content-Type 헤더를 수동 설정하지 말 것
   → 브라우저가 boundary를 포함한 Content-Type을 자동 생성
주의: File/Blob 객체는 setState로 저장 시 원본 참조 유지됨 (engine-v1.19.0+)
   → deepMergeWithState가 non-plain 객체(File, Blob, Date 등)를 spread 없이 직접 할당
필수: 파일 업로드 API는 백엔드에서 multipart/form-data를 기대해야 함

apiCall body에서 조건부 필드 제외 (undefined 패턴)

드라이버/모드 선택에 따라 특정 필드만 전송해야 하는 경우, undefined를 반환하는 삼항 표현식을 사용합니다. JSON.stringify()는 값이 undefined인 키를 자동으로 제거합니다.

{
  "handler": "apiCall",
  "target": "/api/admin/settings/test-mail",
  "params": {
    "method": "POST",
    "body": {
      "mailer": "{{_local.form?.mail?.mailer || 'smtp'}}",
      "from_address": "{{_local.form?.mail?.from_address || ''}}",
      "host": "{{_local.form?.mail?.mailer === 'smtp' ? (_local.form?.mail?.host ?? '') : undefined}}",
      "mailgun_domain": "{{_local.form?.mail?.mailer === 'mailgun' ? (_local.form?.mail?.mailgun_domain ?? '') : undefined}}"
    }
  }
}

동작 원리:

조건 표현식 결과 JSON 직렬화
mailer === 'smtp' host: "smtp.example.com" "host":"smtp.example.com" (포함)
mailer === 'mailgun' host: undefined 키 자체가 제거됨

핵심 규칙:

주의: 불필요한 필드를 빈 문자열('')로 전송하지 말 것
필수: undefined 패턴으로 해당 드라이버 필드만 전송
공통 필드(from_address, from_name 등)는 조건 없이 항상 전송
주의: 모든 드라이버 필드를 항상 전송하면 서버 측 불필요한 처리 유발

삼항 표현식 패턴:

현재 드라이버 필드:   조건 ? (값 ?? 기본값) : undefined
다른 드라이버 필드:   undefined (JSON에서 제거)
공통 필드:           조건 없이 항상 포함

CSRF 토큰

apiCall은 자동으로 CSRF 토큰을 처리합니다:

  • POST, PUT, PATCH, DELETE 요청 시 /sanctum/csrf-cookie 호출
  • 쿠키에서 XSRF-TOKEN을 추출하여 X-XSRF-TOKEN 헤더에 포함

globalHeaders 자동 적용

버전: engine-v1.16.0+

레이아웃에 globalHeaders가 정의되어 있으면, apiCall 핸들러도 해당 패턴에 매칭되는 API에 헤더를 자동으로 포함합니다.

// 레이아웃 최상위
{
  "globalHeaders": [
    { "pattern": "/api/modules/sirsoft-ecommerce/*", "headers": { "X-Cart-Key": "{{_global.cartKey}}" } }
  ]
}

// apiCall 핸들러 - X-Cart-Key 헤더 자동 포함
{
  "handler": "apiCall",
  "target": "/api/modules/sirsoft-ecommerce/cart/add",
  "params": {
    "method": "POST",
    "body": { "productId": "{{item.id}}" }
  }
}

헤더 우선순위: params.headers > globalHeaders

{
  "handler": "apiCall",
  "target": "/api/modules/sirsoft-ecommerce/cart",
  "params": {
    "method": "GET",
    "headers": { "X-Cart-Key": "custom-key" }  // globalHeaders보다 우선
  }
}

상세 문서: layout-json.md


setState

상태를 변경합니다.

병합 모드 (merge 옵션)

버전: engine-v1.18.0+ (replace 모드 추가)

setState 핸들러의 params에 merge 속성을 지정하여 상태 병합 방식을 선택할 수 있습니다.

merge 값 동작 사용 시점
생략 또는 "deep" 재귀적 깊은 병합 (중첩 객체 보존) 개별 필드 업데이트 (기본값)
"shallow" 최상위 키만 덮어쓰기 (1단계) 프리셋 적용, 필터 초기화
"replace" 기존 상태 완전 무시, 새 값으로 교체 폼 초기화, 서버 데이터로 전체 교체
// replace 모드: 기존 _local을 완전히 payload로 교체
{
  "handler": "setState",
  "params": {
    "target": "local",
    "merge": "replace",
    "formData": { "name": "", "price": 0 }
  }
}

// shallow 모드: 최상위 키만 덮어쓰기
{
  "handler": "setState",
  "params": {
    "target": "local",
    "merge": "shallow",
    "filter": { "searchField": "all", "status": "all" }
  }
}

상세 비교: state-management-forms.md

전역 상태 변경 (target: "global")

{
  "type": "change",
  "handler": "setState",
  "params": {
    "target": "global",
    "selectedIds": "{{$args[0]}}",
    "searchQuery": "{{$event.target.value}}"
  }
}

로컬 상태 변경 (target: "local" 또는 생략)

{
  "type": "change",
  "handler": "setState",
  "params": {
    "isExpanded": true
  }
}

격리된 상태 변경 (target: "isolated")

버전: engine-v1.14.0+

isolatedState 속성이 정의된 컴포넌트 내에서만 동작합니다. 해당 영역의 상태 변경 시 전체 레이아웃이 아닌 격리된 영역만 리렌더링됩니다.

{
  "type": "click",
  "handler": "setState",
  "params": {
    "target": "isolated",
    "selectedId": "{{item.id}}",
    "currentStep": 2
  }
}

사용 요건

주의: target: "isolated"는 isolatedState 속성이 정의된 컴포넌트 내에서만 동작합니다.
격리 스코프 외부에서 호출 시 → _local로 폴백되며 경고 로그 출력
isolatedState가 있는 컴포넌트 내에서 호출 시 → 격리된 상태만 업데이트

레이아웃 정의 예시

{
  "type": "Div",
  "isolatedState": {
    "selectedCategories": [null, null, null, null],
    "currentStep": 1
  },
  "isolatedScopeId": "category-selector",
  "children": [
    {
      "type": "basic",
      "name": "Button",
      "props": {
        "label": "다음 단계",
        "onClick": {
          "handler": "setState",
          "params": {
            "target": "isolated",
            "currentStep": "{{_isolated.currentStep + 1}}"
          }
        }
      }
    }
  ]
}

target별 비교

target 상태 스코프 리렌더링 범위 사용 시점
local (기본) _local 전체 레이아웃 일반 폼 데이터, 필터
global _global 전체 앱 사용자 인증, 사이드바 상태
isolated _isolated 격리된 영역만 빈번한 인터랙션 (카테고리 선택, 드래그)

주요 사용 패턴

// 체크박스 선택 ID 저장
{
  "type": "change",
  "handler": "setState",
  "params": {
    "target": "global",
    "selectedIds": "{{$args[0]}}"
  }
}

// 검색어 입력 시 상태 저장
{
  "type": "change",
  "handler": "setState",
  "params": {
    "searchQuery": "{{$event.target.value}}"
  }
}

// 토글 상태 변경
{
  "type": "click",
  "handler": "setState",
  "params": {
    "isExpanded": "{{!_local.isExpanded}}"
  }
}

setError

에러 상태를 설정합니다. apiError 상태 키에 저장됩니다.

{
  "handler": "setError",
  "target": "{{error.response.message}}"
}

target 값

형식 설명
{{error.message}} 데이터 바인딩 (에러 객체에서 추출)
$t:errors.login_failed 다국어 키
"로그인에 실패했습니다." 직접 문자열

refetchDataSource

특정 데이터 소스를 다시 fetch합니다.

{
  "handler": "refetchDataSource",
  "params": {
    "dataSourceId": "modules"
  }
}

refetchDataSource params 구조

필드 타입 필수 기본값 설명
dataSourceId string ✅ - 다시 fetch할 데이터 소스 ID
sync boolean ❌ false true면 즉시 동기 렌더링

sync 옵션

버전: engine-v1.4.0+

sync: true를 사용하면 React의 startTransition 없이 즉시 동기적으로 렌더링합니다.

{
  "handler": "refetchDataSource",
  "params": {
    "dataSourceId": "admin_menu",
    "sync": true
  }
}
케이스 sync 필요 여부
드래그 앤 드롭 순서 변경 ✅ sync: true
토글/체크박스 상태 변경 ✅ sync: true
일반 데이터 갱신 ❌ 기본값 사용

상태 오버라이드 (engine-v1.17.0+)

refetch 시 상태를 임시로 오버라이드하여 데이터소스 파라미터 치환에 반영할 수 있습니다.

필드 타입 필수 엔진 버전 설명
globalStateOverride object ❌ engine-v1.17.0+ _global 값 임시 오버라이드
localStateOverride object ❌ engine-v1.19.0+ _local 값 임시 오버라이드
isolatedStateOverride object ❌ engine-v1.19.0+ _isolated 값 임시 오버라이드
{
  "handler": "refetchDataSource",
  "params": {
    "dataSourceId": "products",
    "sync": true,
    "globalStateOverride": {
      "currentPage": 1
    },
    "localStateOverride": {
      "filter": { "status": "active" }
    }
  }
}
오버라이드는 해당 refetch에만 적용 (실제 상태 미변경)
✅ 상세 문서: data-sources-advanced.md "상태 오버라이드 파라미터" 섹션

updateDataSource

데이터 소스를 직접 업데이트합니다. API 응답을 사용하여 데이터 소스를 갱신할 때 사용합니다. refetchDataSource와 달리 추가 API 요청 없이 즉시 데이터를 업데이트합니다.

{
  "handler": "apiCall",
  "target": "/api/checkout",
  "params": { "method": "PUT", "body": "{{_local.checkout}}" },
  "onSuccess": [
    {
      "handler": "updateDataSource",
      "params": {
        "dataSourceId": "checkoutData",
        "data": "{{response}}"
      }
    }
  ]
}

updateDataSource params 구조

필드 타입 필수 기본값 설명
dataSourceId string ✅ - 업데이트할 데이터 소스 ID
data any ✅ - 새로운 데이터 (API 응답 등)

refetchDataSource vs updateDataSource

핸들러 동작 네트워크 요청 사용 시점
refetchDataSource 데이터 소스의 endpoint를 다시 호출 ✅ 발생 서버에서 최신 데이터를 가져와야 할 때
updateDataSource 전달받은 data로 직접 교체 ❌ 없음 PUT/POST 응답으로 즉시 갱신할 때

사용 예시

PUT API 응답으로 데이터 소스 갱신 (추가 GET 요청 없이):

{
  "handler": "apiCall",
  "target": "/api/modules/sirsoft-ecommerce/checkout",
  "params": { "method": "PUT", "body": "{{_local.checkout}}" },
  "onSuccess": [
    {
      "handler": "updateDataSource",
      "params": {
        "dataSourceId": "checkoutData",
        "data": "{{response}}"
      }
    },
    {
      "handler": "toast",
      "params": { "type": "success", "message": "$t:common.saved" }
    }
  ]
}
주의: onSuccess 콜백에서 response 변수를 사용할 때 $response가 아닌 response 사용
"data": "{{$response}}"  → undefined로 평가됨
"data": "{{response}}"   → 정상 동작

appendDataSource

기존 데이터 소스에 새 데이터를 병합합니다. 무한스크롤 구현에 유용합니다.

{
  "handler": "appendDataSource",
  "params": {
    "dataSourceId": "templates",
    "dataPath": "data",
    "newData": "{{response.data}}"
  }
}

appendDataSource params 구조

필드 타입 필수 기본값 설명
dataSourceId string ✅ - 대상 데이터 소스 ID
dataPath string ❌ null 병합할 데이터 경로 (예: "data")
newData array ✅ - 병합할 새 데이터 배열

무한스크롤 예시

{
  "type": "scroll",
  "debounce": 200,
  "handler": "sequence",
  "actions": [
    {
      "handler": "switch",
      "params": {
        "value": "{{$event.target.scrollHeight - $event.target.scrollTop <= $event.target.clientHeight + 100 && _global.infiniteScroll.hasMore && !_global.infiniteScroll.isLoadingMore}}"
      },
      "cases": {
        "true": {
          "handler": "sequence",
          "actions": [
            {
              "handler": "setState",
              "params": {
                "target": "global",
                "infiniteScroll.isLoadingMore": true
              }
            },
            {
              "handler": "apiCall",
              "auth_required": true,
              "target": "/api/items",
              "params": {
                "method": "GET",
                "query": {
                  "page": "{{_global.infiniteScroll.currentPage + 1}}",
                  "per_page": 20
                }
              },
              "onSuccess": [
                {
                  "handler": "appendDataSource",
                  "params": {
                    "dataSourceId": "items",
                    "dataPath": "data",
                    "newData": "{{response.data}}"
                  }
                },
                {
                  "handler": "setState",
                  "params": {
                    "target": "global",
                    "infiniteScroll.currentPage": "{{_global.infiniteScroll.currentPage + 1}}",
                    "infiniteScroll.hasMore": "{{(response.data?.length ?? 0) >= 20}}",
                    "infiniteScroll.isLoadingMore": false
                  }
                }
              ]
            }
          ]
        }
      }
    }
  ]
}

scroll 이벤트 속성

scroll 이벤트에서 $event.target으로 접근 가능한 속성:

속성 타입 설명
scrollHeight number 전체 콘텐츠 높이
scrollTop number 현재 스크롤 위치 (상단 기준)
clientHeight number 보이는 영역 높이
scrollWidth number 전체 콘텐츠 너비
scrollLeft number 현재 스크롤 위치 (좌측 기준)
clientWidth number 보이는 영역 너비

remount

컴포넌트를 강제로 리마운트합니다.

{
  "handler": "remount",
  "params": {
    "componentId": "template_card_grid"
  }
}

사용 사례

  • Toggle/Checkbox 상태 복원
  • 폼 초기화
  • 컴포넌트 상태 리셋

sortable (드래그앤드롭 정렬)

버전: engine-v1.14.0+ 기반: @dnd-kit/core + @dnd-kit/sortable (React 네이티브 D&D 라이브러리)

레이아웃 JSON의 sortable 속성을 사용하여 드래그앤드롭 정렬을 구현합니다. HTML5 네이티브 D&D가 아닌 @dnd-kit 기반으로 동작하며, DynamicRenderer가 자동으로 SortableContainer/SortableItemWrapper를 렌더링합니다.

sortable 속성 구조

필드 타입 필수 기본값 설명
source string ✅ - 배열 바인딩 표현식 (예: "{{_local.form.options}}")
itemKey string ❌ "id" 아이템 고유 키 필드명
strategy string ❌ "verticalList" 정렬 전략: verticalList / horizontalList / rectSorting
handle string ❌ - 드래그 핸들 CSS 선택자 (예: "[data-drag-handle]")
itemVar string ❌ "$item" 아이템 컨텍스트 변수명
indexVar string ❌ "$index" 인덱스 컨텍스트 변수명

기본 사용 예시

{
  "type": "basic",
  "name": "Div",
  "sortable": {
    "source": "{{_local.form.additional_options}}",
    "itemKey": "id",
    "strategy": "verticalList",
    "handle": "[data-drag-handle]"
  },
  "itemTemplate": {
    "type": "basic",
    "name": "Div",
    "props": {
      "className": "flex items-center gap-2 p-2 border rounded"
    },
    "children": [
      {
        "type": "basic",
        "name": "Div",
        "props": {
          "data-drag-handle": true,
          "className": "cursor-grab"
        },
        "children": [{ "type": "basic", "name": "Icon", "props": { "name": "fa-grip-vertical" } }]
      },
      {
        "type": "basic",
        "name": "Span",
        "props": { "textContent": "{{$item.name}}" }
      }
    ]
  },
  "actions": [
    {
      "event": "onSortEnd",
      "handler": "setState",
      "params": {
        "target": "local",
        "form.additional_options": "{{$sortedItems}}"
      }
    }
  ]
}

sortable 전용 이벤트

이벤트 설명 컨텍스트 변수
onSortEnd 정렬 완료 시 $sortedItems (정렬된 배열), $oldIndex, $newIndex
onSortStart 드래그 시작 시 $activeId (드래그 중인 아이템 ID)

드래그 핸들

handle 속성을 지정하면 해당 CSS 선택자를 가진 요소만 드래그 가능합니다. data-drag-handle 속성이 있는 요소에 DynamicRenderer가 자동으로 드래그 리스너를 바인딩합니다.

{
  "sortable": {
    "source": "{{_local.form.items}}",
    "handle": "[data-drag-handle]"
  }
}

핸들을 지정하지 않으면 아이템 전체가 드래그 가능합니다.

주의 사항

sortable은 iteration보다 우선 처리됨 (같은 컴포넌트에 둘 다 있으면 sortable만 동작)
sortable 아이템 내부에서는 폼 자동 바인딩(auto-binding)이 비활성화됨
   → 인덱스 기반 경로가 정렬 후 stale 값을 참조하는 문제 방지
   → 핸들러(setState 등)에서 직접 상태를 관리해야 함
onSortEnd에서 setState로 소스 배열을 업데이트해야 정렬 결과가 반영됨
아이템에 고유한 id 필드가 필수 (itemKey로 지정)
HTML5 네이티브 D&D(dragstart/dragover/drop 이벤트)는 sortable에 사용하지 않음
   → @dnd-kit이 PointerSensor/KeyboardSensor로 자체 처리

onSuccess/onError 후속 액션

API 호출 등의 액션 후에 후속 액션을 실행할 수 있습니다.

배열 지원

onSuccess와 onError는 단일 액션 또는 배열을 지원합니다.

{
  "type": "click",
  "handler": "apiCall",
  "target": "/api/admin/users/bulk-status",
  "params": {
    "method": "PATCH",
    "body": {
      "ids": "{{_global.selectedIds}}",
      "status": "active"
    }
  },
  "onSuccess": [
    { "handler": "closeModal" },
    { "handler": "setState", "params": { "target": "global", "selectedIds": [] } },
    { "handler": "navigate", "params": { "path": "/admin/users", "mergeQuery": true, "query": {} } },
    { "handler": "toast", "params": { "type": "success", "message": "$t:admin.users.modals.bulk_activate_success" } }
  ]
}

onError에서 접근 가능한 데이터

바인딩 설명
{{error.message}} 에러 메시지 (번역됨)
{{error.status}} HTTP 상태 코드
{{error.data}} API 응답 전체 (success, message, errors 포함)
{{error.data.errors}} errors 객체 (추가 에러 정보)

API 데이터 바인딩 규칙

API 응답 데이터 구조를 절대 추측하지 않음

1. 새로운 API 연동 시 → 실제 API 응답 구조를 컨트롤러 코드에서 확인
2. onSuccess/onError 콜백 → 정확한 컨텍스트 변수 경로 확인 필수
3. 불확실한 경우 → 기존 레이아웃의 유사 패턴 참조

그누보드7 API 응답 표준 구조

ResponseHelper를 사용하는 모든 API는 다음 구조를 따릅니다:

성공 응답 (success):

{
  "success": true,
  "message": "번역된 메시지",
  "data": { ... }
}

에러 응답 (error):

{
  "success": false,
  "message": "번역된 에러 메시지",
  "errors": { ... }
}

콜백별 컨텍스트 변수 경로

주의: onSuccess/onError 콜백에서 `$response` 사용 금지 — `response` 사용
올바른 변수명: `response` ($ 접두사 없음)
`$response`는 ActionDispatcher 컨텍스트에 존재하지 않는 변수 → undefined 반환
   → preprocessOptionalChaining이 `$response?.data?.xxx`로 변환 → undefined (에러 없이 조용히 실패)
   → fallback 값이 있으면 항상 fallback만 표시되어 버그가 은폐됨
콜백 변수 실제 경로 설명
onSuccess response - 전체 API 응답 (권장)
onSuccess response.data - 성공 시 data 필드
onSuccess result - response와 동일 (하위 호환성)
onError error.message - 번역된 에러 메시지
onError error.status - HTTP 상태 코드
onError error.data - 전체 API 응답
onError error.data.errors - errors 객체
onError error.data.errors.필드명 - 특정 에러 필드

API 데이터 바인딩 체크리스트

□ API 응답 구조를 실제로 확인했는가? (추측 금지)
□ ResponseHelper의 success/error 반환 구조를 이해했는가?
□ 중첩된 객체 경로가 정확한가? (예: error.data.errors.field)
□ 배열 vs 객체 구분이 명확한가?
□ Optional Chaining(?.)을 적절히 사용했는가?

자주 하는 실수

// ❌ 잘못된 예: 응답 구조 추측
"deactivateWarningData": "{{error.response}}"
"deactivateWarningData": "{{error.dependent_templates}}"

// ✅ 올바른 예: 실제 구조 확인 후 사용
"deactivateWarningData": "{{error.data}}"
// 모달에서: {{_global.deactivateWarningData.errors.dependent_templates}}

에러 핸들링 시스템 (errorHandling)

버전: engine-v1.6.0+

HTTP 에러 코드별로 다른 처리 로직을 정의할 수 있습니다.

errorHandling vs onError 역할 분리

속성 역할 실행 조건
errorHandling 특정 에러 코드별 처리 해당 코드 또는 default가 정의된 경우
onError 범용 에러 처리 (폴백) errorHandling에 해당 코드가 없을 때

처리 흐름

API 에러 발생 (예: 403)
     ↓
errorHandling[403] 있음?
     ├── ✅ → errorHandling[403] 실행 (종료)
     ↓ ❌
errorHandling[default] 있음?
     ├── ✅ → errorHandling[default] 실행 (종료)
     ↓ ❌
onError 있음?
     ├── ✅ → onError 실행 (종료)
     ↓ ❌
상위 레벨 errorHandling 확인

기본 사용법

{
  "type": "click",
  "handler": "apiCall",
  "auth_required": true,
  "target": "/api/admin/users/{{id}}",
  "params": { "method": "DELETE" },
  "errorHandling": {
    "403": {
      "handler": "openModal",
      "target": "permission_denied_modal"
    },
    "404": {
      "handler": "toast",
      "params": { "type": "warning", "message": "$t:errors.user_not_found" }
    }
  },
  "onError": {
    "handler": "toast",
    "params": { "type": "error", "message": "{{error.message}}" }
  }
}

관련 문서