@ncds/ui-admin-mcp 1.0.0-alpha.22 → 1.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,7 @@ You are an agent that builds UI using NCUA (NCDS UI Admin) design system compone
47
47
 
48
48
  ### Step 3: HTML Generation
49
49
 
50
+ - **Canonical output verification**: For components with `coverageScore ≥ 0.7` (from list_composition_overrides in Step 1), call **render_to_html** with the canonical example props (including children) BEFORE composing the final page. Inspect the returned HTML to learn the actual DOM structure, class names, and attribute patterns. Use this real output as ground truth — do NOT assume or guess the component's HTML structure from memory or training data. This step prevents hallucinated class names, invented BEM elements, and wrong nesting order.
50
51
  - When building a page with multiple components, use **render_to_html_batch** to render them all in one call (max 30). This is much more efficient than calling render_to_html repeatedly.
51
52
  - For a single component, use **render_to_html**.
52
53
  - Use the returned html as-is. Do NOT modify class names, structure, or attributes.
@@ -428,5 +428,30 @@
428
428
  },
429
429
  "methods": ["toggle()", "open()", "close()", "closeAndRestoreFocus()", "destroy()"],
430
430
  "example": "const dropdown = new window.ncua.Dropdown(document.getElementById('my-host'), {\n trigger: { type: 'button', text: '메뉴' },\n groups: [{\n items: [\n { id: 'edit', text: '수정', onClick: () => console.log('edit') },\n { id: 'delete', text: '삭제', type: 'danger', onClick: () => console.log('delete') },\n ],\n }],\n});"
431
+ },
432
+ "Draggable": {
433
+ "className": "Draggable",
434
+ "constructor": "new window.ncua.Draggable(tbodyElement, options)",
435
+ "constructorParams": {
436
+ "tbodyElement": "HTMLElement — 드래그 대상 컨테이너 (tbody)",
437
+ "options.selectAllEl": "HTMLInputElement (optional) — 전체선택 체크박스 요소. 전달 시 행 체크박스 change 위임·전체선택·indeterminate 상태를 Draggable이 자동 관리",
438
+ "options.moveButtons": "{ top?, up?, down?, bottom?: HTMLElement } (optional) — 이동 버튼 요소. 전달 시 클릭 핸들러와 disabled 상태를 Draggable이 자동 관리",
439
+ "options.draggable": "boolean (default: true) — 드래그 핸들 활성화 여부. false면 체크박스·이동버튼 기능은 유지되고 드래그만 비활성화",
440
+ "options.itemSelector": "string (default: '[data-draggable-id]') — 드래그 행 선택자",
441
+ "options.handleSelector": "string (default: '.ncua-table__drag-handle') — 드래그 핸들 선택자",
442
+ "options.onDrop": "(fromId: string, toId: string, edge: 'top' | 'bottom', selectedIds: string[]) => void (optional) — 드롭 완료 콜백. Draggable은 DOM을 이동하지 않으므로 페이지에서 직접 DOM 재배치 후 API 호출 등 비즈니스 로직 처리",
443
+ "options.onMove": "(direction: 'top' | 'up' | 'down' | 'bottom', selectedIds: string[]) => void (optional) — 이동 버튼 클릭 콜백. onDrop과 달리 Draggable이 DOM 행 이동을 자동 완료한 뒤 호출되므로 페이지에서 DOM 재배치 불필요 — 서버 API 호출 등 비즈니스 로직만 작성",
444
+ "options.onSelect": "(selectedIds: string[]) => void (optional) — 선택 변경 콜백. 선택 카운트 표시 등 UI 업데이트에 활용"
445
+ },
446
+ "methods": [
447
+ "setDraggable(enabled: boolean) — 드래그 핸들 on/off (체크박스·이동버튼 기능은 유지)",
448
+ "setMoveButtonsEnabled(enabled: boolean) — 이동 버튼 그룹 활성화/비활성화 (드래그·체크박스 기능은 유지)",
449
+ "getSelectedIds(): string[] — 현재 선택된 행 ID 목록 반환",
450
+ "clearSelection() — 선택 초기화",
451
+ "destroy()",
452
+ "Draggable.destroyAll()"
453
+ ],
454
+ "example": "const tbody = document.getElementById('main-tbody');\nconst draggable = new window.ncua.Draggable(tbody, {\n selectAllEl: document.getElementById('select-all'),\n moveButtons: {\n bottom: document.getElementById('btn-move-bottom'),\n down: document.getElementById('btn-move-down'),\n up: document.getElementById('btn-move-up'),\n top: document.getElementById('btn-move-top'),\n },\n onDrop: function(fromId, toId, edge, selectedIds) {\n // 페이지에서 DOM 행 이동 처리\n const rows = Array.from(tbody.querySelectorAll('tr[data-draggable-id]'));\n const toRow = rows.find(function(r) { return r.dataset.draggableId === toId; });\n const anchor = edge === 'bottom' ? toRow.nextSibling || null : toRow;\n if (selectedIds.length > 1) {\n const idSet = new Set(selectedIds);\n rows.filter(function(r) { return idSet.has(r.dataset.draggableId); })\n .forEach(function(r) { tbody.insertBefore(r, anchor); });\n } else {\n const fromRow = rows.find(function(r) { return r.dataset.draggableId === fromId; });\n if (fromRow) tbody.insertBefore(fromRow, anchor);\n }\n },\n onMove: function(direction, selectedIds) {\n // DOM 이동은 Draggable 내부에서 완료됨 — 서버 API 호출 등 비즈니스 로직만 작성\n console.log('moved', direction, selectedIds);\n },\n onSelect: function(selectedIds) {\n document.getElementById('selection-count').textContent =\n selectedIds.length > 0 ? selectedIds.length + '개 선택됨' : '선택 없음';\n },\n});\n\n// 드래그 비활성화 (체크박스·이동버튼은 유지)\ndraggable.setDraggable(false);\n\n// 이동 버튼 비활성화 (드래그·체크박스는 유지)\ndraggable.setMoveButtonsEnabled(false);\n\n// 해제\ndraggable.destroy();",
455
+ "cdnPattern": "D"
431
456
  }
432
457
  }
@@ -4,6 +4,7 @@
4
4
  "You MUST generate component HTML using the render_to_html tool. Never write HTML/CSS manually.",
5
5
  "Use the HTML returned by render_to_html as-is. Do NOT modify class names, structure, or attributes.",
6
6
  "MANDATORY in Phase 1: call list_composition_overrides before any render_to_html call. Components with coverageScore >= 0.7 have canonical examples — read them and use the matching scenario. Skipping this call is the leading cause of page-prefix BEM mimicry and placeholder-only output.",
7
+ "Canonical output verification: for components with coverageScore >= 0.7, call render_to_html with the canonical example props (including children) from list_composition_overrides BEFORE composing the final page. Inspect the returned HTML to learn the actual DOM structure, class names, and attributes. Use this real output as ground truth — never assume or guess component HTML from memory or training data.",
7
8
  "Use search_component to find the right component by Korean/English keywords before building any UI.",
8
9
  "Use list_components to browse available components by category and choose the appropriate one.",
9
10
  "When building a page with multiple components, use render_to_html_batch to render them all in one call instead of calling render_to_html repeatedly.",
@@ -2,7 +2,7 @@
2
2
  "_comment": "Composition Overrides — extract-mcp-data.ts 산출물(data/*.json) 위에 병합되는 추가 메타. 컴포넌트 디렉토리 무수정 원칙을 지키면서 P1-4(allowedChildren/Parents), P1-6(canonicalExample), P0-3c(bemClassesExtra) 를 한곳에서 관리.",
3
3
  "data-grid": {
4
4
  "_note": "P5/P10: DataGrid 는 compound — DataGrid.Table(필수) + DataGrid.Summary / DataGrid.Pagination / DataGrid.SearchFilter / DataGrid.FilterBar / DataGrid.ActionBar 자식. 검색결과 목록 화면 표준. forbiddenInContext: DataGrid.Table 안에 Table.Pagination 금지 — DataGrid.Pagination 슬롯 사용.",
5
- "descriptionExtra": "[중요] 검색결과·관리자 목록 화면 표준 컴포넌트. DataGrid.Table(필수, NCDS Table 자동 감싸기) + Summary(건수 표시) + ActionBar(상단 일괄 액션) + Pagination(하단). DataGrid 안에서 Table 의 Pagination 슬롯은 사용 금지 — DataGrid.Pagination 으로 분리. variant: search-result(검색결과 페이지 표준) / simple(단순 목록) / readonly(편집 불가). Storybook DataGrid SearchResultDemo 의 children 트리와 동일 구조 유지.",
5
+ "descriptionExtra": "[중요] 검색결과·관리자 목록 화면 표준 컴포넌트. DataGrid.Table(필수, NCDS Table 자동 감싸기) + Summary(건수 표시) + ActionBar(상단 일괄 액션) + Pagination(하단). DataGrid 안에서 Table 의 Pagination 슬롯은 사용 금지 — DataGrid.Pagination 으로 분리. variant: search-result(검색결과 페이지 표준) / simple(단순 목록) / readonly(편집 불가). Storybook DataGrid SearchResultDemo 의 children 트리와 동일 구조 유지. ⚠️ CDN HTML 모드 전용 — DataGrid 안에 selectable Table이 있을 때 2가지를 반드시 수동 추가해야 한다: (1) ncua-table div에 ncua-table--in-data-grid 클래스 추가 — React는 DataGrid.Table이 자동으로 추가하지만 CDN 모드는 수동이다. 누락 시 DataGrid wrapper의 border-left 1px와 selectable의 box-shadow inset 1px이 겹쳐 좌측 2px 이중 테두리가 된다. (2) ncua-data-grid__table-wrapper의 마지막 자식으로 <div class='ncua-data-grid__action-bar ncua-data-grid__action-bar--bottom'></div> 추가 — React DataGrid는 selectable=true일 때 이 빈 div를 자동 생성하여 하단 여백/구분선을 처리한다. 누락 시 테이블 하단 여백이 사라진다. ⚠️ CDN DnD(draggable) 조합 시 추가 규칙: ncua-table div에 ncua-table--draggable 클래스 추가. ncua-table-wrapper에 --ncua-table-header-height: 40px CSS 변수 설정. 이동 버튼이 필요하면 ncua-data-grid__action-bar 안에 ncua-button-group--xs 패턴으로 배치. canonicalExamples.dnd-with-selectable 시나리오 참고.",
6
6
  "aliasesExtra": [
7
7
  "데이터 그리드",
8
8
  "데이터그리드",
@@ -797,6 +797,21 @@
797
797
  ]
798
798
  }
799
799
  },
800
+ "dnd-with-selectable": {
801
+ "description": "DataGrid + 체크박스 선택 + 행 드래그앤드롭 + 이동 버튼 조합. CDN HTML 모드 전용 완전 구조. ⚠️ 필수 규칙 — 아래 5가지를 모두 지켜야 올바른 렌더링이 보장된다: (1) ncua-table div에 ncua-table--selectable ncua-table--draggable ncua-table--in-data-grid 3개 클래스 모두 포함 — ncua-table--in-data-grid 누락 시 좌측 2px 이중 테두리 발생. (2) ncua-table-wrapper style에 --ncua-table-header-height:40px CSS 변수 설정 — 드래그 미리보기 오프셋 계산에 사용. (3) 이동 버튼(ButtonGroup)은 ncua-data-grid__action-bar 안에 배치, disabled 상태로 초기화. (4) ncua-data-grid__table-wrapper 마지막 자식으로 빈 ncua-data-grid__action-bar--bottom div 추가 — 누락 시 테이블 하단 여백 소멸. (5) CDN JS: const d = new window.ncua.Draggable(tbody, { onDrop, onSelect, onMove }); d.setMoveButtonsEnabled(true) 로 이동 버튼 연결. onDrop: 페이지가 서버 API 처리. onMove: Draggable이 DOM 이동 완료 후 호출하는 알림 — 페이지에서 DOM 조작 금지.",
802
+ "_htmlStructure": {
803
+ "_note": "CDN HTML 모드 전용 구조. React 모드에서는 DataGrid.Table + Table.DragHeaderCell + Table.DragCell + selectable 조합으로 대체.",
804
+ "outerStructure": "<div class='ncua-data-grid'><div class='ncua-data-grid__table-wrapper'>[action-bar--top][ncua-data-grid__table][action-bar--bottom(빈 div)]</div></div>",
805
+ "tableDivClasses": "ncua-table ncua-table--horizontal ncua-table--hoverable ncua-table--selectable ncua-table--draggable ncua-table--in-data-grid",
806
+ "tableWrapperStyle": "--ncua-table-header-height:40px",
807
+ "actionBarTop": "<div class='ncua-data-grid__action-bar ncua-data-grid__action-bar--top'><div class='ncua-button-group ncua-button-group--xs has-border'><button class='ncua-button-group__item is-only-icon' id='btn-move-bottom' disabled><!-- 맨아래 SVG --></button><button class='ncua-button-group__item is-only-icon' id='btn-move-down' disabled><!-- 아래 SVG --></button><button class='ncua-button-group__item is-only-icon' id='btn-move-up' disabled><!-- 위 SVG --></button><button class='ncua-button-group__item is-only-icon' id='btn-move-top' disabled><!-- 맨위 SVG --></button></div></div>",
808
+ "actionBarBottom": "<div class='ncua-data-grid__action-bar ncua-data-grid__action-bar--bottom'></div>",
809
+ "dragHeaderCell": "<th class='ncua-table__header-cell ncua-table__drag-header-cell'><span class='ncua-table__drag-cell-inner'><span class='ncua-table__drag-header-icon'><!-- 6점 grip SVG --></span><span class='ncua-checkbox-input ncua-checkbox-input--sm'><input id='select-all' type='checkbox' aria-label='전체 선택'></span></span></th>",
810
+ "dragDataCell": "<td class='ncua-table__drag-cell'><span class='ncua-table__drag-cell-inner'><button class='ncua-table__drag-handle' type='button' aria-label='행 순서 변경'><!-- 6점 grip SVG --></button><span class='ncua-checkbox-input ncua-checkbox-input--sm'><input type='checkbox' aria-label='행 선택'></span></span></td>",
811
+ "trAttr": "data-draggable-id='row-1'",
812
+ "cdnInit": "const tbody = document.querySelector('tbody'); const d = new window.ncua.Draggable(tbody, { onDrop: (fromId, toId, edge) => { /* 서버 API */ }, onSelect: (ids) => { /* 선택 변경 */ }, onMove: (dir, ids) => { /* 알림만 — DOM 이동은 Draggable 내부 처리 */ } }); d.setMoveButtonsEnabled(true);"
813
+ }
814
+ },
800
815
  "search-result-readonly": {
801
816
  "description": "검색결과 + 선택 없음(체크박스 없음, ActionBar 없음). Summary + Table + Pagination 만. 단순 조회 화면.",
802
817
  "props": {
@@ -879,7 +894,7 @@
879
894
  },
880
895
  "table": {
881
896
  "_note_required": "P7: ncua-table__required 는 NCDS Table SCSS([_table.scss:619]) 와 VanillaJS classNames 상수([classNames.ts:26])에는 정의되어 있으나 React Table.tsx 본체가 사용하지 않아 자동 추출되지 않음. Vertical Table 의 필수 라벨에 `<span class='ncua-table__required'>*</span>` 형태로 라벨 텍스트 앞에 prepend 하여 사용. validate_html 화이트리스트 통과를 위해 bemClassesExtra 에 명시 등록.",
882
- "descriptionExtra": "[중요] table 컴포넌트는 두 가지 모드 지원: (1) type=horizontal — 데이터 그리드(thead+tbody, 컬럼별 헤더). (2) type=vertical — 폼 레이아웃(tbody only, 좌측 라벨-우측 값 2-cell row). 폼의 라벨-입력 쌍 구성 시 vertical 모드 사용 — 절대 BEM 수동 작성 금지. canonicalExamples 의 'vertical-form-label' / 'vertical-form-label-with-required' 시나리오 참고. ⚠️ <th class='ncua-table__header-cell'> 은 horizontal 컬럼 헤더 전용 — vertical 라벨에는 <th scope='row' class='ncua-table__cell'> 사용 (Table.Cell with isHeader=true). ⚠️ 테이블 스크롤(가로/세로)은 반드시 Table 컴포넌트의 내장 props 로만 처리 — 커스텀 scroll wrapper(예: pbp-table-scroll, page-table__scroll, sgr-table-scroll 등 프로젝트 prefix div) 절대 생성 금지. 올바른 스크롤 설정: horizontalScroll=true(가로 스크롤 활성화 → ncua-table__h-scroll-container 구조 자동 생성), fixedHeader=true(헤더 고정 → ncua-table--fixed-header), maxHeight=340(세로 스크롤 최대 높이 → ncua-table__scroll-container max-height), minWidth=1140(전체 테이블 최소 너비 → --ncua-table-min-width CSS 변수 자동 주입). Table.ColGroup의 widths/minWidths 배열로 각 컬럼 너비 지정. canonicalExamples 의 'horizontal-with-scroll' 시나리오 참고. ⚠️ ncua-table 외곽 wrapper div에 border·overflow:hidden 절대 적용 금지 — (1) border: ncua-table이 자체 border: 1px solid var(--gray-100)를 보유하므로 외부에 border를 추가하면 2px 이중 테두리가 된다. (2) overflow:hidden: 테이블 셀 안의 SelectBox·DatePicker·Tooltip 등 드롭다운이 컨테이너 경계에서 잘린다. 테이블을 감싸는 div가 필요한 경우 아무 CSS 없이 빈 wrapper만 사용할 것.",
897
+ "descriptionExtra": "[중요] table 컴포넌트는 두 가지 모드 지원: (1) type=horizontal — 데이터 그리드(thead+tbody, 컬럼별 헤더). (2) type=vertical — 폼 레이아웃(tbody only, 좌측 라벨-우측 값 2-cell row). 폼의 라벨-입력 쌍 구성 시 vertical 모드 사용 — 절대 BEM 수동 작성 금지. canonicalExamples 의 'vertical-form-label' / 'vertical-form-label-with-required' 시나리오 참고. ⚠️ <th class='ncua-table__header-cell'> 은 horizontal 컬럼 헤더 전용 — vertical 라벨에는 <th scope='row' class='ncua-table__cell'> 사용 (Table.Cell with isHeader=true). ⚠️ 테이블 스크롤(가로/세로)은 반드시 Table 컴포넌트의 내장 props 로만 처리 — 커스텀 scroll wrapper(예: pbp-table-scroll, page-table__scroll, sgr-table-scroll 등 프로젝트 prefix div) 절대 생성 금지. 올바른 스크롤 설정: horizontalScroll=true(가로 스크롤 활성화 → ncua-table__h-scroll-container 구조 자동 생성), fixedHeader=true(헤더 고정 → ncua-table--fixed-header), maxHeight=340(세로 스크롤 최대 높이 → ncua-table__scroll-container max-height), minWidth=1140(전체 테이블 최소 너비 → --ncua-table-min-width CSS 변수 자동 주입). Table.ColGroup의 widths/minWidths 배열로 각 컬럼 너비 지정. canonicalExamples 의 'horizontal-with-scroll' 시나리오 참고. ⚠️ ncua-table 외곽 wrapper div에 border·overflow:hidden 절대 적용 금지 — (1) border: ncua-table이 자체 border: 1px solid var(--gray-100)를 보유하므로 외부에 border를 추가하면 2px 이중 테두리가 된다. (2) overflow:hidden: 테이블 셀 안의 SelectBox·DatePicker·Tooltip 등 드롭다운이 컨테이너 경계에서 잘린다. 테이블을 감싸는 div가 필요한 경우 아무 CSS 없이 빈 wrapper만 사용할 것. ⚠️ ncua-table--in-data-grid (CDN HTML 모드 전용 수동 추가): selectable=true 테이블이 DataGrid(ncua-data-grid__table-wrapper) 안에 있을 때, React는 DataGrid.Table이 자동으로 ncua-table--in-data-grid 클래스를 추가해 첫 번째 열의 inset shadow(box-shadow: inset 1px 0 0 0 var(--gray-100))를 제거한다. CDN HTML 모드에서는 ncua-table div에 직접 ncua-table--in-data-grid 클래스를 추가해야 한다 — 누락 시 DataGrid wrapper의 border-left 1px와 selectable의 inset box-shadow 1px이 시각적으로 겹쳐 좌측 2px 이중 테두리가 된다.",
883
898
  "aliasesExtra": [
884
899
  "vertical-table",
885
900
  "form-table",
@@ -1176,6 +1191,58 @@
1176
1191
  ]
1177
1192
  }
1178
1193
  },
1194
+ "dnd-row-sorting": {
1195
+ "description": "Horizontal Table — 행 드래그앤드롭 정렬. [React 모드] draggable=true + Table.DragHeaderCell(헤더) + Table.DragCell(각 행) + Table.Row.dragId(문자열 식별자) + Table.Body.onRowDrop(콜백). 반드시 type='horizontal'. DragCell에는 CheckboxInput 등 공동 배치 가능. useState로 rows 배열을 관리하고 onRowDrop에서 배열 재정렬: const [rows, setRows] = useState(initialRows); const handleDrop = (fromId, toId, edge) => { setRows(prev => { const from = prev.findIndex(r => r.id === fromId); const to = prev.findIndex(r => r.id === toId); const next = [...prev]; const [item] = next.splice(from, 1); const insertAt = edge === 'top' ? to : to + 1; next.splice(insertAt > from ? insertAt - 1 : insertAt, 0, item); return next; }); }; [CDN 모드] React DnD props(draggable/dragId/onRowDrop) 대신 window.ncua.Draggable 사용. 각 <tr>에 data-draggable-id 속성 필수. CDN 초기화: const sortable = new window.ncua.Draggable(tbodyEl, { onDrop: function(fromId, toId, edge) { /* 서버 API 호출 — DOM 재정렬은 Draggable이 완료 후 호출 */ }, onSelect: function(selectedIds) { /* 선택 상태 변경 알림 */ }, onMove: function(direction, selectedIds) { /* 이동 버튼 클릭 알림 — DOM 이동은 Draggable 내부 처리 후 호출 */ } }); ⚠️ onDrop: 드래그 종료 시 페이지가 서버 API 등 비즈니스 로직 처리. ⚠️ onMove: 이동 버튼 클릭 시 Draggable이 DOM 행 이동을 완료한 후 호출되는 알림 — 페이지에서 DOM 재정렬 코드를 작성하지 말 것. ⚠️ DataGrid 안에 배치 시 CDN HTML 모드 필수 조건: (1) ncua-table div에 ncua-table--in-data-grid 클래스 추가(누락 시 2px 이중 테두리), (2) ncua-table-wrapper에 --ncua-table-header-height: 40px CSS 변수 설정. 이동 버튼 그룹은 ncua-data-grid__action-bar 안에 ncua-button-group--xs 패턴으로 배치 + Draggable.setMoveButtonsEnabled(true)로 활성화. data-grid.canonicalExamples.dnd-with-selectable 시나리오 참고.",
1196
+ "props": {
1197
+ "type": "horizontal",
1198
+ "draggable": true,
1199
+ "hoverable": true,
1200
+ "children": [
1201
+ {
1202
+ "component": "table.header",
1203
+ "children": [
1204
+ {
1205
+ "component": "table.row",
1206
+ "children": [
1207
+ { "component": "table.drag-header-cell" },
1208
+ { "component": "table.header-cell", "children": ["상품명"] },
1209
+ { "component": "table.header-cell", "children": ["카테고리"] },
1210
+ { "component": "table.header-cell", "children": ["순서"] }
1211
+ ]
1212
+ }
1213
+ ]
1214
+ },
1215
+ {
1216
+ "component": "table.body",
1217
+ "props": {
1218
+ "onRowDrop": "handleDrop"
1219
+ },
1220
+ "children": [
1221
+ {
1222
+ "component": "table.row",
1223
+ "props": { "dragId": "row-1" },
1224
+ "children": [
1225
+ { "component": "table.drag-cell" },
1226
+ { "component": "table.cell", "children": ["프리미엄 노트북"] },
1227
+ { "component": "table.cell", "children": ["전자기기"] },
1228
+ { "component": "table.cell", "children": ["1"] }
1229
+ ]
1230
+ },
1231
+ {
1232
+ "component": "table.row",
1233
+ "props": { "dragId": "row-2" },
1234
+ "children": [
1235
+ { "component": "table.drag-cell" },
1236
+ { "component": "table.cell", "children": ["무선 마우스"] },
1237
+ { "component": "table.cell", "children": ["주변기기"] },
1238
+ { "component": "table.cell", "children": ["2"] }
1239
+ ]
1240
+ }
1241
+ ]
1242
+ }
1243
+ ]
1244
+ }
1245
+ },
1179
1246
  "vertical-form-label": {
1180
1247
  "description": "Vertical Table — 폼 라벨 레이아웃 (필수 마크 없는 버전). 각 row 의 좌측 라벨 셀은 Table.Cell with isHeader=true (출력: <th scope='row' class='ncua-table__cell'>). Table.HeaderCell 사용 금지 — 그 클래스는 Horizontal Table 의 thead 컬럼 헤더 전용. thead 없이 tbody 만 사용하며, 각 row 는 [라벨 cell(isHeader), 값 cell] 의 2-cell 구조. 필수 라벨 표기가 필요하면 'vertical-form-label-with-required', 라벨 옆 안내 tooltip 이 필요하면 'vertical-form-label-with-tooltip' 시나리오 사용.",
1181
1248
  "props": {
@@ -2359,7 +2426,7 @@
2359
2426
  },
2360
2427
  "switch": {
2361
2428
  "_note": "P22: Switch 는 좌/우 두 옵션 토글 컴포넌트 (Apple segment-control 패턴). left/right props 로 두 옵션 정의, value 로 현재 선택. 사이즈: xxs/xs/sm/md. ⚠️ godomall5 페이지에서 'CDN 미제공 active/inactive 스타일' 이라 단언하고 fallback CSS 직접 작성한 hallucination 발생 — NCDS CDN CSS 가 .ncua-switch__option--active / --inactive 등 모든 modifier 완비. 반드시 render_to_html('switch', {left, right, value, size}) 호출해 정확한 BEM 출력 사용. ncua-switch__option--active 를 페이지 SCSS 에서 override 하지 말 것.",
2362
- "descriptionExtra": "[중요] 두 선택지 토글 컴포넌트 (좌/우 영역 선택). left/right props (각각 { id, label }) + value(선택된 id) + size(xxs/xs/sm/md) + disabled. 출력 BEM: ncua-switch + --{size} modifier + ncua-switch__option + --active/--inactive/--left/--right. ⚠️ 페이지 SCSS 에서 .ncua-switch__option--active 등 override CSS 작성 금지 — CDN CSS 가 완비. 발명 modifier(--toggled / --selected) 사용 금지.",
2429
+ "descriptionExtra": "[중요] 두 선택지 토글 컴포넌트 (좌/우 영역 선택). left/right props (각각 { value, label }) + value(선택된 value) + size(xxs/xs/sm/md) + disabled. ⚠️ Switch HTML 은 반드시 render_to_html('switch', props) 로만 생성 수동 HTML 작성 시 ncua-switch__radio 클래스 누락으로 native radio 버튼이 그대로 노출됨. 페이지 SCSS 에서 .ncua-switch__option--active 등 override CSS 작성 금지 — CDN CSS 가 완비. 발명 modifier(--toggled / --selected) 사용 금지.",
2363
2430
  "aliasesExtra": [
2364
2431
  "스위치",
2365
2432
  "토글",
@@ -2380,15 +2447,15 @@
2380
2447
  "bemClassesExtra": [],
2381
2448
  "canonicalExamples": {
2382
2449
  "default": {
2383
- "description": "P22: 기본 토글 스위치 (좌/우 두 옵션). value 로 현재 선택, size='md' 기본. left/right 의 id 는 value 와 매칭.",
2450
+ "description": "P22: 기본 토글 스위치 (좌/우 두 옵션). value 로 현재 선택, size='md' 기본. left/right 의 value최상위 value 와 매칭.",
2384
2451
  "props": {
2385
2452
  "size": "md",
2386
2453
  "left": {
2387
- "id": "list",
2454
+ "value": "list",
2388
2455
  "label": "목록"
2389
2456
  },
2390
2457
  "right": {
2391
- "id": "grid",
2458
+ "value": "grid",
2392
2459
  "label": "그리드"
2393
2460
  },
2394
2461
  "value": "list"
@@ -2399,11 +2466,11 @@
2399
2466
  "props": {
2400
2467
  "size": "sm",
2401
2468
  "left": {
2402
- "id": "active",
2469
+ "value": "active",
2403
2470
  "label": "활성"
2404
2471
  },
2405
2472
  "right": {
2406
- "id": "inactive",
2473
+ "value": "inactive",
2407
2474
  "label": "비활성"
2408
2475
  },
2409
2476
  "value": "active"
@@ -2414,11 +2481,11 @@
2414
2481
  "props": {
2415
2482
  "size": "md",
2416
2483
  "left": {
2417
- "id": "yes",
2484
+ "value": "yes",
2418
2485
  "label": "사용함"
2419
2486
  },
2420
2487
  "right": {
2421
- "id": "no",
2488
+ "value": "no",
2422
2489
  "label": "사용 안 함"
2423
2490
  },
2424
2491
  "value": "yes",
@@ -445,9 +445,30 @@ const buildPatternC = (id, className, options) => [
445
445
  ' });',
446
446
  '</script>',
447
447
  ].join('\n');
448
+ /**
449
+ * 패턴 D: React 렌더 HTML을 wrapper로 감싼 뒤 Draggable init 스크립트를 append.
450
+ * 패턴 A/B/C와 달리 React 렌더링을 대체하지 않고 보완한다.
451
+ * wrapper div가 tbody를 찾아 window.ncua.Draggable을 초기화한다.
452
+ */
453
+ const buildPatternD = (id, reactHtml, className) => [
454
+ `<div id="${id}">`,
455
+ reactHtml,
456
+ '<script>',
457
+ " document.addEventListener('DOMContentLoaded', function () {",
458
+ ` const tbody = document.querySelector('#${id} tbody');`,
459
+ ` if (tbody) new window.ncua.${className}(tbody, {`,
460
+ ' onDrop: function(fromId, toId, edge) {',
461
+ ' // TODO: 순서 업데이트 처리 (fromId 행을 toId 행의 edge 방향으로 이동)',
462
+ ' }',
463
+ ' });',
464
+ ' });',
465
+ '</script>',
466
+ '</div>',
467
+ ].join('\n');
448
468
  /**
449
469
  * CDN init 스크립트 생성. 패턴은 `jsApi.cdnPattern` (A/B/C)으로 결정한다.
450
470
  * 호출자는 `cdnPattern` 부재 시 isCdnInput=false로 분기하여 React renderToStaticMarkup 경로를 사용한다.
471
+ * 패턴 D는 React 렌더 완료 후 buildPatternD를 별도 호출한다.
451
472
  */
452
473
  const buildCdnInitScript = (pattern, componentName, instanceId, className, options) => {
453
474
  const id = `ncua-${componentName}-${instanceId}`;
@@ -808,13 +829,21 @@ const renderToHtml = (params) => {
808
829
  }
809
830
  }
810
831
  }
811
- // HTML 출력: CDN-input은 init script, 그 외는 React 정적 HTML (renderToStaticMarkup은 필요할 때만 호출)
812
- const cdnInitScript = isCdnInput && jsApi?.cdnPattern
832
+ // HTML 출력: 패턴별 분기
833
+ // - 패턴 A/B/C: CDN init 스크립트만 (renderToStaticMarkup 호출 안 함)
834
+ // - 패턴 D: React 정적 HTML + Draggable init 스크립트
835
+ // - cdnPattern 없음: React 정적 HTML
836
+ // renderToStaticMarkup은 필요한 분기(D, 패턴 없음)에서만 호출한다.
837
+ const isPatternD = jsApi?.cdnPattern === 'D';
838
+ const cdnInitScript = isCdnInput && jsApi?.cdnPattern && !isPatternD
813
839
  ? buildCdnInitScript(jsApi.cdnPattern, normalized, instanceId, jsApi.className, removeBlockedProps(mergedCdnProps))
814
840
  : null;
841
+ const buildReactHtml = () => `<!-- ncua:${normalized} start -->\n${reactRuntime.renderToStaticMarkup(reactRuntime.createElement(Component, resolvedProps))}\n<!-- ncua:${normalized} end -->`;
815
842
  const html = cdnInitScript !== null
816
843
  ? cdnInitScript
817
- : `<!-- ncua:${normalized} start -->\n${reactRuntime.renderToStaticMarkup(reactRuntime.createElement(Component, resolvedProps))}\n<!-- ncua:${normalized} end -->`;
844
+ : isPatternD && jsApi
845
+ ? buildPatternD(`ncua-${normalized}-${instanceId}`, buildReactHtml(), jsApi.className)
846
+ : buildReactHtml();
818
847
  // Story 5.1-8: jsRequired 컴포넌트는 React data 의 default 무시 (vanilla 정의에 없는 React-only default 누수 차단).
819
848
  // React 컴포넌트는 기존 동작 유지.
820
849
  const isJsRequired = componentData.jsRequired === true;
package/bin/types.d.ts CHANGED
@@ -124,9 +124,10 @@ export interface JsApiInfo {
124
124
  * - A: `new ncua.X(element, options)` — element가 곧 컨테이너
125
125
  * - B: `new ncua.X(options)` — options.container로 자동 append
126
126
  * - C: `new ncua.X(options)` — getElement() + 수동 append
127
+ * - D: React 렌더 HTML을 `<div id>` 래퍼로 감싸고, tbody 타겟 Draggable init `<script>` 를 append. Draggable 전용.
127
128
  * 부재 시 React 정적 HTML 경로로 fallback.
128
129
  */
129
- cdnPattern?: 'A' | 'B' | 'C';
130
+ cdnPattern?: 'A' | 'B' | 'C' | 'D';
130
131
  }
131
132
  export interface ComplianceRuleHint {
132
133
  attr: string;
@@ -59,6 +59,7 @@ const setupDomEnvironment = () => {
59
59
  'Node',
60
60
  'Event',
61
61
  'CustomEvent',
62
+ 'Image',
62
63
  ]) {
63
64
  if (typeof globalThis[key] === 'undefined') {
64
65
  globalThis[key] = dom.window[key];
package/bin/version.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * scripts/generate-version-ts.ts 가 ui-admin/package.json 의 version 으로부터 자동 생성한다.
4
4
  * 수동 편집 금지 — 빌드(`pnpm generate:version-ts` 또는 `pnpm build`) 시 덮어쓰여진다.
5
5
  */
6
- export declare const VERSION = "1.8.9";
6
+ export declare const VERSION = "1.8.12";
package/bin/version.js CHANGED
@@ -6,4 +6,4 @@ exports.VERSION = void 0;
6
6
  * scripts/generate-version-ts.ts 가 ui-admin/package.json 의 version 으로부터 자동 생성한다.
7
7
  * 수동 편집 금지 — 빌드(`pnpm generate:version-ts` 또는 `pnpm build`) 시 덮어쓰여진다.
8
8
  */
9
- exports.VERSION = '1.8.9';
9
+ exports.VERSION = '1.8.12';
package/data/_icons.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "totalCount": 2542,
2
+ "totalCount": 2547,
3
3
  "fillCount": 1198,
4
4
  "icons": [
5
5
  {
@@ -3292,6 +3292,21 @@
3292
3292
  "kebab": "color-file-html-solid",
3293
3293
  "fill": false
3294
3294
  },
3295
+ {
3296
+ "name": "ColorFileHwpDefault",
3297
+ "kebab": "color-file-hwp-default",
3298
+ "fill": false
3299
+ },
3300
+ {
3301
+ "name": "ColorFileHwpGray",
3302
+ "kebab": "color-file-hwp-gray",
3303
+ "fill": false
3304
+ },
3305
+ {
3306
+ "name": "ColorFileHwpSolid",
3307
+ "kebab": "color-file-hwp-solid",
3308
+ "fill": false
3309
+ },
3295
3310
  {
3296
3311
  "name": "ColorFileImageDefault",
3297
3312
  "kebab": "color-file-image-default",
@@ -8152,6 +8167,11 @@
8152
8167
  "kebab": "maximize02-fill",
8153
8168
  "fill": true
8154
8169
  },
8170
+ {
8171
+ "name": "Maximize03",
8172
+ "kebab": "maximize03",
8173
+ "fill": false
8174
+ },
8155
8175
  {
8156
8176
  "name": "MedicalCircle",
8157
8177
  "kebab": "medical-circle",
@@ -8812,6 +8832,11 @@
8812
8832
  "kebab": "navigation-pointer-off02-fill",
8813
8833
  "fill": true
8814
8834
  },
8835
+ {
8836
+ "name": "New",
8837
+ "kebab": "new",
8838
+ "fill": false
8839
+ },
8815
8840
  {
8816
8841
  "name": "NotificationBox",
8817
8842
  "kebab": "notification-box",
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "context-tab",
3
+ "exportName": "ContextTab",
4
+ "importPath": "@ncds/ui-admin",
5
+ "jsRequired": true,
6
+ "category": "navigation",
7
+ "description": "ContextTab은 페이지 전체의 작업 컨텍스트(scope)를 전환하는 가로 탭 컴포넌트입니다. 멀티 테넌트 어드민(다중 쇼핑몰·계정·워크스페이스 등)에서 좌측 ☰ 전체 목록 Dropdown과 우측 ← → Slider Navigation을 항상 통합 제공합니다.",
8
+ "aliases": [
9
+ "ContextTab",
10
+ "컨텍스트탭",
11
+ "컨텍스트 탭",
12
+ "다중쇼핑몰",
13
+ "쇼핑몰전환",
14
+ "워크스페이스전환",
15
+ "scope",
16
+ "1depth",
17
+ "Dropdown",
18
+ "Slider",
19
+ "multi-tenant",
20
+ "네비게이션",
21
+ "NCDS"
22
+ ],
23
+ "hasChildren": false,
24
+ "whenToUse": [
25
+ "페이지 전체 데이터/설정의 적용 대상(쇼핑몰·계정·워크스페이스 등)을 전환",
26
+ "다중 컨텍스트(2개 이상, 실 사용 무제한)를 한 탭 바에서 노출",
27
+ "컨텍스트 전환 시 페이지 데이터 재로드를 동반",
28
+ "모든 페이지의 PageTitle 외부 직하단 같은 자리에 고정 배치"
29
+ ],
30
+ "forbiddenRules": [
31
+ "PageTitle 카드 내부 배치 금지",
32
+ "블록 타이틀 하단·블록 내부 배치 금지(콘텐츠 영역 탭은 HorizontalTab 사용)",
33
+ "Size 변경 금지(md 단일)",
34
+ "Dropdown 또는 Slider Navigation 생략 금지",
35
+ "한 페이지에 ContextTab 복수 노출 금지",
36
+ "1개 컨텍스트 단독 노출 금지(PageTitle Title 영역에 컨텍스트명 표기로 대체)",
37
+ "콘텐츠 변경 없이 탭만 전환 금지(데이터 재로드 동반)",
38
+ "Dropdown Active 마커에 dot 형태 또는 Red 사용 금지(✓ Check icon 단일)",
39
+ "콘텐츠 영역 전환에 ContextTab 사용 금지(HorizontalTab 사용)",
40
+ "탭 높이 변형 금지(36px 고정)",
41
+ "활성 탭에 테두리 추가 금지(Shadow/sm 단독 raised)",
42
+ "Dropdown panel 헤더 영역 추가 금지"
43
+ ],
44
+ "seeAlso": [
45
+ "horizontal-tab",
46
+ "vertical-tab",
47
+ "badge"
48
+ ],
49
+ "props": {
50
+ "activeTab": {
51
+ "type": "string",
52
+ "required": false
53
+ },
54
+ "className": {
55
+ "type": "string",
56
+ "required": false
57
+ },
58
+ "menus": {
59
+ "type": "object",
60
+ "required": false,
61
+ "rawType": "import(\"/home/runner/_work/ncds/ncds/packages/ui-admin/src/components/navigation/context-tab/ContextTab\").ContextTabItemProps[] | undefined",
62
+ "isArray": true,
63
+ "properties": {
64
+ "id": {
65
+ "type": "string",
66
+ "required": true
67
+ },
68
+ "label": {
69
+ "type": "string",
70
+ "required": true
71
+ },
72
+ "isNew": {
73
+ "type": "boolean",
74
+ "required": false
75
+ },
76
+ "badgeLabel": {
77
+ "type": "string",
78
+ "required": false
79
+ },
80
+ "disabled": {
81
+ "type": "boolean",
82
+ "required": false
83
+ }
84
+ },
85
+ "default": "[]"
86
+ },
87
+ "onTabChange": {
88
+ "type": "function",
89
+ "required": false
90
+ },
91
+ "visibleTabsCount": {
92
+ "type": "number",
93
+ "required": false,
94
+ "default": "DEFAULT_VISIBLE_TABS_COUNT"
95
+ }
96
+ },
97
+ "html": {},
98
+ "bemClasses": [
99
+ "ncua-context-tab",
100
+ "ncua-context-tab__bar",
101
+ "ncua-context-tab__control ncua-context-tab__control--menu",
102
+ "ncua-context-tab__control ncua-context-tab__control--next",
103
+ "ncua-context-tab__control ncua-context-tab__control--prev",
104
+ "ncua-context-tab__nav",
105
+ "ncua-context-tab__option",
106
+ "ncua-context-tab__option-check",
107
+ "ncua-context-tab__option-label",
108
+ "ncua-context-tab__panel",
109
+ "ncua-context-tab__panel-column",
110
+ "ncua-context-tab__tab",
111
+ "ncua-context-tab__tab-label"
112
+ ],
113
+ "usage": {
114
+ "import": "import { ContextTab } from '@ncds/ui-admin';",
115
+ "react": {
116
+ "default": "<ContextTab />"
117
+ }
118
+ }
119
+ }
@@ -163,6 +163,10 @@
163
163
  "type": "string",
164
164
  "required": false
165
165
  },
166
+ "draggable": {
167
+ "type": "boolean",
168
+ "required": false
169
+ },
166
170
  "fixedHeader": {
167
171
  "type": "boolean",
168
172
  "required": false
package/data/table.json CHANGED
@@ -53,7 +53,17 @@
53
53
  "Portal",
54
54
  "Floating UI",
55
55
  "스크롤 컨테이너",
56
- "scrollbar token"
56
+ "scrollbar token",
57
+ "draggable",
58
+ "DnD",
59
+ "drag and drop",
60
+ "행 드래그",
61
+ "행 순서 변경",
62
+ "드래그앤드롭",
63
+ "Table.DragCell",
64
+ "Table.DragHeaderCell",
65
+ "dragId",
66
+ "onRowDrop"
57
67
  ],
58
68
  "hasChildren": true,
59
69
  "whenToUse": [
@@ -81,6 +91,11 @@
81
91
  "type": "ReactNode",
82
92
  "required": true
83
93
  },
94
+ "draggable": {
95
+ "type": "boolean",
96
+ "required": false,
97
+ "default": false
98
+ },
84
99
  "fixedHeader": {
85
100
  "type": "boolean",
86
101
  "required": false,
@@ -143,11 +158,19 @@
143
158
  "className": {
144
159
  "type": "string",
145
160
  "required": false
161
+ },
162
+ "onRowDrop": {
163
+ "type": "function",
164
+ "required": false
146
165
  }
147
166
  }
148
167
  },
149
168
  "Table.Row": {
150
169
  "props": {
170
+ "dragId": {
171
+ "type": "string",
172
+ "required": false
173
+ },
151
174
  "selected": {
152
175
  "type": "boolean",
153
176
  "required": false
@@ -162,6 +185,34 @@
162
185
  }
163
186
  }
164
187
  },
188
+ "Table.DragHeaderCell": {
189
+ "props": {
190
+ "children": {
191
+ "type": "ReactNode",
192
+ "required": false
193
+ },
194
+ "className": {
195
+ "type": "string",
196
+ "required": false
197
+ }
198
+ }
199
+ },
200
+ "Table.DragCell": {
201
+ "props": {
202
+ "children": {
203
+ "type": "ReactNode",
204
+ "required": false
205
+ },
206
+ "className": {
207
+ "type": "string",
208
+ "required": false
209
+ },
210
+ "disabled": {
211
+ "type": "boolean",
212
+ "required": false
213
+ }
214
+ }
215
+ },
165
216
  "Table.HeaderCell": {
166
217
  "props": {
167
218
  "minWidth": {
@@ -253,6 +304,8 @@
253
304
  "html": {},
254
305
  "bemClasses": [
255
306
  "ncua-table",
307
+ "ncua-table-",
308
+ "ncua-table--draggable",
256
309
  "ncua-table--fixed-header",
257
310
  "ncua-table--horizontal",
258
311
  "ncua-table--hoverable",
@@ -261,6 +314,11 @@
261
314
  "ncua-table-wrapper",
262
315
  "ncua-table__body",
263
316
  "ncua-table__cell",
317
+ "ncua-table__drag-cell",
318
+ "ncua-table__drag-cell-inner",
319
+ "ncua-table__drag-handle",
320
+ "ncua-table__drag-header-cell",
321
+ "ncua-table__drag-header-icon",
264
322
  "ncua-table__empty",
265
323
  "ncua-table__footer",
266
324
  "ncua-table__h-scroll-container",
@@ -274,6 +332,8 @@
274
332
  "ncua-table__header-cell-text",
275
333
  "ncua-table__pagination",
276
334
  "ncua-table__row",
335
+ "ncua-table__row--drag-over-bottom",
336
+ "ncua-table__row--drag-over-top",
277
337
  "ncua-table__row--error",
278
338
  "ncua-table__row--selected",
279
339
  "ncua-table__row--warning",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncds/ui-admin-mcp",
3
- "version": "1.0.0-alpha.22",
3
+ "version": "1.0.0-alpha.24",
4
4
  "description": "NCDS UI Admin MCP 서버 — AI 에이전트가 NCUA 컴포넌트를 조회하고 HTML을 검증할 수 있는 MCP 서버",
5
5
  "bin": {
6
6
  "ncua-mcp": "./bin/server.mjs"
@@ -44,7 +44,7 @@
44
44
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
45
45
  "@atlaskit/pragmatic-drag-and-drop-react-accessibility": "1.1.4",
46
46
  "@modelcontextprotocol/sdk": "^1.27.1",
47
- "@ncds/ui-admin-icon": "0.1.9",
47
+ "@ncds/ui-admin-icon": "0.1.12",
48
48
  "classnames": "2.5.1",
49
49
  "dompurify": "3.4.1",
50
50
  "flatpickr": "4.6.13",