@ncds/ui-admin-mcp 1.0.0-alpha.21 → 1.0.0-alpha.23

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.
@@ -31,16 +31,19 @@ You are an agent that builds UI using NCUA (NCDS UI Admin) design system compone
31
31
  ### Step 1: Component Discovery
32
32
 
33
33
  - Call **list_components** first to see all available components and their categories. This prevents reinventing components that already exist.
34
- - Use **search_component** with Korean/English keywords to find the right component
34
+ - Call **list_composition_overrides** to see which components have canonical examples and composition constraints. Components with `coverageScore 0.7` have ready-to-use scenarios always check these before rendering. This call is MANDATORY before Step 3.
35
+ - Use **search_component** with Korean/English keywords to find the right component.
35
36
  - Example: "비밀번호" → password-input, "모달" → modal
37
+ - If icons are needed, use **search_icon** or **list_icons** at this stage — NEVER write SVG manually.
36
38
  - **IMPORTANT**: input is for plain text only. For passwords, always use password-input.
37
39
 
38
- ### Step 2: Props Check
40
+ ### Step 2: Props & Token Check
39
41
 
40
42
  - Use **get_component_props** to see available properties BEFORE calling render_to_html.
41
43
  - Pass all required props. Choose enum values only from the allowed list.
42
44
  - Check `type: "object"` props carefully — they often expect arrays or structured objects. Read the `rawType` field for the actual TypeScript type.
43
45
  - For icon props (`leadingIcon`, `trailingIcon`, `icon`), pass `{ type: "icon", icon: "IconName" }` where IconName is a PascalCase name from search_icon.
46
+ - If the page has custom areas (non-NCUA elements), call **get_design_tokens** NOW — before rendering — to get available tokens for colors, spacing, and typography. Do NOT hardcode hex/px values.
44
47
 
45
48
  ### Step 3: HTML Generation
46
49
 
@@ -83,7 +86,7 @@ Example: DatePicker with JS initialization:
83
86
 
84
87
  - Combine render_to_html results to build the final page
85
88
  - Do NOT modify NCUA component styles
86
- - Apply spacing between components using NCUA design tokens: call **get_design_tokens** with category "spacing" to get available spacing values (e.g. var(--spacing-4), var(--spacing-8))
89
+ - Apply spacing between components using NCUA design tokens fetched in Step 2 (e.g. var(--spacing-4), var(--spacing-8)). If you skipped Step 2, call **get_design_tokens** now before writing any custom CSS.
87
90
  - When deciding component size or hierarchy, document your reasoning in an HTML comment (e.g. `<!-- size:sm chosen for compact sidebar layout -->`)
88
91
  - If a commerce-rag MCP server is available, query it for page layout patterns and spacing guidelines specific to the project
89
92
 
@@ -93,6 +93,86 @@
93
93
  ]
94
94
  }
95
95
  },
96
+ "RangeDatePicker": {
97
+ "className": "DatePicker",
98
+ "cdnPattern": "A",
99
+ "constructor": "new window.ncua.DatePicker(wrapper, options)",
100
+ "constructorParams": {
101
+ "wrapper": "HTMLElement — container element",
102
+ "options.size": "'xs' | 'sm'",
103
+ "options.datePickerOptions": "Array<{ element: string, attrName?: string, placeholder?: string, options: FlatpickrOptions }> — 각 요소의 options.static:true 필수(누락 시 flatpickr-wrapper 미생성으로 레이아웃 파괴). options: { mode:'range', static:true, dateFormat:'Y-m-d', clickOpens:true, allowInvalidPreload:true, locale:'ko' }",
104
+ "options.buttons": "Array<{ text: string, period: number, unit: 'days'|'weeks'|'months'|'years', isCurrent: boolean }>",
105
+ "options.onValidationError": "(error: ValidationError) => void",
106
+ "options.autoComplete": "'on' | 'off'"
107
+ },
108
+ "methods": ["getDates()", "setDate(dates: string[])", "setMultipleDates(dates: string[])"],
109
+ "example": "const dp = new window.ncua.DatePicker(document.getElementById('my-range-dp'), {\n size: 'xs',\n datePickerOptions: [\n { element: 'start-date', placeholder: '시작일', options: { dateFormat: 'Y-m-d', locale: 'ko' } },\n { element: 'end-date', placeholder: '종료일', options: { dateFormat: 'Y-m-d', locale: 'ko' } }\n ]\n});",
110
+ "cdnDefaults": {
111
+ "size": "xs",
112
+ "datePickerOptions": [
113
+ {
114
+ "options": {
115
+ "mode": "range",
116
+ "static": true,
117
+ "dateFormat": "Y-m-d",
118
+ "clickOpens": true,
119
+ "allowInvalidPreload": true,
120
+ "locale": "ko"
121
+ }
122
+ },
123
+ {
124
+ "options": {
125
+ "mode": "range",
126
+ "static": true,
127
+ "dateFormat": "Y-m-d",
128
+ "clickOpens": true,
129
+ "allowInvalidPreload": true,
130
+ "locale": "ko"
131
+ }
132
+ }
133
+ ]
134
+ }
135
+ },
136
+ "RangeDatePickerWithButtons": {
137
+ "className": "DatePicker",
138
+ "cdnPattern": "A",
139
+ "constructor": "new window.ncua.DatePicker(wrapper, options)",
140
+ "constructorParams": {
141
+ "wrapper": "HTMLElement — container element",
142
+ "options.size": "'xs' | 'sm'",
143
+ "options.datePickerOptions": "Array<{ element: string, attrName?: string, placeholder?: string, options: FlatpickrOptions }> — 각 요소의 options.static:true 필수(누락 시 flatpickr-wrapper 미생성으로 레이아웃 파괴). options: { mode:'range', static:true, dateFormat:'Y-m-d', clickOpens:true, allowInvalidPreload:true, locale:'ko' }",
144
+ "options.buttons": "Array<{ text: string, period: number, unit: 'days'|'weeks'|'months'|'years', isCurrent: boolean }> — 권장 세트: 오늘(period:0,unit:days)/7일/15일/1개월(unit:months)/3개월/1년(unit:years). ENTIRE·1주일(unit:weeks) CDN 미지원 제외.",
145
+ "options.onValidationError": "(error: ValidationError) => void",
146
+ "options.autoComplete": "'on' | 'off'"
147
+ },
148
+ "methods": ["getDates()", "setDate(dates: string[])", "setMultipleDates(dates: string[])"],
149
+ "example": "const dp = new window.ncua.DatePicker(document.getElementById('my-range-dp'), {\n size: 'xs',\n datePickerOptions: [\n { element: 'start-date', placeholder: '시작일', options: { dateFormat: 'Y-m-d', locale: 'ko' } },\n { element: 'end-date', placeholder: '종료일', options: { dateFormat: 'Y-m-d', locale: 'ko' } }\n ],\n buttons: [\n { text: '오늘', period: 0, unit: 'days', isCurrent: false },\n { text: '7일', period: 7, unit: 'days', isCurrent: false },\n { text: '15일', period: 15, unit: 'days', isCurrent: false },\n { text: '1개월', period: 1, unit: 'months', isCurrent: false },\n { text: '3개월', period: 3, unit: 'months', isCurrent: false },\n { text: '1년', period: 1, unit: 'years', isCurrent: false }\n ]\n});",
150
+ "cdnDefaults": {
151
+ "size": "xs",
152
+ "datePickerOptions": [
153
+ {
154
+ "options": {
155
+ "mode": "range",
156
+ "static": true,
157
+ "dateFormat": "Y-m-d",
158
+ "clickOpens": true,
159
+ "allowInvalidPreload": true,
160
+ "locale": "ko"
161
+ }
162
+ },
163
+ {
164
+ "options": {
165
+ "mode": "range",
166
+ "static": true,
167
+ "dateFormat": "Y-m-d",
168
+ "clickOpens": true,
169
+ "allowInvalidPreload": true,
170
+ "locale": "ko"
171
+ }
172
+ }
173
+ ]
174
+ }
175
+ },
96
176
  "Slider": {
97
177
  "className": "Slider",
98
178
  "cdnPattern": "A",
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "workflow": [
3
+ "Tool call order (strictly follow): Phase 0 — ping (once per session). Phase 1 — list_components + list_composition_overrides + search_component (+ search_icon if icons needed). Phase 2 — get_component_props + get_design_tokens (if custom areas exist). Phase 3 — render_to_html_batch or render_to_html. Phase 4 — validate_html (loop until score=1.0).",
3
4
  "You MUST generate component HTML using the render_to_html tool. Never write HTML/CSS manually.",
4
5
  "Use the HTML returned by render_to_html as-is. Do NOT modify class names, structure, or attributes.",
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.",
5
7
  "Use search_component to find the right component by Korean/English keywords before building any UI.",
6
8
  "Use list_components to browse available components by category and choose the appropriate one.",
7
- "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."
9
+ "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.",
10
+ "Call get_design_tokens in Phase 2 (before rendering), not after. Custom area CSS written without tokens first leads to hardcoded hex/px values that violate design system rules."
8
11
  ],
9
12
  "componentSelection": [
10
13
  "For password fields, you MUST use password-input. The input component is for plain text only.",
@@ -45,14 +48,14 @@
45
48
  "composition": [
46
49
  "Vertical Table row labels MUST use Table.Cell with isHeader=true (renders <th scope='row' class='ncua-table__cell'>). Do NOT use Table.HeaderCell in Vertical Table — that class (ncua-table__header-cell) is exclusively for Horizontal Table column headers inside <thead>. Pick the correct scenario from canonicalExamples: 'vertical-form-label' for form layouts, 'horizontal-with-header-cell' for data lists.",
47
50
  "For required field labels in Vertical Table, use the official NCDS element <span class='ncua-table__required'>*</span> prepended INSIDE the <th scope='row'> cell, BEFORE the label text. Do NOT invent page-prefix variants like sgr-label__required / form-required / etc. — ncua-table__required is the canonical required-mark element. See canonicalExamples key 'vertical-form-label-with-required'.",
48
- "For page-level title/breadcrumb/action area, ALWAYS use the NCUA 'page-title' component (PageTitle) via render_to_html — props: breadcrumbItems, title, primaryAction, secondaryAction, guideButton, onBack, variant (default/detail/fixed). Do NOT invent page-prefix custom classes like sgr-page-title, page-header, gd-page-title — page-title is in the catalog (list_components → category=layout). Output BEM is ncua-page-title__page-header / __title-container / __title-row / __title / __action-area / __breadcrumb-item etc. Pick the right canonicalExamples scenario (default/with-guide-button/detail-with-back/fixed-on-scroll) and modify only the values, not the props shape.",
51
+ "For page-level title/breadcrumb/action area, ALWAYS use the NCUA 'page-title' component (PageTitle) via render_to_html — props: breadcrumbItems, title, primaryAction, secondaryAction, guideButton, onBack, variant (default/detail/fixed). Do NOT invent page-prefix custom classes like sgr-page-title, page-header, gd-page-title — page-title is in the catalog (list_components → category=layout). Output BEM is ncua-page-title__page-header / __title-container / __title-row / __title / __action-area / __breadcrumb-item etc. Pick the right canonicalExamples scenario (default/with-guide-button/detail-with-back/fixed-on-scroll) and modify only the values, not the props shape. ⚠️ P20 — ReactNode props(primaryAction / secondaryAction / guideButton)는 반드시 {component: 'button', props: {...}} 형태로 전달. {label:..., size:..., hierarchy:...} raw 객체를 직접 넘기면 'Objects are not valid as a React child' 오류로 render_to_html 전체 실패(breadcrumb 포함). 모든 canonicalExamples 가 {component, props} 패턴임을 확인하고 그대로 따를 것.",
49
52
  "For tooltips on form fields, ALWAYS use the NCUA 'tooltip' component via render_to_html('tooltip', props={...}). Do NOT invent custom classes like tooltip-custom-code, tooltip-helper, sgr-tooltip — tooltip is in the catalog and supports content/title/iconStyle/iconType/position/size/tooltipType/type props. See canonicalExamples 'icon-help-form-field' / 'with-title-and-body' / 'alert-warning'.",
50
53
  "NCDS does NOT provide RadioGroup or CheckboxGroup wrapper components. When grouping multiple radio/checkbox items, wrap them with a PAGE-PREFIX class (e.g. <div class='sgr-radio-group'> or <div class='order-form__check-group'>) — NEVER use inline style attribute (e.g. style='display:flex;gap:16px') and NEVER reuse ncua- prefix for the wrapper. Define wrapper CSS in page-level SCSS using NCDS spacing/color tokens (var(--spacing-md), var(--gray-100)) — no hardcoded hex/px values.",
51
54
  "⚠️ React props ≠ CDN JS options. The component's React props (in get_component_props.props) and CDN JS constructor options (in render_to_html response's js.api.constructorParams) ARE DIFFERENT. React-only props that do NOT appear in js.api.constructorParams MUST NOT be passed as CDN options. Concrete example: Tooltip.iconStyle exists as React prop ('help-circle' | 'alert-circle') but is NOT a CDN option (allowed CDN options: position / type / tooltipType / iconType / size / hideArrow / zIndex / useShadowDOM / className). When generating CDN HTML mode code, copy js.api.example AS-IS from render_to_html response — do NOT mix React props into CDN options.",
52
55
  "⚠️ When js.api.cdnPattern is 'C' (Tooltip / Notification / Slider etc.), the component uses a STATIC FACTORY, NOT new Component(options). Use exactly the signature shown in js.api.constructor (e.g. window.ncua.Tooltip.createShort(title, content, options)). Never write `new ncua.Tooltip({title, content, ...options})` — that pattern is wrong because title/content are positional arguments, not options. Always reference render_to_html response's js.api.example and copy verbatim.",
53
56
  "⚠️ P13 — Vertical form layouts MUST be rendered by render_to_html('table', { type: 'vertical', ... }). The same 'table' component handles BOTH horizontal (data grid) AND vertical (form layout) modes via the `type` prop. Do NOT compose vertical table HTML from scratch. Do NOT use <th class='ncua-table__header-cell'> for vertical labels — that class is horizontal-only. For vertical labels, render_to_html outputs <th scope='row' class='ncua-table__cell'> (Table.Cell with isHeader=true). The required mark in vertical is <span class='ncua-table__required'>*</span> prepended inside the <th> — NOT a page-prefix span like gp-required / sgr-required / pw-required / form-required.",
54
57
  "For form layouts (label-value rows), ALWAYS call get_component_props('table') first and read canonicalExamples 'vertical-form-label' / 'vertical-form-label-with-required'. Search keywords that resolve to 'table' component: vertical-table, form-table, form-layout, label-value, 폼 레이아웃, 라벨-입력, 세로 테이블. Do NOT compose vertical table HTML by hand.",
55
- "⚠️ P14 — BlockContainer 의 본문은 반드시 BlockContainer.Body (component descriptor: 'block-container.body') 로 wrap. 직접 <section class='ncua-block-container'> 아래에 table/data-grid 를 자식으로 넣으면 ncua-block-container__body 패딩(16px 24px) 누락 — 시각적 호흡 사라짐. 표준 구조: BlockContainer > BlockHeader + BlockContainer.Body > [실제 컨텐츠]. canonicalExamples 'form-block' / 'data-block' / 'info-block' 참고.",
58
+ "⚠️ P14 — BlockContainer 의 본문은 반드시 BlockContainer.Body (component descriptor: 'block-container.body') 로 wrap. 직접 <section class='ncua-block-container'> 아래에 table/data-grid 를 자식으로 넣으면 ncua-block-container__body 패딩(16px 24px) 누락 — 시각적 호흡 사라짐. 표준 구조: BlockContainer > BlockHeader + BlockContainer.Body > [실제 컨텐츠]. canonicalExamples 'form-block' / 'data-block' / 'info-block' 참고. ⚠️ 페이지 고유 prefix를 붙인 커스텀 block wrapper({page}-block / {page}-section / {page}-panel 등 어떤 이름이든) 생성 절대 금지 — 모두 block-container 로 대체 가능.",
56
59
  "⚠️ P15 — horizontal-tab / carousel components are Swiper.js based. Their internal markup is `.ncua-horizontal-tab > .swiper > .swiper-wrapper > .swiper-slide.ncua-horizontal-tab__item > .ncua-tab-button`. Active tab uses `.ncua-tab-button.is-active`. NEVER hand-write markup with invented classes like `.ncua-horizontal-tab__menu` or `.ncua-horizontal-tab__menu--active` — those classes DO NOT EXIST. Always call render_to_html('horizontal-tab', { menus, activeTab, ... }) — pass menus as a data array, never as JSX children. The CDN JS auto-initializes the swiper instance.",
57
60
  "⚠️ P15 — JS-instantiated components (those with js.required=true and a cdnPattern, e.g. image-file-input / tooltip / notification / slider / pagination) return a placeholder `<div id> + <script>` from render_to_html. The visible markup appears EMPTY until DOMContentLoaded fires and the CDN JS populates it. This is CORRECT behavior — do NOT replace the placeholder with hand-written `<div class='ncua-image-file-input'>...<button class='ncua-btn'>파일 찾기</button>...</div>` markup because it 'looks empty in the wireframe'. The hand-written version diverges from the actual CDN runtime output. Use the placeholder + script as-is.",
58
61
  "⚠️ P16 — Vertical Table 라벨 옆 안내 아이콘은 반드시 Table.Cell 의 `tooltip` prop (TooltipProps) 으로 전달. Cell 내부에서 `ncua-table__cell-inner` / `ncua-table__cell-label` / `.ncua-tooltip` 구조로 자동 wrapping 되고 `ncua-table__cell--with-tooltip` modifier 가 부착된다. 라벨 셀 외부에 별도 Tooltip 컴포넌트를 배치하거나 페이지 prefix wrapper (예: page-vlabel, sgr-tooltip-wrapper, gd-label-with-help) 로 라벨과 아이콘을 묶지 말 것 — 정렬 어긋남 + 발명 클래스 hallucination 의 주원인. canonicalExamples 'vertical-form-label-with-tooltip' 시나리오 참고.",
@@ -70,8 +73,9 @@
70
73
  "Custom areas MUST use ONLY existing NCUA design tokens (CSS variables from CDN) for colors, fonts, spacing, and shadows. Do NOT hardcode hex values like #5B5BD6 or rgb values.",
71
74
  "Do NOT invent token names. If no exact token exists, choose the closest available token. Never fall back to raw color/size values.",
72
75
  "Before writing custom CSS, call get_design_tokens to see available tokens. Filter by category (color, typography, spacing, shadow) to reduce response size.",
76
+ "⚠️ P26 — Spacing token naming is single/double-letter suffixes, NOT Bootstrap/Tailwind-style. Correct NCDS spacing tokens: --spacing-xs(4px) / --spacing-s(8px) / --spacing-m(16px) / --spacing-l(24px) / --spacing-xl(32px) / --spacing-xxl(40px). --spacing-sm and --spacing-md DO NOT EXIST in NCDS (those are Bootstrap/Tailwind conventions that collide). validate_html will report invalid_token with get_design_tokens('spacing') suggestion when these are detected. Always call get_design_tokens('spacing') to verify token names before writing CSS.",
73
77
  "For spacing, use spacing tokens (--spacing-xs through --spacing-xxl) instead of hardcoded px values. Follow the principle: inner-block spacing < between-block spacing.",
74
- "Even inside inline `style=` attributes, spacing properties (gap / padding / margin / min-width / max-width / width / height) MUST use NCDS tokens via `var(--spacing-xs|sm|md|lg|xl|xxl)`. Raw px values like `gap:8px`, `padding:40px`, `min-width:280px` are forbidden in inline style as well. Pure layout properties (display:flex, flex-direction, align-items, justify-content, position) are allowed inline since they have no token equivalent. If a wrapper needs styling beyond pure layout, move it to a page-prefix CSS class in your stylesheet and reference tokens there.",
78
+ "Even inside inline `style=` attributes, spacing properties (gap / padding / margin / min-width / max-width / width / height) MUST use NCDS tokens via `var(--spacing-xs|s|m|l|xl|xxl)`. Raw px values like `gap:8px`, `padding:40px`, `min-width:280px` are forbidden in inline style as well. Pure layout properties (display:flex, flex-direction, align-items, justify-content, position) are allowed inline since they have no token equivalent. If a wrapper needs styling beyond pure layout, move it to a page-prefix CSS class in your stylesheet and reference tokens there.",
75
79
  "⚠️ P24 — Custom area (sgr-* / page-prefix) 가 텍스트를 노출하면 font-family 도 NCDS 표준(CommerceSans) 으로 명시. CDN CSS 는 ncua-* 컴포넌트에만 폰트를 깔아주므로 custom area 는 inheritance 가 끊길 수 있다. 표준 표기: `font-family: var(--font-families-commerce-sans);` (권장) 또는 풀 스택 `CommerceSans, 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif`. 자세한 금지/허용 패턴은 fontFamily 섹션 P14/P15/P24 참고."
76
80
  ],
77
81
  "compliance": [
@@ -879,7 +879,7 @@
879
879
  },
880
880
  "table": {
881
881
  "_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).",
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만 사용할 것.",
883
883
  "aliasesExtra": [
884
884
  "vertical-table",
885
885
  "form-table",
@@ -895,7 +895,19 @@
895
895
  "수직 테이블",
896
896
  "form-vertical-table"
897
897
  ],
898
- "bemClassesExtra": ["ncua-table--in-data-grid", "ncua-table__required"],
898
+ "bemClassesExtra": [
899
+ "ncua-table--in-data-grid",
900
+ "ncua-table__required",
901
+ "ncua-table--fixed-header",
902
+ "ncua-table__h-scroll-container",
903
+ "ncua-table__h-scroll-inner",
904
+ "ncua-table__scroll-area",
905
+ "ncua-table__scroll-container",
906
+ "ncua-table__h-scrollbar",
907
+ "ncua-table__h-scrollbar-thumb",
908
+ "ncua-table__scrollbar",
909
+ "ncua-table__scrollbar-thumb"
910
+ ],
899
911
  "allowedChildren": {
900
912
  "Table": ["Table.Header", "Table.Body", "Table.Footer", "Table.ColGroup", "Table.Empty"],
901
913
  "Table.Header": ["Table.Row"],
@@ -954,30 +966,38 @@
954
966
  },
955
967
  "canonicalExamples": {
956
968
  "_note": "P6: NCDS Table 은 같은 <th> 태그를 두 컨텍스트로 사용한다. Horizontal=컬럼 헤더(Table.HeaderCell→ncua-table__header-cell), Vertical=행 라벨(Table.Cell with isHeader=true→<th scope='row' class='ncua-table__cell'>). 두 시나리오를 분리 등록하여 AI 가 정확히 분기 학습하도록 한다. 절대 vertical 에 Table.HeaderCell 을 사용하지 말 것.",
957
- "horizontal-with-page-scroll-wrapper": {
958
- "description": "P10: 컬럼 수가 많아 페이지 너비 초과 NCDS Table 외곽을 페이지 prefix wrapper 감싸 가로 스크롤 처리. 표준 wrapper 이름은 'page-table__scroll' (또는 프로젝트 prefix 통일, 예: 'shop-table__scroll'). wrapper 안에 'page-table__scroll-inner' (min-width 적용) 단계 더, 안에 ncua-table-wrapper. CSS: .page-table__scroll { overflow-x: auto; } .page-table__scroll-inner { min-width: 1200px; }. 동일 프로젝트 내에서 같은 wrapper 이름 사용 매번 다른 이름(sgr-comb-scroll/opt-scroll 등) 생성 금지. 트리 내부의 Table 구조는 horizontal-with-header-cell 동일.",
969
+ "horizontal-with-scroll": {
970
+ "description": "컬럼 수가 많아 가로 스크롤이 필요하거나 수가 많아 세로 스크롤이 필요한 경우 Table 컴포넌트의 내장 props 사용. horizontalScroll=true ncua-table__h-scroll-container 구조 자동 생성(커스텀 wrapper 불필요). fixedHeader=true 헤더 고정(ncua-table--fixed-header). maxHeight 세로 스크롤 높이 제한. minWidth 전체 테이블 최소 너비(--ncua-table-min-width CSS 변수). Table.ColGroup widths/minWidths 배열로 컬럼 너비 명시. ⚠️ pbp-table-scroll / page-table__scroll 같은 커스텀 CSS wrapper 절대 생성 금지 NCDS가 모든 scroll DOM을 자동 생성한다.",
959
971
  "props": {
960
972
  "type": "horizontal",
973
+ "horizontalScroll": true,
974
+ "fixedHeader": true,
961
975
  "hoverable": true,
976
+ "maxHeight": 340,
977
+ "minWidth": 1140,
962
978
  "children": [
979
+ {
980
+ "component": "table.col-group",
981
+ "props": {
982
+ "widths": [80, 160, 80, 80, 64, 80, 80, 80, 80],
983
+ "minWidths": [80, 160, 80, 80, 64, 80, 80, 80, 80]
984
+ }
985
+ },
963
986
  {
964
987
  "component": "table.header",
965
988
  "children": [
966
989
  {
967
990
  "component": "table.row",
968
991
  "children": [
969
- {
970
- "component": "table.header-cell",
971
- "children": ["컬럼 1"]
972
- },
973
- {
974
- "component": "table.header-cell",
975
- "children": ["컬럼 2"]
976
- },
977
- {
978
- "component": "table.header-cell",
979
- "children": ["컬럼 3"]
980
- }
992
+ { "component": "table.header-cell", "children": ["ID"] },
993
+ { "component": "table.header-cell", "children": ["상품명"] },
994
+ { "component": "table.header-cell", "children": ["카테고리"] },
995
+ { "component": "table.header-cell", "children": ["가격"] },
996
+ { "component": "table.header-cell", "children": ["상태"] },
997
+ { "component": "table.header-cell", "children": ["재고"] },
998
+ { "component": "table.header-cell", "children": ["등록일"] },
999
+ { "component": "table.header-cell", "children": ["담당자"] },
1000
+ { "component": "table.header-cell", "children": ["메모"] }
981
1001
  ]
982
1002
  }
983
1003
  ]
@@ -1433,7 +1453,7 @@
1433
1453
  },
1434
1454
  "block-container": {
1435
1455
  "_note": "P10: BlockContainer 는 페이지 블록의 외곽 컨테이너. children 으로 block-header + 본문 컴포넌트(table/data-grid/자유영역) 조합. AI 가 sgr-block / sgr-block-container 로 우회하지 않도록 다중 시나리오 등록.",
1436
- "descriptionExtra": "[중요] 페이지 블록 외곽 컨테이너. 표준 자식 구조: BlockHeader + BlockContainer.Body(필수 wrapping — P14 룰). 본문은 Body 안에 table/data-grid/form 컴포넌트 배치. 직접 <section class='ncua-block-container'> 마크업 금지 — overflow:hidden / border-radius 등 CDN CSS 보호 받지 못함. 페이지 prefix wrapper (sgr-block / page-section) 우회 절대 금지.",
1456
+ "descriptionExtra": "[중요] 페이지 블록 외곽 컨테이너. 표준 자식 구조: BlockHeader + BlockContainer.Body(필수 wrapping — P14 룰). 본문은 Body 안에 table/data-grid/form 컴포넌트 배치. 직접 <section class='ncua-block-container'> 마크업 금지 — overflow:hidden / border-radius 등 CDN CSS 보호 받지 못함. ⚠️ 페이지에 필요한 모든 '섹션/박스/블록/패널' 영역은 block-container로 구현한다 — 페이지 고유 prefix 붙인 커스텀 wrapper({page}-block / {page}-section / {page}-panel 등 어떤 이름이든) 따로 만들지 말 것. 접을 수 있는(collapsible) 블록도 동일하게 block-container 사용.",
1437
1457
  "aliasesExtra": [
1438
1458
  "블록 컨테이너",
1439
1459
  "블록",
@@ -1452,7 +1472,11 @@
1452
1472
  "흰 박스",
1453
1473
  "정보 블록",
1454
1474
  "폼 블록",
1455
- "데이터 블록"
1475
+ "데이터 블록",
1476
+ "collapsible block",
1477
+ "접을 수 있는 블록",
1478
+ "지표 블록",
1479
+ "kpi 블록"
1456
1480
  ],
1457
1481
  "bemClassesExtra": [],
1458
1482
  "allowedChildren": {
@@ -1814,7 +1838,7 @@
1814
1838
  },
1815
1839
  "page-title": {
1816
1840
  "_note": "P7: PageTitle 은 props 기반 (subComponents 없음). breadcrumbItems / title / primaryAction / secondaryAction / guideButton / onBack / variant prop 으로 구성. AI 가 sgr-page-title 같은 페이지-prefix 로 우회하지 않도록 명시적 canonicalExamples 등록. 출력은 ncua-page-title__page-header / __title-container / __title-row / __title / __action-area 등 정밀 BEM.",
1817
- "descriptionExtra": "[중요] 페이지 최상단 타이틀 영역 표준. breadcrumbItems + title + primaryAction(우측 강조 버튼) + secondaryAction(취소/목록) + guideButton + onBack + variant(default/detail/fixed). detail variant 는 onBack 으로 좌측 back button 노출. fixed variant 는 스크롤 시 상단 고정. 페이지 prefix(sgr-page-title / page-header) 우회 금지.",
1841
+ "descriptionExtra": "[중요] 페이지 최상단 타이틀 영역 표준. breadcrumbItems + title + primaryAction(우측 강조 버튼) + secondaryAction(취소/목록) + guideButton + onBack + variant(default/detail/fixed). detail variant 는 onBack 으로 좌측 back button 노출. fixed variant 는 스크롤 시 상단 고정. 페이지 prefix(sgr-page-title / page-header) 우회 금지. ⚠️ ReactNode props(guideButton / primaryAction / secondaryAction)는 반드시 {component: 'button', props: {...}} 형태로 전달 — {label:..., size:..., hierarchy:...} raw 객체를 직접 넘기면 React child 오류로 렌더링 전체 실패(breadcrumb 포함). canonicalExamples 의 구조를 반드시 따를 것.",
1818
1842
  "aliasesExtra": [
1819
1843
  "페이지 타이틀",
1820
1844
  "페이지 제목",
@@ -1925,14 +1949,20 @@
1925
1949
  "title": "상품 상세",
1926
1950
  "onBack": "noop",
1927
1951
  "secondaryAction": {
1928
- "label": "삭제",
1929
- "size": "md",
1930
- "hierarchy": "destructive-secondary"
1952
+ "component": "button",
1953
+ "props": {
1954
+ "label": "삭제",
1955
+ "size": "md",
1956
+ "hierarchy": "destructive-secondary"
1957
+ }
1931
1958
  },
1932
1959
  "primaryAction": {
1933
- "label": "수정",
1934
- "size": "md",
1935
- "hierarchy": "primary"
1960
+ "component": "button",
1961
+ "props": {
1962
+ "label": "수정",
1963
+ "size": "md",
1964
+ "hierarchy": "primary"
1965
+ }
1936
1966
  }
1937
1967
  }
1938
1968
  },
@@ -2469,5 +2499,278 @@
2469
2499
  }
2470
2500
  }
2471
2501
  }
2502
+ },
2503
+ "date-picker": {
2504
+ "descriptionExtra": "[중요] 단일 날짜 또는 날짜+시간 입력에 사용하는 DatePicker. CDN에서는 window.ncua.DatePicker 단일 클래스로 동작한다. datePickerOptions 배열에 1개를 전달하면 single 모드, 2개를 전달하면 range 모드가 된다. ⚠️ datePickerOptions 각 요소의 options.static:true 절대 생략 금지 — 누락 시 flatpickr-wrapper div 가 생성되지 않아 flatpickr-calendar 가 input 필드 밖으로 밀려나 레이아웃이 파괴된다. options 객체는 canonicalExample 을 그대로 복사할 것: { mode:'single', static:true, dateFormat:'Y-m-d', clickOpens:true, allowInvalidPreload:true, locale:'ko' }. ⚠️ .ncua-date-picker__input 에 border·shadow·padding 을 직접 적용하는 커스텀 CSS 절대 금지 — 이 스타일들은 CDN CSS 가 .flatpickr-wrapper 에 자동 적용하며, __input 에 덮어쓰면 JS 초기화 후 이중 border 등 시각적 충돌이 발생한다. DatePicker 컨테이너는 JS 초기화 전 빈 div 이며, JS 가 .flatpickr-wrapper → .ncua-date-picker__input 구조를 동적으로 생성한다. width 조정이 필요하다면 컨테이너 div 에만 적용할 것(xs 기준 138px 은 CDN CSS 기본값). ⚠️ rangeMode 클래스는 range-date-picker(기간 범위) 전용 — 단일 DatePicker 의 flatpickr-calendar 에 rangeMode 를 참조하거나 추가 금지. JS 초기화 후 올바른 DOM 구조: .flatpickr-wrapper(input + flatpickr-calendar 래핑) 이후 .ncua-date-picker__ico label 이 flatpickr-wrapper 밖에 위치한다. ⚠️ 캘린더 아이콘 SVG 직접 작성 절대 금지 — ncua-date-picker__ico label 안의 SVG 는 컴포넌트가 stroke='currentColor' 로 자동 렌더링하며, stroke='black' 등 하드코딩하면 다크 테마/CDN 스타일 충돌 발생.",
2505
+ "aliasesExtra": [
2506
+ "날짜 선택",
2507
+ "단일 날짜",
2508
+ "date input",
2509
+ "flatpickr",
2510
+ "날짜 입력",
2511
+ "datepicker",
2512
+ "date picker",
2513
+ "single date",
2514
+ "날짜+시간",
2515
+ "time picker"
2516
+ ],
2517
+ "bemClassesExtra": [],
2518
+ "notApplicableAreas": ["allowedChildren", "bemClassesExtra"],
2519
+ "canonicalExamples": {
2520
+ "single": {
2521
+ "description": "단일 날짜 선택 (기본)",
2522
+ "props": {
2523
+ "size": "xs",
2524
+ "datePickerOptions": [
2525
+ {
2526
+ "element": "date-input",
2527
+ "placeholder": "날짜 선택",
2528
+ "options": {
2529
+ "mode": "single",
2530
+ "static": true,
2531
+ "dateFormat": "Y-m-d",
2532
+ "clickOpens": true,
2533
+ "allowInvalidPreload": true,
2534
+ "locale": "ko"
2535
+ }
2536
+ }
2537
+ ]
2538
+ }
2539
+ },
2540
+ "with-time": {
2541
+ "description": "날짜+시간 선택",
2542
+ "props": {
2543
+ "size": "xs",
2544
+ "datePickerOptions": [
2545
+ {
2546
+ "element": "datetime-input",
2547
+ "placeholder": "날짜·시간 선택",
2548
+ "options": {
2549
+ "mode": "single",
2550
+ "static": true,
2551
+ "dateFormat": "Y-m-d H:i",
2552
+ "enableTime": true,
2553
+ "clickOpens": true,
2554
+ "allowInvalidPreload": true,
2555
+ "locale": "ko"
2556
+ }
2557
+ }
2558
+ ]
2559
+ }
2560
+ }
2561
+ }
2562
+ },
2563
+ "range-date-picker": {
2564
+ "descriptionExtra": "[중요] 시작일·종료일 기간 범위 선택. CDN에서는 window.ncua.DatePicker 에 datePickerOptions 2개를 전달해 range 모드로 동작한다. 빠른 기간 버튼이 필요하면 range-date-picker-with-buttons 를 사용한다. ⚠️ datePickerOptions 각 요소의 options.static:true 절대 생략 금지 — 누락 시 flatpickr-wrapper div 가 생성되지 않아 flatpickr-calendar 가 input 필드 밖으로 밀려나 레이아웃이 파괴된다. options 객체는 canonicalExample 을 그대로 복사할 것: { mode:'range', static:true, dateFormat:'Y-m-d', clickOpens:true, allowInvalidPreload:true, locale:'ko' }. ⚠️ .ncua-date-picker__input 에 border·shadow·padding 을 직접 적용하는 커스텀 CSS 절대 금지 — CDN CSS 가 .flatpickr-wrapper 에 자동 적용하며 __input 에 덮어쓰면 이중 border 충돌 발생. flatpickr 가 range 모드 캘린더에 rangeMode 클래스를 자동 부가 — 직접 작성 불필요. ⚠️ 캘린더 아이콘 SVG 직접 작성 절대 금지 — stroke='black' 등 하드코딩 금지, 컴포넌트가 stroke='currentColor' 로 자동 처리한다.",
2565
+ "aliasesExtra": [
2566
+ "기간 선택",
2567
+ "시작일 종료일",
2568
+ "날짜 범위",
2569
+ "range date",
2570
+ "date range",
2571
+ "start end date",
2572
+ "기간 입력",
2573
+ "범위 날짜"
2574
+ ],
2575
+ "bemClassesExtra": [],
2576
+ "notApplicableAreas": ["allowedChildren", "bemClassesExtra"],
2577
+ "canonicalExamples": {
2578
+ "range": {
2579
+ "description": "시작일~종료일 기간 범위 선택 (xs 사이즈, 기본)",
2580
+ "props": {
2581
+ "size": "xs",
2582
+ "datePickerOptions": [
2583
+ {
2584
+ "element": "start-date",
2585
+ "placeholder": "시작일",
2586
+ "options": {
2587
+ "mode": "range",
2588
+ "static": true,
2589
+ "dateFormat": "Y-m-d",
2590
+ "clickOpens": true,
2591
+ "allowInvalidPreload": true,
2592
+ "locale": "ko"
2593
+ }
2594
+ },
2595
+ {
2596
+ "element": "end-date",
2597
+ "placeholder": "종료일",
2598
+ "options": {
2599
+ "mode": "range",
2600
+ "static": true,
2601
+ "dateFormat": "Y-m-d",
2602
+ "clickOpens": true,
2603
+ "allowInvalidPreload": true,
2604
+ "locale": "ko"
2605
+ }
2606
+ }
2607
+ ]
2608
+ }
2609
+ },
2610
+ "range-sm": {
2611
+ "description": "시작일~종료일 기간 범위 선택 (sm 사이즈)",
2612
+ "props": {
2613
+ "size": "sm",
2614
+ "datePickerOptions": [
2615
+ {
2616
+ "element": "start-date",
2617
+ "placeholder": "시작일",
2618
+ "options": {
2619
+ "mode": "range",
2620
+ "static": true,
2621
+ "dateFormat": "Y-m-d",
2622
+ "clickOpens": true,
2623
+ "allowInvalidPreload": true,
2624
+ "locale": "ko"
2625
+ }
2626
+ },
2627
+ {
2628
+ "element": "end-date",
2629
+ "placeholder": "종료일",
2630
+ "options": {
2631
+ "mode": "range",
2632
+ "static": true,
2633
+ "dateFormat": "Y-m-d",
2634
+ "clickOpens": true,
2635
+ "allowInvalidPreload": true,
2636
+ "locale": "ko"
2637
+ }
2638
+ }
2639
+ ]
2640
+ }
2641
+ }
2642
+ }
2643
+ },
2644
+ "range-date-picker-with-buttons": {
2645
+ "descriptionExtra": "[중요] 시작일·종료일 기간 범위 선택 + 빠른 기간 버튼 내장. CDN에서는 window.ncua.DatePicker 에 datePickerOptions 2개 + buttons 배열을 전달한다. buttons의 권장 세트: 오늘/7일/15일/1개월/3개월/1년. ENTIRE(전체) 및 1주일(unit='weeks') 은 CDN 미지원이므로 제외한다. ⚠️ datePickerOptions 각 요소의 options.static:true 절대 생략 금지 — 누락 시 flatpickr-wrapper div 가 생성되지 않아 flatpickr-calendar 가 input 필드 밖으로 밀려나 레이아웃이 파괴된다. options 객체는 canonicalExample 을 그대로 복사할 것: { mode:'range', static:true, dateFormat:'Y-m-d', clickOpens:true, allowInvalidPreload:true, locale:'ko' }. ⚠️ .ncua-date-picker__input 에 border·shadow·padding 을 직접 적용하는 커스텀 CSS 절대 금지 — CDN CSS 가 .flatpickr-wrapper 에 자동 적용하며 __input 에 덮어쓰면 이중 border 충돌 발생. flatpickr 가 range 모드 캘린더에 rangeMode 클래스를 자동 부가 — 직접 작성 불필요. ⚠️ 캘린더 아이콘 SVG 직접 작성 절대 금지 — stroke='black' 등 하드코딩 금지, 컴포넌트가 stroke='currentColor' 로 자동 처리한다.",
2646
+ "aliasesExtra": [
2647
+ "기간 선택 버튼",
2648
+ "빠른 기간",
2649
+ "기간 버튼",
2650
+ "date range buttons",
2651
+ "period buttons",
2652
+ "오늘 버튼",
2653
+ "1개월 버튼",
2654
+ "기간 프리셋",
2655
+ "date preset"
2656
+ ],
2657
+ "bemClassesExtra": [],
2658
+ "notApplicableAreas": ["allowedChildren", "bemClassesExtra"],
2659
+ "canonicalExamples": {
2660
+ "range-with-buttons": {
2661
+ "description": "시작일~종료일 + 빠른 기간 버튼 (권장 세트: 오늘/7일/15일/1개월/3개월/1년)",
2662
+ "props": {
2663
+ "size": "xs",
2664
+ "datePickerOptions": [
2665
+ {
2666
+ "element": "start-date",
2667
+ "placeholder": "시작일",
2668
+ "options": {
2669
+ "mode": "range",
2670
+ "static": true,
2671
+ "dateFormat": "Y-m-d",
2672
+ "clickOpens": true,
2673
+ "allowInvalidPreload": true,
2674
+ "locale": "ko"
2675
+ }
2676
+ },
2677
+ {
2678
+ "element": "end-date",
2679
+ "placeholder": "종료일",
2680
+ "options": {
2681
+ "mode": "range",
2682
+ "static": true,
2683
+ "dateFormat": "Y-m-d",
2684
+ "clickOpens": true,
2685
+ "allowInvalidPreload": true,
2686
+ "locale": "ko"
2687
+ }
2688
+ }
2689
+ ],
2690
+ "buttons": [
2691
+ { "text": "오늘", "period": 0, "unit": "days", "isCurrent": false },
2692
+ { "text": "7일", "period": 7, "unit": "days", "isCurrent": false },
2693
+ { "text": "15일", "period": 15, "unit": "days", "isCurrent": false },
2694
+ { "text": "1개월", "period": 1, "unit": "months", "isCurrent": false },
2695
+ { "text": "3개월", "period": 3, "unit": "months", "isCurrent": false },
2696
+ { "text": "1년", "period": 1, "unit": "years", "isCurrent": false }
2697
+ ]
2698
+ }
2699
+ },
2700
+ "range-with-buttons-sm": {
2701
+ "description": "시작일~종료일 + 빠른 기간 버튼 (sm 사이즈, 최소 세트: 오늘/1개월/3개월)",
2702
+ "props": {
2703
+ "size": "sm",
2704
+ "datePickerOptions": [
2705
+ {
2706
+ "element": "start-date",
2707
+ "placeholder": "시작일",
2708
+ "options": {
2709
+ "mode": "range",
2710
+ "static": true,
2711
+ "dateFormat": "Y-m-d",
2712
+ "clickOpens": true,
2713
+ "allowInvalidPreload": true,
2714
+ "locale": "ko"
2715
+ }
2716
+ },
2717
+ {
2718
+ "element": "end-date",
2719
+ "placeholder": "종료일",
2720
+ "options": {
2721
+ "mode": "range",
2722
+ "static": true,
2723
+ "dateFormat": "Y-m-d",
2724
+ "clickOpens": true,
2725
+ "allowInvalidPreload": true,
2726
+ "locale": "ko"
2727
+ }
2728
+ }
2729
+ ],
2730
+ "buttons": [
2731
+ { "text": "오늘", "period": 0, "unit": "days", "isCurrent": true },
2732
+ { "text": "1개월", "period": 1, "unit": "months", "isCurrent": false },
2733
+ { "text": "3개월", "period": 3, "unit": "months", "isCurrent": false }
2734
+ ]
2735
+ }
2736
+ }
2737
+ }
2738
+ },
2739
+ "bread-crumb": {
2740
+ "_note": "BreadCrumb 는 items 배열(label + href?) 로 동작. 첫 항목은 Home 아이콘 자동 렌더 — icon prop 불필요. 마지막 항목은 current(href 없음). page-title 의 breadcrumbItems 와 별개 — page-title 안 breadcrumb 를 BreadCrumb 로 중복 사용 금지.",
2741
+ "descriptionExtra": "[중요] 경로 탐색 네비게이션 컴포넌트. items 배열에 {label, href?} 전달: 첫 항목은 Home 아이콘 자동 표시(href 지정 권장), 마지막 항목은 현재 페이지(href 생략). ⚠️ guideButton / primaryAction 같은 ReactNode 슬롯 없음 — items 배열과 className만 허용. ⚠️ page-title 내부에 breadcrumbItems prop 으로 이미 breadcrumb 가 내장되므로, PageTitle 과 BreadCrumb 를 동시에 사용하면 breadcrumb 이 중복된다. 단독 nav 영역이나 page-title 없이 breadcrumb 만 필요한 경우에만 사용.",
2742
+ "aliasesExtra": [
2743
+ "브레드크럼",
2744
+ "브레드크럼바",
2745
+ "경로 탐색",
2746
+ "breadcrumb",
2747
+ "breadcrumbs",
2748
+ "navigation trail",
2749
+ "페이지 경로",
2750
+ "계층 탐색",
2751
+ "현재 위치",
2752
+ "홈 > 카테고리",
2753
+ "네비게이션 경로"
2754
+ ],
2755
+ "bemClassesExtra": [],
2756
+ "notApplicableAreas": ["allowedChildren", "bemClassesExtra"],
2757
+ "canonicalExamples": {
2758
+ "basic": {
2759
+ "description": "3단계 경로. 첫 항목 Home 아이콘 자동, 중간 항목은 링크, 마지막 항목은 현재 페이지(href 없음).",
2760
+ "props": {
2761
+ "items": [
2762
+ { "label": "홈", "href": "/" },
2763
+ { "label": "상품 관리", "href": "/goods" },
2764
+ { "label": "상품 간편 등록" }
2765
+ ]
2766
+ }
2767
+ },
2768
+ "two-level": {
2769
+ "description": "2단계 경로. Home + 현재 페이지만 있는 최소 구성.",
2770
+ "props": {
2771
+ "items": [{ "label": "홈", "href": "/" }, { "label": "대시보드" }]
2772
+ }
2773
+ }
2774
+ }
2472
2775
  }
2473
2776
  }
package/bin/server.js CHANGED
@@ -90,6 +90,12 @@ const main = async () => {
90
90
  const instructions = (0, dataLoader_js_1.loadInstructions)(definitionsDir);
91
91
  const complianceRules = (0, dataLoader_js_1.loadComplianceRules)(definitionsDir);
92
92
  const jsApiMap = (0, dataLoader_js_1.loadJsApi)(definitionsDir);
93
+ // ── DOM + React 런타임 초기화 — 번들 로드 전에 실행해야 unifyModuleResolution 이 적용됨 ──
94
+ // setupDomEnvironment()가 Module._resolveFilename 을 override 하여 react / @ncds/ui-admin-icon 경로를
95
+ // MCP 기준으로 pin 한다. 번들 로드 이후에 실행하면 번들 내 require() 가 pin 이전 경로로 캐시되어
96
+ // React 인스턴스 불일치($$typeof Symbol mismatch) 가 발생한다.
97
+ const reactRuntime = (0, domEnvironment_js_1.setupDomEnvironment)();
98
+ logger_js_1.logger.info('DOM + React 런타임 초기화 완료');
93
99
  // ── 데이터 로딩 ──
94
100
  const { map: componentMap, compositionOverrides } = (0, dataLoader_js_1.loadComponentsFromDir)(dataLoader_js_1.DEFAULT_DATA_DIR);
95
101
  const { cdn: cdnMeta, icon: iconMeta } = (0, dataLoader_js_1.loadMeta)(dataLoader_js_1.DEFAULT_DATA_DIR);
@@ -109,9 +115,6 @@ const main = async () => {
109
115
  catch {
110
116
  logger_js_1.logger.warn('아이콘 번들 로딩 실패 — 아이콘 resolve 비활성화');
111
117
  }
112
- // ── DOM + React 런타임 초기화 (유틸로 추출 § 3.9) ──
113
- const reactRuntime = (0, domEnvironment_js_1.setupDomEnvironment)();
114
- logger_js_1.logger.info('DOM + React 런타임 초기화 완료');
115
118
  // ── 파생 데이터 사전 계산 (code-guide § 5.1 반복 계산 제거) ──
116
119
  const rootClassMap = (0, validateHtml_js_1.buildRootClassMap)(componentMap);
117
120
  const tokenValueMap = (0, validateHtml_js_1.buildTokenValueMap)(tokenData);