# PDS 합성 패턴 가이드

PDS를 사용하는 에이전트가 TypeScript type/JSDoc만으로는 파악하기 어려운 컴포넌트 합성 제약과 잘못된 사용 패턴을 8개 클래스로 정리한다.

> **이 가이드에 없는 것**: prop 이름, optional/required 여부, default value, type. 이런 정보는 `*.props.txt` 파일의 Props 표에서 읽어라. 이 가이드는 type/JSDoc만으로 자명하지 않은 _합성 규칙_만 다룬다.

---

## 클래스 #1 — Flat API (dot-notation 없음)

### 무엇이 문제인가

React 컴포넌트 라이브러리 중 일부(Radix UI, ShadCN, Chakra 등)는 `\<Modal.Body\>`, `\<Menu.Item\>`, `\<Tabs.Tab\>` 같은 dot-notation compound 패턴을 쓴다. PDS는 이 패턴을 **사용하지 않는다**. 자식 컴포넌트는 항상 별도의 named export다.

### 잘못된 패턴

```tsx

```

### 올바른 PDS 패턴

```tsx

```

### 실제 컴포넌트 코드 근거

`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로 붙여 둔 하위 컴포넌트는 소스 어디에도 없다.

---

## 클래스 #2 — requiredParent (context throw)

### 무엇이 문제인가

일부 PDS 컴포넌트는 특정 부모 컴포넌트의 Context 없이 단독으로 사용하면 런타임 오류가 발생한다. TypeScript 타입은 이 제약을 표현하지 못한다.

### 잘못된 패턴

```tsx

```

### 올바른 PDS 패턴

| 자식 컴포넌트 | 필수 부모 | 오류 메시지 | | ----------------------- | ----------- | -------------------------------- | | `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` |

`PanelTabGroup`은 context를 직접 소비하지 않아 단독 사용이 가능하지만, `PanelTabs` 안에서 그룹화 역할로 쓰는 것이 설계 의도다(`src/components/tabs/PanelTabs/PanelTabGroup.tsx` 확인).

```tsx

```

---

## 클래스 #3 — Portal 위치 (FloatingPortal)

### 무엇이 문제인가

일부 PDS 컴포넌트는 `FloatingPortal`을 통해 `document.body`가 아닌 `id="pds-floating-root"` DOM 노드에 렌더링된다. 이 노드가 없으면 컴포넌트가 `document.body`에 폴백 렌더링될 수 있다.

에이전트가 이 동작을 모르고 z-index 조정이나 CSS overflow 처리 시 "팝업이 특정 컨테이너 밖으로 나온다"는 예상 동작을 이해하지 못하는 경우가 있다. Portal 노드를 수동으로 생성하려 하거나, `FloatingTree`를 추가로 선언해야 한다고 오해하는 경우도 있다.

### Portal 사용 컴포넌트 목록

| 컴포넌트 | 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`에 렌더링 |

### 올바른 PDS 패턴

```tsx

```

### 실제 컴포넌트 코드 근거

`src/constants/floating.ts`의 `FLOATING_ROOT_ID = 'pds-floating-root'` 상수를 `BottomSheet.tsx`, `Drawer.tsx`, `ModalOverlay.tsx`, `Popover.tsx`, `Tooltip.tsx`, `DropdownOptions.tsx` 등이 `FloatingPortal`에 전달한다.

---

## 클래스 #4 — asChild 패턴 (Radix Slot)

### 무엇이 문제인가

Button 계열 컴포넌트에는 `asChild` prop이 있다. `asChild=true`로 설정하면 `\<button\>` DOM 요소를 렌더링하지 않고 children의 단일 ReactElement에 모든 props와 이벤트 핸들러를 전달한다(Radix Slot 패턴).

에이전트가 이 패턴을 모르고 `\<a\>` 태그 링크 버튼을 만들 때 `\<Button\>` 안에 `\<a\>`를 중첩(button 안의 a — HTML 비유효)하는 경우가 있다. 커스텀 컴포넌트에 버튼 스타일을 적용하는 방법을 찾지 못하는 경우도 있다.

### asChild 지원 컴포넌트

`Button`, `TextButton`, `IconButton`, `FloatingButton`, `AccordionButton`, `PopoverButton`

### 잘못된 패턴

```tsx

```

### 올바른 PDS 패턴

```tsx

```

> **주의**: `asChild=true` 시 children은 반드시 단일 ReactElement여야 한다. Fragment(`\<\>`)나 여러 요소를 전달하면 Slot이 올바르게 작동하지 않는다.

### 실제 컴포넌트 코드 근거

`src/components/button/Button.tsx:71` — `const Comp = asChild ? Slot : 'button';` `src/components/button/types.ts:20` — `asChild?: boolean;` `@radix-ui/react-slot`의 `Slot` 컴포넌트를 사용한다.

---

## 클래스 #5 — directChildInjection (FormField의 cloneElement)

### 무엇이 문제인가

`FormField`는 `status` prop을 **React Context가 아니라 `cloneElement`로** 직접 자식에 주입한다. 이 메커니즘은 children이 Fragment(`\<\>`) 또는 래퍼 div 안에 있으면 작동하지 않는다.

에이전트가 이 제약을 모르고 children을 Fragment나 래퍼로 감싸는 경우가 있다. `DatePicker` 같이 주입 대상이 아닌 컴포넌트도 자동으로 status를 받는다고 가정하는 경우도 있다.

### status 자동 주입 대상 컴포넌트

`Input`, `Dropdown`, `DropdownInput`, `NumericInput` (총 4개)

### 잘못된 패턴

```tsx

```

### 올바른 PDS 패턴

```tsx

```

### 실제 컴포넌트 코드 근거

`src/components/form/FormField.tsx:60–80` —

```javascript

```

---

## 클래스 #6 — renderForbidden (Hook-only API)

### 무엇이 문제인가

`Toast`와 `Notification`은 JSX 선언형 렌더링이 **금지**된다. TypeScript 타입 정의가 있고 Storybook에도 등장하지만, 이는 내부 구조 문서화용이지 직접 렌더링을 허용하는 것이 아니다.

에이전트가 `\<Toast content="..." /\>` 또는 `\<Notification title="..." /\>`를 JSX로 직접 렌더링하는 코드를 작성하는 경우가 있다.

### 잘못된 패턴

```tsx

```

### 올바른 PDS 패턴

```tsx

```

> **내부 동작**: `Toast`와 `Notification`은 `MessageManager`가 관리하는 별도 DOM에 주입된다. `useToast()`/`useNotification()`은 컴포넌트 함수 바디 최상위에서 호출해야 한다 (React Hook 규칙).

### 실제 컴포넌트 코드 근거

`src/components/toast/Toast.stories.tsx`의 주석(삭제 전):

> "`\<Toast\>` JSX 직접 렌더링 금지. `useToast()` Hook 전용 API."

`src/components/notification/Notification.stories.tsx`의 주석(삭제 전):

> "`\<Notification\>` JSX 직접 렌더링 금지. `useNotification()` Hook 전용 API."

---

## 클래스 #7 — Named Slot Props (명시적 슬롯)

### 무엇이 문제인가

일부 PDS 컴포넌트는 `ReactNode` 타입의 named prop을 "슬롯"으로 사용한다. 타입은 prop 이름과 `ReactNode` 타입을 보여주지만, _어떤 prop이 슬롯이고 어떤 UI 영역을 교체하는지_는 타입만으로 자명하지 않다.

슬롯을 모르는 에이전트가 `children`에 헤더/푸터까지 직접 구현하거나, `header` prop이 있는데도 직접 구현하는 경우가 있다.

### 주요 Named Slot 패턴

**Modal (BasicModal)**

```tsx

```

**BottomSheet**

```tsx

```

**Input**

```tsx

```

**FormField**

```tsx

```

**Popover / FloatingModal**

```tsx

```

---

## 클래스 #8 — Negative Guidance (흔한 잘못된 패턴)

### 무엇이 문제인가

특정 컴포넌트는 API 형태가 직관적이지 않아 흔히 잘못 사용된다. 이 클래스는 타입 레벨에서 막기 어려운 _의미론적 오용 패턴_을 차단한다.

---

### 8-A. DropdownComposite: top-level options 없음

```tsx

```

`DropdownComposite`는 Dropdown + Input의 복합 컴포넌트다. Dropdown 관련 props는 `dropdownProps`에, Input 관련 props는 `inputProps`에 전달한다.

---

### 8-B. Tooltip/Popover children: Fragment 금지

```tsx

```

`Tooltip`과 `Popover`는 `cloneElement`로 children에 `ref`와 이벤트 핸들러를 주입한다. children이 Fragment이거나 여러 요소면 주입이 실패한다.

---

### 8-C. Dropdown: value만 전달하면 read-only

```tsx

```

`Dropdown`은 내부 선택 state가 없다. `value`+`onChange` 없이 사용하면 옵션이 선택되지 않는 것처럼 보인다.

---

### 8-D. Alert/Confirm: JSX 렌더링 vs 함수 호출

```tsx

```

> `Confirm()`: `Promise\<boolean\>` — 확인 클릭 시 `true`, 취소/ESC 시 `false`. `Alert()`: `Promise\<boolean | undefined\>` — 닫힘 시 `undefined`. `onConfirm`이 `Promise\<void\>`를 반환하면 resolve 전까지 확인 버튼이 자동 `disabled` 상태가 되므로 별도 loading state를 구현할 필요가 없다.

---

### 8-E. tabs onChange는 사실상 필수

```tsx

```

`LineTabs`, `BoxTabs`, `PanelTabs`는 내부 탭 선택 state가 없다. `activeTabId`와 `onChange`는 타입상 optional이지만, 외부 제어 없이는 탭이 전환되지 않는다.