@hjmds/design-contracts 1.12.1 โ†’ 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/dist/avatar-fallback.d.ts +11 -0
  2. package/dist/avatar-fallback.d.ts.map +1 -1
  3. package/dist/avatar-fallback.js +21 -0
  4. package/dist/avatar-fallback.js.map +1 -1
  5. package/dist/base-recipes.d.ts +17 -0
  6. package/dist/base-recipes.d.ts.map +1 -1
  7. package/dist/base-recipes.js +17 -0
  8. package/dist/base-recipes.js.map +1 -1
  9. package/dist/catalog.d.ts +26 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/command-palette.d.ts +14 -9
  12. package/dist/command-palette.d.ts.map +1 -1
  13. package/dist/command-palette.js +8 -9
  14. package/dist/command-palette.js.map +1 -1
  15. package/dist/component-recipes.d.ts +31 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +20 -0
  18. package/dist/component-recipes.js.map +1 -1
  19. package/dist/provider-button.d.ts.map +1 -1
  20. package/dist/provider-button.js +3 -0
  21. package/dist/provider-button.js.map +1 -1
  22. package/dist/reactions.d.ts +10 -0
  23. package/dist/reactions.d.ts.map +1 -1
  24. package/dist/reactions.js +7 -0
  25. package/dist/reactions.js.map +1 -1
  26. package/dist/screen-patterns.d.ts +147 -0
  27. package/dist/screen-patterns.d.ts.map +1 -0
  28. package/dist/screen-patterns.js +149 -0
  29. package/dist/screen-patterns.js.map +1 -0
  30. package/dist/slider.d.ts +8 -0
  31. package/dist/slider.d.ts.map +1 -1
  32. package/dist/slider.js +6 -1
  33. package/dist/slider.js.map +1 -1
  34. package/dist/upload-item.d.ts +5 -0
  35. package/dist/upload-item.d.ts.map +1 -1
  36. package/dist/upload-item.js +5 -0
  37. package/dist/upload-item.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/version.js.map +1 -1
  41. package/docs/action-session.md +3 -3
  42. package/docs/agreement.md +5 -0
  43. package/docs/avatar-fallback.md +7 -0
  44. package/docs/bottom-navigation.md +6 -0
  45. package/docs/brand-boundary.md +1 -1
  46. package/docs/button-label.md +6 -0
  47. package/docs/clipboard.md +3 -0
  48. package/docs/command-palette.md +45 -2
  49. package/docs/consumer-policy.md +5 -1
  50. package/docs/data-table.md +6 -4
  51. package/docs/dialog.md +8 -2
  52. package/docs/form.md +51 -0
  53. package/docs/generated/component-maturity.md +1 -1
  54. package/docs/generated/renderer-evidence.json +3 -3
  55. package/docs/generated/renderer-evidence.md +1 -1
  56. package/docs/generated/showcase-manifest.json +1 -1
  57. package/docs/link.md +8 -0
  58. package/docs/migration-native-legacy-removal.md +45 -1
  59. package/docs/optional-adapters.md +1 -1
  60. package/docs/password-field.md +5 -0
  61. package/docs/product-composition-adoption.md +40 -0
  62. package/docs/progress.md +19 -1
  63. package/docs/provider-button.md +13 -0
  64. package/docs/result.md +3 -0
  65. package/docs/screen-chrome.md +10 -0
  66. package/docs/screen-patterns.md +378 -0
  67. package/docs/sheet.md +21 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +13 -0
  71. package/docs/tour.md +7 -1
  72. package/docs/tree.md +5 -2
  73. package/docs/upload-item.md +7 -0
  74. package/docs/usage/README.md +236 -0
  75. package/docs/usage/STANDARD.md +108 -0
  76. package/docs/usage/components/accordion.md +107 -0
  77. package/docs/usage/components/activity-heatmap.md +104 -0
  78. package/docs/usage/components/affix.md +86 -0
  79. package/docs/usage/components/agreement.md +129 -0
  80. package/docs/usage/components/alert-dialog.md +130 -0
  81. package/docs/usage/components/anchor.md +96 -0
  82. package/docs/usage/components/aspect-ratio.md +89 -0
  83. package/docs/usage/components/asset.md +126 -0
  84. package/docs/usage/components/auth-provider-button.md +116 -0
  85. package/docs/usage/components/auth-screen-layout.md +129 -0
  86. package/docs/usage/components/avatar.md +114 -0
  87. package/docs/usage/components/badge.md +84 -0
  88. package/docs/usage/components/bottom-cta.md +125 -0
  89. package/docs/usage/components/bottom-info.md +99 -0
  90. package/docs/usage/components/bottom-navigation.md +136 -0
  91. package/docs/usage/components/breadcrumb.md +81 -0
  92. package/docs/usage/components/button.md +118 -0
  93. package/docs/usage/components/calendar.md +122 -0
  94. package/docs/usage/components/card.md +110 -0
  95. package/docs/usage/components/carousel.md +113 -0
  96. package/docs/usage/components/celebration.md +96 -0
  97. package/docs/usage/components/chat-message.md +122 -0
  98. package/docs/usage/components/chat-screen.md +112 -0
  99. package/docs/usage/components/checkbox-group.md +104 -0
  100. package/docs/usage/components/checkbox.md +103 -0
  101. package/docs/usage/components/chip.md +107 -0
  102. package/docs/usage/components/code-block.md +111 -0
  103. package/docs/usage/components/collapsible.md +112 -0
  104. package/docs/usage/components/color-picker.md +86 -0
  105. package/docs/usage/components/combobox.md +137 -0
  106. package/docs/usage/components/command-palette.md +125 -0
  107. package/docs/usage/components/comment-thread-screen.md +125 -0
  108. package/docs/usage/components/container.md +98 -0
  109. package/docs/usage/components/content-transition.md +101 -0
  110. package/docs/usage/components/context-menu.md +136 -0
  111. package/docs/usage/components/counter-badge.md +107 -0
  112. package/docs/usage/components/data-table.md +122 -0
  113. package/docs/usage/components/date-picker.md +142 -0
  114. package/docs/usage/components/date-range-picker.md +111 -0
  115. package/docs/usage/components/description-list.md +103 -0
  116. package/docs/usage/components/design-system-provider.md +124 -0
  117. package/docs/usage/components/dialog.md +176 -0
  118. package/docs/usage/components/divider.md +89 -0
  119. package/docs/usage/components/editor-screen.md +126 -0
  120. package/docs/usage/components/effect-surface.md +120 -0
  121. package/docs/usage/components/empty-state.md +114 -0
  122. package/docs/usage/components/field.md +129 -0
  123. package/docs/usage/components/file-picker.md +114 -0
  124. package/docs/usage/components/floating-action-button.md +138 -0
  125. package/docs/usage/components/form.md +162 -0
  126. package/docs/usage/components/grid.md +99 -0
  127. package/docs/usage/components/heading.md +87 -0
  128. package/docs/usage/components/icon-button.md +126 -0
  129. package/docs/usage/components/icon.md +105 -0
  130. package/docs/usage/components/image.md +122 -0
  131. package/docs/usage/components/keyboard-avoiding.md +93 -0
  132. package/docs/usage/components/keyboard-dock.md +110 -0
  133. package/docs/usage/components/keyboard-form-scroll-view.md +95 -0
  134. package/docs/usage/components/keyboard-motion-provider.md +86 -0
  135. package/docs/usage/components/layout.md +117 -0
  136. package/docs/usage/components/link.md +121 -0
  137. package/docs/usage/components/list-detail-screen.md +103 -0
  138. package/docs/usage/components/list-row.md +124 -0
  139. package/docs/usage/components/list.md +119 -0
  140. package/docs/usage/components/load-more.md +115 -0
  141. package/docs/usage/components/masonry.md +109 -0
  142. package/docs/usage/components/media-selection-screen.md +119 -0
  143. package/docs/usage/components/mentions.md +119 -0
  144. package/docs/usage/components/menu.md +129 -0
  145. package/docs/usage/components/menubar.md +93 -0
  146. package/docs/usage/components/message-composer.md +124 -0
  147. package/docs/usage/components/moderation-screen.md +113 -0
  148. package/docs/usage/components/notice.md +106 -0
  149. package/docs/usage/components/notification-inbox-screen.md +97 -0
  150. package/docs/usage/components/notification-item.md +98 -0
  151. package/docs/usage/components/number-field.md +131 -0
  152. package/docs/usage/components/onboarding-screen.md +106 -0
  153. package/docs/usage/components/otp-field.md +101 -0
  154. package/docs/usage/components/pagination.md +82 -0
  155. package/docs/usage/components/password-field.md +137 -0
  156. package/docs/usage/components/permission-screen.md +107 -0
  157. package/docs/usage/components/photo-source-sheet.md +119 -0
  158. package/docs/usage/components/popover.md +108 -0
  159. package/docs/usage/components/profile-screen.md +89 -0
  160. package/docs/usage/components/progress.md +122 -0
  161. package/docs/usage/components/qr-code.md +122 -0
  162. package/docs/usage/components/radio-group.md +124 -0
  163. package/docs/usage/components/radio.md +104 -0
  164. package/docs/usage/components/result.md +116 -0
  165. package/docs/usage/components/saved-items-screen.md +126 -0
  166. package/docs/usage/components/screen-layout.md +119 -0
  167. package/docs/usage/components/search-field.md +120 -0
  168. package/docs/usage/components/search-screen.md +221 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +146 -0
  171. package/docs/usage/components/select.md +142 -0
  172. package/docs/usage/components/settings-screen.md +126 -0
  173. package/docs/usage/components/shared-transition-element.md +111 -0
  174. package/docs/usage/components/shared-transition-screen.md +86 -0
  175. package/docs/usage/components/sheet.md +157 -0
  176. package/docs/usage/components/side-panel.md +104 -0
  177. package/docs/usage/components/sidebar.md +107 -0
  178. package/docs/usage/components/skeleton.md +105 -0
  179. package/docs/usage/components/skip-nav.md +76 -0
  180. package/docs/usage/components/slider.md +121 -0
  181. package/docs/usage/components/sortable-collection.md +127 -0
  182. package/docs/usage/components/spinner.md +86 -0
  183. package/docs/usage/components/splitter.md +103 -0
  184. package/docs/usage/components/stack.md +93 -0
  185. package/docs/usage/components/statistic.md +123 -0
  186. package/docs/usage/components/steps.md +110 -0
  187. package/docs/usage/components/surface.md +91 -0
  188. package/docs/usage/components/swipe-actions.md +124 -0
  189. package/docs/usage/components/switch.md +120 -0
  190. package/docs/usage/components/tabs.md +134 -0
  191. package/docs/usage/components/tag.md +84 -0
  192. package/docs/usage/components/tags-input.md +111 -0
  193. package/docs/usage/components/text-area.md +112 -0
  194. package/docs/usage/components/text-format.md +75 -0
  195. package/docs/usage/components/text-transition.md +104 -0
  196. package/docs/usage/components/text.md +101 -0
  197. package/docs/usage/components/thinking-orb.md +105 -0
  198. package/docs/usage/components/timeline.md +105 -0
  199. package/docs/usage/components/toast.md +145 -0
  200. package/docs/usage/components/toggle-group.md +95 -0
  201. package/docs/usage/components/tooltip.md +103 -0
  202. package/docs/usage/components/top-bar.md +124 -0
  203. package/docs/usage/components/top.md +89 -0
  204. package/docs/usage/components/tour.md +118 -0
  205. package/docs/usage/components/transfer-list.md +115 -0
  206. package/docs/usage/components/tree.md +91 -0
  207. package/docs/usage/components/upload-item.md +99 -0
  208. package/docs/usage/components/virtual-list.md +105 -0
  209. package/docs/usage/components/visually-hidden.md +72 -0
  210. package/docs/usage/components/watermark.md +78 -0
  211. package/docs/usage/compositions/action-recovery-optimistic.md +180 -0
  212. package/docs/usage/compositions/action-recovery-save.md +235 -0
  213. package/docs/usage/compositions/action-recovery-undo.md +193 -0
  214. package/docs/usage/compositions/common-message.md +132 -0
  215. package/docs/usage/compositions/common-notification.md +101 -0
  216. package/docs/usage/compositions/compound-controls.md +186 -0
  217. package/docs/usage/compositions/data-layouts.md +157 -0
  218. package/docs/usage/compositions/disclosure.md +144 -0
  219. package/docs/usage/compositions/environment-matrix.md +139 -0
  220. package/docs/usage/compositions/expo-interactions.md +149 -0
  221. package/docs/usage/compositions/family-drawer.md +201 -0
  222. package/docs/usage/compositions/floating-action-button.md +197 -0
  223. package/docs/usage/compositions/input-sheet.md +148 -0
  224. package/docs/usage/compositions/interaction-adapters.md +190 -0
  225. package/docs/usage/compositions/interaction-flow-apply.md +205 -0
  226. package/docs/usage/compositions/interaction-flow-draft.md +188 -0
  227. package/docs/usage/compositions/interaction-flow-search.md +171 -0
  228. package/docs/usage/compositions/native-renderers.md +106 -0
  229. package/docs/usage/compositions/navigation-bar-collection.md +164 -0
  230. package/docs/usage/compositions/optional-adapters.md +169 -0
  231. package/docs/usage/compositions/optional-motion.md +109 -0
  232. package/docs/usage/compositions/photo-source.md +104 -0
  233. package/docs/usage/compositions/purpose-input-comment.md +110 -0
  234. package/docs/usage/compositions/purpose-input-message.md +119 -0
  235. package/docs/usage/compositions/reference-first.md +96 -0
  236. package/docs/usage/compositions/reference-review.md +107 -0
  237. package/docs/usage/compositions/reference-settings.md +107 -0
  238. package/docs/usage/compositions/selection-scope.md +174 -0
  239. package/docs/usage/compositions/stea-event-ticket.md +166 -0
  240. package/docs/usage/compositions/stea-flip-card.md +162 -0
  241. package/docs/usage/compositions/stea-order-progress.md +184 -0
  242. package/docs/usage/compositions/stea-otp-verify.md +215 -0
  243. package/docs/usage/compositions/stea-pixel-empty.md +140 -0
  244. package/docs/usage/compositions/stea-schedule-card.md +169 -0
  245. package/docs/usage/compositions/stea-stat-summary.md +154 -0
  246. package/docs/usage/compositions/time-selection.md +174 -0
  247. package/docs/usage/compositions/toast-layout.md +128 -0
  248. package/docs/usage/compositions/visual-foundations.md +185 -0
  249. package/docs/usage/compositions/web-additions.md +146 -0
  250. package/docs/usage/compositions/web-navigation.md +143 -0
  251. package/docs/usage/screens/common-chat.md +127 -0
  252. package/docs/usage/screens/common-comments.md +108 -0
  253. package/docs/usage/screens/common-inbox.md +110 -0
  254. package/docs/usage/screens/common-login.md +98 -0
  255. package/docs/usage/screens/common-profile.md +221 -0
  256. package/docs/usage/screens/common-saved.md +127 -0
  257. package/docs/usage/screens/common-search.md +279 -0
  258. package/docs/usage/screens/common-settings.md +126 -0
  259. package/docs/usage/screens/common-shell.md +108 -0
  260. package/docs/usage/screens/dashboard.md +245 -0
  261. package/docs/usage/screens/discovery-gallery.md +306 -0
  262. package/docs/usage/screens/flow-collection.md +96 -0
  263. package/docs/usage/screens/flow-editor.md +120 -0
  264. package/docs/usage/screens/flow-media.md +111 -0
  265. package/docs/usage/screens/flow-moderation.md +120 -0
  266. package/docs/usage/screens/flow-onboarding.md +193 -0
  267. package/docs/usage/screens/flow-permission.md +103 -0
  268. package/docs/usage/screens/landing.md +347 -0
  269. package/docs/usage/screens/mockup-studio.md +190 -0
  270. package/docs/usage/screens/notification-settings.md +206 -0
  271. package/docs/usage/screens/reference-comparison.md +159 -0
  272. package/docs/usage/templates/component.md +61 -0
  273. package/docs/usage/templates/composition.md +47 -0
  274. package/docs/usage/templates/screen.md +56 -0
  275. package/docs/usage/templates/token.md +32 -0
  276. package/docs/usage/tokens/color.md +142 -0
  277. package/docs/usage/tokens/elevation-opacity.md +86 -0
  278. package/docs/usage/tokens/layers.md +98 -0
  279. package/docs/usage/tokens/layout.md +114 -0
  280. package/docs/usage/tokens/motion.md +88 -0
  281. package/docs/usage/tokens/radius.md +53 -0
  282. package/docs/usage/tokens/size.md +74 -0
  283. package/docs/usage/tokens/spacing.md +73 -0
  284. package/docs/usage/tokens/stroke.md +50 -0
  285. package/docs/usage/tokens/theme-studio.md +70 -0
  286. package/docs/usage/tokens/typography-studio.md +70 -0
  287. package/docs/usage/tokens/typography.md +89 -0
  288. 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 ์˜ˆ์ œ ๋ฐ˜์˜).