@hjmds/design-contracts 1.12.1 → 1.13.0
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/dist/avatar-fallback.d.ts +11 -0
- package/dist/avatar-fallback.d.ts.map +1 -1
- package/dist/avatar-fallback.js +21 -0
- package/dist/avatar-fallback.js.map +1 -1
- package/dist/base-recipes.d.ts +17 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/catalog.d.ts +25 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/command-palette.d.ts +14 -9
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +8 -9
- package/dist/command-palette.js.map +1 -1
- package/dist/component-recipes.d.ts +17 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +5 -0
- package/dist/component-recipes.js.map +1 -1
- package/dist/provider-button.d.ts.map +1 -1
- package/dist/provider-button.js +3 -0
- package/dist/provider-button.js.map +1 -1
- package/dist/reactions.d.ts +10 -0
- package/dist/reactions.d.ts.map +1 -1
- package/dist/reactions.js +7 -0
- package/dist/reactions.js.map +1 -1
- package/dist/screen-patterns.d.ts +147 -0
- package/dist/screen-patterns.d.ts.map +1 -0
- package/dist/screen-patterns.js +149 -0
- package/dist/screen-patterns.js.map +1 -0
- package/dist/slider.d.ts +8 -0
- package/dist/slider.d.ts.map +1 -1
- package/dist/slider.js +6 -1
- package/dist/slider.js.map +1 -1
- package/dist/upload-item.d.ts +5 -0
- package/dist/upload-item.d.ts.map +1 -1
- package/dist/upload-item.js +5 -0
- package/dist/upload-item.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/action-session.md +3 -3
- package/docs/agreement.md +5 -0
- package/docs/avatar-fallback.md +7 -0
- package/docs/bottom-navigation.md +6 -0
- package/docs/brand-boundary.md +1 -1
- package/docs/button-label.md +6 -0
- package/docs/clipboard.md +3 -0
- package/docs/command-palette.md +45 -2
- package/docs/consumer-policy.md +5 -1
- package/docs/data-table.md +6 -4
- package/docs/dialog.md +8 -2
- package/docs/form.md +51 -0
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/link.md +8 -0
- package/docs/migration-native-legacy-removal.md +45 -1
- package/docs/optional-adapters.md +1 -1
- package/docs/password-field.md +5 -0
- package/docs/product-composition-adoption.md +40 -0
- package/docs/progress.md +19 -1
- package/docs/provider-button.md +13 -0
- package/docs/result.md +3 -0
- package/docs/screen-chrome.md +10 -0
- package/docs/screen-patterns.md +376 -0
- package/docs/sheet.md +12 -0
- package/docs/splitter.md +8 -2
- package/docs/theming.md +36 -29
- package/docs/toggle-group.md +7 -0
- package/docs/tour.md +7 -1
- package/docs/tree.md +5 -2
- package/docs/upload-item.md +7 -0
- package/docs/usage/README.md +236 -0
- package/docs/usage/STANDARD.md +108 -0
- package/docs/usage/components/accordion.md +107 -0
- package/docs/usage/components/activity-heatmap.md +104 -0
- package/docs/usage/components/affix.md +86 -0
- package/docs/usage/components/agreement.md +129 -0
- package/docs/usage/components/alert-dialog.md +130 -0
- package/docs/usage/components/anchor.md +96 -0
- package/docs/usage/components/aspect-ratio.md +89 -0
- package/docs/usage/components/asset.md +126 -0
- package/docs/usage/components/auth-provider-button.md +116 -0
- package/docs/usage/components/auth-screen-layout.md +129 -0
- package/docs/usage/components/avatar.md +114 -0
- package/docs/usage/components/badge.md +84 -0
- package/docs/usage/components/bottom-cta.md +125 -0
- package/docs/usage/components/bottom-info.md +99 -0
- package/docs/usage/components/bottom-navigation.md +136 -0
- package/docs/usage/components/breadcrumb.md +81 -0
- package/docs/usage/components/button.md +118 -0
- package/docs/usage/components/calendar.md +122 -0
- package/docs/usage/components/card.md +110 -0
- package/docs/usage/components/carousel.md +113 -0
- package/docs/usage/components/celebration.md +96 -0
- package/docs/usage/components/chat-message.md +122 -0
- package/docs/usage/components/chat-screen.md +112 -0
- package/docs/usage/components/checkbox-group.md +104 -0
- package/docs/usage/components/checkbox.md +103 -0
- package/docs/usage/components/chip.md +104 -0
- package/docs/usage/components/code-block.md +111 -0
- package/docs/usage/components/collapsible.md +112 -0
- package/docs/usage/components/color-picker.md +86 -0
- package/docs/usage/components/combobox.md +137 -0
- package/docs/usage/components/command-palette.md +125 -0
- package/docs/usage/components/comment-thread-screen.md +125 -0
- package/docs/usage/components/container.md +98 -0
- package/docs/usage/components/content-transition.md +101 -0
- package/docs/usage/components/context-menu.md +136 -0
- package/docs/usage/components/counter-badge.md +107 -0
- package/docs/usage/components/data-table.md +122 -0
- package/docs/usage/components/date-picker.md +142 -0
- package/docs/usage/components/date-range-picker.md +111 -0
- package/docs/usage/components/description-list.md +103 -0
- package/docs/usage/components/design-system-provider.md +124 -0
- package/docs/usage/components/dialog.md +176 -0
- package/docs/usage/components/divider.md +89 -0
- package/docs/usage/components/editor-screen.md +126 -0
- package/docs/usage/components/effect-surface.md +120 -0
- package/docs/usage/components/empty-state.md +114 -0
- package/docs/usage/components/field.md +129 -0
- package/docs/usage/components/file-picker.md +114 -0
- package/docs/usage/components/floating-action-button.md +138 -0
- package/docs/usage/components/form.md +162 -0
- package/docs/usage/components/grid.md +99 -0
- package/docs/usage/components/heading.md +87 -0
- package/docs/usage/components/icon-button.md +126 -0
- package/docs/usage/components/icon.md +105 -0
- package/docs/usage/components/image.md +122 -0
- package/docs/usage/components/keyboard-avoiding.md +93 -0
- package/docs/usage/components/keyboard-dock.md +110 -0
- package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
- package/docs/usage/components/keyboard-motion-provider.md +86 -0
- package/docs/usage/components/layout.md +117 -0
- package/docs/usage/components/link.md +121 -0
- package/docs/usage/components/list-detail-screen.md +103 -0
- package/docs/usage/components/list-row.md +124 -0
- package/docs/usage/components/list.md +119 -0
- package/docs/usage/components/load-more.md +115 -0
- package/docs/usage/components/masonry.md +109 -0
- package/docs/usage/components/media-selection-screen.md +119 -0
- package/docs/usage/components/mentions.md +119 -0
- package/docs/usage/components/menu.md +129 -0
- package/docs/usage/components/menubar.md +93 -0
- package/docs/usage/components/message-composer.md +124 -0
- package/docs/usage/components/moderation-screen.md +113 -0
- package/docs/usage/components/notice.md +106 -0
- package/docs/usage/components/notification-inbox-screen.md +97 -0
- package/docs/usage/components/notification-item.md +98 -0
- package/docs/usage/components/number-field.md +131 -0
- package/docs/usage/components/onboarding-screen.md +106 -0
- package/docs/usage/components/otp-field.md +101 -0
- package/docs/usage/components/pagination.md +82 -0
- package/docs/usage/components/password-field.md +137 -0
- package/docs/usage/components/permission-screen.md +107 -0
- package/docs/usage/components/photo-source-sheet.md +119 -0
- package/docs/usage/components/popover.md +108 -0
- package/docs/usage/components/profile-screen.md +89 -0
- package/docs/usage/components/progress.md +122 -0
- package/docs/usage/components/qr-code.md +122 -0
- package/docs/usage/components/radio-group.md +124 -0
- package/docs/usage/components/radio.md +104 -0
- package/docs/usage/components/result.md +116 -0
- package/docs/usage/components/saved-items-screen.md +126 -0
- package/docs/usage/components/screen-layout.md +119 -0
- package/docs/usage/components/search-field.md +120 -0
- package/docs/usage/components/search-screen.md +215 -0
- package/docs/usage/components/section.md +111 -0
- package/docs/usage/components/segmented-control.md +138 -0
- package/docs/usage/components/select.md +142 -0
- package/docs/usage/components/settings-screen.md +126 -0
- package/docs/usage/components/shared-transition-element.md +111 -0
- package/docs/usage/components/shared-transition-screen.md +86 -0
- package/docs/usage/components/sheet.md +151 -0
- package/docs/usage/components/side-panel.md +104 -0
- package/docs/usage/components/sidebar.md +107 -0
- package/docs/usage/components/skeleton.md +105 -0
- package/docs/usage/components/skip-nav.md +76 -0
- package/docs/usage/components/slider.md +121 -0
- package/docs/usage/components/sortable-collection.md +127 -0
- package/docs/usage/components/spinner.md +86 -0
- package/docs/usage/components/splitter.md +103 -0
- package/docs/usage/components/stack.md +93 -0
- package/docs/usage/components/statistic.md +123 -0
- package/docs/usage/components/steps.md +110 -0
- package/docs/usage/components/surface.md +91 -0
- package/docs/usage/components/swipe-actions.md +124 -0
- package/docs/usage/components/switch.md +120 -0
- package/docs/usage/components/tabs.md +134 -0
- package/docs/usage/components/tag.md +84 -0
- package/docs/usage/components/tags-input.md +111 -0
- package/docs/usage/components/text-area.md +112 -0
- package/docs/usage/components/text-format.md +75 -0
- package/docs/usage/components/text-transition.md +104 -0
- package/docs/usage/components/text.md +101 -0
- package/docs/usage/components/thinking-orb.md +105 -0
- package/docs/usage/components/timeline.md +105 -0
- package/docs/usage/components/toast.md +145 -0
- package/docs/usage/components/toggle-group.md +95 -0
- package/docs/usage/components/tooltip.md +103 -0
- package/docs/usage/components/top-bar.md +124 -0
- package/docs/usage/components/top.md +89 -0
- package/docs/usage/components/tour.md +118 -0
- package/docs/usage/components/transfer-list.md +115 -0
- package/docs/usage/components/tree.md +91 -0
- package/docs/usage/components/upload-item.md +99 -0
- package/docs/usage/components/virtual-list.md +105 -0
- package/docs/usage/components/visually-hidden.md +72 -0
- package/docs/usage/components/watermark.md +78 -0
- package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
- package/docs/usage/compositions/action-recovery-save.md +235 -0
- package/docs/usage/compositions/action-recovery-undo.md +193 -0
- package/docs/usage/compositions/common-message.md +132 -0
- package/docs/usage/compositions/common-notification.md +101 -0
- package/docs/usage/compositions/compound-controls.md +186 -0
- package/docs/usage/compositions/data-layouts.md +157 -0
- package/docs/usage/compositions/disclosure.md +144 -0
- package/docs/usage/compositions/environment-matrix.md +139 -0
- package/docs/usage/compositions/expo-interactions.md +149 -0
- package/docs/usage/compositions/family-drawer.md +201 -0
- package/docs/usage/compositions/floating-action-button.md +197 -0
- package/docs/usage/compositions/input-sheet.md +148 -0
- package/docs/usage/compositions/interaction-adapters.md +190 -0
- package/docs/usage/compositions/interaction-flow-apply.md +205 -0
- package/docs/usage/compositions/interaction-flow-draft.md +188 -0
- package/docs/usage/compositions/interaction-flow-search.md +171 -0
- package/docs/usage/compositions/native-renderers.md +106 -0
- package/docs/usage/compositions/navigation-bar-collection.md +164 -0
- package/docs/usage/compositions/optional-adapters.md +169 -0
- package/docs/usage/compositions/optional-motion.md +109 -0
- package/docs/usage/compositions/photo-source.md +104 -0
- package/docs/usage/compositions/purpose-input-comment.md +110 -0
- package/docs/usage/compositions/purpose-input-message.md +119 -0
- package/docs/usage/compositions/reference-first.md +96 -0
- package/docs/usage/compositions/reference-review.md +107 -0
- package/docs/usage/compositions/reference-settings.md +107 -0
- package/docs/usage/compositions/selection-scope.md +174 -0
- package/docs/usage/compositions/stea-event-ticket.md +166 -0
- package/docs/usage/compositions/stea-flip-card.md +162 -0
- package/docs/usage/compositions/stea-order-progress.md +184 -0
- package/docs/usage/compositions/stea-otp-verify.md +215 -0
- package/docs/usage/compositions/stea-pixel-empty.md +140 -0
- package/docs/usage/compositions/stea-schedule-card.md +169 -0
- package/docs/usage/compositions/stea-stat-summary.md +154 -0
- package/docs/usage/compositions/time-selection.md +174 -0
- package/docs/usage/compositions/toast-layout.md +128 -0
- package/docs/usage/compositions/visual-foundations.md +185 -0
- package/docs/usage/compositions/web-additions.md +146 -0
- package/docs/usage/compositions/web-navigation.md +143 -0
- package/docs/usage/screens/common-chat.md +127 -0
- package/docs/usage/screens/common-comments.md +108 -0
- package/docs/usage/screens/common-inbox.md +110 -0
- package/docs/usage/screens/common-login.md +98 -0
- package/docs/usage/screens/common-profile.md +221 -0
- package/docs/usage/screens/common-saved.md +127 -0
- package/docs/usage/screens/common-search.md +274 -0
- package/docs/usage/screens/common-settings.md +126 -0
- package/docs/usage/screens/common-shell.md +108 -0
- package/docs/usage/screens/dashboard.md +245 -0
- package/docs/usage/screens/discovery-gallery.md +306 -0
- package/docs/usage/screens/flow-collection.md +96 -0
- package/docs/usage/screens/flow-editor.md +120 -0
- package/docs/usage/screens/flow-media.md +111 -0
- package/docs/usage/screens/flow-moderation.md +120 -0
- package/docs/usage/screens/flow-onboarding.md +193 -0
- package/docs/usage/screens/flow-permission.md +103 -0
- package/docs/usage/screens/landing.md +347 -0
- package/docs/usage/screens/mockup-studio.md +190 -0
- package/docs/usage/screens/notification-settings.md +206 -0
- package/docs/usage/screens/reference-comparison.md +159 -0
- package/docs/usage/templates/component.md +61 -0
- package/docs/usage/templates/composition.md +47 -0
- package/docs/usage/templates/screen.md +56 -0
- package/docs/usage/templates/token.md +32 -0
- package/docs/usage/tokens/color.md +142 -0
- package/docs/usage/tokens/elevation-opacity.md +86 -0
- package/docs/usage/tokens/layers.md +98 -0
- package/docs/usage/tokens/layout.md +114 -0
- package/docs/usage/tokens/motion.md +88 -0
- package/docs/usage/tokens/radius.md +53 -0
- package/docs/usage/tokens/size.md +74 -0
- package/docs/usage/tokens/spacing.md +73 -0
- package/docs/usage/tokens/stroke.md +50 -0
- package/docs/usage/tokens/theme-studio.md +70 -0
- package/docs/usage/tokens/typography-studio.md +70 -0
- package/docs/usage/tokens/typography.md +89 -0
- package/package.json +7 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# 저장한 항목
|
|
2
|
+
|
|
3
|
+
- 단계: 화면
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/저장한 항목`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/화면/콘텐츠/저장한 항목`
|
|
10
|
+
|
|
11
|
+
## 목적
|
|
12
|
+
|
|
13
|
+
SavedItemsScreen을 사용해 저장한 항목 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
|
|
14
|
+
|
|
15
|
+
## 영역 구조
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
host: 남은 높이·safe area·키보드
|
|
19
|
+
└─ 컬렉션 2열 → 게시물 3열 → 상세·해제/Undo → 격자 복귀
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| 바깥 틀 | SavedItemsScreen | route 본문 | [API 배치 규칙](../components/saved-items-screen.md#배치), host 남은 높이 |
|
|
25
|
+
| 내용 | SavedItemsScreen 내부 Grid | 첫 화면 컬렉션 격자 → 컬렉션 안 게시물 격자 → 상세(`renderDetail`) | 컬렉션 `columns={{compact:2}}`·gap `spacing.md` 16·최소 열 80, 게시물 `columns={{compact:3}}`·gap `spacing.xxs` 4·최소 열 44 |
|
|
26
|
+
| 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
|
|
27
|
+
|
|
28
|
+
## 버튼과 행동 위치
|
|
29
|
+
|
|
30
|
+
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| 새 컬렉션 | Button·ghost(내장, `labels.createCollection`) | 첫 화면 머리 오른쪽 actions 자리 | 하나. 누르면 `onCreateCollection` → 제품 Sheet(footer에 만들기 Button 하나) |
|
|
33
|
+
| 뒤로 | Button·ghost(내장, `labels.back`) | 컬렉션·상세 머리 왼쪽 leading 자리 | 하나. 상세 → 격자 → 컬렉션 순으로 `onBack` |
|
|
34
|
+
| 게시물 열기 | 격자 칸(내장 버튼) | 게시물 격자 | 칸마다 하나, `onOpenItem` |
|
|
35
|
+
| 저장 해제 | IconButton·ghost(제품이 `renderDetail`에 둔다) | 상세 본문 머리 행 오른쪽(왼쪽은 작성자) | 하나. 해제 후 상세를 닫고 격자로 복귀 |
|
|
36
|
+
| 실행 취소 | Button·secondary·small | `notice`(상태 문구 오른쪽) | 해제 직후 하나 |
|
|
37
|
+
| 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
|
|
38
|
+
|
|
39
|
+
## 상태
|
|
40
|
+
|
|
41
|
+
| 상태 | 화면 모습 | 행동 |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| 기본 | 컬렉션 2열 → 게시물 3열 → 상세 → 저장 해제·실행 취소 → 격자 복귀 | 각 공개 콜백을 제품 상태에 연결 |
|
|
44
|
+
| 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
|
|
45
|
+
| 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
|
|
46
|
+
| 오류 | 목록 비어 있음과 조회 실패를 구분하고 재시도 제공 | 실패 원인과 재시도 경로 제공 |
|
|
47
|
+
|
|
48
|
+
## 사용하는 지침
|
|
49
|
+
|
|
50
|
+
| 지침 | 쓰는 곳 |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| [SavedItemsScreen](../components/saved-items-screen.md) | 필수 props·슬롯·플랫폼 차이 |
|
|
53
|
+
| [Button](../components/button.md) | 동작·로딩·보조 행동 |
|
|
54
|
+
| [SavedItemsScreen](../components/saved-items-screen.md) | 화면 높이·본문 교체·스크롤 소유 |
|
|
55
|
+
|
|
56
|
+
## 코드 골격
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Web
|
|
60
|
+
import { IconButton } from "@hjmds/react/actions";
|
|
61
|
+
import { Stack, Text } from "@hjmds/react/layout";
|
|
62
|
+
import { SavedItemsScreen } from "@hjmds/react/saved-items";
|
|
63
|
+
|
|
64
|
+
<SavedItemsScreen title={t("saved.title")} items={items} collections={collections}
|
|
65
|
+
{...(collectionId !== undefined ? { collectionId } : {})}
|
|
66
|
+
selectedItemId={selectedItemId} labels={labels} state={state}
|
|
67
|
+
onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
|
|
68
|
+
onCreateCollection={openCreateSheet}
|
|
69
|
+
notice={undoNotice}
|
|
70
|
+
renderThumbnail={renderThumbnail}
|
|
71
|
+
renderDetail={(item) => <Stack gap="md">
|
|
72
|
+
<Stack axis="inline" align="center" justify="between">
|
|
73
|
+
<Text emphasis="strong">{item.author}</Text>
|
|
74
|
+
<IconButton label={t("saved.unsave", { title: item.title })} tone="ghost" onClick={() => unsave(item.id)}><BookmarkGlyph /></IconButton>
|
|
75
|
+
</Stack>
|
|
76
|
+
{renderBody(item)}
|
|
77
|
+
</Stack>} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// Native
|
|
82
|
+
import { IconButton } from "@hjmds/react-native/actions";
|
|
83
|
+
import { Stack, Text } from "@hjmds/react-native/primitives";
|
|
84
|
+
import { SavedItemsScreen } from "@hjmds/react-native/saved-items";
|
|
85
|
+
|
|
86
|
+
<SavedItemsScreen title={t("saved.title")} items={items} collections={collections}
|
|
87
|
+
{...(collectionId !== undefined ? { collectionId } : {})}
|
|
88
|
+
selectedItemId={selectedItemId} labels={labels} state={state}
|
|
89
|
+
onOpenCollection={openCollection} onOpenItem={openItem} onBack={goBack}
|
|
90
|
+
onCreateCollection={openCreateSheet}
|
|
91
|
+
notice={undoNotice}
|
|
92
|
+
renderThumbnail={renderThumbnail}
|
|
93
|
+
renderDetail={(item) => <Stack gap="md">
|
|
94
|
+
<Stack axis="inline" align="center" justify="between">
|
|
95
|
+
<Text emphasis="strong">{item.author}</Text>
|
|
96
|
+
<IconButton label={t("saved.unsave", { title: item.title })} tone="ghost" onPress={() => unsave(item.id)}><BookmarkGlyph /></IconButton>
|
|
97
|
+
</Stack>
|
|
98
|
+
{renderBody(item)}
|
|
99
|
+
</Stack>} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`collectionId`를 생략하면 컬렉션 첫 화면, `null`이면 모든 게시물이다. 저장 해제 버튼은 API에 내장되지 않는다:
|
|
103
|
+
제품이 `renderDetail` 머리 행에 두고, 해제 뒤 `selectedItemId`를 `null`로 돌리고 `notice`에 실행 취소를 띄운다.
|
|
104
|
+
콜백·데이터·지역화 함수는 제품에서 공급한다.
|
|
105
|
+
|
|
106
|
+
## 큰 글자·다크·좁은 폭
|
|
107
|
+
|
|
108
|
+
| 조건 | 바뀌는 것 |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
|
|
111
|
+
| 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
|
|
112
|
+
| 좁은 폭 | 320px에서도 컬렉션 2열·게시물 3열. 제목은 줄바꿈하며 가로 넘침 없이 본문 하나만 스크롤 |
|
|
113
|
+
| 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
|
|
114
|
+
|
|
115
|
+
## 플랫폼 차이
|
|
116
|
+
|
|
117
|
+
| 항목 | Web | Native |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| 열기·해제 콜백 | `onClick` | `onPress` |
|
|
120
|
+
| `Stack`·`Text` import | `@hjmds/react/layout` | `@hjmds/react-native/primitives` |
|
|
121
|
+
|
|
122
|
+
## 함정
|
|
123
|
+
|
|
124
|
+
- 목록 비어 있음과 조회 실패를 구분하고 재시도 제공.
|
|
125
|
+
- Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
|
|
126
|
+
|
|
127
|
+
- 새 컬렉션은 Sheet footer에 만들기 하나, 본문은 스크롤한다. Native는 `scrollable keyboardAvoidance`를 사용하고 사진 선택 Button은 `growWithContent`로 이미지 높이를 보존한다. 고정 44 높이에 사진을 넣으면 행이 겹친다(2026-10-06 시뮬레이터 재현).
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# 검색 결과와 필터
|
|
2
|
+
|
|
3
|
+
- 단계: 화면
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`SearchScreen` 두 단계 검색 prop, 2026-10-06 사용자 위임 결정으로 미리보기 로직을 공개 API로 올림), 예제 `showcase/*/search-discovery-preview.tsx`·`showcase/shared/search-discovery.ts`. 2026-10-06 사용자 요청("실험에 있는 검색화면 UI도 개편해. 검색과 필터 있는 앱 서비스 따라해.")에 따라 Material 3 Search·Apple HIG search fields·Baymard·NN/g·eBay Playbook 필터 패턴과 국내 커머스·생활 서비스 모바일 웹의 **구조·상호작용만** 조사해 다시 설계했다(브랜드 색·로고·문구·자산은 가져오지 않았다). 2026-10-06 사용자 승인으로 실험 `공통 화면/검색`을 배포하면서 같은 일을 직접 조립하던 배포 `화면/검색`(내 기록 검색)을 대체했다(Web id `patterns-search` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/화면/검색/검색 결과와 필터`
|
|
10
|
+
|
|
11
|
+
## 목적
|
|
12
|
+
|
|
13
|
+
입력 중 제안 → 확정 → 결과·필터의 두 단계 검색 화면을 SearchScreen 하나로 구성한다. 단계 전환·확정 경로·섹션 배치·필터 시트 초안·0건 복구·포커스·개수 낭독은 SearchScreen이 소유하고 제품은 데이터·요청·문구·아이콘만 공급한다(2026-10-06 전에는 이 모두가 Storybook 미리보기 코드에만 있었다). 스토리는 상태별 정지 화면(`입력 중`·`결과`·`필터 적용`·`필터 시트`·`불러오는 중`·`결과 없음`·`오류`·`로그인 필요`)과 `실패와 복구`(다음 검색 실패 → 다시 시도)를 보여 주고, `어두운 테마`·`큰 글자`는 필터 적용 화면으로 연다. `기본`(검색 전)에서 검색 전 → 입력 → 확정 → 필터 → 상세 → 돌아오기에서 검색어·조건·정렬·스크롤이 남는 흐름을 직접 조작한다. 2026-10-06 같은 예제를 그리던 `기본 흐름/검색과 필터`(복구 흐름)와 `구성/검색/여러 조건 적용과 초기화`(초안·적용 필터 시트)를 이 항목으로 합쳤다([옛 링크](../../../../../docs/STORYBOOK_NAVIGATION.md#실험-정리와-합친-항목-2026-10-06)). 데이터·제안·권한·문구는 제품이 공급하며 예제의 메모리 검색을 운영 검색으로 취급하지 않는다.
|
|
14
|
+
배포의 옛 직접 조립 `내 기록 검색`(검색어를 칠 때마다 바로 거르는 SearchField + 카테고리 SegmentedControl + 결과 List + 상세 Sheet 한 세로 스크롤)도 이 항목으로 대체했다. 확정·제안·필터 시트가 필요 없는 수십 개짜리 내 목록을 그 자리에서 거르는 정도라면 화면 대신 [SearchField](../components/search-field.md)와 [SegmentedControl](../components/segmented-control.md)을 목록 위에 직접 두고, 결과 수는 상태 문구로 알린다.
|
|
15
|
+
|
|
16
|
+
## 영역 구조
|
|
17
|
+
|
|
18
|
+
좁은 폭(Web < 600, Native) — 결과 + 필터 적용:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
┌ safe area(host) ─────────────────────────────┐
|
|
22
|
+
│ [←] 검색 │ 머리: leading IconButton + 제목. description 없음
|
|
23
|
+
│ [🔍 산책 (×)] │ ┐ 고정(notice): SearchField medium 44, label 숨김(placeholder)
|
|
24
|
+
│ ↕ spacing.sm 12 │ │
|
|
25
|
+
│ [☰ 필터 2][✓사진 있음][저장함][기간: 올해 ▾][종류 ▾] →│ ┘ trigger(HJM) + filters(제품), 한 줄 스크롤
|
|
26
|
+
├──────────────────────────────────────────────┤ ── 여기부터 스크롤 ──
|
|
27
|
+
│ 결과 5개 [정렬: 관련도순 ▾]│ Text · Button ghost small → Menu(single)
|
|
28
|
+
│ [사진 있음 ×][기간: 올해 ×] 모두 해제 │ 적용 조건 Chip(action, trailing ×) · Button ghost small
|
|
29
|
+
│ ↕ spacing.lg 20 │
|
|
30
|
+
│ 산책길에서 찾은 작은 여유 │ ListRow 두 줄(제목, 종류 · 날짜)
|
|
31
|
+
│ 사진 · 10월 6일 │ 일치 부분은 굵게(Web)
|
|
32
|
+
│ … │
|
|
33
|
+
│ [더 보기] │ LoadMore manual(8개씩)
|
|
34
|
+
└ safe area(host) ─────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
검색 전 · 입력 중:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
검색 전(검색어 공백) 입력 중(확정 전)
|
|
41
|
+
│ [←] 검색 │ │ [←] 검색 │
|
|
42
|
+
│ [🔍 기록 검색 ] │ │ [🔍 산| (×)] │ ← 칩 줄 없음
|
|
43
|
+
├──────────────────────────────────┤ ├──────────────────────────────────┤
|
|
44
|
+
│ 최근 검색 전체 삭제 │ │ 🔍 ‘산’ 검색 │ ← 첫 행 = 그대로 확정
|
|
45
|
+
│ 🕘 카페 (×) │ │ 🔍 산책 │ ListRow compact 44 ×≤6
|
|
46
|
+
│ 🕘 아침 (×) │ │ 🔍 산책 모임 첫날 │
|
|
47
|
+
│ ↕ spacing.xl 24 │ │ … │
|
|
48
|
+
│ 자주 찾는 주제 │
|
|
49
|
+
│ [산책][카페][책][아침][정원] │ Chip action, 누르면 확정
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
필터 시트와 Web 넓은 폭:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
Native · Web < 960: 아래 시트(size large) Web ≥ 960(expanded): 옆 시트(placement end, 폭 min(28rem,100%))
|
|
56
|
+
╭──────────────────────────────╮ ┌──── ScreenLayout max 720 ────┐ ╭──────────────╮
|
|
57
|
+
│ 필터 [×] │ │ [←] 검색 │ │ 필터 [×] │
|
|
58
|
+
│ 종류 [기록][사진][장소] │ multiple │ [🔍 산책 (×)] │ │ 종류 … │
|
|
59
|
+
│ 기간 [전체][이번 주][✓올해] │ single │ [☰ 필터 2][✓사진 있음]… │ │ 기간 … │
|
|
60
|
+
│ 조건 사진 있는 기록만 (●) │ Switch row │ 결과 5개 [정렬 ▾] │ │ 조건 … │
|
|
61
|
+
│ 저장한 기록만 (○) │ │ … │ │ │
|
|
62
|
+
├──────────────────────────────┤ footer 고정 └───────────────────────────────┘ │[초기화][5개…]│
|
|
63
|
+
│ Web [초기화] [5개 결과 보기]│ [보조][주] ╰──────────────╯
|
|
64
|
+
│ Native [ 5개 결과 보기 ] │ 주 먼저, fullWidth
|
|
65
|
+
│ [ 초기화 ] │
|
|
66
|
+
╰──────────────────────────────╯
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| 바깥 틀 | SearchScreen(ScreenLayout) | route 본문, host가 남은 높이·safe area·키보드 처리 | 폭 최대 `layout.readingMaxWidth` 720, 좌우 `spacing.md` 16. Native 본문 `scrollProps={{ keyboardDismissMode: "on-drag" }}` |
|
|
72
|
+
| 머리 | `title` + `leading`(IconButton 뒤로) | 맨 위 | `description`은 비운다(고정 영역 한 줄 절약) |
|
|
73
|
+
| 검색 입력 | 기본 SearchField(`queryLabel`·`queryLabelVisibility="hidden"`·`queryClearLabel`·`onSubmit`·`searching`) | 머리 아래, 고정 | `medium` 44 |
|
|
74
|
+
| 필터 칩 줄 | HJM `filterSheet.trigger` Chip + 제품 `filters`(Stack inline gap `xs` > Chip `small`) | 검색 입력 아래, 고정, 결과 단계에만 | 입력과 `spacing.sm` 12, 칩 36(+Native hitSlop 4), 칩 사이 `spacing.xs` 8. `filtersOverflow="scroll"` |
|
|
75
|
+
| 결과 머리 | `resultSummary`: 개수 Text(muted) + 정렬 Menu(Button ghost small) | 본문 첫 줄 | `justify="between"` wrap, 큰 글자에서 두 줄 |
|
|
76
|
+
| 적용 조건 | `appliedFilters`: Chip action(trailing ×) + Button ghost small `모두 해제` | 결과 머리 아래 | 머리와 `searchScreenRecipe.headerGap`(`spacing.sm` 12), 칩 사이 `spacing.xs` 8 |
|
|
77
|
+
| 결과 | 제품 `children`: ListRow(두 줄 68) + LoadMore `manual` | 적용 조건 아래 | 위 `spacing.lg` 20(SearchScreen 본문 Stack) |
|
|
78
|
+
| 최근 검색 | `recentQueries`: Heading level5 + Button ghost small `전체 삭제` + ListRow compact(Web: 옆 IconButton ×, Native: `trailingAction`) | 검색 전 본문 맨 위 | `searchScreenRecipe.recentVisible` 5행 표시(보관 개수는 제품), 섹션 사이 `searchScreenRecipe.sectionGap`(`spacing.xl` 24) |
|
|
79
|
+
| 추천 검색어 | `suggestedQueries`: Heading level5 + Chip action wrap | 최근 검색 아래, 검색어 때문인 0건 아래 | 칩 사이 `spacing.xs` 8 |
|
|
80
|
+
| 자동완성 | `suggestions`: ListRow compact ×(1 + `searchScreenRecipe.suggestionVisible` 6) | 입력 중 본문 | 행 최소 44 |
|
|
81
|
+
| 필터 시트 | `filterSheet`: Sheet(`size` 기본 `large`, 섹션은 제품이 `auto`, Native `scrollable`, Web ≥ 960 `placement="end"`) | 오버레이 | 머리·footer 고정, 섹션은 제품 `renderContent`(섹션 사이 `spacing.xl` 24) |
|
|
82
|
+
|
|
83
|
+
## 버튼과 행동 위치
|
|
84
|
+
|
|
85
|
+
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
86
|
+
| --- | --- | --- | --- |
|
|
87
|
+
| 검색 확정 | 키보드 Enter·검색 키(`onSubmit`), 제안 첫 행 `‘q’ 검색`, 제안·최근·추천 선택 | 입력·본문 | 확정 때만 결과 조회와 최근 검색 저장 |
|
|
88
|
+
| 검색어 지우기 | SearchField × | 입력 끝 | 하나. 별도 "검색어 지우기" 버튼을 두지 않는다 |
|
|
89
|
+
| 전체 필터 열기 | `filterSheet.trigger` → Chip action `필터 N`(이름 "필터, N개 적용됨", Web `aria-haspopup="dialog"`) | 칩 줄 맨 앞(HJM이 그린다) | 하나 |
|
|
90
|
+
| 빠른 조건 | Chip `multiple` | `필터` 다음 | 즉시 적용·재조회 |
|
|
91
|
+
| 항목별 조건 | Chip action `기간: 올해 ▾`(이름 "…, 바꾸기") | 빠른 조건 다음 | 제품이 범위를 정하고 `filterSheet.open`을 켠다. 같은 시트에 그 섹션만 |
|
|
92
|
+
| 정렬 | Menu `single`, 트리거 Button ghost small | 결과 머리 끝 | 즉시 적용, 목록 맨 위로 |
|
|
93
|
+
| 조건 하나 해제 · 모두 해제 | Chip action(trailing ×) · Button ghost small | 결과 머리 아래 | 칩들 뒤에 `모두 해제` |
|
|
94
|
+
| 시트 적용 | Button primary `N개 결과 보기`(0이면 `결과 없음`·비활성) | 시트 footer | Web [초기화][적용], Native 적용 먼저 세로 |
|
|
95
|
+
| 시트 초기화 | Button secondary | 시트 footer | 항상 같은 자리, 초안이 기본값이면 비활성. 열린 섹션만 초기화 |
|
|
96
|
+
| 최근 검색 삭제 · 전체 삭제 | IconButton ghost × · Button ghost small | 행 끝 · 섹션 제목 끝 | 행마다 하나 |
|
|
97
|
+
| 복구 | Notice `danger` action(Button secondary small `다시 시도`) · EmptyState action(Button secondary `모두 해제`) | 결과 위 · 빈 결과 | 하나 |
|
|
98
|
+
|
|
99
|
+
## 상태
|
|
100
|
+
|
|
101
|
+
| 상태 | 화면 모습 | 행동 |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| 기본 | 검색 전(검색어 공백): 머리 · 입력 / 최근 검색(있을 때) · 자주 찾는 주제. 칩 줄 없음 | 최근·주제 선택 = 확정 |
|
|
104
|
+
| 입력 중 | 칩 줄 숨김. `‘q’ 검색` 행 + 제안(일치 부분 굵게, Web) | 제안 개수 polite 알림 |
|
|
105
|
+
| 결과 | 칩 줄 · `결과 N개` · 정렬 · 목록 · 더 보기 | 결과 개수 알림 |
|
|
106
|
+
| 필터 적용 | `필터 N`, 선택된 빠른 칩, 항목 칩 라벨이 값으로, 결과 위 적용 조건 칩 | ×로 하나씩, `모두 해제` |
|
|
107
|
+
| 필터 시트 | 초안을 고치는 동안 결과는 그대로, footer 개수가 초안 기준으로 바뀜 | 닫기(×·scrim·Escape·back) = 초안 버림 |
|
|
108
|
+
| 로딩 | `resultSummary.count = null`: 입력·칩 줄 유지, 개수 자리 Skeleton + `children` 대신 4행(Web ListRow `loading`, Native Skeleton). `searching`이면 입력 끝 진행 표시와 `searchingLabel` 낭독 | 두 플랫폼 모두 입력을 잠그지 않는다(1.12.1 이하 Native는 잠갔다) |
|
|
109
|
+
| 빈 | `결과 없음` 스토리(검색·필터 0건이라 `NoResults`). 필터 때문이면 칩 줄·적용 조건 유지 + EmptyState `모두 해제`. 검색어만으로 0건이면 칩 줄 숨김 + EmptyState + 추천 검색어 | 원인을 지울 수 있는 행동 하나 |
|
|
110
|
+
| 오류 | 이전 결과를 남기고 위에 Notice `danger` | `다시 시도` |
|
|
111
|
+
| 실패와 복구 | `실패와 복구` 스토리: 다음 검색 실패를 예약한 뒤 검색하면 오류 행이 되고 `다시 시도`로 같은 검색어·조건을 다시 조회 | 검색어·조건·정렬은 그대로 |
|
|
112
|
+
| 로그인 필요 | `state={{ kind: "restricted" }}`로 본문 교체 | `stateAction` 로그인 |
|
|
113
|
+
|
|
114
|
+
## 사용하는 지침
|
|
115
|
+
|
|
116
|
+
| 지침 | 쓰는 곳 |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| [SearchScreen](../components/search-screen.md) | 화면 틀, 두 단계 검색 prop 전체, debounce·취소 |
|
|
119
|
+
| [SearchField](../components/search-field.md) | 기본 입력·지우기 |
|
|
120
|
+
| [Chip](../components/chip.md) | 필터·빠른 조건·적용 조건·추천 검색어 |
|
|
121
|
+
| [Sheet](../components/sheet.md) | 필터 시트(초안·적용) |
|
|
122
|
+
| [Menu](../components/menu.md) | 정렬 |
|
|
123
|
+
| [ListRow](../components/list-row.md) | 최근 검색·제안·결과 행 |
|
|
124
|
+
| [EmptyState](../components/empty-state.md) · [Notice](../components/notice.md) · [Skeleton](../components/skeleton.md) | 빈 결과 · 오류 · 로딩 |
|
|
125
|
+
| [LoadMore](../components/load-more.md) | 결과 더 보기 |
|
|
126
|
+
| [Switch](../components/switch.md) | 시트의 조건 행 |
|
|
127
|
+
| [선택 후 적용·취소](../compositions/interaction-flow-apply.md) | 초안·적용 분리 규칙 단독 예제 |
|
|
128
|
+
| [늦은 응답보다 최신 검색 유지](../compositions/interaction-flow-search.md) | 취소·늦은 응답 무시 |
|
|
129
|
+
|
|
130
|
+
## 코드 골격
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// Web
|
|
134
|
+
import { SearchScreen } from "@hjmds/react/screen-flows";
|
|
135
|
+
|
|
136
|
+
<SearchScreen
|
|
137
|
+
title={t("search.title")}
|
|
138
|
+
queryLabel={t("search.query")}
|
|
139
|
+
queryLabelVisibility="hidden"
|
|
140
|
+
queryClearLabel={t("common.clearSearch")}
|
|
141
|
+
query={query}
|
|
142
|
+
onQueryChange={setQuery}
|
|
143
|
+
onSearch={(value, { signal }) => loadSuggestions(value, signal)} // 입력 중: 제안만
|
|
144
|
+
committedQuery={committed}
|
|
145
|
+
onSubmit={(value) => { setCommitted(value); saveRecent(value); }} // 모든 확정이 여기로 온다
|
|
146
|
+
searching={isFetching}
|
|
147
|
+
searchingLabel={t("search.searching")}
|
|
148
|
+
recentQueries={{
|
|
149
|
+
items: recents, title: t("search.recent"), clearAllLabel: t("search.recentClearAll"), onClearAll: clearRecents,
|
|
150
|
+
removeLabel: (item) => t("search.recentRemove", { item }), onRemove: removeRecent,
|
|
151
|
+
}}
|
|
152
|
+
suggestedQueries={{ title: t("search.topics"), items: topics }}
|
|
153
|
+
suggestions={{
|
|
154
|
+
items: suggestions,
|
|
155
|
+
commitLabel: (q) => t("search.commitRow", { q }),
|
|
156
|
+
countLabel: (n) => t("search.suggestionCount", { n }),
|
|
157
|
+
}}
|
|
158
|
+
resultSummary={{
|
|
159
|
+
count: isFetching ? null : results.length, // null = 불러오는 중(로딩 행)
|
|
160
|
+
countLabel: (n) => t("search.resultCount", { n }),
|
|
161
|
+
loadingLabel: t("search.loading"),
|
|
162
|
+
sort: { label: t("search.sort"), triggerLabel: sortLabel, value: sort, options: sortOptions, onChange: setSort },
|
|
163
|
+
empty: { title: t("search.emptyTitle"), description: t("search.emptyBody") },
|
|
164
|
+
}}
|
|
165
|
+
appliedFilters={{
|
|
166
|
+
items: appliedChips, removeLabel: (label) => t("search.removeFilter", { label }), onRemove: removeFilter,
|
|
167
|
+
clearAllLabel: t("search.clearFilters"), onClearAll: clearFilters,
|
|
168
|
+
}}
|
|
169
|
+
filterSheet={{
|
|
170
|
+
open: sheetOpen, onOpenChange: setSheetOpen, title: t("search.filter"),
|
|
171
|
+
value: filters, onApply: setFilters, count: (draft) => countResults(committed, draft),
|
|
172
|
+
reset: () => defaultFilters, isDefault,
|
|
173
|
+
renderContent: (draft, setDraft) => <FilterSections draft={draft} onChange={setDraft} />,
|
|
174
|
+
labels: { close: t("common.close"), reset: t("search.reset"), apply: (n) => t("search.showResults", { n: n ?? 0 }) },
|
|
175
|
+
trigger: { label: (n) => t("search.filterCount", { n }), accessibilityLabel: (n) => t("search.filterApplied", { n }) },
|
|
176
|
+
}}
|
|
177
|
+
filtersOverflow="scroll"
|
|
178
|
+
filters={<QuickFilterChips />}
|
|
179
|
+
>
|
|
180
|
+
<ResultList items={results} />
|
|
181
|
+
</SearchScreen>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// Native — 정렬 Menu에 dismissLabel이 더 필요하다
|
|
186
|
+
import { SearchScreen } from "@hjmds/react-native/screen-flows";
|
|
187
|
+
|
|
188
|
+
<SearchScreen
|
|
189
|
+
title={t("search.title")}
|
|
190
|
+
queryLabel={t("search.query")}
|
|
191
|
+
queryLabelVisibility="hidden"
|
|
192
|
+
queryClearLabel={t("common.clearSearch")}
|
|
193
|
+
query={query}
|
|
194
|
+
onQueryChange={setQuery}
|
|
195
|
+
onSearch={(value, { signal }) => loadSuggestions(value, signal)} // 입력 중: 제안만
|
|
196
|
+
committedQuery={committed}
|
|
197
|
+
onSubmit={(value) => { setCommitted(value); saveRecent(value); }} // 모든 확정이 여기로 온다
|
|
198
|
+
searching={isFetching}
|
|
199
|
+
searchingLabel={t("search.searching")}
|
|
200
|
+
recentQueries={{
|
|
201
|
+
items: recents, title: t("search.recent"), clearAllLabel: t("search.recentClearAll"), onClearAll: clearRecents,
|
|
202
|
+
removeLabel: (item) => t("search.recentRemove", { item }), onRemove: removeRecent,
|
|
203
|
+
}}
|
|
204
|
+
suggestedQueries={{ title: t("search.topics"), items: topics }}
|
|
205
|
+
suggestions={{
|
|
206
|
+
items: suggestions,
|
|
207
|
+
commitLabel: (q) => t("search.commitRow", { q }),
|
|
208
|
+
countLabel: (n) => t("search.suggestionCount", { n }),
|
|
209
|
+
}}
|
|
210
|
+
resultSummary={{
|
|
211
|
+
count: isFetching ? null : results.length, // null = 불러오는 중(로딩 행)
|
|
212
|
+
countLabel: (n) => t("search.resultCount", { n }),
|
|
213
|
+
loadingLabel: t("search.loading"),
|
|
214
|
+
sort: { label: t("search.sort"), triggerLabel: sortLabel, value: sort, options: sortOptions, onChange: setSort, dismissLabel: t("common.close") },
|
|
215
|
+
empty: { title: t("search.emptyTitle"), description: t("search.emptyBody") },
|
|
216
|
+
}}
|
|
217
|
+
appliedFilters={{
|
|
218
|
+
items: appliedChips, removeLabel: (label) => t("search.removeFilter", { label }), onRemove: removeFilter,
|
|
219
|
+
clearAllLabel: t("search.clearFilters"), onClearAll: clearFilters,
|
|
220
|
+
}}
|
|
221
|
+
filterSheet={{
|
|
222
|
+
open: sheetOpen, onOpenChange: setSheetOpen, title: t("search.filter"),
|
|
223
|
+
value: filters, onApply: setFilters, count: (draft) => countResults(committed, draft),
|
|
224
|
+
reset: () => defaultFilters, isDefault,
|
|
225
|
+
renderContent: (draft, setDraft) => <FilterSections draft={draft} onChange={setDraft} />,
|
|
226
|
+
labels: { close: t("common.close"), reset: t("search.reset"), apply: (n) => t("search.showResults", { n: n ?? 0 }) },
|
|
227
|
+
trigger: { label: (n) => t("search.filterCount", { n }), accessibilityLabel: (n) => t("search.filterApplied", { n }) },
|
|
228
|
+
}}
|
|
229
|
+
filtersOverflow="scroll"
|
|
230
|
+
filters={<QuickFilterChips />}
|
|
231
|
+
>
|
|
232
|
+
<ResultList items={results} />
|
|
233
|
+
</SearchScreen>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
데이터(`recents`·`topics`·`suggestions`·`results`·`appliedChips`), 요청(`loadSuggestions`·`countResults`), 저장(`saveRecent`)과
|
|
237
|
+
`FilterSections`·`QuickFilterChips`·`ResultList`는 제품이 공급한다. 섹션만 여는 패싯 칩은 제품이 자기 범위 상태를 바꾸고
|
|
238
|
+
`filterSheet.open`을 켠다(`title`·`reset`·`isDefault`·`renderContent`가 그 범위를 따른다). 검색 결과 개수와 `count(draft)`는
|
|
239
|
+
같은 계산(서버 count API 등)에서 나와야 "N개 결과 보기"와 적용 뒤 개수가 어긋나지 않는다.
|
|
240
|
+
`searching`은 제안 조회와 확정 결과 요청 모두에 켤 수 있다. 진행 중에도 두 플랫폼이 입력을 받는다(1.12.1 이하 Native는 SearchField `busy`가 입력을 무시해 확정 요청에만 켜야 했다).
|
|
241
|
+
|
|
242
|
+
## 큰 글자·다크·좁은 폭
|
|
243
|
+
|
|
244
|
+
| 조건 | 바뀌는 것 |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| 큰 글자 | 칩 줄은 줄바꿈 없이 한 줄 가로 스크롤로 남아 고정 영역 = 머리 + 입력 + 칩 한 줄로 묶인다. 2026-10-06 Web 실측(2배, 높이 844): 390 폭 고정 영역 258(머리 96 + 입력·칩 162), 본문 502. 320 폭은 머리가 두 줄(156)로 접혀 고정 318, 본문 442. 결과 머리는 개수·정렬이 두 줄로 접힌다. Switch 행은 1.6배 이상에서 트랙이 아래로 내려간다 |
|
|
247
|
+
| 다크 | semantic token만 쓰므로 따로 처리하지 않는다. 일치 강조는 색이 아니라 굵기라 다크·색각 차이에서도 남는다. 예제 색을 제품 기본값으로 복사하지 않는다 |
|
|
248
|
+
| 좁은 폭 | 320에서 칩 줄은 화면 끝까지 스크롤, 적용 조건 칩은 줄바꿈, 시트는 아래 전폭 |
|
|
249
|
+
| 넓은 폭(Web ≥ 960) | 한 열(최대 720) 유지, 필터 시트는 `placement="end"` 옆 시트로 결과를 가리지 않는다. 상시 왼쪽 필터 패널은 ScreenLayout 폭 계약 밖이라 두지 않는다 |
|
|
250
|
+
| 키보드 | Native 본문 ScrollView가 `keyboardShouldPersistTaps="handled"`라 키보드가 떠 있어도 제안 행이 한 번에 눌린다. 검색 키로 확정하면 키보드가 닫힌다 |
|
|
251
|
+
|
|
252
|
+
## 플랫폼 차이
|
|
253
|
+
|
|
254
|
+
| 항목 | Web | Native |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| 확정 신호 | Enter(IME 조합 중 Enter 무시) | 키보드 검색 키 |
|
|
257
|
+
| 일치 강조 | `suggestions.items[].match` 범위를 굵게. 결과 행은 제품이 ListRow `title`에 `Text emphasis="strong"` | ListRow `title`이 문자열이라 강조하지 않는다(`match`는 무시) |
|
|
258
|
+
| 개수 알림(SearchScreen) | 숨긴 `role="status"` 영역 하나 | `AccessibilityInfo.announceForAccessibilityWithOptions(…, { queue: true })` |
|
|
259
|
+
| 조건·최근 검색 삭제 뒤 포커스 | 다음 칩·× → 없으면 `필터` 칩·검색 입력 | 이동하지 않는다(스크린 리더 커서 유지) |
|
|
260
|
+
| 상세에서 돌아오기 | 검색을 `hidden`으로 마운트 유지 + 본문 scrollTop 복원 + 누른 행으로 포커스 | `display: "none"`으로 마운트 유지 |
|
|
261
|
+
| 정렬 메뉴 | 트리거 옆 목록(`align="end"`) | Menu 표면, `sort.dismissLabel` 필요 |
|
|
262
|
+
| 정렬 뒤 맨 위로 | 본문 `.hjm-screen__body` scrollTop 0 | 본문 ScrollView `scrollTo({ y: 0 })`(제품 `scrollRef`도 그대로 받는다) |
|
|
263
|
+
| 시트 footer | 오른쪽 정렬 [초기화][N개 결과 보기] | 세로 fullWidth, 주 행동 먼저 |
|
|
264
|
+
|
|
265
|
+
## 함정
|
|
266
|
+
|
|
267
|
+
- 최근 검색을 `onSearch`에서 저장하면 debounce된 부분 입력("산", "산책")이 따로 쌓인다. 2026-10-06 개편 전 예제의 결함이다.
|
|
268
|
+
- 0건 원인 판정(`resolveSearchEmptyCause`)은 `appliedFilters.items` 개수로 한다. 적용 조건을 `appliedFilters`로 넘기지 않으면 필터 때문인 0건도 검색어 때문으로 보고 칩 줄을 숨긴다.
|
|
269
|
+
- 정렬을 필터 시트 안에 넣으면 정렬 하나 바꾸는 데 시트를 열고 적용해야 한다. 결과 머리의 Menu로 즉시 적용한다(개편 전 `검색과 필터` 예제의 문제).
|
|
270
|
+
- Web Chip은 `onClick`을 자기 핸들러로 덮어써서 Menu `trigger`로 쓰면 메뉴가 열리지 않는다. 정렬 트리거는 Button을 쓴다.
|
|
271
|
+
- `모두 해제`는 적용 조건만 지우고 검색어는 남긴다. 검색어까지 지우는 행동을 두면 이름에 범위를 적는다(`검색어와 조건 초기화`). 닫기·취소는 적용값을 바꾸지 않는다(흡수한 `여러 조건 적용과 초기화`의 규칙).
|
|
272
|
+
- `committedQuery`를 주고 `children`에 제안·로딩·0건을 다시 그리면 SearchScreen이 그린 것과 겹친다. 두 단계 검색에서 `children`은 결과 목록만 담는다.
|
|
273
|
+
- 최근 검색 저장소를 `onSearch`에 연결하지 않는다. SearchScreen은 확정만 `onSubmit`으로 보내므로 거기서만 저장한다.
|
|
274
|
+
- Storybook은 실제 검색 서버·라우터 연동 증거가 아니다. Native 화면은 2026-10-06 개편에서 시뮬레이터로 확인하지 않았다(구조는 렌더 테스트로만 확인).
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 앱 설정
|
|
2
|
+
|
|
3
|
+
- 단계: 화면
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/설정`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/화면/설정/앱 설정`
|
|
10
|
+
|
|
11
|
+
## 목적
|
|
12
|
+
|
|
13
|
+
SettingsScreen을 사용해 설정 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
|
|
14
|
+
|
|
15
|
+
## 영역 구조
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
host: 남은 높이·safe area·키보드
|
|
19
|
+
└─ 제목 → 프로필 → 설정 섹션과 행
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| 바깥 틀 | SettingsScreen | route 본문 | [API 배치 규칙](../components/settings-screen.md#배치), host 남은 높이 |
|
|
25
|
+
| 내용 | 공개 슬롯 | 제목 → 프로필 → 설정 섹션과 행 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
|
|
26
|
+
| 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
|
|
27
|
+
|
|
28
|
+
## 버튼과 행동 위치
|
|
29
|
+
|
|
30
|
+
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| 작업 | SettingsScreen 공개 행동 슬롯 | Switch 행 자체로 토글; 언어 등은 행에서 선택 표면 열기 | 같은 표면에 경쟁하는 primary 하나만 |
|
|
33
|
+
| 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
|
|
34
|
+
|
|
35
|
+
## 상태
|
|
36
|
+
|
|
37
|
+
| 상태 | 화면 모습 | 행동 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| 기본 | 제목 → 프로필 → 설정 섹션과 행 | 각 공개 콜백을 제품 상태에 연결 |
|
|
40
|
+
| 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
|
|
41
|
+
| 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
|
|
42
|
+
| 오류 | 저장 실패는 notice로 알리고 마지막 확인값으로 복구 | 실패 원인과 재시도 경로 제공 |
|
|
43
|
+
|
|
44
|
+
## 사용하는 지침
|
|
45
|
+
|
|
46
|
+
| 지침 | 쓰는 곳 |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| [SettingsScreen](../components/settings-screen.md) | 필수 props·슬롯·플랫폼 차이 |
|
|
49
|
+
| [Button](../components/button.md) | 동작·로딩·보조 행동 |
|
|
50
|
+
| [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
|
|
51
|
+
|
|
52
|
+
## 코드 골격
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Web
|
|
56
|
+
import { SettingsScreen } from "@hjmds/react/screens";
|
|
57
|
+
import { Switch } from "@hjmds/react/selection";
|
|
58
|
+
import { ListRow } from "@hjmds/react/display";
|
|
59
|
+
|
|
60
|
+
<SettingsScreen
|
|
61
|
+
title={t("settings.title")}
|
|
62
|
+
sections={[
|
|
63
|
+
{ id: "notifications", title: t("settings.notifications"), children: (
|
|
64
|
+
<Switch
|
|
65
|
+
presentation="row"
|
|
66
|
+
label={t("settings.push.label")}
|
|
67
|
+
description={t("settings.push.description")}
|
|
68
|
+
checked={pushEnabled}
|
|
69
|
+
onCheckedChange={setPushEnabled}
|
|
70
|
+
/>
|
|
71
|
+
) },
|
|
72
|
+
{ id: "account", title: t("settings.account"), children: (
|
|
73
|
+
<ListRow title={t("settings.language")} description={currentLanguageName} onClick={openLanguageSheet} />
|
|
74
|
+
) },
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Native
|
|
81
|
+
import { SettingsScreen } from "@hjmds/react-native/screens";
|
|
82
|
+
import { Switch } from "@hjmds/react-native/inputs";
|
|
83
|
+
import { ListRow } from "@hjmds/react-native/data-display";
|
|
84
|
+
|
|
85
|
+
<SettingsScreen
|
|
86
|
+
title={t("settings.title")}
|
|
87
|
+
sections={[
|
|
88
|
+
{ id: "notifications", title: t("settings.notifications"), children: (
|
|
89
|
+
<Switch
|
|
90
|
+
presentation="row"
|
|
91
|
+
label={t("settings.push.label")}
|
|
92
|
+
description={t("settings.push.description")}
|
|
93
|
+
checked={pushEnabled}
|
|
94
|
+
onCheckedChange={setPushEnabled}
|
|
95
|
+
/>
|
|
96
|
+
) },
|
|
97
|
+
{ id: "account", title: t("settings.account"), children: (
|
|
98
|
+
<ListRow title={t("settings.language")} description={currentLanguageName} onPress={openLanguageSheet} />
|
|
99
|
+
) },
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
콜백·데이터·지역화 함수는 제품에서 공급한다. 필수 props와 플랫폼별 차이는 위 API 지침에서 확인한다.
|
|
105
|
+
|
|
106
|
+
## 큰 글자·다크·좁은 폭
|
|
107
|
+
|
|
108
|
+
| 조건 | 바뀌는 것 |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
|
|
111
|
+
| 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
|
|
112
|
+
| 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
|
|
113
|
+
| 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
|
|
114
|
+
|
|
115
|
+
## 플랫폼 차이
|
|
116
|
+
|
|
117
|
+
| 항목 | Web | Native |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `Switch` import | `@hjmds/react/selection` | `@hjmds/react-native/inputs` |
|
|
120
|
+
| `ListRow` import·누르기 | `@hjmds/react/display`, `onClick` | `@hjmds/react-native/data-display`, `onPress` |
|
|
121
|
+
|
|
122
|
+
## 함정
|
|
123
|
+
|
|
124
|
+
- 저장 실패는 notice로 알리고 마지막 확인값으로 복구.
|
|
125
|
+
- 2026-10-06 인자 없이 기본과 같은 화면이던 `새 계정 기본 설정`·`선택과 프로필 수정` 스토리를 뺐다. 선택·프로필 수정은 `기본`에서 직접 조작한다.
|
|
126
|
+
- Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# 화면 골격과 상태
|
|
2
|
+
|
|
3
|
+
- 단계: 화면
|
|
4
|
+
- 상태: 배포
|
|
5
|
+
- 지원: Web · Native
|
|
6
|
+
- 적용: 미게시(1.12.1 이후)
|
|
7
|
+
- 검토일: 2026-10-06
|
|
8
|
+
- 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/화면 골격`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
|
|
9
|
+
- 스토리북: `배포/화면/화면 틀과 도구/화면 골격과 상태`
|
|
10
|
+
|
|
11
|
+
## 목적
|
|
12
|
+
|
|
13
|
+
ScreenLayout을 사용해 화면 골격 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
|
|
14
|
+
|
|
15
|
+
## 영역 구조
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
host: 남은 높이·safe area·키보드
|
|
19
|
+
└─ 헤더 → notice → 본문 → footer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| 영역 | 컴포넌트 | 위치 | 크기·간격 |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| 바깥 틀 | ScreenLayout | route 본문 | [API 배치 규칙](../components/screen-layout.md#배치), host 남은 높이 |
|
|
25
|
+
| 내용 | 공개 슬롯 | 헤더 → notice → 본문 → footer | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
|
|
26
|
+
| 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
|
|
27
|
+
|
|
28
|
+
## 버튼과 행동 위치
|
|
29
|
+
|
|
30
|
+
| 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| 작업 | ScreenLayout 공개 행동 슬롯 | 뒤로는 leading, 화면 도구는 actions, 주 행동은 footer | 같은 표면에 경쟁하는 primary 하나만 |
|
|
33
|
+
| 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
|
|
34
|
+
|
|
35
|
+
## 상태
|
|
36
|
+
|
|
37
|
+
| 상태 | 화면 모습 | 행동 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| 기본 | 헤더 → notice → 본문 → footer | 각 공개 콜백을 제품 상태에 연결 |
|
|
40
|
+
| 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
|
|
41
|
+
| 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
|
|
42
|
+
| 오류 | 초기 로딩만 본문 교체; 갱신 실패는 notice로 본문 유지 | 실패 원인과 재시도 경로 제공 |
|
|
43
|
+
|
|
44
|
+
## 사용하는 지침
|
|
45
|
+
|
|
46
|
+
| 지침 | 쓰는 곳 |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| [ScreenLayout](../components/screen-layout.md) | 필수 props·슬롯·플랫폼 차이 |
|
|
49
|
+
| [Button](../components/button.md) | 동작·로딩·보조 행동 |
|
|
50
|
+
| [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
|
|
51
|
+
|
|
52
|
+
## 코드 골격
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Web
|
|
56
|
+
import { IconButton } from "@hjmds/react/actions";
|
|
57
|
+
import { BottomCTA } from "@hjmds/react/bottom-cta";
|
|
58
|
+
import { ScreenLayout } from "@hjmds/react/screens";
|
|
59
|
+
|
|
60
|
+
<ScreenLayout
|
|
61
|
+
title={t("saved.title")}
|
|
62
|
+
leading={<IconButton label={t("common.back")} onClick={goBack}><BackGlyph /></IconButton>}
|
|
63
|
+
state={isLoading ? { kind: "loading", title: t("saved.loading") } : { kind: "ready" }}
|
|
64
|
+
footer={<BottomCTA primaryAction={{ label: t("saved.add"), onClick: add }} />}
|
|
65
|
+
>
|
|
66
|
+
<SavedList items={items} />
|
|
67
|
+
</ScreenLayout>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Native — host가 safe area를 한 번 처리한다
|
|
72
|
+
import { IconButton } from "@hjmds/react-native/actions";
|
|
73
|
+
import { BottomCTA } from "@hjmds/react-native/bottom-cta";
|
|
74
|
+
import { ScreenLayout } from "@hjmds/react-native/screens";
|
|
75
|
+
|
|
76
|
+
<ScreenLayout
|
|
77
|
+
title={t("saved.title")}
|
|
78
|
+
leading={<IconButton label={t("common.back")} onPress={goBack}><BackGlyph /></IconButton>}
|
|
79
|
+
state={isLoading ? { kind: "loading", title: t("saved.loading") } : { kind: "ready" }}
|
|
80
|
+
footer={<BottomCTA primaryAction={{ label: t("saved.add"), onPress: add }} />}
|
|
81
|
+
>
|
|
82
|
+
<SavedList items={items} />
|
|
83
|
+
</ScreenLayout>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
콜백·데이터·지역화 함수는 제품에서 공급한다. 필수 props와 플랫폼별 차이는 위 API 지침에서 확인한다.
|
|
87
|
+
|
|
88
|
+
## 큰 글자·다크·좁은 폭
|
|
89
|
+
|
|
90
|
+
| 조건 | 바뀌는 것 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
|
|
93
|
+
| 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
|
|
94
|
+
| 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
|
|
95
|
+
| 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
|
|
96
|
+
|
|
97
|
+
## 플랫폼 차이
|
|
98
|
+
|
|
99
|
+
| 항목 | Web | Native |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `BottomCTA` 행동 | `primaryAction.onClick` | `primaryAction.onPress` |
|
|
102
|
+
| `BottomCTA` 위치 | `position`(`flow`·`sticky`) | 없음. `ScreenLayout` `footer`가 본문 아래 고정 |
|
|
103
|
+
|
|
104
|
+
## 함정
|
|
105
|
+
|
|
106
|
+
- 초기 로딩만 본문 교체; 갱신 실패는 notice로 본문 유지.
|
|
107
|
+
- 이 화면에는 전송 행동이 없어 `실패와 초안 복구` 스토리를 2026-10-06 뺐다. 갱신 실패 복구는 `연결 복구`로 확인한다.
|
|
108
|
+
- Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
|