portal-foundation 기본 컴포넌트 라이브러리 설계
1. 설계 목적
portal-foundation은 포털 애플리케이션의 공통 레이아웃과 콘텐츠 표현 규칙을 제공하는 React·TypeScript 기반 UI 라이브러리다. 소비 포털은 업무 데이터와 화면 조합을 소유하고, portal-foundation은 공통 구조, 시각 토큰, 상호작용, 접근성, 상태 표현을 소유한다.
소비 포털은 무엇을 보여줄지와 어떻게 조합할지를 결정한다.
portal-foundation은 그것이 어떤 모양과 동작으로 표현될지를 결정한다.
이 원칙에 따라 라이브러리는 다음 세 계층으로 구성한다.
| 계층 | 책임 | 소비 포털의 자유도 |
|---|---|---|
| Foundation | 토큰, 타이포그래피, 아이콘, 공통 상태, 접근성 기반 | 사용하지 않거나 조합만 가능 |
| Shell | 상단바, 사이드바, 검색, 사용자 메뉴, 콘텐츠 프레임 | 메뉴와 데이터만 주입 |
| Content | 버튼, 폼, 테이블, 트리, 카드, 탭, 모달, 상태 화면 | 배치와 업무 데이터 조합 |
Shell의 높이, 폭, 스크롤, 검색 위치, 사용자 메뉴 위치는 소비 포털이 변경할 수 없다. 콘텐츠 영역의 배치와 업무별 전용 컴포넌트는 소비 포털이 소유하되, 표현은 표준 Content 컴포넌트를 우선 사용한다.
2. 패키지 구조
초기 구현은 현재 workspace 구조를 유지하면서 기능 경계를 명확히 한다. 파일을 지나치게 작은 패키지로 분리하지 않고, 공개 API는 ui-react의 진입점에서 제공한다.
portal-foundation/
├─ packages/
│ ├─ design-tokens/
│ │ └─ src/
│ │ ├─ index.ts
│ │ ├─ manifest.ts
│ │ ├─ colors.ts
│ │ ├─ typography.ts
│ │ └─ components.ts
│ ├─ theme-client/
│ │ └─ src/index.tsx
│ └─ ui-react/
│ ├─ src/
│ │ ├─ index.tsx # 공개 API와 legacy 호환 export
│ │ ├─ PortalShell.tsx # 공통 Shell
│ │ ├─ shell/
│ │ │ ├─ PortalTopNav.tsx
│ │ │ ├─ PortalSidebar.tsx
│ │ │ └─ PortalMain.tsx
│ │ ├─ content/
│ │ │ ├─ PortalPage.tsx
│ │ │ ├─ PortalButton.tsx
│ │ │ ├─ PortalForm.tsx
│ │ │ ├─ PortalDataTable.tsx
│ │ │ ├─ PortalTree.tsx
│ │ │ ├─ PortalCard.tsx
│ │ │ ├─ PortalTabs.tsx
│ │ │ ├─ PortalDialog.tsx
│ │ │ └─ PortalState.tsx
│ │ ├─ icons/
│ │ └─ hooks/
│ └─ styles/
│ ├─ tokens.css
│ ├─ styles.css
│ └─ content.css
└─ docs/
└─ component-library-design.mdindex.tsx는 공개 API의 단일 진입점이다. 소비 포털은 내부 경로를 직접 import하지 않는다. 내부 컴포넌트의 파일명과 구현은 호환성 계약이 아니며, 공개 타입과 props만 호환성 계약으로 취급한다.
3. 공개 API 원칙
모든 컴포넌트는 다음 규칙을 따른다.
| 규칙 | 설계 기준 |
|---|---|
| HTML 확장 | 가능한 경우 대응 HTML 속성을 ...props로 전달한다. |
| className | 외부 className은 허용하되 구조를 바꾸는 override는 금지한다. |
| variant | 문자열 자유 입력이 아니라 제한된 union type을 사용한다. |
| 상태 | loading, disabled, error, empty를 명시적 props로 표현한다. |
| 이벤트 | 이벤트 callback은 변경된 값과 원본 이벤트를 구분해 제공한다. |
| 접근성 | 이름, 역할, 상태, 키보드 동작을 컴포넌트가 보장한다. |
| 제어 방식 | 복합 컴포넌트는 controlled와 기본 uncontrolled 방식을 모두 지원한다. |
| 스타일 | 임의 CSS 문자열, inline style 토큰 주입, raw HTML은 공개 API로 제공하지 않는다. |
기본 공통 타입은 다음과 같다.
import type { ReactNode } from "react";
export type PortalSize = "sm" | "md" | "lg";
export type PortalDensity = "compact" | "comfortable";
export type PortalTone = "neutral" | "info" | "success" | "warning" | "danger";
export type PortalInteractiveState = {
disabled?: boolean;
loading?: boolean;
readOnly?: boolean;
};
export type PortalCommonProps = {
className?: string;
children?: ReactNode;
testId?: string;
};4. Foundation 계층
4.1 Theme 계약
테마는 공통 UI의 시각 토큰과 제한된 variant를 버전 단위로 묶는다. 포털명, 로고, 메뉴, API, 업무 콘텐츠는 테마에 포함하지 않는다.
export type PortalThemeDefinition = {
manifestVersion: "1";
themeKey: string;
displayName: string;
version: string;
foundationRange: string;
preference: {
accent?: "terracotta" | "evergreen" | "cobalt" | "violet";
background?: "warm" | "mist" | "sky" | "lavender";
mode?: "light" | "dark";
heroVisible?: boolean;
};
tokens?: Record<`--pf-${string}`, string | number>;
componentVariants?: {
button?: "solid" | "outline" | "ghost";
panel?: "quiet" | "bordered" | "raised";
statusBadge?: "soft" | "solid";
table?: "plain" | "striped";
tree?: "line" | "none";
};
metadata?: {
description?: string;
author?: string;
previewImageUrl?: string;
};
};Shell geometry는 theme token으로 노출하지 않는다. 예를 들어 상단바 높이, 접힌 사이드바 폭, active indicator 두께, 검색창 구조는 portal-foundation의 고정 규칙이다.
콘텐츠 프레임(플레인 타입) — 관리자 메뉴 화면의 우측 프레임 레이아웃은 셸이 소유한다. PortalShell / PortalAdminShell에 contentVariant="plain"을 주면 프레임이 플레인 타입으로 그려진다 — 카드 보더·그림자·둥근 모서리를 지우고, 붙어 있는 구간을 헤어라인과 16px 리듬(--pf-section-gap)으로만 구분한다. 규칙은 PortalPanel variant="plain"과 같고, 프레임 안의 AdminSection·PortalPanel·PortalCard가 자동으로 이 규칙을 따른다(소비 앱은 메뉴마다 레이아웃을 다시 짜지 않는다). 기본값 "default"는 기존 카드형 레이아웃이다.
4.2 Typography
export type PortalTextProps = PortalCommonProps & {
as?: "span" | "p" | "strong" | "label";
size?: "xs" | "sm" | "md" | "lg" | "xl";
weight?: "regular" | "medium" | "semibold" | "bold";
tone?: "default" | "muted" | "subtle" | "danger";
};
export function PortalText(props: PortalTextProps): JSX.Element;
export function PortalHeading(props: PortalCommonProps & {
level?: 1 | 2 | 3 | 4;
size?: "page" | "section" | "subsection";
}): JSX.Element;Shell의 포털명과 콘텐츠 제목은 현재 기준인 20px을 기본값으로 사용한다. 콘텐츠 내부 제목은 page, section, subsection 계층으로 표현하며 소비 포털이 임의의 픽셀 값을 전달하지 않도록 한다.
4.3 Icon
아이콘은 문자열 SVG를 직접 전달하는 방식보다 등록된 아이콘 이름과 표준 크기를 우선한다.
export type PortalIconName =
| "search" | "menu" | "chevron-down" | "chevron-right"
| "plus" | "edit" | "trash" | "settings" | "logout"
| "folder" | "file" | "user" | "users" | "check" | "close";
export type PortalIconProps = {
name: PortalIconName;
size?: 14 | 16 | 18 | 20 | 24;
label?: string;
className?: string;
};
export function PortalIcon(props: PortalIconProps): JSX.Element;장식 아이콘은 aria-hidden="true"로 처리하고, 의미 있는 아이콘은 label 또는 주변 텍스트로 접근 가능한 이름을 제공한다.
5. Shell 계층
5.1 PortalShell
export type PortalShellNavItem =
| PortalNavigationLeaf
| PortalNavigationGroup;
export type PortalShellProps = {
productName: ReactNode;
productMark?: ReactNode;
navigationItems?: PortalShellNavItem[];
activeNavigationId?: string;
userProfile?: PortalUserProfile;
accountMenuItems?: PortalAccountMenuItem[];
onAccountMenuItemSelect?: (item: PortalAccountMenuItem) => void;
onLogout?: () => void;
logoutPending?: boolean;
locale?: "ko" | "en";
onLocaleChange?: (locale: "ko" | "en") => void;
searchValue?: string;
onSearchChange?: (value: string) => void;
onSearchSubmit?: (value: string) => void;
searchPlaceholder?: string;
showSearch?: boolean;
sidebarHeader?: ReactNode;
sidebarFooter?: ReactNode;
sidebarOpen?: boolean;
defaultSidebarOpen?: boolean;
onSidebarOpenChange?: (open: boolean) => void;
mainHeader?: ReactNode;
children: ReactNode;
className?: string;
};PortalShell은 상단바, 사이드바, 콘텐츠 프레임을 조합한다. 사이드바는 펼침·접힘을 공통으로 관리하며, 접힌 상태에서는 아이콘만 남기고 메뉴명을 tooltip으로 제공한다. 콘텐츠만 세로 스크롤하고 상단바와 사이드바는 viewport에 고정한다.
5.2 Navigation
export type PortalNavigationLeaf = {
id: string;
label: ReactNode;
icon?: ReactNode;
href?: string;
active?: boolean;
headerTitle?: ReactNode;
hero?: PortalHeroProps;
onSelect?: () => void;
target?: string;
rel?: string;
};
export type PortalNavigationGroup = {
id: string;
label: ReactNode;
icon?: ReactNode;
children: PortalNavigationLeaf[];
defaultExpanded?: boolean;
};메뉴 아이콘의 비활성 색상, active 색상, active 하단 indicator는 Shell CSS가 소유한다. 소비 포털은 메뉴별 색상 className을 전달할 수 없다.
5.3 User profile and settings
export type PortalUserProfile = {
name: string;
displayName?: string;
email?: string;
role?: string;
roleLabel?: string;
avatarUrl?: string;
};
export type PortalAccountMenuItem = {
id: string;
label: ReactNode;
icon?: ReactNode;
href?: string;
disabled?: boolean;
tone?: "default" | "danger";
dividerBefore?: boolean;
onSelect?: () => void;
};사용자 메뉴는 공통적으로 계정 메뉴, 로그아웃, 화면 설정을 제공한다. 화면 설정은 theme-client의 setMode와 setHeroVisible을 사용하며, 포털별로 별도 구현하지 않는다.
6. Content 계층
6.1 Page and layout
export type PortalPageProps = PortalCommonProps & {
title?: ReactNode;
description?: ReactNode;
actions?: ReactNode;
breadcrumbs?: PortalBreadcrumbItem[];
density?: PortalDensity;
};
export type PortalBreadcrumbItem = {
id: string;
label: ReactNode;
href?: string;
current?: boolean;
};
export function PortalPage(props: PortalPageProps): JSX.Element;
export function PortalPageHeader(props: {
title: ReactNode;
description?: ReactNode;
actions?: ReactNode;
breadcrumbs?: PortalBreadcrumbItem[];
}): JSX.Element;
export function PortalToolbar(props: PortalCommonProps & {
filters?: ReactNode;
actions?: ReactNode;
align?: "start" | "between";
}): JSX.Element;
export function PortalStack(props: PortalCommonProps & {
direction?: "row" | "column";
gap?: 1 | 2 | 3 | 4 | 6 | 8;
align?: "start" | "center" | "stretch";
}): JSX.Element;페이지 헤더의 제목 계층과 toolbar 간격은 표준화한다. 업무별 페이지는 콘텐츠 순서와 배치만 결정한다.
6.2 Button
export type PortalButtonProps =
React.ButtonHTMLAttributes<HTMLButtonElement> &
PortalCommonProps & PortalInteractiveState & {
variant?: "primary" | "secondary" | "outline" | "ghost" | "danger";
size?: "sm" | "md" | "lg";
icon?: ReactNode;
iconPosition?: "start" | "end";
fullWidth?: boolean;
};
export function PortalButton(props: PortalButtonProps): JSX.Element;버튼은 업무 의미가 아닌 동작의 중요도로 variant를 선택한다. destructive 동작은 danger를 사용하며, loading 상태에서는 중복 제출을 막고 accessible name을 유지한다.
6.3 Form controls
export type PortalFieldProps = PortalCommonProps & {
id: string;
label: ReactNode;
required?: boolean;
description?: ReactNode;
error?: ReactNode;
};
export type PortalTextFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> &
PortalFieldProps & { size?: PortalSize };
export type PortalSelectOption = {
value: string;
label: ReactNode;
disabled?: boolean;
};
export type PortalSelectProps = PortalFieldProps & {
value?: string;
defaultValue?: string;
options: PortalSelectOption[];
placeholder?: string;
onValueChange?: (value: string) => void;
};
export function PortalTextField(props: PortalTextFieldProps): JSX.Element;
export function PortalTextArea(props: PortalFieldProps & React.TextareaHTMLAttributes<HTMLTextAreaElement>): JSX.Element;
export function PortalSelect(props: PortalSelectProps): JSX.Element;
export function PortalCheckbox(props: PortalFieldProps & { checked?: boolean; onCheckedChange?: (checked: boolean) => void }): JSX.Element;
export function PortalRadioGroup(props: PortalFieldProps & { value?: string; options: PortalSelectOption[]; onValueChange?: (value: string) => void }): JSX.Element;모든 field는 label, description, error를 연결하는 aria-describedby와 aria-invalid를 자동으로 관리한다. 소비 포털은 label 없는 입력창을 만들지 않는다.
6.4 Data table
export type PortalTableColumn<T> = {
id: string;
header: ReactNode;
width?: string;
align?: "start" | "center" | "end";
sortable?: boolean;
cell?: (row: T, index: number) => ReactNode;
};
export type PortalDataTableProps<T> = PortalCommonProps & {
columns: PortalTableColumn<T>[];
rows: T[];
getRowId: (row: T) => string;
selectedRowIds?: string[];
onRowSelect?: (row: T) => void;
onSelectionChange?: (rowIds: string[]) => void;
sort?: { columnId: string; direction: "asc" | "desc" };
onSortChange?: (sort: PortalDataTableProps<T>["sort"]) => void;
loading?: boolean;
emptyState?: ReactNode;
density?: PortalDensity;
selectable?: boolean;
};
export function PortalDataTable<T>(props: PortalDataTableProps<T>): JSX.Element;테이블은 행 식별자, 정렬, 선택, loading, empty 상태를 명시적으로 지원한다. 페이지네이션과 서버 정렬은 데이터 fetching layer가 담당하고, 테이블은 상태와 callback만 표현한다.
6.5 Tree
트리는 조직, 디렉터리, 문서, 권한 계층처럼 계층 데이터가 필요한 화면에서 사용한다.
export type PortalTreeNode = {
id: string;
label: ReactNode;
icon?: ReactNode;
children?: PortalTreeNode[];
disabled?: boolean;
loading?: boolean;
};
export type PortalTreeProps = PortalCommonProps & {
nodes: PortalTreeNode[];
expandedIds?: string[];
defaultExpandedIds?: string[];
selectedId?: string;
onExpandedChange?: (ids: string[]) => void;
onSelect?: (node: PortalTreeNode) => void;
density?: PortalDensity;
connector?: "line" | "none";
loading?: boolean;
emptyState?: ReactNode;
};
export function PortalTree(props: PortalTreeProps): JSX.Element;트리는 role="tree", 노드는 role="treeitem", 하위 목록은 role="group"을 사용한다. 방향키, Enter, Space, Home, End의 키보드 동작을 표준화하며, 선택 상태와 펼침 상태는 aria 속성으로 노출한다. 포털은 들여쓰기 폭, 연결선, 펼침 아이콘의 CSS를 직접 바꾸지 않는다.
6.6 Panel, card, tabs
export type PortalPanelProps = PortalCommonProps & {
title?: ReactNode;
description?: ReactNode;
actions?: ReactNode;
variant?: "quiet" | "bordered" | "raised";
padding?: "none" | "sm" | "md" | "lg";
};
export function PortalPanel(props: PortalPanelProps): JSX.Element;
export function PortalCard(props: PortalPanelProps & { interactive?: boolean; onClick?: () => void }): JSX.Element;
export type PortalTab = { id: string; label: ReactNode; disabled?: boolean };
export function PortalTabs(props: PortalCommonProps & {
tabs: PortalTab[];
value?: string;
defaultValue?: string;
onValueChange?: (id: string) => void;
}): JSX.Element;Panel과 Card는 콘텐츠 배치의 기본 표면이다. 색상, border, radius, shadow는 theme token을 사용한다.
6.7 Dialog, drawer, notification
export type PortalDialogProps = PortalCommonProps & {
open: boolean;
title: ReactNode;
description?: ReactNode;
onOpenChange: (open: boolean) => void;
size?: "sm" | "md" | "lg";
actions?: ReactNode;
};
export function PortalDialog(props: PortalDialogProps): JSX.Element;
export function PortalDrawer(props: PortalDialogProps & { side?: "left" | "right" }): JSX.Element;
export type PortalNotice = {
id: string;
tone: PortalTone;
title?: ReactNode;
message: ReactNode;
durationMs?: number;
};
export function PortalNoticeRegion(props: { notices: PortalNotice[]; onDismiss?: (id: string) => void }): JSX.Element;Dialog와 Drawer는 열릴 때 focus를 내부로 이동하고, Escape로 닫으며, 닫힌 뒤 원래 trigger로 focus를 돌려준다. 외부 클릭 닫기 여부는 기본값을 안전하게 유지하고, 중요한 입력에서는 명시적으로 설정한다.
6.8 State components
export function PortalLoadingState(props: { label?: ReactNode }): JSX.Element;
export function PortalEmptyState(props: { title: ReactNode; description?: ReactNode; action?: ReactNode; icon?: ReactNode }): JSX.Element;
export function PortalErrorState(props: { title?: ReactNode; description?: ReactNode; retry?: () => void }): JSX.Element;
export function PortalStatusBadge(props: { tone?: PortalTone; variant?: "soft" | "solid"; children: ReactNode }): JSX.Element;
export function PortalProgress(props: { value?: number; label?: ReactNode; indeterminate?: boolean }): JSX.Element;업무 화면은 빈 결과, 로딩, 오류를 임의 문구와 임의 색상으로 구현하지 않고 표준 상태 컴포넌트를 사용한다.
6.9 Picker (조직·사용자 선택)
export function PortalPickerField(props: PortalFieldProps & { value?: ReactNode; placeholder?: ReactNode; buttonLabel?: ReactNode; changeButtonLabel?: ReactNode; clearLabel?: ReactNode; open?: boolean; disabled?: boolean; onOpen: () => void; onClear?: () => void }): JSX.Element;
export function PortalTreePickerDialog(props: { open: boolean; title: ReactNode; nodes: PortalPickerTreeNode[]; selectedId?: string; loading?: boolean; onConfirm: (node: PortalTreeNode) => void; onCancel: () => void }): JSX.Element;
export function PortalOrgPicker(props: PortalFieldProps & { nodes: PortalPickerTreeNode[]; value?: string; onChange: (id: string | undefined, node?: PortalTreeNode) => void }): JSX.Element;
export function PortalUserPicker(props: PortalFieldProps & { users: PortalUserPickerOption[]; nodes?: PortalPickerTreeNode[]; mode?: "single" | "multiple"; value?: string | string[]; onChange: (id: string | string[], users?: PortalUserPickerOption | PortalUserPickerOption[]) => void }): JSX.Element;
export function PortalUserPickerDialog(props: { open: boolean; title: ReactNode; users: PortalUserPickerOption[]; nodes?: PortalPickerTreeNode[]; multiple?: boolean; onConfirm: (ids: string[]) => void; onCancel: () => void }): JSX.Element;
export function buildPickerTree(rows: readonly PortalPickerTreeRow[]): PortalPickerTreeNode[];
export function findPickerPath(nodes: readonly PortalPickerTreeNode[], id: string | undefined): PortalPickerTreeNode[];
export function findPickerNode(nodes: readonly PortalPickerTreeNode[], id: string | undefined): PortalPickerTreeNode | undefined;
export function pickerPathLabel(nodes: readonly PortalPickerTreeNode[], id: string | undefined, separator?: string): ReactNode;계층이 있는 값(부서 등)과 목록이 긴 값(사용자) 은 좁은 select 목록에 눌러 담지 않는다 — 필드는 값만 보여 주고 버튼으로 레이어 팝업을 연다. 데이터는 소비 포털이 주입하고(공통 UI 는 fetch 를 소유하지 않는다), 식별자 의미는 호출자가 정한다. 규칙·데이터 계약·검증 명령은 PICKER_CONVENTION.md.
7. 토큰과 CSS 규칙
토큰은 semantic token을 우선한다. 컴포넌트가 #2563eb 같은 원시 색상을 직접 사용하지 않고 var(--pf-color-action-primary)를 사용해야 한다.
:root {
--pf-color-shell-topnav: #1f2937;
--pf-color-shell-sidebar: #1f2937;
--pf-color-content-header: #3157a6;
--pf-color-canvas: #f3f4f6;
--pf-color-surface: #ffffff;
--pf-color-border: #e5e7eb;
--pf-color-text: #111827;
--pf-color-text-muted: #6b7280;
--pf-color-action-primary: #2563eb;
--pf-color-action-primary-text: #ffffff;
--pf-color-state-success: #16a34a;
--pf-color-state-warning: #d97706;
--pf-color-state-danger: #dc2626;
--pf-font-family: ui-sans-serif, system-ui, sans-serif;
--pf-font-size-body: 14px;
--pf-font-size-page-title: 20px;
--pf-radius-sm: 6px;
--pf-radius-md: 8px;
--pf-space-1: 4px;
--pf-space-2: 8px;
--pf-space-3: 12px;
--pf-space-4: 16px;
--pf-space-6: 24px;
}Theme manifest의 tokens는 검증된 --pf- namespace만 덮어쓴다. 임의 selector, arbitrary CSS, JavaScript code, URL stylesheet import는 저장하지 않는다.
8. 소비 포털 사용 예시
import {
PortalButton,
PortalDataTable,
PortalPage,
PortalPageHeader,
PortalPanel,
PortalToolbar,
PortalTree,
} from "@hohyun94/portal-foundation";
export function DirectoryUsersPage() {
return (
<PortalPage>
<PortalPageHeader
title="사용자 관리"
description="조직 사용자와 접근 권한을 관리합니다."
actions={<PortalButton variant="primary">사용자 등록</PortalButton>}
/>
<PortalToolbar
filters={<PortalTextField id="user-search" label="검색" visuallyHidden />}
actions={<PortalButton variant="outline">필터</PortalButton>}
/>
<PortalPanel variant="quiet">
<PortalDataTable
columns={columns}
rows={users}
getRowId={user => user.id}
loading={isLoading}
emptyState={<PortalEmptyState title="사용자가 없습니다." />}
/>
</PortalPanel>
</PortalPage>
);
}소비 포털은 columns, rows, 메뉴 데이터, event handler를 소유한다. 버튼의 높이, 표의 행 간격, 선택 색상, 오류 상태의 표현은 라이브러리가 소유한다.
9. 버전 및 확장 정책
컴포넌트 추가는 기존 공개 API를 침해하지 않는 한 minor 변경으로 처리한다. 기존 props의 의미 변경, 기본 동작 변경, 제거는 major 변경으로 처리한다. 소비 포털의 요구가 생기면 먼저 기존 variant로 표현 가능한지 검토하고, 반복되는 요구만 새 표준 컴포넌트 또는 제한된 variant로 승격한다.
다음 항목은 공개 API로 승인하지 않는다.
style로 Shell geometry 변경- raw
css또는customCssmanifest - 포털별 active 메뉴 색상 className
- 컴포넌트 내부 DOM selector 의존
- 표준 컴포넌트를 우회하는 임의
<button>,<input>,<table>스타일 복제 - 테마에 메뉴 목록, 권한, API endpoint, 업무 콘텐츠 저장
10. 구현 순서
PortalPage,PortalPageHeader,PortalToolbar,PortalButton,PortalPanel을 1차 공통 컴포넌트로 구현한다.PortalTextField,PortalSelect,PortalCheckbox,PortalStatusBadge를 추가한다.PortalDataTable과PortalTree를 구현하고 키보드·접근성 테스트를 작성한다.PortalDialog,PortalDrawer, 상태 컴포넌트를 추가한다.- 현재
PortalConsole와UiPlatform의 콘텐츠를 표준 컴포넌트로 점진적으로 전환한다. - 소비 포털에 남은 공통 UI CSS override를 검색해 제거한다.
- 각 컴포넌트의 visual regression과 타입 계약 테스트를 추가한다.
초기 범위에서는 기존 Shell을 다시 설계하지 않는다. 먼저 콘텐츠 컴포넌트를 추가하고, 이미 확정된 Shell 계약을 유지한다. 이를 통해 공통 UI의 안정성을 보존하면서 우측 콘텐츠 영역의 일관성을 단계적으로 확보할 수 있다.
11. 완료 기준
기본 컴포넌트 라이브러리는 다음 조건을 만족해야 한다.
| 기준 | 검증 방법 |
|---|---|
| 공개 API 일관성 | index.tsx export와 TypeScript contract test |
| 접근성 | axe 검사, 키보드 시나리오, aria snapshot |
| 테마 적용 | light/dark 및 default manifest visual test |
| 반응형 | 고정 Shell과 콘텐츠 overflow Playwright test |
| 상태 표현 | loading, empty, error, disabled snapshot |
| 소비 포털 규율 | raw component CSS override 검색 및 review |
| 호환성 | PortalConsole·UiPlatform typecheck, test, build |
이 구조를 기준으로 하면 portal-foundation은 레이아웃만 제공하는 패키지가 아니라, Shell과 콘텐츠 표현 규칙을 함께 관리하는 포털 UI 플랫폼으로 확장된다.