feature: 레이아웃 편집기 배열 빌트인 — array 노드 에디터(ARRAY-PROP 속성 패널) description: | 컴포넌트의 정적 배열 prop(node.props[arrayProp] — 예 TabNavigation tabs, Breadcrumb items, Dropdown/ActionMenu items, DataGrid columns, PageHeader actions)을 속성 모달 본체에서 항목 단위로 구조 편집한다. capability 의 일반 슬롯 `nodeEditor:{kind:"array",params:{arrayProp, itemLabel,fields,newItem}}`(부록4-ter)로 선언하고, 코어 빌트인 ArrayItemsEditor 를 registerCoreEditors 가 registerNodeEditor('array', ...) 로 일반 레지스트리에 등록한다(특권 분기 0 — 코어는 kind 만 알고 컴포넌트명/배열 prop 이름을 가정하지 않음. arrayProp/항목 필드 스키마는 params 가 공급 → 임의 배열 prop 컴포넌트에 동일 에디터 재사용). children 노드 에디터(ChildrenListControl)가 실제 자식 노드 트리를 다루는 것과 달리, array 에디터는 props 안의 정적 데이터 배열(객체/원시 항목)을 추가/삭제/정렬/필드편집한다. 필드 위젯: text(평문 즉시 반영) / i18n-text(평문 blur → createCustomKey → $t:custom.* 기록, 기존 키 → updateCustomKeyValue 현재 로케일만) / select(options) / boolean / icon(아이콘명 문자열). primary 필드(미지정 시 첫 필드)는 행 주 입력. 원시 string[] 항목은 primary 필드로 값 자체 편집. 정적-바인딩 가드: props[arrayProp] 가 `{{...}}` 데이터바인딩이거나 비-배열이면 편집 비대상 → "바인딩됨(코드 편집)" 디그레이드(덮어쓰기 위험 차단). 항목 텍스트 다국어는 children/table 항목과 공용 모델(createCustomKey) SSoT. axes: operation: [add, remove, move, edit_text_field, edit_i18n_field, edit_select, edit_boolean, edit_icon] array_value: [static_array, binding_expression, non_array, empty] field_widget: [text, i18n_text, select, boolean, icon] template: [sirsoft-basic, sirsoft-admin_basic] exclusions: - { operation: add, array_value: binding_expression, reason: "바인딩식 배열 prop 은 편집 비대상(디그레이드) — CRUD 불가" } - { operation: remove, array_value: binding_expression, reason: "동일" } - { operation: move, array_value: binding_expression, reason: "동일" } - { operation: edit_text_field, array_value: binding_expression, reason: "동일" } - { operation: edit_i18n_field, array_value: binding_expression, reason: "동일" } - { operation: edit_select, array_value: binding_expression, reason: "동일" } - { operation: edit_boolean, array_value: binding_expression, reason: "동일" } - { operation: edit_icon, array_value: binding_expression, reason: "동일" } - { operation: add, array_value: non_array, reason: "비-배열 prop 은 디그레이드 — CRUD 불가" } - { operation: remove, array_value: non_array, reason: "동일" } - { operation: move, array_value: non_array, reason: "동일" } - { operation: edit_text_field, array_value: non_array, reason: "동일" } - { operation: edit_i18n_field, array_value: non_array, reason: "동일" } - { operation: edit_select, array_value: non_array, reason: "동일" } - { operation: edit_boolean, array_value: non_array, reason: "동일" } - { operation: edit_icon, array_value: non_array, reason: "동일" } - { operation: remove, array_value: empty, reason: "빈 배열은 제거 대상 없음" } - { operation: move, array_value: empty, reason: "빈 배열은 이동 대상 없음" } - { operation: edit_text_field, array_value: empty, reason: "빈 배열은 편집 대상 없음(추가만 가능)" } - { operation: edit_i18n_field, array_value: empty, reason: "동일" } - { operation: edit_select, array_value: empty, reason: "동일" } - { operation: edit_boolean, array_value: empty, reason: "동일" } - { operation: edit_icon, array_value: empty, reason: "동일" } - { operation: edit_text_field, field_widget: i18n_text, reason: "필드 위젯 축은 edit 종류로 이미 분기 — 교차 중복" } - { operation: edit_text_field, field_widget: select, reason: "동일" } - { operation: edit_text_field, field_widget: boolean, reason: "동일" } - { operation: edit_text_field, field_widget: icon, reason: "동일" } - { operation: edit_i18n_field, field_widget: text, reason: "동일" } - { operation: edit_i18n_field, field_widget: select, reason: "동일" } - { operation: edit_i18n_field, field_widget: boolean, reason: "동일" } - { operation: edit_i18n_field, field_widget: icon, reason: "동일" } - { operation: edit_select, field_widget: text, reason: "동일" } - { operation: edit_select, field_widget: i18n_text, reason: "동일" } - { operation: edit_select, field_widget: boolean, reason: "동일" } - { operation: edit_select, field_widget: icon, reason: "동일" } - { operation: edit_boolean, field_widget: text, reason: "동일" } - { operation: edit_boolean, field_widget: i18n_text, reason: "동일" } - { operation: edit_boolean, field_widget: select, reason: "동일" } - { operation: edit_boolean, field_widget: icon, reason: "동일" } - { operation: edit_icon, field_widget: text, reason: "동일" } - { operation: edit_icon, field_widget: i18n_text, reason: "동일" } - { operation: edit_icon, field_widget: select, reason: "동일" } - { operation: edit_icon, field_widget: boolean, reason: "동일" } - { operation: add, field_widget: i18n_text, reason: "구조 연산(add/remove/move)은 필드 위젯 종류 무관" } - { operation: add, field_widget: select, reason: "동일" } - { operation: add, field_widget: boolean, reason: "동일" } - { operation: add, field_widget: icon, reason: "동일" } - { operation: remove, field_widget: i18n_text, reason: "동일" } - { operation: remove, field_widget: select, reason: "동일" } - { operation: remove, field_widget: boolean, reason: "동일" } - { operation: remove, field_widget: icon, reason: "동일" } - { operation: move, field_widget: i18n_text, reason: "동일" } - { operation: move, field_widget: select, reason: "동일" } - { operation: move, field_widget: boolean, reason: "동일" } - { operation: move, field_widget: icon, reason: "동일" } effects: # capability/등록 - array_capability_declared_on_p1_components_both_templates - array_editor_registered_via_registercoreeditors_kind_agnostic - property_modal_dispatches_array_node_editor_in_props_tab_by_kind # 가드 - binding_expression_array_prop_degrades_to_bound_notice - non_array_prop_value_degrades_to_bound_notice - missing_array_prop_capability_shows_misconfigured_notice # 구조 연산 - add_item_appends_newitem_skeleton_patches_whole_node - add_item_synthesizes_empty_item_from_fields_when_no_newitem - remove_item_drops_only_that_index - move_up_down_swaps_adjacent_with_boundary_guard # 필드 편집 - text_field_updates_item_immediately - icon_field_updates_item_with_icon_name_string - select_field_updates_item_with_chosen_value - boolean_field_toggles_item_flag - i18n_text_plain_blur_creates_custom_key_and_writes_dollar_t_custom - existing_custom_key_blur_updates_current_locale_value_keeps_key - primitive_string_item_edits_value_via_primary_field - node_prop_change_clears_draft_buffer_no_stale # 라이브(E2E) - live_add_tab_edit_label_reorder_save_persists_to_user_page test_files: - resources/js/core/template-engine/layout-editor/__tests__/components/ArrayItemsEditor.test.tsx - resources/js/core/template-engine/layout-editor/__tests__/spec/editorRegistries.test.ts - tests/Playwright/specs/layout-editor/array-editor.spec.ts validation: note: | Vitest 단위가 axes cross product 회귀(추가/삭제/정렬, text/select/boolean/icon/i18n-text 필드 편집, 바인딩/비-배열/미선언 디그레이드, 원시 string[] 항목, 노드 파생 무상태)와 capability 부착·등록(kind-agnostic)을 잠근다. Playwright E2E 가 라이브 편집기에서 TabNavigation 선택 → array 에디터 마운트 → 탭 추가 → 라벨 편집 → 정렬 → 저장(PUT 200) → 실사용자화면 반영을 브라우저로 검증한다. 캔버스 인플레이스 레퍼런스(TabNavigation registerCanvasOverlay)는 단계 4-b 산출물 — 본 단계는 속성 패널 경로. e2e_spec: tests/Playwright/specs/layout-editor/array-editor.spec.ts