@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.
- package/package.json +1 -1
- package/pds-skills/SKILL.md +5 -3
- package/pds-skills/resources/components/component-banner.props.txt +1 -1
- package/pds-skills/resources/components/component-button-accordionbutton.props.txt +2 -2
- package/pds-skills/resources/components/component-button-button.props.txt +2 -2
- package/pds-skills/resources/components/component-button-iconbutton.props.txt +2 -2
- package/pds-skills/resources/components/component-button-popoverbutton.props.txt +2 -2
- package/pds-skills/resources/components/component-button-textbutton.props.txt +1 -1
- package/pds-skills/resources/components/component-chip-inputchip.props.txt +1 -1
- package/pds-skills/resources/components/component-chipinput.props.txt +1 -1
- package/pds-skills/resources/components/component-colorpicker.props.txt +1 -1
- package/pds-skills/resources/components/component-control-radio.props.txt +1 -1
- package/pds-skills/resources/components/component-control-radiogroup-boxradio.props.txt +1 -1
- package/pds-skills/resources/components/component-control-radiogroup-radio.props.txt +1 -1
- package/pds-skills/resources/components/component-control-segmentedcontrol.props.txt +1 -1
- package/pds-skills/resources/components/component-datepickerv2-datepickerv2.props.txt +1 -1
- package/pds-skills/resources/components/component-dropdown.props.txt +1 -1
- package/pds-skills/resources/components/component-dropdownfilter.props.txt +2 -2
- package/pds-skills/resources/components/component-dropdowninput.props.txt +3 -3
- package/pds-skills/resources/components/component-dropdownmultiselect.props.txt +3 -3
- package/pds-skills/resources/components/component-input.props.txt +1 -1
- package/pds-skills/resources/components/component-inputstepper.props.txt +1 -1
- package/pds-skills/resources/components/component-modal-alertmodal.props.txt +1 -1
- package/pds-skills/resources/components/component-modal-basicmodal.props.txt +2 -2
- package/pds-skills/resources/components/component-modal-confirmmodal.props.txt +4 -4
- package/pds-skills/resources/components/component-modal-floatingmodal.props.txt +2 -2
- package/pds-skills/resources/components/component-numericinput.props.txt +2 -2
- package/pds-skills/resources/components/component-textarea.props.txt +1 -1
- package/pds-skills/resources/components/component-timepicker.props.txt +1 -1
- package/pds-skills/resources/components/component-timerangepicker.props.txt +4 -4
- package/pds-skills/resources/components/component-upload-fileuploadbutton.props.txt +1 -1
- package/pds-skills/resources/components/guide-component-decision-tree.txt +84 -0
- package/pds-skills/resources/components/guide-composition-patterns.txt +537 -0
- package/pds-skills/resources/components/intro.txt +1 -1
- package/pds-skills/resources/guides/guide-component-decision-tree.md +84 -0
- package/pds-skills/resources/guides/guide-composition-patterns.md +537 -0
- 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이지만, 외부 제어 없이는 탭이 전환되지 않는다.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Component Decision Tree
|
|
2
|
+
|
|
3
|
+
유사한 역할의 PDS 컴포넌트 중 어느 것을 써야 하는지의 판단 기준을 모은 가이드. prop/API 비교가 아니라 **사용 상황과 의도** 기준으로 정리한다.
|
|
4
|
+
|
|
5
|
+
> 컴포넌트의 prop 정의·타입·기본값은 **Guide / Composition Patterns** 페이지와 각 컴포넌트의 `.props.txt`(Tier 1)를 참조하라. 본 가이드는 _어느 컴포넌트를 쓸지_만 다룬다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 단일 선택 — `Radio` / `RadioGroup` / `BoxRadioGroup` / `SegmentedControl`
|
|
10
|
+
|
|
11
|
+
모두 단일 선택이지만 레이아웃 방향과 선택이 즉시 반영되느냐가 다르다. `Switch`는 on/off 이진 상태 전환이라 이 그룹과 목적이 달라 별도 취급한다.
|
|
12
|
+
|
|
13
|
+
| 판단 기준 | `Radio` / `RadioGroup` | `BoxRadioGroup` | `SegmentedControl` | | ----------------------- | ---------------------- | --------------- | ------------------- | | 레이아웃 방향 | 가로 / 세로 | 가로 / 그리드 | 가로 고정 | | 옵션 수 | 제한 없음 | 2~6개 권장 | 2~5개 | | 선택 결과 반영 시점 | 폼 제출 후 | 폼 제출 후 | 즉각 반영 (뷰 전환) | | 아이콘/이미지가 포함된 옵션 | 불가 | 불가 | 가능 | | 옵션 helper text | 가능 | 가능 | 불가 | | 주요 사용 위치 | 폼, 설정 | 필터, 옵션 선택 | 뷰 전환, 탭 대체 |
|
|
14
|
+
|
|
15
|
+
**선택 가이드**
|
|
16
|
+
|
|
17
|
+
- 폼 안에서 가로/세로 배치로 단일 선택 → `Radio` / `RadioGroup`
|
|
18
|
+
- 선택지를 카드형으로 시각 강조하거나 가로 배치 → `BoxRadioGroup`
|
|
19
|
+
- 선택 즉시 콘텐츠가 전환되어야 할 때 → `SegmentedControl`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. 탭 — `LineTabs` / `PanelTabs` / `BoxTabs`
|
|
24
|
+
|
|
25
|
+
탭 컴포넌트는 사용 위계와 시각적 강조 수준으로 구분한다.
|
|
26
|
+
|
|
27
|
+
| 판단 기준 | `LineTabs` | `PanelTabs` | `BoxTabs` | | ----------------- | ----------- | ----------- | ---------------------------- | | 사용 위계 | 페이지 레벨 | 섹션 레벨 | 섹션 레벨 | | 스타일 | 밑줄 | 패널 배경 | 박스형 배경 | | 상단 고정(sticky) | 가능 | 불가 | 불가 | | 탭 수 | 2~7개 | 2~8개 | 제한 없음 (사용자 추가 가능) |
|
|
28
|
+
|
|
29
|
+
**선택 가이드**
|
|
30
|
+
|
|
31
|
+
- 페이지 전체의 주요 네비게이션 → `LineTabs`
|
|
32
|
+
- 카드/패널 내부 콘텐츠 전환 → `PanelTabs`
|
|
33
|
+
- 탭 자체를 버튼처럼 강조해야 할 때, 또는 사용자가 직접 탭을 추가하는 경우 → `BoxTabs`
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. 보조 정보 표시 — `Tooltip` / `Popover`
|
|
38
|
+
|
|
39
|
+
두 컴포넌트 모두 hover와 click 트리거를 지원한다. 구분 기준은 트리거 방식이 아니라 **콘텐츠 복잡도**다.
|
|
40
|
+
|
|
41
|
+
| 판단 기준 | `Tooltip` | `Popover` | | ---------------------------- | -------------------------- | ------------------------- | | 트리거 | hover / click 모두 지원 | hover / click 모두 지원 | | 콘텐츠 타입 | 텍스트만 | 텍스트 + 복합 콘텐츠 가능 | | 내부 인터랙션 (버튼·링크 등) | 불가 | 가능 | | 레이아웃 옵션 | 단일 | short form / long form | | `kind` (의미적 변형) | neutral / accent / negative | — | | 모바일 사용 | 가능 | 가능 |
|
|
42
|
+
|
|
43
|
+
**선택 가이드**
|
|
44
|
+
|
|
45
|
+
- 짧은 텍스트 정보만 표시하고 내부 인터랙션이 필요 없을 때 → `Tooltip`
|
|
46
|
+
- 버튼·링크·복합 콘텐츠가 포함될 때 → `Popover`
|
|
47
|
+
- 모바일 환경에서 추가 정보가 필요하면 `Tooltip`보다 `Popover` 사용 (모바일은 hover가 안정적이지 않다)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 4. Modal 계열 — 유형 선택
|
|
52
|
+
|
|
53
|
+
PDS의 모달은 5개 유형으로 구성된다. 각 유형은 **사용 조건**으로 구분한다.
|
|
54
|
+
|
|
55
|
+
| 유형 | 컴포넌트 / 호출 | 주요 목적 | 버튼 구성 | | -------- | ------------------------------------------------------------ | ----------------------------------- | ----------- | | Alert | `await Alert(\{...\})` (`AlertModal` 함수형 호출) | 오류·경고 등 즉각 인지 필요 | 단일 확인 | | Confirm | `await Confirm(\{...\})` (`ConfirmModal` 함수형 호출) | 사용자 의사 결정 요청 | 취소 + 확인 | | Basic | `\<Modal opened=\{...\} onCancel=\{...\}\>` (JSX 직접) | 범용 콘텐츠 표시 | 상황에 따라 | | Notice | `\<NoticeModal opened=\{...\} onCancel=\{...\}\>` (JSX 직접) | 공지·프로모션 등 이미지가 포함된 콘텐츠 | 상황에 따라 | | Floating | `\<FloatingModal trigger=\{...\}\>` (JSX 직접, 트리거 기반) | 화면 일부 위에 떠있는 형태 | 상황에 따라 |
|
|
56
|
+
|
|
57
|
+
**유형 선택 가이드**
|
|
58
|
+
|
|
59
|
+
| 유형 | 사용 조건 | Do | Don't | | ------- | ---------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- | | Alert | 특정 행동의 결과를 알릴 때 | 필요한 최소한의 기능만 담는다. 오류는 `kind="error"` 사용 | "확인 / 닫기 / X" 같은 동일 기능 버튼 중복. title과 heading에 동일 정보 중복 | | Confirm | 행동에 대한 의사를 물어볼 때 | 필요한 최소한의 기능만 담는다 | "취소 / X" 동일 기능 버튼 중복. 불필요한 title 사용 | | Basic | 항목의 상세 내역을 보여줄 때 | contents area에 정보를 자유롭게 담되 정해진 스펙 안에서 | 정해진 스펙 외 임의 커스텀(X 버튼 제거 등) |
|
|
60
|
+
|
|
61
|
+
**`Modal` (Basic) 사이즈 가이드**
|
|
62
|
+
|
|
63
|
+
| 사이즈 | 너비 | 사용 시점 | | -------- | ------ | --------------------------------------------------- | | `xlarge` | 1280px | 좌우에 테이블이 2개 있는 등 확장된 형태가 필요할 때 | | `large` | 900px | 조회용 테이블이 있는 경우 | | `medium` | 720px | 달력·작은 테이블·이미지 등이 들어갈 때 | | `small` | 480px | 간단한 정보를 입력해야 할 때 | | `xsmall` | 375px | 모바일 미리보기를 보여줄 때 |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 5. Modal 계열 — 오버레이 유형 선택
|
|
68
|
+
|
|
69
|
+
같은 오버레이라도 `Modal` / `BottomSheet` / `Drawer`는 진입 방향과 플랫폼 맥락이 다르다.
|
|
70
|
+
|
|
71
|
+
| 판단 기준 | `Modal` | `BottomSheet` | `Drawer` | | ----------- | ------------------------------------------- | --------------------- | ---------------------- | | 주 플랫폼 | 웹 / 데스크톱 | 모바일 | 웹 / 데스크톱 | | 진입 방향 | 중앙 팝업 | 하단에서 위로 | 좌/우 측면 | | 주요 목적 | 확인 / 경고 / 정보 표시 | 추가 옵션 / 상세 정보 | 네비게이션 / 상세 패널 | | 콘텐츠 길이 | 짧음 권장 | 스크롤 가능 | 스크롤 가능 | | 변형 유형 | Alert / Confirm / Basic / Notice / Floating | regular / full | — | | Backdrop | 항상 | 항상 | 항상 |
|
|
72
|
+
|
|
73
|
+
**선택 가이드**
|
|
74
|
+
|
|
75
|
+
- 사용자의 즉각적인 확인/결정이 필요한 경우 → `Modal` (또는 함수형 `Alert()` / `Confirm()`)
|
|
76
|
+
- 모바일에서 하단으로 추가 정보나 옵션을 제공할 때 → `BottomSheet`
|
|
77
|
+
- 측면에서 슬라이드로 펼쳐 네비게이션이나 상세를 보여줄 때 → `Drawer`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 참고 가이드
|
|
82
|
+
|
|
83
|
+
- 합성 패턴 (portal·slots·subcomponent 등): **Guide / Composition Patterns** 페이지
|
|
84
|
+
- prop 정의·타입·기본값: 각 컴포넌트의 `resources/components/component-\{slug\}.props.txt`
|