@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,78 @@
|
|
|
1
|
+
# Watermark
|
|
2
|
+
|
|
3
|
+
- ๋จ๊ณ: ์ปดํฌ๋ํธ
|
|
4
|
+
- ์ํ: ๋ฐฐํฌ
|
|
5
|
+
- ์ง์: Web
|
|
6
|
+
- ์ ์ฉ: 1.12.1
|
|
7
|
+
- ๊ฒํ ์ผ: 2026-10-06
|
|
8
|
+
- ๊ทผ๊ฑฐ: [Watermark](../../watermark.md), `src/watermark.ts`(`watermarkRecipe`)
|
|
9
|
+
- ์คํ ๋ฆฌ๋ถ: `๋ฐฐํฌ/์ปดํฌ๋ํธ/์ํ์ ์๋ฆผ/์ํฐ๋งํฌ`
|
|
10
|
+
|
|
11
|
+
## ์ธ์ ์ฐ๋
|
|
12
|
+
|
|
13
|
+
Web์์ ๋ฌธ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐยท์ด์ ํ๋ฉด ์์ "์ด์", ํ๋ก์ ํธ ์ด๋ฆ ๊ฐ์ **์ฅ์์ฉ ์ถ์ฒ ํ์ ํ
์คํธ**๋ฅผ ๋น์ค๋ฌํ
|
|
14
|
+
๋ฐ๋ณตํด ๊น ๋ ์ด๋ค. inline SVG pattern์ด๋ผ ์ธ๋ถ ์์ฒญยทcanvas๊ฐ ์๊ณ , ์ค๋ฒ๋ ์ด๋ ํด๋ฆญยทํ
์คํธ ์ ํ์ ๋ง์ง ์๋๋ค.
|
|
15
|
+
|
|
16
|
+
## ์ฐ์ง ์์ ๋
|
|
17
|
+
|
|
18
|
+
| ์ํฉ | ๋์ ์ธ ๊ฒ |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| ์คํฌ๋ฆฐ์ทยท๋ณต์ ยท์ ์ถ ๋ฐฉ์ง | ์์. Watermark๋ ๋ณด์ ๊ธฐ๋ฅ์ด ์๋๋ค |
|
|
21
|
+
| ๋ฐ๋์ ์ฝํ์ผ ํ๋ ์ถ์ฒยท๋ณด์ ๊ณ ์ง | ๋ณธ๋ฌธ [Notice](notice.md) ๋๋ [Text](text.md) |
|
|
22
|
+
| ์ด๋ฏธ์ง ์ํฐ๋งํฌ, ๋ค์ด๋ก๋ ํ์ผ์ ์ฝ์
| ์์(์ ๊ณตํ์ง ์์) |
|
|
23
|
+
| ํ๋ฉด ์ํ ๋ฐฐ์ง("์ด์" ํ ๊ณณ ํ์) | [Badge](badge.md), [Tag](tag.md) |
|
|
24
|
+
|
|
25
|
+
## ๊ณต๊ฐ ์ด๋ฆ๊ณผ import
|
|
26
|
+
|
|
27
|
+
| ์ด๋ฆ | ์ญํ | Web | Native |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `Watermark` | ๊ธฐ๋ณธ | `/watermark`๋ง(root ์์) | โ |
|
|
30
|
+
|
|
31
|
+
## ์ต์ ์ฌ์ฉ ์
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Web
|
|
35
|
+
import { Watermark } from "@hjmds/react/watermark";
|
|
36
|
+
|
|
37
|
+
<Watermark text={[t("document.draft"), projectName]}>
|
|
38
|
+
<DocumentPreview />
|
|
39
|
+
</Watermark>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Native: ์์.
|
|
43
|
+
|
|
44
|
+
## ์ถ๊ณผ ๊ธฐ๋ณธ๊ฐ
|
|
45
|
+
|
|
46
|
+
| prop | ๊ฐ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช
|
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| `text` | ๋ฌธ์์ด ๋๋ 1~3์ค ๋ฐฐ์ด | โ | ๊ฐ ์ค์ ๋น์ด ์์ง ์๊ณ 120์ ์ดํ |
|
|
49
|
+
| `tileWidth` | 80 ์ด์ | 240 | ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด `TypeError` |
|
|
50
|
+
| `tileHeight` | 60 ์ด์ | 160 | ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด `TypeError` |
|
|
51
|
+
| `rotate` | -90~90 | -22 | ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด `TypeError` |
|
|
52
|
+
| `opacity` | 0~1 | 0.12 | ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด `TypeError` |
|
|
53
|
+
| `children` | `ReactNode` | โ (ํ์) | ํ์๋ฅผ ๊น ๋ด์ฉ |
|
|
54
|
+
| `layoutStyle` | `HjmCompositionStyleProp` | โ | ๊ฐ์ธ๋ ๋ฃจํธ์ ๋ฐ๊นฅ ๋ฐฐ์น(marginยทํญยทflexยท`alignSelf`) |
|
|
55
|
+
|
|
56
|
+
๊ธ์ ์์ `--hjm-color-text-sub` ํ ํฐ์ด๋ค. ์ prop์ ์๋ค.
|
|
57
|
+
|
|
58
|
+
## ๋ฐฐ์น
|
|
59
|
+
|
|
60
|
+
| ํญ๋ชฉ | ๊ฐ | ๊ทผ๊ฑฐ |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| ํฌ๊ธฐ | ์์ฒด ํฌ๊ธฐยท์ฌ๋ฐฑ์ด ์๋ค. ๊ฐ์ผ ๋ด์ฉ์ด ๊ทธ๋๋ก ํ๋ฅด๊ณ ์ค๋ฒ๋ ์ด๊ฐ ๊ทธ ์์ญ ์ ์ฒด(`inset: 0`)๋ฅผ ๋ฎ์ผ๋ฉฐ, ๋์น๋ ๋ฌด๋ฌ๋ ์์ญ ๊ฒฝ๊ณ์์ ์๋ฆฐ๋ค. ํ์ผ ๊ธฐ๋ณธ 240ร160, -22ยฐ ํ์ | `.hjm-watermark__overlay`, `watermarkRecipe` |
|
|
63
|
+
| ๊ฐ๊ฒฉ | ์์ญ ํฌ๊ธฐ์ ๋ฐ๊นฅ ๊ฐ๊ฒฉ์ `layoutStyle`์ด๋ ๊ฐ์ธ๋ ์์๊ฐ ์ ํ๋ค | `react/src/watermark.tsx` |
|
|
64
|
+
| ์์ยท์ ๋ ฌ | ๋ด์ฉ(z-index 0) ์์ ์ค๋ฒ๋ ์ด(z-index 1), ํด๋ฆญยท์ ํ์ ๋ด์ฉ์ผ๋ก ํต๊ณผ | `.hjm-watermark__content`, `.hjm-watermark__overlay`(pointer-events: none) |
|
|
65
|
+
| ๊ณ ์ ยท์คํฌ๋กค | ํ์๋ฅผ ๊น ์์ญ(๋ฌธ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นด๋ยท์ด์ ๋ณธ๋ฌธ)๋ง ๊ฐ์ผ๋ค. ํ๋ฉด ์ ์ฒด๋ TopBarยทBottomCTA ๊ฐ์ ๊ณ ์ ์์ญ๊น์ง ๊ฐ์ธ์ง ์๋๋ค. ์คํฌ๋กค ์์ญ ์์ ๋๋ฉด ๋ด์ฉ๊ณผ ํจ๊ป ์คํฌ๋กค๋๊ณ , ์คํฌ๋กค ์ปจํ
์ด๋ ์์ฒด๋ฅผ ๊ฐ์ธ๋ฉด ๋ฌด๋ฌ๋ ๋ณด์ด๋ ์ฐฝ์ ๊ณ ์ ๋๋ค | `.hjm-watermark`(position: relative) |
|
|
66
|
+
| ์ข์ ํญยทํฐ ๊ธ์ | ์์ญ์ด ํ์ผ ํ๋๋ณด๋ค ์์ผ๋ฉด ๋ฌธ๊ตฌ๊ฐ ์๋ ค ๋ณด์ด๋ฏ๋ก `tileWidth`ยท`tileHeight`๋ฅผ ์์ญ์ ๋ง๊ฒ ์ค์ธ๋ค(์ต์ 80ร60) | `resolveWatermark` |
|
|
67
|
+
|
|
68
|
+
## ๊ผญ ์งํฌ ๊ฒ
|
|
69
|
+
|
|
70
|
+
- ํ์ ๋ฌธ๊ตฌ๋ i18n ํค๋ก ๋ง๋ค๊ณ , ํ๋ก์ ํธ๋ช
๊ฐ์ ๋ฐ์ดํฐ๋ ๊ทธ๋๋ก ๋๊ธด๋ค. ๋ฌธ์์ด์ React text node๋ก๋ง ๋ค์ด๊ฐ ๋งํฌ์
์ผ๋ก ํด์๋์ง ์๋๋ค.
|
|
71
|
+
- ๊ธด ๋ฌธ๊ตฌ๋ ์ค์ ์๋ฅด์ง ๋ง๊ณ `tileWidth`ยท`tileHeight`๋ฅผ ํค์ด๋ค.
|
|
72
|
+
- ์ค๋ฒ๋ ์ด๋ `aria-hidden`์ด๋ค. ๋ณด์กฐ๊ธฐ์ ์ฌ์ฉ์์๊ฒ๋ ํ์ํ ๋ด์ฉ์ด๋ฉด ๋ณธ๋ฌธ์ ๋ฐ๋ก ์ด๋ค.
|
|
73
|
+
- `className`ยท`style`ยท`ref`๋ฅผ ๋ฐ์ง ์๋๋ค. ๋ฐฐ์น๋ `layoutStyle`์ด๋ ๊ฐ์ธ๋ ์์์์ ํ๋ค. ์ค๋ฒ๋ ์ด ์์น๋ `hjm-watermark`์
|
|
74
|
+
`position: relative`์ ์์กดํ๋ฏ๋ก `@hjmds/react`์ `styles.css`๊ฐ ํ์ํ๋ค.
|
|
75
|
+
|
|
76
|
+
## ํจ์
|
|
77
|
+
|
|
78
|
+
- SVG๋ DOM ์ฝํ
์ธ ๋ผ ์ธ์์๋ ๋์จ๋ค. ์ธ์ ๊ฒฐ๊ณผ๋ ์๋น ํ๊ฒฝ์์ ํ์ธํ๋ค(๊ณ์ฝ ๋ฌธ์ ๊ธฐ์ค, HJM์ด ์ธ์๋ฅผ ๊ฒ์ฆํ์ง ์์).
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# ์ฆ์ ๋ฐ์๊ณผ ๋ณต๊ตฌ
|
|
2
|
+
|
|
3
|
+
- ๋จ๊ณ: ๊ตฌ์ฑ
|
|
4
|
+
- ์ํ: ๋ฐฐํฌ
|
|
5
|
+
- ์ง์: Web ยท Native
|
|
6
|
+
- ์ ์ฉ: 1.12.1
|
|
7
|
+
- ๊ฒํ ์ผ: 2026-10-06
|
|
8
|
+
- ๊ทผ๊ฑฐ: [๊ณตํต ์คํ๊ณผ ์คํจ ๋ณต๊ตฌ](../../action-session.md), `src/action-session.ts`, `showcase/web/src/patterns/action-recovery-previews.tsx`, `showcase/native/src/action-recovery-previews.tsx`. 2026-10-06 ์ฌ์ฉ์ ์น์ธ์ผ๋ก ์คํ ๋ฆฌ๋ถ ๋ฐฐํฌ(์ด์ `์คํ/๊ตฌ์ฑ/๊ณตํต ๋์/์ฆ์ ๋ฐ์๊ณผ ๋ณต๊ตฌ`, [์น์ธ ๊ธฐ๋ก](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-์ ์ฒด-์น๊ฒฉ๊ณผ-๊ท๊ฒฉ-ํ์ ))
|
|
9
|
+
- ์คํ ๋ฆฌ๋ถ: `๋ฐฐํฌ/๊ตฌ์ฑ/ํผ๋๋ฐฑ๊ณผ ๋ณต๊ตฌ/์ฆ์ ๋ฐ์๊ณผ ๋ณต๊ตฌ`
|
|
10
|
+
|
|
11
|
+
## ์ธ์ ์ฐ๋
|
|
12
|
+
|
|
13
|
+
๋ถ๋งํฌยท์ข์์ยท์๋ฆผ ์ผ๊ธฐ์ฒ๋ผ ๋๋๋ ค๋ ํผํด๊ฐ ์๋ ์ ์ํ ํ ๊ธ์ ๋๋ฅด๋ ์ฆ์ ํ๋ฉด์ ๋ฐ์ํ๊ณ , ์๋ฒ๊ฐ ์คํจํ๋ฉด ์ง์ ํ์ธ ๊ฐ์ผ๋ก ๋๋๋ฆด ๋ ์ด๋ค.
|
|
14
|
+
|
|
15
|
+
๊ฒฐ์ ยท์๊ตฌ ์ญ์ ยท๊ถํ ๋ณ๊ฒฝ์ฒ๋ผ ์คํจ๋ฅผ ํ๋ฉด๋ง ๋๋๋ ค ์จ๊ธฐ๋ฉด ์ ๋๋ ์์
์๋ ์ฐ์ง ์๋๋ค. ๊ทธ๋๋ [์ ์ฅ๊ณผ ์ฌ์๋](action-recovery-save.md)์ฒ๋ผ
|
|
16
|
+
์๋ฒ ํ์ธ ๋ค ํ์ํ๋ค.
|
|
17
|
+
|
|
18
|
+
๊ฐ์ `๊ณตํต ๋์` ๋ฌถ์: [์ ์ฅ๊ณผ ์ฌ์๋](action-recovery-save.md) ยท [๋ณด๊ด๊ณผ ์คํ ์ทจ์](action-recovery-undo.md).
|
|
19
|
+
์ธ์
์์ฑยท๊ตฌ๋
ยท์ํ ์๋ฆผ์ [์ ์ฅ๊ณผ ์ฌ์๋์ ๊ณตํต ์ ](action-recovery-save.md#๊ณตํต-์ธ์
๊ณผ-์ํ-์๋ฆผ)์ ๋ฐ๋ฅธ๋ค.
|
|
20
|
+
|
|
21
|
+
## ๊ตฌ์ฑ ์์
|
|
22
|
+
|
|
23
|
+
| ์ปดํฌ๋ํธ | ์ญํ | ์ง์นจ |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `createActionSession` + `optimisticValue` | pending ๋์ ์ ๊ฐ์ ๋จผ์ ๋ณด์ด๊ณ , ์คํจํ๋ฉด ์ด์ ํ์ธ ๊ฐ์ผ๋ก ๋ณต๊ตฌ | [๊ณ์ฝ](../../action-session.md) |
|
|
26
|
+
| `Heading` | ๋ฌถ์ ์ ๋ชฉ(์ ํ). ํฌ๊ธฐ `level3`(24), ๋ฌธ์ ๋จ๊ณ๋ ํ๋ฉด ๊ตฌ์กฐ์ ๋ง์ถฐ `semanticLevel` | [Heading](../components/heading.md) |
|
|
27
|
+
| `Button selected` | ์ผ๊ณ ๋๋ ํ ๊ธ. `selected`=ํ์ฌ(๋๊ด) ๊ฐ, pending ์ค `disabled` | [Button](../components/button.md) |
|
|
28
|
+
| `Button` ๋ค์ ๋ฐ์ | error์ผ ๋๋ง ๋ํ๋๋ ๋ณด์กฐ ํ๋, `tone="secondary"` | [Button](../components/button.md) |
|
|
29
|
+
| `Text` | ์ํ ๋ฌธ๊ตฌ. Web `role="status"`, Native ์ ๊ทผ์ฑ ์๋ฆผ | [Text](../components/text.md) |
|
|
30
|
+
| `Stack` | ์ธ๋ก ์๊ธฐ `gap="md"` | [Stack](../components/stack.md) |
|
|
31
|
+
| `Container` ยท `ScrollView` | ๋ฐ๊นฅ ํ. ์ ํ ํ๋ฉด์ด ์์ ํ๋ค | [Container](../components/container.md), [ํ๋ฉด ์ฌ๋ฐฑ](../tokens/layout.md) |
|
|
32
|
+
|
|
33
|
+
์คํ ๋ฆฌ์ `tone="ghost"` "์๋ฒ์์ ์ ์ํ ๋ถ๋ฌ์ค๊ธฐ"๋ `reset`์ด ์ง๋ ์๋ต์ ๋ฒ๋ฆฌ๋ ๊ฒ์ ๋ณด์ฌ ์ฃผ๋ ๋ฐ๋ชจ ๋ฒํผ์ด๋ค.
|
|
34
|
+
์ ํ์์๋ ์ด ๋์์ ๋ฒํผ์ด ์๋๋ผ ์ฌ์กฐํยท๊ณ์ ๋ณ๊ฒฝ ์์ ์ ์ฐ๊ฒฐํ๋ค. "๋ค์ ์์ฒญ ์คํจ์ํค๊ธฐ"์ 350ms ์ง์ฐ๋ ๋ฐ๋ชจ ์ ์ฉ์ด๋ค.
|
|
35
|
+
|
|
36
|
+
## ๋ฐฐ์น
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
โ ํ๋ฉด ๋ฐ๊นฅ ํ(์ ํ ์์ ) โโโโโโโโโโโโโโโโโโ
|
|
40
|
+
โ Web: ๋ฌธ์ ์คํฌ๋กค ยท Native: ScrollView โ
|
|
41
|
+
โ โ Native ์์๋ spacing.lg 20 โ
|
|
42
|
+
โ โgutter 16|20โ Container โgutter 16|20โ โ
|
|
43
|
+
โ โ Stack gap="md" โโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
44
|
+
โ โ ์ ๋ชฉ Heading level3 (์ ํ) โ โ
|
|
45
|
+
โ โ โ spacing.md 16 โ โ
|
|
46
|
+
โ โ [ ๋ถ๋งํฌ ์ถ๊ฐ / ํด์ (selected) ] โ โ โ ํ ๊ธ, ๋์ด 44
|
|
47
|
+
โ โ โ spacing.md 16 โ โ
|
|
48
|
+
โ โ [ ๋ถ๋งํฌ ๋ค์ ๋ฐ์ (secondary) ] โ โ โ error์ผ ๋๋ง
|
|
49
|
+
โ โ โ spacing.md 16 โ โ
|
|
50
|
+
โ โ ์ํ ๋ฌธ๊ตฌ (role=status) โ โ
|
|
51
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
52
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| ์์ญ | ์ปดํฌ๋ํธ | ์์น | ํฌ๊ธฐยท๊ฐ๊ฒฉ |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| ๋ฐ๊นฅ ํ | Web ๋ฌธ์ ์คํฌ๋กค > `Container`. Native `ScrollView` > `Container` | ์ด ๊ตฌ์ฑ์ ๊ฐ์ธ๋ ์ ํ ํ๋ฉด(๋์ ์ฝํ
์ธ ์์ธ ๋ฑ). ์๋จ ์์ ์์ญ์ ๋ด๋น๊ฒ์ด์
ํค๋(๋๋ [TopBar](../components/top-bar.md))๊ฐ ๋งก๋๋ค | ์ข์ฐ `Container gutter`: ํญ 600 ๋ฏธ๋ง `compact` 16, ์ด์ `regular` 20([ํ๋ฉด ์ฌ๋ฐฑ](../tokens/layout.md)). Native ์์๋๋ `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
|
|
58
|
+
| ์ ๋ชฉ(์ ํ) | `Heading level="level3"` | Stack ๋งจ ์ | ์๋ `spacing.md` 16 |
|
|
59
|
+
| ํ ๊ธ | `Button selected` | ๋์ ์ฝํ
์ธ ์๋, ์คํฌ๋กค | `medium` 44(`control.buttonHeight`), ์ด์ `spacing.md` 16. Stack ๊ธฐ๋ณธ `align="stretch"`๋ก ๊ฝ ์ฐฌ ํญ |
|
|
60
|
+
| ์ฌ์๋ | `Button tone="secondary"` | ํ ๊ธ ์๋, error์ผ ๋๋ง | `medium` 44, `spacing.md` 16. ๋ผ๋ฒจ์ ๋ฌด์์ ๋ค์ ํ๋์ง ์ ๋๋ค(`t("bookmark.retry")`) |
|
|
61
|
+
| ์ํ | `Text` | ํ๋ ์๋ | `spacing.md` 16 |
|
|
62
|
+
|
|
63
|
+
- ํ ๊ธ์ด ๋ชฉ๋ก ํ ๋์ ๋ค์ด๊ฐ๋ฉด Button ์ง์นจ๋๋ก `size="small"`(36, `control.buttonHitSlop.small` 4๋ก ํฐ์น 44)๊ณผ `secondary`ยท`ghost`๋ฅผ ์ด๋ค.
|
|
64
|
+
์ํ ๋ฌธ๊ตฌ๋ ํ๋ง๋ค ๋์ง ์๊ณ [Toast](../components/toast.md) ๋ฑ ํ๋ฉด ๋จ์๋ก ํ ๋ฒ ์๋ฆฐ๋ค.
|
|
65
|
+
- ๋ผ๋ฒจ์ ์ํ์ ๋ฐ๋ผ ๋ฐ๋๋ค(`t("bookmark.add")` โ `t("bookmark.remove")`). ๋ ๋ผ๋ฒจ ๋ชจ๋ ๋ ์ค ์์ ๋ค์ด๊ฐ์ผ ํ๋ค.
|
|
66
|
+
- ๊บผ์ง ํ ๊ธ(`selected={false}`)์ ๊ธฐ๋ณธ tone primary๋ก ์น ํด์ง๊ณ , ์ผ์ง ํ ๊ธ์ `selected` ์ฒ๋ฆฌ๋ก ์น ํด์ ธ primary ์์ ์ธ์ง ์๋๋ค
|
|
67
|
+
([Button](../components/button.md) ๊ผญ ์งํฌ ๊ฒ). ๊ทธ๋์ ํ ํ๋ฉด primary ํ๋๋ฅผ ์งํค๋ ค๋ฉด ์ฌ์๋๋ `secondary`๋ค.
|
|
68
|
+
|
|
69
|
+
๊ทผ๊ฑฐ: `src/foundations.ts`(`spacing`, `control`, `layout`), `src/action-session.ts`(`optimisticValue`)
|
|
70
|
+
|
|
71
|
+
## ํ๋ฆ๊ณผ ์ํ
|
|
72
|
+
|
|
73
|
+
1. ํ๋ฉด์ ์๋ฒ์์ ํ์ธํ ๊ฐ์ผ๋ก ํ ๊ธ์ ๊ทธ๋ฆฐ๋ค(`createActionSession(serverValue)`).
|
|
74
|
+
2. ๋๋ฅด๋ฉด `next = !state.value`๋ก `session.run(() => api.set(next), { optimisticValue: next, retryable: true })`๋ฅผ ๋ถ๋ฅธ๋ค.
|
|
75
|
+
3. pending ๋์ `state.value`๊ฐ ๊ณง๋ฐ๋ก `next`๊ฐ ๋์ด ํ ๊ธ์ด ๋ฐ๋๊ณ , ๋ฒํผ์ `disabled`๋ค. ๊ฐ์ ์ธ์
์ ์ runยทretry๋ `blocked`๋ค.
|
|
76
|
+
4. ์ฑ๊ณตํ๋ฉด ์๋ฒ๊ฐ ๋๋ ค์ค ๊ฐ์ผ๋ก ํ์ ํ๋ค.
|
|
77
|
+
5. ์คํจ(๋คํธ์ํฌยท์๋ฒ ์ค๋ฅ, ์์
ํจ์์ throwยทreject)ํ๋ฉด `state.value`๊ฐ ์คํ ์ ํ์ธ ๊ฐ์ผ๋ก ๋์๊ฐ๊ณ ์ฌ์๋ ๋ฒํผ์ด ๋์จ๋ค.
|
|
78
|
+
`session.retry()`๋ ๊ฐ์ `optimisticValue`๋ก ๋ค์ ์คํํด ํ ๊ธ์ด ๋ค์ ์ ๊ฐ์ผ๋ก ๋ฐ๋๋ค. ์ฌ์๋๋ ์คํจํ๋ฉด ๋ค์ ์ด์ ๊ฐ์ผ๋ก ๋์๊ฐ๊ณ
|
|
79
|
+
์ฌ์๋ ๋ฒํผ์ด ๋จ๋๋ค. error ์ค ํ ๊ธ์ ๋ค์ ๋๋ฅด๋ฉด ์ run์ด ๋๊ณ ์ด์ ์คํจ ์์
์ ๋ฒ๋ ค์ง๋ค.
|
|
80
|
+
6. ์ฌ์กฐํยท๊ณ์ ๋ณ๊ฒฝ ์ `session.reset(serverValue)`๋ฅผ ๋ถ๋ฅด๋ฉด ๋ฆ๊ฒ ์จ ์ด์ ์๋ต๊ณผ ์ฌ์๋๊ฐ ๋ฌด์๋๋ค.
|
|
81
|
+
|
|
82
|
+
| ์ํ | ๋ชจ์ต | ํฌ์ปค์คยท์๋ฆผ |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| ๊ธฐ๋ณธ | idle. ํ์ธ๋ ๊ฐ์ ํ ๊ธ, ์ํ ๋ฌธ๊ตฌ `t("bookmark.hint")` | ์๋ฆผ ์์(์ฒซ ๋ ๋๋ ์๋ฆฌ์ง ์๋๋ค) |
|
|
85
|
+
| ์งํ ์ค | pending. ํ ๊ธ์ด ์ ๊ฐ์ผ๋ก ๋ฐ๋๊ณ `disabled`, ์ํ ๋ฌธ๊ตฌ `t("bookmark.pending")` | ํฌ์ปค์ค๋ ํ ๊ธ์ ์ ์ง, ์ํ ๋ฌธ๊ตฌ ์๋ฆผ |
|
|
86
|
+
| ์ฑ๊ณต | success. ์๋ฒ ๊ฐ์ผ๋ก ํ์ , `t("bookmark.saved")` | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ |
|
|
87
|
+
| ์คํจ | error. ์ด์ ๊ฐ์ผ๋ก ๋ณต๊ท, ์ฌ์๋ ๋ฒํผ, `t("bookmark.reverted")` | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ, ํฌ์ปค์ค ์ด๋ ์์ |
|
|
88
|
+
| ์ฌ์๋ ์คํจ | error ์ ์ง. ์ฌ์๋ โ pending โ error๋ก ๋ฌธ๊ตฌ๊ฐ ๋ฐ๋๋ฏ๋ก ๋ค์ ์๋ฆฐ๋ค | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ, ํฌ์ปค์ค ์ด๋ ์์ |
|
|
89
|
+
|
|
90
|
+
- ์ํโ๋ฌธ๊ตฌ ํค๋ ์์ ํ(`bookmarkStatusKey`)๋ก ๋๋ค. ํ
ํ๋ฆฟ ๋ฌธ์์ด ํค(`` `bookmark.${status}` ``)๋ ํค ์ถ์ถยท๋๋ฝ ๊ฒ์ฌ๊ฐ ์ฐพ์ง ๋ชปํ๊ณ
|
|
91
|
+
์ ํ์ ํค์ ์ด๊ธ๋๋ค.
|
|
92
|
+
- ์ค๋๋ ์์ฒญ์ด ์๋ฒ์ ๋ฐ์๋์ ์ ์์ผ๋ฉด `reset`๋ง์ผ๋ก ์ ํฉ์ฑ์ด ๋ณด์ฅ๋์ง ์๋๋ค. ์ ํ์ด ์ฌ์กฐํยท์บ์ ๋ฌดํจํ๋ก ๋ง์ถ๋ค.
|
|
93
|
+
- ๊ฐ์ฒด ๊ฐ์ ๋ถ๋ณ ๋ฐ์ดํฐ๋ก ๋๊ธด๋ค.
|
|
94
|
+
- ์ค๋ฅ ๋ฌธ๊ตฌ๋ ์ ํ์ด ์ง์ญํํ๋ค. raw exception(`state.error`)์ ๊ทธ๋๋ก ๋ณด์ด์ง ์๋๋ค.
|
|
95
|
+
|
|
96
|
+
## ์ฝ๋ ๊ณจ๊ฒฉ
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// Web
|
|
100
|
+
import { useState, useSyncExternalStore } from "react";
|
|
101
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
102
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
103
|
+
import { Button } from "@hjmds/react/actions";
|
|
104
|
+
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
105
|
+
|
|
106
|
+
const bookmarkStatusKey = { idle: "bookmark.hint", pending: "bookmark.pending", success: "bookmark.saved", error: "bookmark.reverted" } as const;
|
|
107
|
+
|
|
108
|
+
function BookmarkToggle({ initial, setBookmark }: { initial: boolean; setBookmark(next: boolean): Promise<boolean> }) {
|
|
109
|
+
const [session] = useState(() => createActionSession(initial)); // ๋ ๋๋ง๋ค ์๋ก ๋ง๋ค์ง ์๋๋ค
|
|
110
|
+
const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
111
|
+
const toggle = () => {
|
|
112
|
+
const next = !state.value;
|
|
113
|
+
void session.run(() => setBookmark(next), { optimisticValue: next, retryable: true });
|
|
114
|
+
};
|
|
115
|
+
const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
|
|
116
|
+
return (
|
|
117
|
+
<Container gutter={gutter}>
|
|
118
|
+
<Stack gap="md">
|
|
119
|
+
<Button selected={state.value} disabled={state.status === "pending"} onClick={toggle}>
|
|
120
|
+
{state.value ? t("bookmark.remove") : t("bookmark.add")}
|
|
121
|
+
</Button>
|
|
122
|
+
{state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("bookmark.retry")}</Button>}
|
|
123
|
+
<Text as="p" role="status">{t(bookmarkStatusKey[state.status])}</Text>
|
|
124
|
+
</Stack>
|
|
125
|
+
</Container>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
// Native
|
|
132
|
+
import { useState, useSyncExternalStore } from "react";
|
|
133
|
+
import { ScrollView, useWindowDimensions } from "react-native";
|
|
134
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
135
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
136
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
137
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
138
|
+
import { Container, Stack } from "@hjmds/react-native/primitives";
|
|
139
|
+
|
|
140
|
+
const bookmarkStatusKey = { idle: "bookmark.hint", pending: "bookmark.pending", success: "bookmark.saved", error: "bookmark.reverted" } as const;
|
|
141
|
+
|
|
142
|
+
function BookmarkToggle({ initial, setBookmark }: { initial: boolean; setBookmark(next: boolean): Promise<boolean> }) {
|
|
143
|
+
const [session] = useState(() => createActionSession(initial));
|
|
144
|
+
const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
145
|
+
const { width } = useWindowDimensions();
|
|
146
|
+
const toggle = () => {
|
|
147
|
+
const next = !state.value;
|
|
148
|
+
void session.run(() => setBookmark(next), { optimisticValue: next, retryable: true });
|
|
149
|
+
};
|
|
150
|
+
return (
|
|
151
|
+
<ScrollView contentContainerStyle={{ paddingVertical: spacing.lg }}>
|
|
152
|
+
<Container gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
|
|
153
|
+
<Stack gap="md">
|
|
154
|
+
<Button selected={state.value} disabled={state.status === "pending"} onPress={toggle}>
|
|
155
|
+
{state.value ? t("bookmark.remove") : t("bookmark.add")}
|
|
156
|
+
</Button>
|
|
157
|
+
{state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("bookmark.retry")}</Button>}
|
|
158
|
+
<StatusText>{t(bookmarkStatusKey[state.status])}</StatusText>
|
|
159
|
+
</Stack>
|
|
160
|
+
</Container>
|
|
161
|
+
</ScrollView>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`setBookmark`(์๋ฒ๊ฐ ํ์ ํ boolean์ ๋๋ ค์ค)๊ณผ `StatusText`๋ ์ ํ ์์ ๋ค([๊ณตํต ์ ](action-recovery-save.md#๊ณตํต-์ธ์
๊ณผ-์ํ-์๋ฆผ)์ helper).
|
|
167
|
+
|
|
168
|
+
## ํ๋ซํผ ์ฐจ์ด
|
|
169
|
+
|
|
170
|
+
| ํญ๋ชฉ | Web | Native |
|
|
171
|
+
| --- | --- | --- |
|
|
172
|
+
| `selected` ์๋ฆผ | `aria-pressed` | ์ ๊ทผ์ฑ state |
|
|
173
|
+
| ์ด๋ฒคํธ | `onClick` | `onPress` |
|
|
174
|
+
| ๋ฐ๊นฅ ์คํฌ๋กค | ๋ฌธ์ ์คํฌ๋กค | `ScrollView`(์์๋ `spacing.lg`) |
|
|
175
|
+
| ์ํ ์๋ฆผ | `role="status"` | live region(Android) + iOS ์๋ฆผ ํธ์ถ |
|
|
176
|
+
|
|
177
|
+
## ํจ์
|
|
178
|
+
|
|
179
|
+
- ๊ตฌํ ์ ๋ชฉ์ Heading์ผ๋ก ํ์ํ๋ค. ์ด์ Text heading ์์ ๋ 2026-10-06 ์ ๋ชฉ ์๋ฏธ ๊ตฌ์กฐ๋ฅผ ๋ง์ถ๋ฉด์ ์์ ํ๋ค.
|
|
180
|
+
- ์ฌ์๋๋ secondary๋ก ๋์ด ์ ์ฅ๊ณผ ๊ฒฝ์ํ๋ primary๋ฅผ ๋ง๋ค์ง ์๋๋ค(2026-10-06 ์์ ๋ฐ์).
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# ์ ์ฅ๊ณผ ์ฌ์๋
|
|
2
|
+
|
|
3
|
+
- ๋จ๊ณ: ๊ตฌ์ฑ
|
|
4
|
+
- ์ํ: ๋ฐฐํฌ
|
|
5
|
+
- ์ง์: Web ยท Native
|
|
6
|
+
- ์ ์ฉ: 1.12.1
|
|
7
|
+
- ๊ฒํ ์ผ: 2026-10-06
|
|
8
|
+
- ๊ทผ๊ฑฐ: [๊ณตํต ์คํ๊ณผ ์คํจ ๋ณต๊ตฌ](../../action-session.md), `src/action-session.ts`, `showcase/web/src/patterns/action-recovery-previews.tsx`, `showcase/native/src/action-recovery-previews.tsx`. 2026-10-06 ์ฌ์ฉ์ ์น์ธ์ผ๋ก ์คํ ๋ฆฌ๋ถ ๋ฐฐํฌ(์ด์ `์คํ/๊ตฌ์ฑ/๊ณตํต ๋์/์ ์ฅ๊ณผ ์ฌ์๋`, [์น์ธ ๊ธฐ๋ก](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-์ ์ฒด-์น๊ฒฉ๊ณผ-๊ท๊ฒฉ-ํ์ ))
|
|
9
|
+
- ์คํ ๋ฆฌ๋ถ: `๋ฐฐํฌ/๊ตฌ์ฑ/ํผ๋๋ฐฑ๊ณผ ๋ณต๊ตฌ/์ ์ฅ๊ณผ ์ฌ์๋`
|
|
10
|
+
|
|
11
|
+
## ์ธ์ ์ฐ๋
|
|
12
|
+
|
|
13
|
+
์
๋ ฅํ ๋ด์ฉ์ ์๋ฒ์ ์ ์ฅํ๋ ํผ ํ ๋ฉ์ด๋ฆฌ์์ ์ ์ฅ ์ค ์ค๋ณต ์คํ์ ๋ง๊ณ , ์คํจํ๋ฉด ์
๋ ฅ์ ์ง์ฐ์ง ์์ ์ฑ ์ ์ถํ๋ ๊ฐ ๊ทธ๋๋ก ๋ค์ ๋ณด๋ผ ๋ ์ด๋ค.
|
|
14
|
+
|
|
15
|
+
ํ์ธ์ฐฝ ์์ ๋น๋๊ธฐ ํ์ธ์ AlertDialog ์ธ์
์ด ์ด๋ฏธ ์์ ํ๋ฏ๋ก ์ฌ๊ธฐ์ ์ฐ์ง ์๋๋ค. ์ ํ์ด TanStack Query mutation ๋ฑ์ผ๋ก
|
|
16
|
+
์ํ๋ฅผ ์ด๋ฏธ ์์ ํ๋ฉด ์ธ์
์ ๊ฒน์ณ ๋ง๋ค์ง ์๊ณ ๊ฐ์ ๊ท์น๋ง ์ฐ๊ฒฐํ๋ค.
|
|
17
|
+
|
|
18
|
+
๊ฐ์ `๊ณตํต ๋์` ๋ฌถ์: [์ฆ์ ๋ฐ์๊ณผ ๋ณต๊ตฌ](action-recovery-optimistic.md) ยท [๋ณด๊ด๊ณผ ์คํ ์ทจ์](action-recovery-undo.md).
|
|
19
|
+
์ธ ๊ตฌ์ฑ๊ณผ [๋ฆ์ ์๋ต๋ณด๋ค ์ต์ ๊ฒ์ ์ ์ง](interaction-flow-search.md)๊ฐ ํจ๊ป ์ฐ๋ ์ธ์
์ฐ๊ฒฐยท์ํ ์๋ฆผ์
|
|
20
|
+
์ด ๋ฌธ์์ [๊ณตํต: ์ธ์
๊ณผ ์ํ ์๋ฆผ](#๊ณตํต-์ธ์
๊ณผ-์ํ-์๋ฆผ)์ ์๋ค.
|
|
21
|
+
|
|
22
|
+
## ๊ตฌ์ฑ ์์
|
|
23
|
+
|
|
24
|
+
| ์ปดํฌ๋ํธ | ์ญํ | ์ง์นจ |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `createActionSession` | idle โ pending โ success/error ์ํ, ๊ฐ์ ์ธ์
์ ์ค๋ณต ์คํ ์ฐจ๋จ, `retry` | [๊ณ์ฝ](../../action-session.md) |
|
|
27
|
+
| `Heading` | ๋ฌถ์ ์ ๋ชฉ. ํฌ๊ธฐ `level3`(24), ๋ฌธ์ ๋จ๊ณ๋ ํ๋ฉด ๊ตฌ์กฐ์ ๋ง์ถฐ `semanticLevel` | [Heading](../components/heading.md) |
|
|
28
|
+
| `TextField` | ์ ์ฅํ ์
๋ ฅ. ๊ฐ์ ์ ํ state(์ด์)๊ฐ ์์ | [Field](../components/field.md) |
|
|
29
|
+
| `Button` ์ ์ฅ | ์ฃผ ํ๋. `loading`=pending, ์
๋ ฅ์ด ๋น๋ฉด `disabled` | [Button](../components/button.md) |
|
|
30
|
+
| `Button` ๋ค์ ์ ์ฅ | error์ผ ๋๋ง ๋ํ๋๋ ๋ณด์กฐ ํ๋, `tone="secondary"` | [Button](../components/button.md) |
|
|
31
|
+
| `Text` | ์ํ ๋ฌธ๊ตฌ(์งํยท์ฑ๊ณตยท์คํจ). Web `role="status"`, Native ์ ๊ทผ์ฑ ์๋ฆผ. ํ์ ๊ฐ ์ค(์ ํ) | [Text](../components/text.md) |
|
|
32
|
+
| `Stack` | ์ธ๋ก ์๊ธฐ `gap="md"` | [Stack](../components/stack.md) |
|
|
33
|
+
| `Container` ยท `ScrollView` | ๋ฐ๊นฅ ํ. ์ ํ ํ๋ฉด์ด ์์ ํ๋ค | [Container](../components/container.md), [ํ๋ฉด ์ฌ๋ฐฑ](../tokens/layout.md) |
|
|
34
|
+
|
|
35
|
+
์คํ ๋ฆฌ์ "๋ค์ ์์ฒญ ์คํจ์ํค๊ธฐ"(secondary ํ ๊ธ)์ 350ms ์ง์ฐ์ ๋ฐ๋ชจ ์ ์ฉ์ด๋ค. ์ ํ์ ๋ฃ์ง ์๋๋ค.
|
|
36
|
+
|
|
37
|
+
## ๋ฐฐ์น
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
โ ํ๋ฉด ๋ฐ๊นฅ ํ(์ ํ ์์ ) โโโโโโโโโโโโโโโโโโ
|
|
41
|
+
โ Web: ๋ฌธ์ ์คํฌ๋กค ยท Native: ScrollView โ
|
|
42
|
+
โ โ Native ์์๋ spacing.lg 20 โ
|
|
43
|
+
โ โgutter 16|20โ Container โgutter 16|20โ โ
|
|
44
|
+
โ โ Stack gap="md" โโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
45
|
+
โ โ ์ ๋ชฉ Heading level3 โ โ
|
|
46
|
+
โ โ โ spacing.md 16 โ โ
|
|
47
|
+
โ โ ๋ผ๋ฒจ โ โ
|
|
48
|
+
โ โ [ ์
๋ ฅ ] โ โ TextField, ๋์ด 44
|
|
49
|
+
โ โ โ spacing.md 16 โ โ
|
|
50
|
+
โ โ [ ์ ์ฅ (primary) ] โ โ โ ์ฃผ ํ๋, ๋์ด 44
|
|
51
|
+
โ โ โ spacing.md 16 โ โ
|
|
52
|
+
โ โ [ ์คํจํ ๋ด์ฉ ๋ค์ ์ ์ฅ (secondary)] โ โ โ error์ผ ๋๋ง
|
|
53
|
+
โ โ โ spacing.md 16 โ โ
|
|
54
|
+
โ โ ์ํ ๋ฌธ๊ตฌ (role=status) โ โ
|
|
55
|
+
โ โ ์ ์ฅ๋ ๋ด์ฉ: state.value (์ ํ) โ โ
|
|
56
|
+
โ โ ๋ณด์กฐ ์ค๋ช
(์ ํ ์์ , ์ ํ) โ โ
|
|
57
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
58
|
+
โ ํ๋จ ์์ ์์ญ(Native, ์คํฌ๋กค ๋ ์ฌ๋ฐฑ) โ
|
|
59
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| ์์ญ | ์ปดํฌ๋ํธ | ์์น | ํฌ๊ธฐยท๊ฐ๊ฒฉ |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| ๋ฐ๊นฅ ํ | Web ๋ฌธ์ ์คํฌ๋กค > `Container size="reading"`. Native `ScrollView`(`keyboardShouldPersistTaps="handled"`) > `Container size="reading"` | ์ด ๊ตฌ์ฑ์ ๊ฐ์ธ๋ ์ ํ ํ๋ฉด. ์๋จ ์์ ์์ญ์ ๋ด๋น๊ฒ์ด์
ํค๋(๋๋ [TopBar](../components/top-bar.md))๊ฐ ๋งก๋๋ค | ์ข์ฐ `Container gutter`: ํญ 600 ๋ฏธ๋ง `compact` 16, ์ด์ `regular` 20([ํ๋ฉด ์ฌ๋ฐฑ](../tokens/layout.md)). Native ์์๋๋ `contentContainerStyle` `paddingVertical: spacing.lg` 20 |
|
|
65
|
+
| ์ ๋ชฉ | `Heading level="level3"` | Stack ๋งจ ์, ์คํฌ๋กค | ์๋ `spacing.md` 16 |
|
|
66
|
+
| ์
๋ ฅ | `TextField` | ์ ๋ชฉ ์๋, ์คํฌ๋กค | ๋์ด `control.fieldHeight` 44 |
|
|
67
|
+
| ์ฃผ ํ๋ | `Button` ์ ์ฅ(primary) | ์
๋ ฅ ๋ฐ๋ก ์๋, ์คํฌ๋กค | `medium` 44(`control.buttonHeight`), ์ด์ `spacing.md` 16. Stack ๊ธฐ๋ณธ `align="stretch"`๋ก ๊ฝ ์ฐฌ ํญ์ด ๋๋ฏ๋ก `fullWidth`ยท`layoutStyle`์ ํ์ ์๋ค |
|
|
68
|
+
| ์ฌ์๋ | `Button tone="secondary"` ๋ค์ ์ ์ฅ | ์ฃผ ํ๋ ์๋, error์ผ ๋๋ง | `medium` 44, `spacing.md` 16. ๋ผ๋ฒจ์ ๋ฌด์์ ๋ค์ ํ๋์ง ์ ๋๋ค(์คํ ๋ฆฌ "์คํจํ ๋ด์ฉ ๋ค์ ์ ์ฅ"). ์ผ๋ฐ "๋ค์ ์๋"๋ง ์ฐ์ง ์๋๋ค |
|
|
69
|
+
| ์ํ | `Text` | ํ๋ ์๋ | `spacing.md` 16 |
|
|
70
|
+
| ํ์ ๊ฐ(์ ํ) | `Text` โ `state.value` | ์ํ ๋ฌธ๊ตฌ ์๋ | `spacing.md` 16 |
|
|
71
|
+
|
|
72
|
+
- ๋ชจ๋ ๊ฐ๊ฒฉ์ `Stack gap="md"`(`spacing.md` 16) ํ๋๋ก ๋ง์ถ๋ค. `stackRecipe.defaults.gap`๋ `md`๋ค.
|
|
73
|
+
- ์คํ ๋ฆฌ๋ ์ข์ ํญ ๊ธฐ์ค ๋ฐ๊นฅ ํ(Web `Container gutter="compact" size="reading"`, Native `ScrollView` > ๊ฐ์ Container)์ ๊ทธ๋ฆฐ๋ค. ๋์ ํญ์์๋ ์ ํ์ ์ฒซ ํ๋๋ก `regular`๋ฅผ ๊ณ ๋ฅธ๋ค.
|
|
74
|
+
ํค๋ณด๋๊ฐ ์
๋ ฅยท์ ์ฅ ๋ฒํผ์ ๊ฐ๋ฆฌ๋ Native ํผ์ ์
๋ ฅ์ด ์ฌ๋ฌ ๊ฐ๋ฉด [KeyboardFormScrollView](../components/keyboard-form-scroll-view.md)
|
|
75
|
+
(peer ํ์), peer๊ฐ ์์ผ๋ฉด [KeyboardAvoiding](../components/keyboard-avoiding.md) ๊ธฐ์ค์ ๋ฐ๋ฅธ๋ค.
|
|
76
|
+
- ์ ์ฅ์ด ํ๋ฉด ํ๋จ ๊ณ ์ ํ๋์ด๋ฉด Button ๋์ [BottomCTA](../components/bottom-cta.md)์ ๋๊ณ , ์ฌ์๋ ๋ฒํผ๊ณผ ์ํ ๋ฌธ๊ตฌ๋ ๋ณธ๋ฌธ(์คํฌ๋กค)์ ๋จ๊ธด๋ค.
|
|
77
|
+
- ํ ํ๋ฉด์ primary๋ ํ๋๋ค([Button](../components/button.md)). ์ฌ์๋๋ `secondary`๋ก ๋๊ฑฐ๋ ์ ์ฅ ๋ฒํผ ํ๋๋ก ๋ชจ์๋ค.
|
|
78
|
+
|
|
79
|
+
๊ทผ๊ฑฐ: `src/component-recipes.ts`(`stackRecipe`), `src/foundations.ts`(`spacing`, `control`, `layout`), `src/container.ts`
|
|
80
|
+
|
|
81
|
+
## ํ๋ฆ๊ณผ ์ํ
|
|
82
|
+
|
|
83
|
+
1. ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ค. ์
๋ ฅ์ด ๋น์ด ์์ผ๋ฉด ์ ์ฅ ๋ฒํผ์ `disabled`๋ค.
|
|
84
|
+
2. ์ ์ฅ์ ๋๋ฅด๋ฉด ๊ทธ ์๊ฐ์ ์
๋ ฅ๊ฐ์ ์บก์ฒํด `session.run(() => api.save(submitted), { retryable: true })`๋ฅผ ๋ถ๋ฅธ๋ค.
|
|
85
|
+
3. pending ๋์ ๊ฐ์ ์ธ์
์ ์ runยทretry๋ `blocked`๋ผ ์์
ํจ์๊ฐ ๋ถ๋ฆฌ์ง ์๋๋ค.
|
|
86
|
+
4. ์คํจ(๋คํธ์ํฌยท์๋ฒ ์ค๋ฅ, ์์
ํจ์์ throwยทreject)ํ๋ฉด ์ธ์
๊ฐ์ ์คํ ์ ๊ฐ์ผ๋ก ๋์๊ฐ๊ณ ์
๋ ฅ ์ด์์ ๊ทธ๋๋ก ๋จ์ผ๋ฉฐ
|
|
87
|
+
์ฌ์๋ ๋ฒํผ์ด ๋์จ๋ค. `session.retry()`๋ ์คํจํ ์์
์ ์บก์ฒํ ๊ฐ์ผ๋ก ๋ค์ ๋ถ๋ฅธ๋ค.
|
|
88
|
+
5. ์ฌ์๋๋ ์คํจํ๋ฉด ๋ค์ error๊ฐ ๋๊ณ ์ฌ์๋ ๋ฒํผ์ด ๋จ๋๋ค(`retryable` ์คํ์ ์คํจํ ๋๋ง๋ค ๋ค์ ์ฌ์๋ํ ์ ์๋ค).
|
|
89
|
+
error ์ค ์ฌ์ฉ์๊ฐ ์
๋ ฅ์ ๊ณ ์ณ ์ ์ฅ์ ๋๋ฅด๋ฉด ์ run์ด ๋๊ณ ์ด์ ์คํจ ์์
์ ๋ฒ๋ ค์ง๋ค.
|
|
90
|
+
6. ์ฑ๊ณตํ๋ฉด ์ธ์
`state.value`๊ฐ ์๋ฒ๊ฐ resolveํ ๊ฐ์ด ๋๋ค. ํ์ ๊ฐ์ ๋ณด์ฌ ์ค ๋๋ `state.value`๋ฅผ ์ฝ๊ณ ์
๋ ฅ ์ด์(draft)์
|
|
91
|
+
๋ฎ์ด์ฐ์ง ์๋๋ค(์คํ ๋ฆฌ์ "์ ์ฅ๋ ๋ด์ฉ" ์ค).
|
|
92
|
+
|
|
93
|
+
| ์ํ | ๋ชจ์ต | ํฌ์ปค์คยท์๋ฆผ |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| ๊ธฐ๋ณธ | idle. ์ ์ฅ ๋ฒํผ๋ง, ์ํ ๋ฌธ๊ตฌ `t("save.hint")`. ์
๋ ฅ์ด ๋น๋ฉด ์ ์ฅ `disabled` | ์๋ฆผ ์์(์ฒซ ๋ ๋๋ ์๋ฆฌ์ง ์๋๋ค) |
|
|
96
|
+
| ์งํ ์ค | pending. ์ ์ฅ ๋ฒํผ `loading`(๋ผ๋ฒจ ์๋ฆฌ ์ ์ง, ์ค์ ์คํผ๋ ํ๋), ์ํ ๋ฌธ๊ตฌ `t("save.pending")` | ํฌ์ปค์ค๋ ๋ฒํผ์ ์ ์ง, ์ํ ๋ฌธ๊ตฌ ์๋ฆผ |
|
|
97
|
+
| ์ฑ๊ณต | success. ์ํ ๋ฌธ๊ตฌ `t("save.done")`, ํ์ ๊ฐ ์ค ๊ฐฑ์ | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ |
|
|
98
|
+
| ์คํจ | error. ์
๋ ฅ ์ ์ง, ์ฌ์๋ ๋ฒํผ ๋ฑ์ฅ, ์ํ ๋ฌธ๊ตฌ `t("save.failed")` | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ, ํฌ์ปค์ค ์ด๋ ์์ |
|
|
99
|
+
| ์ฌ์๋ ์คํจ | error ์ ์ง. ์ฌ์๋ โ pending โ error๋ก ๋ฌธ๊ตฌ๊ฐ ๋ฐ๋๋ฏ๋ก ๋ค์ ์๋ฆฐ๋ค | ์ํ ๋ฌธ๊ตฌ ์๋ฆผ, ํฌ์ปค์ค ์ด๋ ์์ |
|
|
100
|
+
|
|
101
|
+
- ์ํโ๋ฌธ๊ตฌ ํค๋ ์์ ํ(`saveStatusKey`)๋ก ๋๋ค. ํ
ํ๋ฆฟ ๋ฌธ์์ด ํค(`` `save.${status}` ``)๋ ํค ์ถ์ถยท๋๋ฝ ๊ฒ์ฌ๊ฐ ์ฐพ์ง ๋ชปํ๊ณ
|
|
102
|
+
์ ํ์ ํค์ ์ด๊ธ๋๋ค.
|
|
103
|
+
- ์ํ ๋ฌธ๊ตฌ `Text`์ tone์ ๊ธฐ๋ณธ๊ฐ์ ์ด๋ค(์คํ ๋ฆฌ). ์ค๋ฅ๋ฅผ ์์ผ๋ก๋ ๊ตฌ๋ถํ๋ ค๋ฉด error์ผ ๋๋ง `tone="danger"`๋ฅผ ์ฃผ๋,
|
|
104
|
+
์๋ง์ผ๋ก ์๋ฏธ๋ฅผ ์ ํ์ง ์๋๋ก ๋ฌธ๊ตฌ ์์ฒด๊ฐ ์คํจ๋ฅผ ๋งํด์ผ ํ๋ค.
|
|
105
|
+
- `retryable` ๊ธฐ๋ณธ๊ฐ์ `false`๋ค. ์ฌ์๋๊ฐ ์์ ํ๋ค๊ณ ์ ํ์ด ํ๋จํ ๊ฒฝ์ฐ๋ง ์ผ ๋ค.
|
|
106
|
+
- ์๋ฒ๊ฐ ๋ฉฑ๋ฑ ํค๋ฅผ ์ง์ํ๋ฉด ๊ฐ์ ์ ์ถ์ ์ฌ์๋์ ๊ฐ์ ์ ํ ์์ฒญ ํค๋ฅผ ์ด๋ค. ์ธ์
์ `operationId`๋ ์๋ฒ ๋ฉฑ๋ฑ ํค๊ฐ ์๋๋ค.
|
|
107
|
+
- ์ค๋ฅ ๋ฌธ๊ตฌ๋ ์ ํ์ด ์ง์ญํํ๋ค. raw exception(`state.error`)์ ๊ทธ๋๋ก ๋ณด์ด์ง ์๋๋ค.
|
|
108
|
+
|
|
109
|
+
## ์ฝ๋ ๊ณจ๊ฒฉ
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// Web
|
|
113
|
+
import { useState, useSyncExternalStore } from "react";
|
|
114
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
115
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
116
|
+
import { Button } from "@hjmds/react/actions";
|
|
117
|
+
import { TextField } from "@hjmds/react/forms";
|
|
118
|
+
import { Heading } from "@hjmds/react/heading";
|
|
119
|
+
import { Container, Stack, Text } from "@hjmds/react/layout";
|
|
120
|
+
|
|
121
|
+
const saveStatusKey = { idle: "save.hint", pending: "save.pending", success: "save.done", error: "save.failed" } as const;
|
|
122
|
+
|
|
123
|
+
function SaveNote({ save }: { save(value: string): Promise<string> }) {
|
|
124
|
+
const [session] = useState(() => createActionSession("")); // ๋ ๋๋ง๋ค ์๋ก ๋ง๋ค์ง ์๋๋ค
|
|
125
|
+
const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
126
|
+
const [draft, setDraft] = useState("");
|
|
127
|
+
const gutter = resolveWindowClass(window.innerWidth) === "compact" ? "compact" : "regular";
|
|
128
|
+
return (
|
|
129
|
+
<Container size="reading" gutter={gutter}>
|
|
130
|
+
<Stack gap="md">
|
|
131
|
+
<Heading level="level3" semanticLevel={2}>{t("note.title")}</Heading>
|
|
132
|
+
<TextField label={t("note.body")} value={draft} onValueChange={setDraft} />
|
|
133
|
+
<Button loading={state.status === "pending"} disabled={!draft.trim()}
|
|
134
|
+
onClick={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
|
|
135
|
+
{t("common.save")}
|
|
136
|
+
</Button>
|
|
137
|
+
{state.status === "error" && <Button tone="secondary" onClick={() => void session.retry()}>{t("save.retry")}</Button>}
|
|
138
|
+
<Text as="p" role="status">{t(saveStatusKey[state.status])}</Text>
|
|
139
|
+
{state.value && <Text as="p">{t("save.savedValue", { value: state.value })}</Text>}
|
|
140
|
+
</Stack>
|
|
141
|
+
</Container>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
// Native
|
|
148
|
+
import { useState, useSyncExternalStore } from "react";
|
|
149
|
+
import { ScrollView, useWindowDimensions } from "react-native";
|
|
150
|
+
import { createActionSession } from "@hjmds/design-contracts/action-session";
|
|
151
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
152
|
+
import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
|
|
153
|
+
import { Button } from "@hjmds/react-native/actions";
|
|
154
|
+
import { Heading } from "@hjmds/react-native/heading";
|
|
155
|
+
import { TextField } from "@hjmds/react-native/inputs";
|
|
156
|
+
import { Container, Stack, Text } from "@hjmds/react-native/primitives";
|
|
157
|
+
|
|
158
|
+
const saveStatusKey = { idle: "save.hint", pending: "save.pending", success: "save.done", error: "save.failed" } as const;
|
|
159
|
+
|
|
160
|
+
function SaveNote({ save }: { save(value: string): Promise<string> }) {
|
|
161
|
+
const [session] = useState(() => createActionSession(""));
|
|
162
|
+
const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
163
|
+
const [draft, setDraft] = useState("");
|
|
164
|
+
const { width } = useWindowDimensions();
|
|
165
|
+
return (
|
|
166
|
+
<ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={{ paddingVertical: spacing.lg }}>
|
|
167
|
+
<Container size="reading" gutter={resolveWindowClass(width) === "compact" ? "compact" : "regular"}>
|
|
168
|
+
<Stack gap="md">
|
|
169
|
+
<Heading level="level3" semanticLevel={2}>{t("note.title")}</Heading>
|
|
170
|
+
<TextField label={t("note.body")} value={draft} onValueChange={setDraft} />
|
|
171
|
+
<Button loading={state.status === "pending"} disabled={!draft.trim()}
|
|
172
|
+
onPress={() => { const submitted = draft; void session.run(() => save(submitted), { retryable: true }); }}>
|
|
173
|
+
{t("common.save")}
|
|
174
|
+
</Button>
|
|
175
|
+
{state.status === "error" && <Button tone="secondary" onPress={() => void session.retry()}>{t("save.retry")}</Button>}
|
|
176
|
+
<StatusText>{t(saveStatusKey[state.status])}</StatusText>{/* ์๋ ๊ณตํต ์ ์ ์ ํ helper */}
|
|
177
|
+
{state.value ? <Text>{t("save.savedValue", { value: state.value })}</Text> : null}
|
|
178
|
+
</Stack>
|
|
179
|
+
</Container>
|
|
180
|
+
</ScrollView>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
๋ฌธ๊ตฌ ํคยท์ ์ฅ API๋ ์ ํ ์์ ๋ค.
|
|
186
|
+
|
|
187
|
+
### ๊ณตํต: ์ธ์
๊ณผ ์ํ ์๋ฆผ
|
|
188
|
+
|
|
189
|
+
- ์ธ์
์ ์ํฐํฐยทํ๋ ํ๋์ ํ๋๋ค. ํ๋ฉด ์ปดํฌ๋ํธ์์๋ `useState(() => createActionSession(initial))`๋ก ํ ๋ฒ ๋ง๋ค๊ณ ,
|
|
190
|
+
ํ๋ฉด๋ณด๋ค ์ค๋ ์ด์์ผ ํ๋ฉด ์ ํ์ ์์
์์ ์(store)์ ๋๋ค. unmount ์ ์๋ reset์ ๊ธฐ๋ณธ์ผ๋ก ๋ฃ์ง ์๋๋ค.
|
|
191
|
+
- ๊ตฌ๋
์ WebยทNative ๋ชจ๋ `useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot)`๋ค. SSR์ ์์ฒญ๋ณ ์ธ์
์ ๋ง๋ ๋ค.
|
|
192
|
+
- `reset(value)`๋ ์ธ๋๋ฅผ ๋ฐ๊ฟ ์ด์ ์๋ต๊ณผ ์ฌ์๋๋ฅผ ๋ฒ๋ฆฐ๋ค. ์๋ฒ ์์
์ ์ทจ์ํ์ง๋ ์๋๋ค.
|
|
193
|
+
- ์ํ ๋ฌธ๊ตฌ ์๋ฆผ: Web์ `<Text role="status">`๋ค. Native๋ `Text`์ `accessibilityLiveRegion="polite"`(Android)๋ฅผ ์ฃผ๊ณ ,
|
|
194
|
+
iOS์์๋ ๋ฌธ๊ตฌ๊ฐ ๋ฐ๋์๊ณ ๋น์ด ์์ง ์์ ๋๋ง, ์ฑ์ด active์ผ ๋ `AccessibilityInfo.announceForAccessibilityWithOptions(message, { queue: true })`๋ฅผ ๋ถ๋ฅธ๋ค.
|
|
195
|
+
์คํ ๋ฆฌ์ `PatternStatus`(`showcase/native/src/pattern-status.tsx`)๋ ๊ณต๊ฐ API๊ฐ ์๋๋ฏ๋ก ์ ํ์์ ๊ฐ์ ๋์์ helper๋ฅผ ๋ง๋ ๋ค.
|
|
196
|
+
๋๋จธ์ง Text prop์ `Omit<ComponentProps<typeof Text>, ...>`๋ก ๋ฐ์ ๊ทธ๋๋ก ๋๊ธด๋ค(optional prop์ `tone?: โฆ`๋ก ๋ฐ๋ก ๋ฐ์ ๋๊ธฐ๋ฉด
|
|
197
|
+
`exactOptionalPropertyTypes`์์ TS2375๊ฐ ๋๋ค).
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// Native
|
|
201
|
+
import { useEffect, useRef, type ComponentProps } from "react";
|
|
202
|
+
import { AccessibilityInfo, AppState, Platform } from "react-native";
|
|
203
|
+
import { Text } from "@hjmds/react-native/primitives";
|
|
204
|
+
|
|
205
|
+
type StatusTextProps = Omit<ComponentProps<typeof Text>, "children" | "accessibilityLiveRegion"> & { children: string };
|
|
206
|
+
|
|
207
|
+
function StatusText({ children, ...props }: StatusTextProps) {
|
|
208
|
+
const previous = useRef(children); // ์ฒซ ๋ ๋๋ ์๋ฆฌ์ง ์๋๋ค
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const changed = previous.current !== children;
|
|
211
|
+
previous.current = children;
|
|
212
|
+
if (changed && children.trim() && Platform.OS === "ios" && AppState.currentState === "active") {
|
|
213
|
+
AccessibilityInfo.announceForAccessibilityWithOptions(children, { queue: true });
|
|
214
|
+
}
|
|
215
|
+
}, [children]);
|
|
216
|
+
return <Text {...props} accessibilityLiveRegion="polite">{children}</Text>;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## ํ๋ซํผ ์ฐจ์ด
|
|
221
|
+
|
|
222
|
+
| ํญ๋ชฉ | Web | Native |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| ์
๋ ฅ importยท์ด๋ฒคํธ | `@hjmds/react/forms`, `onValueChange`(๋ฌธ์์ด ๊ฐ, Native์ ๊ฐ์ ์ด๋ฆ). DOM ์ด๋ฒคํธ๊ฐ ํ์ํ ๋๋ง `onChange`(๋ ๋ค ํธ์ถ๋๋ค) | `@hjmds/react-native/inputs`, `onValueChange` |
|
|
225
|
+
| ๋ฒํผ ์ด๋ฒคํธ | `onClick` | `onPress` |
|
|
226
|
+
| ๋ฐ๊นฅ ์คํฌ๋กค | ๋ฌธ์ ์คํฌ๋กค | `ScrollView`(`keyboardShouldPersistTaps="handled"`, ์์๋ `spacing.lg`) |
|
|
227
|
+
| ์ํ ์๋ฆผ | `role="status"` | live region(Android) + iOS ์๋ฆผ ํธ์ถ |
|
|
228
|
+
| ๋ก๋ฉ ์ค ํฌ์ปค์ค | โ | Button์ด ๊ธฐ๋ณธ์ผ๋ก ํฌ์ปค์ค ์ ์ง(`disableWhileLoading`์ผ๋ก๋ง ์ ๋์) |
|
|
229
|
+
|
|
230
|
+
## ํจ์
|
|
231
|
+
|
|
232
|
+
- ์ฌ์๋์์ ํ์ฌ ์
๋ ฅ state๋ฅผ ๋ค์ ์ฝ์ผ๋ฉด ์คํจ ๋ค ๊ณ ์น ๊ฐ์ด "๊ฐ์ ์ ์ถ์ ์ฌ์๋"๋ก ๋๊ฐ๋ค. ์ ์ถ ์์ ๊ฐ์ ์บก์ฒํ๋ค.
|
|
233
|
+
- ๋ ๋ ์ค `createActionSession`์ ๋ถ๋ฅด๋ฉด ๋งค ๋ ๋ ์ ์ธ์
์ด ์๊ฒจ ์ค๋ณต ์ฐจ๋จ์ด ์ฌ๋ผ์ง๋ค.
|
|
234
|
+
- ๊ตฌํ ์ ๋ชฉ์ Heading์ผ๋ก ํ์ํ๋ค. ์ด์ Text heading ์์ ๋ 2026-10-06 ์ ๋ชฉ ์๋ฏธ ๊ตฌ์กฐ๋ฅผ ๋ง์ถ๋ฉด์ ์์ ํ๋ค.
|
|
235
|
+
- ์ฌ์๋๋ secondary๋ก ๋์ด ์ ์ฅ๊ณผ ๊ฒฝ์ํ๋ primary๋ฅผ ๋ง๋ค์ง ์๋๋ค(2026-10-06 ์์ ๋ฐ์).
|