@croquiscom/pds 16.57.0 → 16.57.1

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/pds-skills/SKILL.md +5 -3
  3. package/pds-skills/resources/components/component-banner.props.txt +1 -1
  4. package/pds-skills/resources/components/component-button-accordionbutton.props.txt +2 -2
  5. package/pds-skills/resources/components/component-button-button.props.txt +2 -2
  6. package/pds-skills/resources/components/component-button-iconbutton.props.txt +2 -2
  7. package/pds-skills/resources/components/component-button-popoverbutton.props.txt +2 -2
  8. package/pds-skills/resources/components/component-button-textbutton.props.txt +1 -1
  9. package/pds-skills/resources/components/component-chip-inputchip.props.txt +1 -1
  10. package/pds-skills/resources/components/component-chipinput.props.txt +1 -1
  11. package/pds-skills/resources/components/component-colorpicker.props.txt +1 -1
  12. package/pds-skills/resources/components/component-control-radio.props.txt +1 -1
  13. package/pds-skills/resources/components/component-control-radiogroup-boxradio.props.txt +1 -1
  14. package/pds-skills/resources/components/component-control-radiogroup-radio.props.txt +1 -1
  15. package/pds-skills/resources/components/component-control-segmentedcontrol.props.txt +1 -1
  16. package/pds-skills/resources/components/component-datepickerv2-datepickerv2.props.txt +1 -1
  17. package/pds-skills/resources/components/component-dropdown.props.txt +1 -1
  18. package/pds-skills/resources/components/component-dropdownfilter.props.txt +2 -2
  19. package/pds-skills/resources/components/component-dropdowninput.props.txt +3 -3
  20. package/pds-skills/resources/components/component-dropdownmultiselect.props.txt +3 -3
  21. package/pds-skills/resources/components/component-input.props.txt +1 -1
  22. package/pds-skills/resources/components/component-inputstepper.props.txt +1 -1
  23. package/pds-skills/resources/components/component-modal-alertmodal.props.txt +1 -1
  24. package/pds-skills/resources/components/component-modal-basicmodal.props.txt +2 -2
  25. package/pds-skills/resources/components/component-modal-confirmmodal.props.txt +4 -4
  26. package/pds-skills/resources/components/component-modal-floatingmodal.props.txt +2 -2
  27. package/pds-skills/resources/components/component-numericinput.props.txt +2 -2
  28. package/pds-skills/resources/components/component-textarea.props.txt +1 -1
  29. package/pds-skills/resources/components/component-timepicker.props.txt +1 -1
  30. package/pds-skills/resources/components/component-timerangepicker.props.txt +4 -4
  31. package/pds-skills/resources/components/component-upload-fileuploadbutton.props.txt +1 -1
  32. package/pds-skills/resources/components/guide-component-decision-tree.txt +84 -0
  33. package/pds-skills/resources/components/guide-composition-patterns.txt +537 -0
  34. package/pds-skills/resources/components/intro.txt +1 -1
  35. package/pds-skills/resources/guides/guide-component-decision-tree.md +84 -0
  36. package/pds-skills/resources/guides/guide-composition-patterns.md +537 -0
  37. package/pds-skills/resources/index.txt +2 -0
@@ -0,0 +1,537 @@
1
+ # PDS 합성 패턴 가이드
2
+
3
+ PDS를 사용하는 에이전트가 TypeScript type/JSDoc만으로는 파악하기 어려운 컴포넌트 합성 제약과 잘못된 사용 패턴을 8개 클래스로 정리한다.
4
+
5
+ > **이 가이드에 없는 것**: prop 이름, optional/required 여부, default value, type. 이런 정보는 `*.props.txt` 파일의 Props 표에서 읽어라. 이 가이드는 type/JSDoc만으로 자명하지 않은 _합성 규칙_만 다룬다.
6
+
7
+ ---
8
+
9
+ ## 클래스 #1 — Flat API (dot-notation 없음)
10
+
11
+ ### 무엇이 문제인가
12
+
13
+ React 컴포넌트 라이브러리 중 일부(Radix UI, ShadCN, Chakra 등)는 `\<Modal.Body\>`, `\<Menu.Item\>`, `\<Tabs.Tab\>` 같은 dot-notation compound 패턴을 쓴다. PDS는 이 패턴을 **사용하지 않는다**. 자식 컴포넌트는 항상 별도의 named export다.
14
+
15
+ ### 잘못된 패턴
16
+
17
+ ```tsx
18
+ // ❌ dot-notation — PDS에 없는 export
19
+ <Modal.Body>내용</Modal.Body>
20
+ <Modal.Footer>버튼</Modal.Footer>
21
+ <Menu.Item id="foo" label="Foo" />
22
+ <LineTabs.Tab id="tab1">탭1</LineTabs.Tab>
23
+ <BoxTabs.Tab id="tab1">탭1</BoxTabs.Tab>
24
+ ```
25
+
26
+ ### 올바른 PDS 패턴
27
+
28
+ ```tsx
29
+ // ✅ 별도 named export 사용
30
+ import { Menu, MenuItem, MenuGroup } from '@croquiscom/pds';
31
+ <Menu>
32
+ <MenuItem id='menu-item-1' label='메뉴 1' />
33
+ <MenuGroup id='group-1' label='그룹'>
34
+ <MenuItem id='menu-item-2' label='메뉴 2' />
35
+ </MenuGroup>
36
+ </Menu>;
37
+ // ✅ LineTabs + LineTab
38
+ import { LineTabs, LineTab } from '@croquiscom/pds';
39
+ <LineTabs activeTabId={activeTab} onChange={setActiveTab}>
40
+ <LineTab id='tab1'>탭 1</LineTab>
41
+ <LineTab id='tab2'>탭 2</LineTab>
42
+ </LineTabs>;
43
+ // ✅ BoxTabs + BoxTab
44
+ import { BoxTabs, BoxTab } from '@croquiscom/pds';
45
+ <BoxTabs activeTabId={activeTab} onChange={setActiveTab}>
46
+ <BoxTab id='tab1'>탭 1</BoxTab>
47
+ <BoxTab id='tab2'>탭 2</BoxTab>
48
+ </BoxTabs>;
49
+ ```
50
+
51
+ ### 실제 컴포넌트 코드 근거
52
+
53
+ `src/components/menu/index.ts`, `src/components/tabs/index.ts`를 보면 `Menu`, `MenuItem`, `MenuGroup`, `LineTabs`, `LineTab`, `BoxTabs`, `BoxTab`, `PanelTabs`, `PanelTab`, `PanelTabGroup`이 각각 별도 named export로 등록되어 있다. `Modal.Body` 같은 static property로 붙여 둔 하위 컴포넌트는 소스 어디에도 없다.
54
+
55
+ ---
56
+
57
+ ## 클래스 #2 — requiredParent (context throw)
58
+
59
+ ### 무엇이 문제인가
60
+
61
+ 일부 PDS 컴포넌트는 특정 부모 컴포넌트의 Context 없이 단독으로 사용하면 런타임 오류가 발생한다. TypeScript 타입은 이 제약을 표현하지 못한다.
62
+
63
+ ### 잘못된 패턴
64
+
65
+ ```tsx
66
+ // ❌ LineTab을 LineTabs 밖에서 사용 → "LineTabContext is not defined"
67
+ <div>
68
+ <LineTab id="tab1">탭 1</LineTab>
69
+ </div>
70
+ // ❌ MenuItem을 Menu 밖에서 사용 → "MenuContext is not defined"
71
+ <div>
72
+ <MenuItem id="item1" label="아이템" />
73
+ </div>
74
+ // ❌ BoxTab을 BoxTabs 밖에서 사용 → "BoxTabContext is not defined"
75
+ <BoxTab id="tab1">탭</BoxTab>
76
+ // ❌ PanelTab을 PanelTabs 밖에서 사용 → "PanelTabContext is not defined"
77
+ <PanelTab id="tab1" title="탭" />
78
+ ```
79
+
80
+ ### 올바른 PDS 패턴
81
+
82
+ | 자식 컴포넌트 | 필수 부모 | 오류 메시지 | | ----------------------- | ----------- | -------------------------------- | | `LineTab` | `LineTabs` | `LineTabContext is not defined` | | `BoxTab` | `BoxTabs` | `BoxTabContext is not defined` | | `PanelTab` | `PanelTabs` | `PanelTabContext is not defined` | | `MenuItem`, `MenuGroup` | `Menu` | `MenuContext is not defined` |
83
+
84
+ `PanelTabGroup`은 context를 직접 소비하지 않아 단독 사용이 가능하지만, `PanelTabs` 안에서 그룹화 역할로 쓰는 것이 설계 의도다(`src/components/tabs/PanelTabs/PanelTabGroup.tsx` 확인).
85
+
86
+ ```tsx
87
+ // ✅ 항상 부모 컴포넌트 안에서 사용
88
+ <LineTabs activeTabId={activeTab} onChange={setActiveTab}>
89
+ <LineTab id="tab1">탭 1</LineTab>
90
+ <LineTab id="tab2">탭 2</LineTab>
91
+ </LineTabs>
92
+ <Menu>
93
+ <MenuItem id="item1" label="아이템 1" />
94
+ <MenuItem id="item2" label="아이템 2" />
95
+ </Menu>
96
+ <PanelTabs activeTabId={activeTab} onChange={setActiveTab}>
97
+ <PanelTabGroup title="그룹">
98
+ <PanelTab id="tab1" title="탭 1" number={3} suffix="건" />
99
+ </PanelTabGroup>
100
+ </PanelTabs>
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 클래스 #3 — Portal 위치 (FloatingPortal)
106
+
107
+ ### 무엇이 문제인가
108
+
109
+ 일부 PDS 컴포넌트는 `FloatingPortal`을 통해 `document.body`가 아닌 `id="pds-floating-root"` DOM 노드에 렌더링된다. 이 노드가 없으면 컴포넌트가 `document.body`에 폴백 렌더링될 수 있다.
110
+
111
+ 에이전트가 이 동작을 모르고 z-index 조정이나 CSS overflow 처리 시 "팝업이 특정 컨테이너 밖으로 나온다"는 예상 동작을 이해하지 못하는 경우가 있다. Portal 노드를 수동으로 생성하려 하거나, `FloatingTree`를 추가로 선언해야 한다고 오해하는 경우도 있다.
112
+
113
+ ### Portal 사용 컴포넌트 목록
114
+
115
+ | 컴포넌트 | Portal 동작 | | -------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `Modal` (BasicModal, AlertModal, ConfirmModal, NoticeModal, FloatingModal) | `pds-floating-root`에 렌더링. `FloatingTreeContext` 래핑 | | `BottomSheet` | `pds-floating-root`에 렌더링. `noUsePortal=true`로 비활성화 가능 | | `Drawer` | `pds-floating-root`에 렌더링. `noUsePortal=true`로 비활성화 가능 | | `Popover` | `pds-floating-root`에 렌더링 | | `Tooltip` | `pds-floating-root`에 렌더링 | | `Dropdown`, `DropdownFilter`, `DropdownInput` | 옵션 목록이 `pds-floating-root`에 렌더링 | | `DropdownComposite` | 옵션 목록이 `pds-floating-root`에 렌더링 | | `DatePicker` | 캘린더 패널이 `pds-floating-root`에 렌더링 | | `TimePicker`, `TimeRangePicker` | 시간 선택 패널이 `pds-floating-root`에 렌더링 | | `ColorPicker` | 컬러 팔레트가 `pds-floating-root`에 렌더링 |
116
+
117
+ ### 올바른 PDS 패턴
118
+
119
+ ```tsx
120
+ // ✅ pds-floating-root 노드를 앱 루트에 추가 (없으면 document.body에 폴백)
121
+ // index.html 또는 앱 초기화 파일에:
122
+ <div id="pds-floating-root" />
123
+ // ✅ Modal 사용 시 별도 FloatingTree 선언 불필요
124
+ // Modal, BottomSheet, Drawer는 FloatingTreeContext로 래핑되어 내부에서 자동 처리
125
+ import { Modal } from '@croquiscom/pds';
126
+ <Modal opened={isOpen} onCancel={handleClose} title="제목">
127
+ {/* 내용 */}
128
+ </Modal>
129
+ // ✅ noUsePortal: 특정 컨테이너 안에 렌더링해야 할 때
130
+ <BottomSheet opened={isOpen} noUsePortal>
131
+ {/* document.body 대신 현재 DOM 트리 안에 렌더링 */}
132
+ </BottomSheet>
133
+ ```
134
+
135
+ ### 실제 컴포넌트 코드 근거
136
+
137
+ `src/constants/floating.ts`의 `FLOATING_ROOT_ID = 'pds-floating-root'` 상수를 `BottomSheet.tsx`, `Drawer.tsx`, `ModalOverlay.tsx`, `Popover.tsx`, `Tooltip.tsx`, `DropdownOptions.tsx` 등이 `FloatingPortal`에 전달한다.
138
+
139
+ ---
140
+
141
+ ## 클래스 #4 — asChild 패턴 (Radix Slot)
142
+
143
+ ### 무엇이 문제인가
144
+
145
+ Button 계열 컴포넌트에는 `asChild` prop이 있다. `asChild=true`로 설정하면 `\<button\>` DOM 요소를 렌더링하지 않고 children의 단일 ReactElement에 모든 props와 이벤트 핸들러를 전달한다(Radix Slot 패턴).
146
+
147
+ 에이전트가 이 패턴을 모르고 `\<a\>` 태그 링크 버튼을 만들 때 `\<Button\>` 안에 `\<a\>`를 중첩(button 안의 a — HTML 비유효)하는 경우가 있다. 커스텀 컴포넌트에 버튼 스타일을 적용하는 방법을 찾지 못하는 경우도 있다.
148
+
149
+ ### asChild 지원 컴포넌트
150
+
151
+ `Button`, `TextButton`, `IconButton`, `FloatingButton`, `AccordionButton`, `PopoverButton`
152
+
153
+ ### 잘못된 패턴
154
+
155
+ ```tsx
156
+ // ❌ button 안에 a 중첩 (HTML 유효성 오류)
157
+ <Button kind="primary">
158
+ <a href="/dashboard">대시보드</a>
159
+ </Button>
160
+ // ❌ 클릭 이벤트를 div에 별도로 달고 Button 스타일을 흉내냄
161
+ <div onClick={handleClick} style={{ ... }}>버튼처럼 보이는 div</div>
162
+ ```
163
+
164
+ ### 올바른 PDS 패턴
165
+
166
+ ```tsx
167
+ // ✅ asChild=true: <a>가 <button>을 대체. Button의 모든 스타일과 props가 <a>에 적용됨
168
+ <Button asChild kind="primary">
169
+ <a href="/dashboard">대시보드</a>
170
+ </Button>
171
+ // ✅ Next.js Link와 함께 사용
172
+ import Link from 'next/link';
173
+ <Button asChild kind="primary">
174
+ <Link href="/dashboard">대시보드</Link>
175
+ </Button>
176
+ // ✅ TextButton + asChild
177
+ <TextButton asChild kind="link">
178
+ <a href="https://example.com" target="_blank">외부 링크</a>
179
+ </TextButton>
180
+ // ✅ IconButton + asChild (aria-label 유지)
181
+ <IconButton asChild aria-label="닫기">
182
+ <button type="button" onClick={handleClose}><IconX /></button>
183
+ </IconButton>
184
+ ```
185
+
186
+ > **주의**: `asChild=true` 시 children은 반드시 단일 ReactElement여야 한다. Fragment(`\<\>`)나 여러 요소를 전달하면 Slot이 올바르게 작동하지 않는다.
187
+
188
+ ### 실제 컴포넌트 코드 근거
189
+
190
+ `src/components/button/Button.tsx:71` — `const Comp = asChild ? Slot : 'button';` `src/components/button/types.ts:20` — `asChild?: boolean;` `@radix-ui/react-slot`의 `Slot` 컴포넌트를 사용한다.
191
+
192
+ ---
193
+
194
+ ## 클래스 #5 — directChildInjection (FormField의 cloneElement)
195
+
196
+ ### 무엇이 문제인가
197
+
198
+ `FormField`는 `status` prop을 **React Context가 아니라 `cloneElement`로** 직접 자식에 주입한다. 이 메커니즘은 children이 Fragment(`\<\>`) 또는 래퍼 div 안에 있으면 작동하지 않는다.
199
+
200
+ 에이전트가 이 제약을 모르고 children을 Fragment나 래퍼로 감싸는 경우가 있다. `DatePicker` 같이 주입 대상이 아닌 컴포넌트도 자동으로 status를 받는다고 가정하는 경우도 있다.
201
+
202
+ ### status 자동 주입 대상 컴포넌트
203
+
204
+ `Input`, `Dropdown`, `DropdownInput`, `NumericInput` (총 4개)
205
+
206
+ ### 잘못된 패턴
207
+
208
+ ```tsx
209
+ // ❌ Fragment로 감싸면 cloneElement가 작동하지 않음
210
+ <FormField status="error" label="이름">
211
+ <>
212
+ <Input placeholder="이름 입력" />
213
+ </>
214
+ </FormField>
215
+ // ❌ 래퍼 div 안에 있으면 주입되지 않음
216
+ <FormField status="error">
217
+ <div>
218
+ <Input placeholder="이름" />
219
+ </div>
220
+ </FormField>
221
+ // ❌ DatePicker는 자동 주입 대상이 아님
222
+ <FormField status="error">
223
+ <DatePicker value={date} onChange={setDate} />
224
+ {/* FormField의 status가 DatePicker에 전달되지 않음 */}
225
+ </FormField>
226
+ ```
227
+
228
+ ### 올바른 PDS 패턴
229
+
230
+ ```tsx
231
+ // ✅ 단일 직접 자식으로 사용해야 cloneElement가 작동
232
+ <FormField status="error" label="이름" formHelperText="필수 항목입니다">
233
+ <Input placeholder="이름 입력" />
234
+ </FormField>
235
+ // ✅ Dropdown도 단일 직접 자식
236
+ <FormField status="error" label="카테고리">
237
+ <Dropdown options={options} value={value} onChange={setValue} />
238
+ </FormField>
239
+ // ✅ DatePicker: 자체 error prop을 직접 전달해야 함
240
+ <FormField status="error" label="날짜" formHelperText="날짜를 선택하세요">
241
+ <DatePicker
242
+ error
243
+ value={date}
244
+ onChange={setDate}
245
+ />
246
+ </FormField>
247
+ ```
248
+
249
+ ### 실제 컴포넌트 코드 근거
250
+
251
+ `src/components/form/FormField.tsx:60–80` —
252
+
253
+ ```javascript
254
+ const hasStatusPropsComponent = (children) =>
255
+ isValidElement(children) && [Input, Dropdown, DropdownInput, NumericInput].includes(children.type);
256
+ const clonedChildren = status && hasStatusPropsComponent(children) ? cloneElement(children, { status }) : children;
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 클래스 #6 — renderForbidden (Hook-only API)
262
+
263
+ ### 무엇이 문제인가
264
+
265
+ `Toast`와 `Notification`은 JSX 선언형 렌더링이 **금지**된다. TypeScript 타입 정의가 있고 Storybook에도 등장하지만, 이는 내부 구조 문서화용이지 직접 렌더링을 허용하는 것이 아니다.
266
+
267
+ 에이전트가 `\<Toast content="..." /\>` 또는 `\<Notification title="..." /\>`를 JSX로 직접 렌더링하는 코드를 작성하는 경우가 있다.
268
+
269
+ ### 잘못된 패턴
270
+
271
+ ```tsx
272
+ // ❌ Toast JSX 직접 렌더링 금지
273
+ import { Toast } from '@croquiscom/pds';
274
+ return <Toast content='저장되었습니다.' kind='success' />;
275
+ // ❌ Notification JSX 직접 렌더링 금지
276
+ import { Notification } from '@croquiscom/pds';
277
+ return <Notification title='알림' content='처리 완료' />;
278
+ ```
279
+
280
+ ### 올바른 PDS 패턴
281
+
282
+ ```tsx
283
+ // ✅ Toast: useToast() Hook으로만 사용
284
+ import { useToast } from '@croquiscom/pds';
285
+ const MyComponent = () => {
286
+ const toast = useToast();
287
+ const handleSave = async () => {
288
+ await saveData();
289
+ toast.show({
290
+ content: '저장되었습니다.',
291
+ kind: 'success',
292
+ duration: 3000,
293
+ });
294
+ };
295
+ return <Button onClick={handleSave}>저장</Button>;
296
+ };
297
+ // ✅ Notification: useNotification() Hook으로만 사용
298
+ import { useNotification } from '@croquiscom/pds';
299
+ const MyComponent = () => {
300
+ const notification = useNotification();
301
+ const handleAction = async () => {
302
+ await processAction();
303
+ notification.show({
304
+ title: '처리 완료',
305
+ content: '작업이 성공적으로 처리되었습니다.',
306
+ });
307
+ };
308
+ return <Button onClick={handleAction}>실행</Button>;
309
+ };
310
+ ```
311
+
312
+ > **내부 동작**: `Toast`와 `Notification`은 `MessageManager`가 관리하는 별도 DOM에 주입된다. `useToast()`/`useNotification()`은 컴포넌트 함수 바디 최상위에서 호출해야 한다 (React Hook 규칙).
313
+
314
+ ### 실제 컴포넌트 코드 근거
315
+
316
+ `src/components/toast/Toast.stories.tsx`의 주석(삭제 전):
317
+
318
+ > "`\<Toast\>` JSX 직접 렌더링 금지. `useToast()` Hook 전용 API."
319
+
320
+ `src/components/notification/Notification.stories.tsx`의 주석(삭제 전):
321
+
322
+ > "`\<Notification\>` JSX 직접 렌더링 금지. `useNotification()` Hook 전용 API."
323
+
324
+ ---
325
+
326
+ ## 클래스 #7 — Named Slot Props (명시적 슬롯)
327
+
328
+ ### 무엇이 문제인가
329
+
330
+ 일부 PDS 컴포넌트는 `ReactNode` 타입의 named prop을 "슬롯"으로 사용한다. 타입은 prop 이름과 `ReactNode` 타입을 보여주지만, _어떤 prop이 슬롯이고 어떤 UI 영역을 교체하는지_는 타입만으로 자명하지 않다.
331
+
332
+ 슬롯을 모르는 에이전트가 `children`에 헤더/푸터까지 직접 구현하거나, `header` prop이 있는데도 직접 구현하는 경우가 있다.
333
+
334
+ ### 주요 Named Slot 패턴
335
+
336
+ **Modal (BasicModal)**
337
+
338
+ ```tsx
339
+ // ✅ header slot: 기본 헤더(title, backButton, closeButton) 대체
340
+ // ✅ footer slot: 기본 푸터(confirmText, cancelText 버튼) 대체
341
+ // ✅ footerExtra slot: 푸터 버튼 좌측 영역에 추가 콘텐츠 삽입
342
+ <Modal
343
+ opened={isOpen}
344
+ onCancel={handleClose}
345
+ header={<CustomHeader />} // 기본 헤더 미표시
346
+ footer={<CustomFooter />} // 기본 푸터 미표시
347
+ footerExtra={<TextButton>도움말</TextButton>} // 푸터 좌측
348
+ >
349
+ 내용
350
+ </Modal>
351
+ ```
352
+
353
+ **BottomSheet**
354
+
355
+ ```tsx
356
+ // ✅ title slot: 상단 제목 텍스트
357
+ // ✅ subTitle slot: 제목 아래 부제목 텍스트
358
+ // ✅ children: 내부 콘텐츠 (ReactNode 또는 render prop)
359
+ <BottomSheet opened={isOpen} title='시트 제목' subTitle='부제목'>
360
+ {({ close }) => <Button onClick={close}>닫기</Button>}
361
+ </BottomSheet>
362
+ ```
363
+
364
+ **Input**
365
+
366
+ ```tsx
367
+ // ✅ startAddon slot: 입력 필드 좌측 고정 요소
368
+ // ✅ endAddon slot: 입력 필드 우측 고정 요소
369
+ <Input startAddon={<span>₩</span>} endAddon={<span>.00</span>} placeholder='금액 입력' />
370
+ ```
371
+
372
+ **FormField**
373
+
374
+ ```tsx
375
+ // ✅ label: 라벨 텍스트 (string 또는 ReactNode)
376
+ // ✅ rightAddon: 라벨 우측 추가 요소
377
+ // ✅ formHelperText: 입력 하단 도움말 텍스트
378
+ <FormField
379
+ label='이름'
380
+ rightAddon={<TextButton kind='link'>선택 사항</TextButton>}
381
+ formHelperText='실명을 입력하세요'
382
+ status='error'
383
+ >
384
+ <Input placeholder='이름 입력' />
385
+ </FormField>
386
+ ```
387
+
388
+ **Popover / FloatingModal**
389
+
390
+ ```tsx
391
+ // ✅ Popover: content slot (팝오버 내부 콘텐츠)
392
+ // children은 트리거 요소 (클릭 시 팝오버 열림)
393
+ <Popover
394
+ content={<div>팝오버 내용</div>}
395
+ // 또는 render prop:
396
+ content={({ close }) => <Button onClick={close}>닫기</Button>}
397
+ >
398
+ <Button>팝오버 열기</Button>
399
+ </Popover>
400
+ // ✅ FloatingModal: trigger slot (모달을 열고 닫는 트리거)
401
+ <FloatingModal
402
+ trigger={<Button>모달 열기</Button>}
403
+ title="제목"
404
+ >
405
+ 내용
406
+ </FloatingModal>
407
+ ```
408
+
409
+ ---
410
+
411
+ ## 클래스 #8 — Negative Guidance (흔한 잘못된 패턴)
412
+
413
+ ### 무엇이 문제인가
414
+
415
+ 특정 컴포넌트는 API 형태가 직관적이지 않아 흔히 잘못 사용된다. 이 클래스는 타입 레벨에서 막기 어려운 _의미론적 오용 패턴_을 차단한다.
416
+
417
+ ---
418
+
419
+ ### 8-A. DropdownComposite: top-level options 없음
420
+
421
+ ```tsx
422
+ // ❌ DropdownComposite는 top-level options/value/onChange를 받지 않음
423
+ <DropdownComposite
424
+ options={options} // ← 이 prop 없음
425
+ value={value} // ← 이 prop 없음
426
+ onChange={setValue} // ← 이 prop 없음
427
+ />
428
+ // ✅ dropdownProps와 inputProps로 분리해서 전달
429
+ <DropdownComposite
430
+ dropdownProps={{
431
+ options: options,
432
+ value: selectedValue,
433
+ }}
434
+ inputProps={{
435
+ width: 300,
436
+ placeholder: '검색어 입력',
437
+ }}
438
+ onSubmit={({ dropdownValue }) => handleSubmit(dropdownValue)}
439
+ onReset={({ dropdownValue }) => handleReset(dropdownValue)}
440
+ />
441
+ ```
442
+
443
+ `DropdownComposite`는 Dropdown + Input의 복합 컴포넌트다. Dropdown 관련 props는 `dropdownProps`에, Input 관련 props는 `inputProps`에 전달한다.
444
+
445
+ ---
446
+
447
+ ### 8-B. Tooltip/Popover children: Fragment 금지
448
+
449
+ ```tsx
450
+ // ❌ Fragment를 children으로 전달하면 cloneElement ref 주입 실패
451
+ <Tooltip content="설명">
452
+ <>
453
+ <Button>버튼</Button>
454
+ </>
455
+ </Tooltip>
456
+ // ❌ 여러 자식 요소 나열
457
+ <Popover content={<div>내용</div>}>
458
+ <Button>버튼1</Button>
459
+ <Button>버튼2</Button> // 두 번째 요소는 트리거가 되지 않음
460
+ </Popover>
461
+ // ✅ 단일 ReactElement
462
+ <Tooltip content="설명">
463
+ <Button>버튼</Button>
464
+ </Tooltip>
465
+ <Popover content={<div>내용</div>}>
466
+ <Button>팝오버 열기</Button>
467
+ </Popover>
468
+ ```
469
+
470
+ `Tooltip`과 `Popover`는 `cloneElement`로 children에 `ref`와 이벤트 핸들러를 주입한다. children이 Fragment이거나 여러 요소면 주입이 실패한다.
471
+
472
+ ---
473
+
474
+ ### 8-C. Dropdown: value만 전달하면 read-only
475
+
476
+ ```tsx
477
+ // ❌ value만 전달하면 선택이 유지되지 않음 (onChange 없으면 내부 state 없음)
478
+ <Dropdown options={options} value={selectedValue} />
479
+ // ✅ value + onChange는 항상 함께
480
+ <Dropdown
481
+ options={options}
482
+ value={selectedValue}
483
+ onChange={(value) => setSelectedValue(value)}
484
+ />
485
+ ```
486
+
487
+ `Dropdown`은 내부 선택 state가 없다. `value`+`onChange` 없이 사용하면 옵션이 선택되지 않는 것처럼 보인다.
488
+
489
+ ---
490
+
491
+ ### 8-D. Alert/Confirm: JSX 렌더링 vs 함수 호출
492
+
493
+ ```tsx
494
+ // ❌ AlertModal/ConfirmModal JSX를 일반 모달처럼 열면 안 됨
495
+ // (기술적으로는 가능하지만 일반적이지 않다)
496
+ const [isOpen, setIsOpen] = useState(false);
497
+ <AlertModal opened={isOpen} onClose={() => setIsOpen(false)} title='알림' />;
498
+ // ✅ Alert()/Confirm() 명령형 함수 호출 권장
499
+ import { Alert, Confirm } from '@croquiscom/pds';
500
+ const handleDelete = async () => {
501
+ const confirmed = await Confirm({
502
+ title: '삭제하시겠어요?',
503
+ text: '이 작업은 되돌릴 수 없습니다.',
504
+ confirmText: '삭제',
505
+ cancelText: '취소',
506
+ });
507
+ if (confirmed) {
508
+ await deleteItem();
509
+ }
510
+ };
511
+ // Alert: await로 닫힘 후 흐름 제어 가능
512
+ await Alert({
513
+ title: '오류',
514
+ text: '처리 중 오류가 발생했습니다.',
515
+ });
516
+ ```
517
+
518
+ > `Confirm()`: `Promise\<boolean\>` — 확인 클릭 시 `true`, 취소/ESC 시 `false`. `Alert()`: `Promise\<boolean | undefined\>` — 닫힘 시 `undefined`. `onConfirm`이 `Promise\<void\>`를 반환하면 resolve 전까지 확인 버튼이 자동 `disabled` 상태가 되므로 별도 loading state를 구현할 필요가 없다.
519
+
520
+ ---
521
+
522
+ ### 8-E. tabs onChange는 사실상 필수
523
+
524
+ ```tsx
525
+ // ❌ activeTabId만 전달하고 onChange 생략 → 탭이 전환되지 않음
526
+ <LineTabs activeTabId={activeTab}>
527
+ <LineTab id="tab1">탭 1</LineTab>
528
+ <LineTab id="tab2">탭 2</LineTab>
529
+ </LineTabs>
530
+ // ✅ activeTabId + onChange 항상 함께
531
+ <LineTabs activeTabId={activeTab} onChange={setActiveTab}>
532
+ <LineTab id="tab1">탭 1</LineTab>
533
+ <LineTab id="tab2">탭 2</LineTab>
534
+ </LineTabs>
535
+ ```
536
+
537
+ `LineTabs`, `BoxTabs`, `PanelTabs`는 내부 탭 선택 state가 없다. `activeTabId`와 `onChange`는 타입상 optional이지만, 외부 제어 없이는 탭이 전환되지 않는다.
@@ -9,6 +9,8 @@
9
9
  - [Guide/Common Mistakes](resources/guides/guide-common-mistakes.md)
10
10
  - [Guide/Foundation Usage](resources/guides/guide-foundation-usage.md)
11
11
  - [Guide/Style Utilities](resources/guides/guide-style-utilities.md)
12
+ - [Guide/Component Decision Tree](resources/guides/guide-component-decision-tree.md)
13
+ - [Guide/Composition Patterns](resources/guides/guide-composition-patterns.md)
12
14
  - [Guide/Icon Unicode Lookup](resources/guides/guide-icon-unicode-lookup.md)
13
15
  - [Guide/Icon Mapping](resources/guides/guide-icon-mapping.md)
14
16
  - [Foundation/Colors](resources/components/foundation-colors.txt)