@hjmds/design-contracts 1.12.0 → 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.
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 +25 -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 +17 -0
  16. package/dist/component-recipes.d.ts.map +1 -1
  17. package/dist/component-recipes.js +5 -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 +376 -0
  67. package/docs/sheet.md +12 -0
  68. package/docs/splitter.md +8 -2
  69. package/docs/theming.md +36 -29
  70. package/docs/toggle-group.md +7 -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 +104 -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 +215 -0
  169. package/docs/usage/components/section.md +111 -0
  170. package/docs/usage/components/segmented-control.md +138 -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 +151 -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 +274 -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,108 @@
1
+ # 댓글
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/댓글`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/소통/댓글`
10
+
11
+ ## 목적
12
+
13
+ CommentThreadScreen을 사용해 댓글 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 원댓글 → 한 단계 답글 → 더 보기 → 작성창
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | CommentThreadScreen | route 본문 | [API 배치 규칙](../components/comment-thread-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 원댓글 → 한 단계 답글 → 더 보기 → 작성창 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | CommentThreadScreen 공개 행동 슬롯 | 좋아요·답글은 각 댓글; 보내기는 하단 composer | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 원댓글 → 한 단계 답글 → 더 보기 → 작성창 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 빈 댓글에서도 작성창 유지; 실패한 전송은 초안 유지 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [CommentThreadScreen](../components/comment-thread-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { CommentThreadScreen } from "@hjmds/react/screen-flows";
57
+ import { MessageComposer } from "@hjmds/react/screens";
58
+
59
+ <CommentThreadScreen
60
+ title={t("comments.title")}
61
+ items={comments.map(toCommentItem)}
62
+ expandedIds={expanded}
63
+ onExpandedChange={toggleExpanded}
64
+ onLike={like}
65
+ onReply={id => setReplyTo(id)}
66
+ replyLabel={t("comments.reply")}
67
+ repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
68
+ composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
69
+ sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
70
+ threadFooter={hasMore ? <LoadMoreButton /> : null}
71
+ />
72
+ ```
73
+
74
+ ```tsx
75
+ // Native
76
+ import { CommentThreadScreen } from "@hjmds/react-native/screen-flows";
77
+ import { MessageComposer } from "@hjmds/react-native/screens";
78
+
79
+ <CommentThreadScreen
80
+ title={t("comments.title")}
81
+ items={comments.map(toCommentItem)}
82
+ expandedIds={expanded}
83
+ onExpandedChange={toggleExpanded}
84
+ onLike={like}
85
+ onReply={id => setReplyTo(id)}
86
+ replyLabel={t("comments.reply")}
87
+ repliesLabel={(count, open) => t(open ? "comments.hideReplies" : "comments.showReplies", { count })}
88
+ composer={<MessageComposer value={draft} label={t("comments.input")} sendLabel={t("comments.send")}
89
+ sendIcon={<ArrowUpIcon />} sendPresentation="circle" onValueChange={setDraft} onSend={send} />}
90
+ threadFooter={hasMore ? <LoadMoreButton /> : null}
91
+ />
92
+ ```
93
+
94
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. 필수 props와 플랫폼별 차이는 위 API 지침에서 확인한다.
95
+
96
+ ## 큰 글자·다크·좁은 폭
97
+
98
+ | 조건 | 바뀌는 것 |
99
+ | --- | --- |
100
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
101
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
102
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
103
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
104
+
105
+ ## 함정
106
+
107
+ - 빈 댓글에서도 작성창 유지; 실패한 전송은 초안 유지.
108
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,110 @@
1
+ # 알림함
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/알림함`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/소통/알림함`
10
+
11
+ ## 목적
12
+
13
+ NotificationInboxScreen을 사용해 알림함 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 제목·모두 읽음 → 필터 → 알림 행
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | NotificationInboxScreen | route 본문 | [API 배치 규칙](../components/notification-inbox-screen.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 제목·모두 읽음 → 필터 → 알림 행 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | NotificationInboxScreen 공개 행동 슬롯 | 행 이동과 모두 읽음은 별도; 필터는 상태 교체 중 유지 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 제목·모두 읽음 → 필터 → 알림 행 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 최초 조회는 본문 상태, 저장은 해당 행동 pending | 중복 제출 차단; 성공을 먼저 표시하지 않음 |
41
+ | 빈 | 실제 조회 0건 또는 아직 작성하지 않은 상태 안내 | 시작·조건 해제 등 맥락에 맞는 대안 |
42
+ | 오류 | 읽음 저장 실패와 초기 조회 실패를 분리 | 실패 원인과 재시도 경로 제공 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [NotificationInboxScreen](../components/notification-inbox-screen.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
50
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
51
+
52
+ ## 코드 골격
53
+
54
+ ```tsx
55
+ // Web
56
+ import { NotificationInboxScreen, NotificationItem } from "@hjmds/react/screens";
57
+
58
+ <NotificationInboxScreen
59
+ title={t("inbox.title")}
60
+ filters={filterControl}
61
+ state={items.length ? { kind: "ready" } : { kind: "empty", title: t("inbox.empty") }}
62
+ >
63
+ {items.map((n) => (
64
+ <NotificationItem key={n.id} title={n.title} description={n.body} timestamp={n.time}
65
+ read={n.read} statusLabel={t(n.read ? "inbox.read" : "inbox.unread")}
66
+ onClick={() => openNotification(n.id)} />
67
+ ))}
68
+ </NotificationInboxScreen>
69
+ ```
70
+
71
+ ```tsx
72
+ // Native
73
+ import { NotificationInboxScreen, NotificationItem } from "@hjmds/react-native/screens";
74
+
75
+ <NotificationInboxScreen
76
+ title={t("inbox.title")}
77
+ filters={filterControl}
78
+ state={items.length ? { kind: "ready" } : { kind: "empty", title: t("inbox.empty") }}
79
+ >
80
+ {items.map((n) => (
81
+ <NotificationItem key={n.id} title={n.title} description={n.body} timestamp={n.time}
82
+ read={n.read} statusLabel={t(n.read ? "inbox.read" : "inbox.unread")}
83
+ onPress={() => openNotification(n.id)} />
84
+ ))}
85
+ </NotificationInboxScreen>
86
+ ```
87
+
88
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. 필수 props와 플랫폼별 차이는 위 API 지침에서 확인한다.
89
+
90
+ ## 큰 글자·다크·좁은 폭
91
+
92
+ | 조건 | 바뀌는 것 |
93
+ | --- | --- |
94
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
95
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
96
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
97
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
98
+
99
+ ## 플랫폼 차이
100
+
101
+ | 항목 | Web | Native |
102
+ | --- | --- | --- |
103
+ | 행 누르기 | `onClick` | `onPress` |
104
+ | `NotificationItem` `description` | `ReactNode` | `string` |
105
+
106
+ ## 함정
107
+
108
+ - 읽음 저장 실패와 초기 조회 실패를 분리.
109
+ - 읽음 저장 실패는 본문을 교체하지 않고 `notice`에 한 줄로 알리며 알림은 읽지 않음으로 남는다(스토리 `읽음 저장 실패와 복구`). 묶음 제목(오늘·이번 주)은 Heading `level5`다.
110
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,98 @@
1
+ # 로그인
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md); 공통 API와 실제 Web·Native 예제의 슬롯·상태를 대조해 중복 조립 방지. 2026-10-06 사용자 승인으로 스토리북 배포(이전 `실험/화면/공통 화면/로그인`, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/계정/로그인`
10
+
11
+ ## 목적
12
+
13
+ AuthScreenLayout을 사용해 로그인 흐름을 구성한다. 제품이 데이터·권한·서버 확정·문구를 공급하며, 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
14
+
15
+ ## 영역 구조
16
+
17
+ ```text
18
+ host: 남은 높이·safe area·키보드
19
+ └─ 히어로 → 제공자 카드 → 하단 동의
20
+ ```
21
+
22
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
23
+ | --- | --- | --- | --- |
24
+ | 바깥 틀 | AuthScreenLayout | route 본문 | [API 배치 규칙](../components/auth-screen-layout.md#배치), host 남은 높이 |
25
+ | 내용 | 공개 슬롯 | 히어로 → 제공자 카드 → 하단 동의 | 화면 recipe의 sectionGap·itemGap; 슬롯 안은 각 지침 토큰 |
26
+ | 상태 | state 또는 해당 API 상태 | 본문 자리·비차단 notice | 입력 중 실패는 본문 높이와 초안을 유지 |
27
+
28
+ ## 버튼과 행동 위치
29
+
30
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
31
+ | --- | --- | --- | --- |
32
+ | 작업 | AuthScreenLayout 공개 행동 슬롯 | 제공자 버튼에는 이름만; 로그인 중 카드 중앙 스피너 하나 | 같은 표면에 경쟁하는 primary 하나만 |
33
+ | 복구 | Button·secondary | 오류 근처 | 재시도할 대상과 범위를 표시 |
34
+
35
+ ## 상태
36
+
37
+ | 상태 | 화면 모습 | 행동 |
38
+ | --- | --- | --- |
39
+ | 기본 | 히어로 → 제공자 카드 → 하단 동의 | 각 공개 콜백을 제품 상태에 연결 |
40
+ | 로딩 | 제공자 로그인 진행 중: `pendingLabel`로 카드 크기를 유지한 채 중앙 로딩 하나(버튼 목록은 숨겨도 자리 유지, 로딩 아래 문구 없음). 제공자 목록 최초 조회 로딩과 다르다(LS-09) | 중복 요청 차단. `예제 로그인 취소`는 실패 문장 없이 버튼만 복구 |
41
+ | 빈 | — (로그인 화면에는 빈 상태가 없다) | — |
42
+ | 오류 | `pendingLabel`을 지워 버튼을 복원하고 카드 안 버튼 위에 Notice `danger` 한 줄(기술 오류 원문 금지) | 같은 제공자 버튼으로 다시 시도 |
43
+
44
+ ## 사용하는 지침
45
+
46
+ | 지침 | 쓰는 곳 |
47
+ | --- | --- |
48
+ | [AuthScreenLayout](../components/auth-screen-layout.md) | 필수 props·슬롯·플랫폼 차이 |
49
+ | [AuthProviderButton](../components/auth-provider-button.md) | 제공자 버튼(이름만) |
50
+ | [Top](../components/top.md) | 히어로 제목·설명(제목을 `Text`로 그리지 않는다) |
51
+ | [Notice](../components/notice.md) | 로그인 실패 한 줄 |
52
+ | [Button](../components/button.md) | 동작·로딩·보조 행동 |
53
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·본문 교체·스크롤 소유 |
54
+
55
+ ## 코드 골격
56
+
57
+ ```tsx
58
+ // Web
59
+ import { AuthScreenLayout } from "@hjmds/react/auth-screen";
60
+
61
+ <AuthScreenLayout
62
+ mainCard
63
+ {...(pending ? { pendingLabel: t("auth.pending") } : {})}
64
+ hero={<ProductHero />} // 제품 마크·제목·설명
65
+ main={<ProviderButtons />} // pending이어도 같은 목록
66
+ footer={<ConsentAndPolicyLinks />}
67
+ />
68
+ ```
69
+
70
+ ```tsx
71
+ // Native
72
+ import { AuthScreenLayout } from "@hjmds/react-native/auth-screen";
73
+
74
+ <AuthScreenLayout
75
+ mainCard
76
+ {...(pending ? { pendingLabel: t("auth.pending") } : {})}
77
+ hero={<ProductHero />} // 제품 마크·제목·설명
78
+ main={<ProviderButtons />} // pending이어도 같은 목록
79
+ footer={<ConsentAndPolicyLinks />}
80
+ />
81
+ ```
82
+
83
+ 콜백·데이터·지역화 함수는 제품에서 공급한다. 필수 props와 플랫폼별 차이는 위 API 지침에서 확인한다.
84
+
85
+ ## 큰 글자·다크·좁은 폭
86
+
87
+ | 조건 | 바뀌는 것 |
88
+ | --- | --- |
89
+ | 큰 글자 | 2배 글자에서 제목·행은 내용 높이로 증가. footer·닫기·입력 필드가 겹치지 않는지 확인 |
90
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환; 예제 브랜드 색을 제품 기본값으로 복사하지 않음 |
91
+ | 좁은 폭 | 320px부터 한 열로 읽기 순서 유지. 가상화 본문은 scroll=content, 중첩 스크롤 금지 |
92
+ | 키보드 | Native host가 safe area와 키보드를 한 번 처리; Web은 포커스된 입력과 footer 가림 확인 |
93
+
94
+ ## 함정
95
+
96
+ - 실패하면 버튼을 복원; 카드 크기 유지와 중복 요청 차단.
97
+ - 스토리는 기본·어두운 테마·큰 글자, `로그인 중`, `로그인 실패와 다시 시도`(첫 시도 실패 → 다시 누르면 성공)다. 빈 화면·로그인 필요 같은 본문 상태는 이 화면에 해당하지 않아 2026-10-06 스토리에서 뺐다.
98
+ - Storybook은 실제 서버·OS 권한·라우터 연동 증거가 아니다. 기본·다크·큰 글자와 실패/복구를 각각 확인한다.
@@ -0,0 +1,221 @@
1
+ # 프로필
2
+
3
+ - 단계: 화면
4
+ - 상태: 배포
5
+ - 지원: Web · Native
6
+ - 적용: 미게시(1.12.1 이후)
7
+ - 검토일: 2026-10-06
8
+ - 근거: [반복 화면 조합](../../screen-patterns.md), Web·Native `src/screen-flows.tsx`(`ProfileScreen`·`EditorScreen`), 예제 `showcase/*/screen-flow-previews.tsx`(`AccountFlowPreview`)·`saved-profile-previews.tsx`(`ProfileEditFields`), 기본 얼굴 `showcase/web/src/patterns/ProfileStudio.previews.tsx`·`showcase/native/src/profile-face-preview.tsx`·`showcase/shared/profile-studio.ts`. 2026-10-06 사용자 승인으로 실험 `공통 화면/프로필`을 배포하면서 같은 프로필 수정을 직접 조립으로 보이던 배포 `화면/프로필 편집`과 합쳤다(API 기반 우선, Web id `patterns-profile-studio` 보존, [승인 기록](../../../../../docs/STORYBOOK_NAVIGATION.md#21-2026-10-06-전체-승격과-규격-확정))
9
+ - 스토리북: `배포/화면/계정/프로필`
10
+
11
+ ## 목적
12
+
13
+ 내 프로필을 보고(요약·게시물·계정 메뉴) 고치는(사진·이름·소개) 화면을 ProfileScreen과 EditorScreen으로 구성한다.
14
+ 보기와 수정은 같은 route 안에서 바뀌고, 수정 저장은 footer primary 하나, 저장하지 않고 닫으면 버리기 확인을 띄운다.
15
+ 스토리는 `기본`(보기에서 수정·계정 메뉴·게시물 상세까지 직접 조작), `프로필 수정`(수정 화면을 바로 연 상태), `기본 얼굴 고르기`,
16
+ `불러오는 중`·`비어 있음`·`오류`·`로그인 필요`, `실패와 복구`(다음 저장 실패 → 다시 저장)다.
17
+ 2026-10-06 같은 예제를 그리던 `기본 흐름/프로필과 계정`(실험 정리 단계)과 배포 직접 조립 `프로필 편집`을 이 항목으로 합쳤고,
18
+ 프로필 편집 고유의 기본 얼굴(사진이 없을 때의 Blobatar) 고르기는 `기본 얼굴 고르기` 스토리와 아래 [기본 얼굴 고르기](#기본-얼굴-고르기) 절로 옮겼다.
19
+ 사용자 데이터·저장·로그아웃은 제품이 공급하며 예제의 메모리 저장을 운영 저장으로 취급하지 않는다.
20
+
21
+ ## 영역 구조
22
+
23
+ ```text
24
+ 보기 — ProfileScreen = ScreenLayout(최대 720, 바깥 padding spacing.md 16), footer 없음
25
+ ┌ 머리(고정): jimin [☰] ← 계정 메뉴 IconButton ghost ┐
26
+ │ (저장 실패 등) notice │
27
+ ├──────────────────────────── 본문 스크롤 ───────────────────────────────────────────┤
28
+ │ summary(제품): 사진 · 게시물/팔로워/팔로잉 수 · 이름 · 소개 · @아이디 │
29
+ │ ↕ spacing.md 16 │
30
+ │ [프로필 수정] ← edit, ghost │
31
+ │ ↕ spacing.xl 24 │
32
+ │ children(제품): [게시물][저장됨] ← ghost+selected, 사진 격자 3열(gap xxs) │
33
+ │ ↕ 24 │
34
+ │ accountActions(선택): 본문 끝 계정 행동 │
35
+ └───────────────────────────────────────────────────────────────────────────────────┘
36
+ 계정 메뉴 Sheet: 연결된 계정 ListRow / footer [로그아웃] → AlertDialog 확인
37
+
38
+ 수정 — EditorScreen(submitPlacement="footer")
39
+ ┌ 머리(고정): [닫기] 프로필 수정 ┐
40
+ ├──────────────────────────── 본문 스크롤 ───────────────────────────────────────────┤
41
+ │ 사진 + [프로필 사진 변경] (secondary small → 사진 고르기 Sheet) │
42
+ │ 공개 프로필: 이름 TextField(최대 30, 비면 오류) · 소개 TextArea(최대 160) + 글자 수 │
43
+ │ 사용자 이름(읽기 전용 Surface) │
44
+ ├──────────────────────────── footer(고정) ──────────────────────────────────────────┤
45
+ │ 초안 상태 caption("이 화면에 초안을 보관했어요") │
46
+ │ [ 변경사항 저장 ] ← primary, 바뀐 것이 없거나 이름이 비면 비활성 │
47
+ └───────────────────────────────────────────────────────────────────────────────────┘
48
+ ```
49
+
50
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
51
+ | --- | --- | --- | --- |
52
+ | 바깥 틀 | ProfileScreen · EditorScreen(둘 다 ScreenLayout) | route 본문, host가 남은 높이·safe area·키보드를 준다 | 폭 최대 720, 바깥 padding `spacing.md` 16([ScreenLayout 배치](../components/screen-layout.md#배치)), 본문 스크롤 |
53
+ | 머리 | `title` + `actions`(IconButton ghost 계정 메뉴) | 맨 위, 고정 | 제목 열 최소 120 × 글자 배율 |
54
+ | 요약 | `summary`(제품) | 본문 맨 위 | 요약 안 배치는 제품 소유 |
55
+ | 수정 진입 | `edit` → Button ghost | 요약 아래 | 요약과 `spacing.md` 16 |
56
+ | 게시물 | `children` > Stack `gap="md"` > 탭 Button 줄 + Grid `columns={{ compact: 3 }}` | 수정 아래 | 요약 묶음과 `spacing.xl` 24, 격자 간격 `spacing.xxs` |
57
+ | 계정 메뉴 | Sheet > ListRow, `footer` > Button ghost 로그아웃 → AlertDialog | 오버레이 | footer 고정 |
58
+ | 수정 화면 | EditorScreen `submitPlacement="footer"` > 사진·이름·소개 필드 | 같은 route에서 보기 대신 | footer: 초안 상태 → 저장 primary, 사이 `spacing.sm` 12 |
59
+
60
+ ### 기본 얼굴 고르기
61
+
62
+ 사진이 없을 때 보일 기본 얼굴(Blobatar)을 고르는 상태다. 옛 `프로필 편집`의 고유 상태라 `기본 얼굴 고르기` 스토리는 그 직접 조립 화면을 연다.
63
+ 제품은 이 고르기를 수정 화면 사진 영역(또는 사진 고르기 Sheet)에 넣는다.
64
+
65
+ ```text
66
+ [◉][◯][◯][◯] ← 얼굴 선택 Button ghost + selected > Avatar 40, wrap gap sm 12
67
+ 미리보기 Avatar 64 renderFallback = 고른 얼굴
68
+ ```
69
+
70
+ | 영역 | 컴포넌트 | 위치 | 크기·간격 |
71
+ | --- | --- | --- | --- |
72
+ | 바깥 틀 | 수정 화면 본문(EditorScreen) 안 | 사진 영역 | 위 표의 수정 화면을 따른다 |
73
+ | 얼굴 줄 | Stack `axis="inline" wrap gap="sm"` > Button `tone="ghost"` `selected` > Avatar(`createBlobatarFallback({ seed })`) | 사진 아래 | 사이 `spacing.sm` 12, 선택지 Avatar 40, 미리보기 64 |
74
+
75
+ ## 버튼과 행동 위치
76
+
77
+ | 행동 | 컴포넌트·tone | 위치 | 개수·순서 |
78
+ | --- | --- | --- | --- |
79
+ | 프로필 수정 | `edit` → Button ghost | 요약 아래 | 1 |
80
+ | 계정 메뉴 | IconButton ghost(접근성 이름 "계정 메뉴") | 머리 끝(`actions`) | 1. 로그아웃·연결 계정은 메뉴 안 |
81
+ | 게시물·저장됨 전환 | Button ghost + `selected` | 게시물 영역 맨 위 | 2. `selected`는 primary로 세지 않는다 |
82
+ | 게시물 열기 | 사진 칸 전체 | 격자 | 칸마다 1. 상세는 ScreenLayout + `leading` 뒤로 |
83
+ | 로그아웃 | 계정 Sheet `footer` Button ghost → AlertDialog `confirm` | Sheet 아래 | 1. 확인 뒤 실행 |
84
+ | 저장 | EditorScreen `submit` → Button primary(`pending`이면 `loading`) | 수정 footer 끝 | 1. 바뀐 것이 없거나 이름이 비면 비활성 |
85
+ | 닫기 | EditorScreen `cancel` → Button ghost(머리 `leading`) | 수정 머리 앞 | 1. dirty면 `discard` 확인(danger) |
86
+ | 사진 변경 | Button `secondary` `small` → Sheet | 수정 본문 사진 아래 | 1 |
87
+ | 기본 얼굴 고르기 | Button ghost + `selected` > Avatar | 사진 영역 | 얼굴 수만큼 |
88
+ | 다시 시도 | `stateAction` Button | 상태 안내 아래 | 오류·로그인 필요일 때 1 |
89
+
90
+ ## 상태
91
+
92
+ | 상태 | 화면 모습 | 행동 |
93
+ | --- | --- | --- |
94
+ | 기본 | 머리 · 요약 · 프로필 수정 · 게시물 탭과 격자 | 수정 · 계정 메뉴 · 게시물 열기 |
95
+ | 프로필 수정 | `프로필 수정` 스토리: EditorScreen, 저장은 footer primary 하나, 초안 상태 caption, 닫기는 dirty면 버리기 확인 | 저장 · 닫기 |
96
+ | 기본 얼굴 고르기 | `기본 얼굴 고르기` 스토리: 얼굴을 고르면 미리보기 Avatar가 바로 바뀌고, 이름이 비면 오류·저장 비활성 | 얼굴 선택 · 적용 · 되돌리기 |
97
+ | 로딩 | `불러오는 중`: `state={{ kind: "loading" }}`로 본문을 상태 안내로 바꾼다. 저장 중에는 저장 Button `loading` | 기다림 |
98
+ | 빈 | `비어 있음`: `state={{ kind: "empty" }}` 안내. 게시물 0개는 제품이 게시물 영역에 빈 안내를 둔다 | 첫 게시물 쓰기 등 맥락 행동 |
99
+ | 오류 | `오류`: 불러오기 실패 안내 + `stateAction` 다시 시도. 저장 실패는 수정 화면에 머물고 이전 프로필과 초안을 모두 보존, 머리 아래 notice 자리에 [Notice](../components/notice.md) `tone="danger"`(Native `announcement="assertive"` — 기본 `none`)로 알린다 | 다시 시도 · 다시 저장 |
100
+ | 로그인 필요 | `로그인 필요`: `state={{ kind: "restricted" }}` + `stateAction` | 로그인 |
101
+ | 실패와 복구 | `실패와 복구`: 다음 저장 실패를 예약한 뒤 저장 → 실패 문구, 초안 유지 → 다시 저장 | 같은 저장 행동 |
102
+ | 로그아웃 확인 | AlertDialog: 제목·설명·[취소][로그아웃] | 확인 뒤 제품이 세션을 끝낸다 |
103
+
104
+ ## 사용하는 지침
105
+
106
+ | 지침 | 쓰는 곳 |
107
+ | --- | --- |
108
+ | [ProfileScreen](../components/profile-screen.md) | 보기 화면의 슬롯·순서 |
109
+ | [EditorScreen](../components/editor-screen.md) | 수정 화면, 저장 위치, 버리기 확인 |
110
+ | [ScreenLayout](../components/screen-layout.md) | 화면 높이·상태 안내·notice·스크롤 소유 |
111
+ | [Notice](../components/notice.md) | 저장 실패(notice 자리) |
112
+ | [IconButton](../components/icon-button.md) | 계정 메뉴 |
113
+ | [Button](../components/button.md) | 수정·탭·저장·사진 변경·얼굴 선택 |
114
+ | [Sheet](../components/sheet.md) · [AlertDialog](../components/alert-dialog.md) | 계정 메뉴 · 로그아웃 확인 |
115
+ | [Field](../components/field.md) | 이름 `TextField`·소개 `TextArea` |
116
+ | [Avatar](../components/avatar.md) | 사진, `createBlobatarFallback` 기본 얼굴 |
117
+ | [Grid](../components/grid.md) | 게시물 격자 |
118
+ | [저장한 항목](common-saved.md) | 저장됨 탭을 따로 키울 때 |
119
+
120
+ ## 코드 골격
121
+
122
+ ```tsx
123
+ // Web
124
+ import { ProfileScreen, EditorScreen } from "@hjmds/react/screen-flows";
125
+ import { IconButton, Button } from "@hjmds/react/actions";
126
+ import { TextField } from "@hjmds/react/forms";
127
+ import { Stack } from "@hjmds/react/layout";
128
+ import { Avatar } from "@hjmds/react/display";
129
+ import { createBlobatarFallback } from "@hjmds/react/avatar-blobatar";
130
+
131
+ const faceRow = <Stack axis="inline" wrap gap="sm">{faces.map((face) =>
132
+ <Button key={face.seed} tone="ghost" selected={draft.seed === face.seed} aria-label={t(face.labelKey)}
133
+ onClick={() => setDraft({ ...draft, seed: face.seed })}>
134
+ <Avatar name={t(face.labelKey)} alt="" size="medium" renderFallback={createBlobatarFallback({ seed: face.seed })} />
135
+ </Button>)}</Stack>;
136
+
137
+ editing ? <EditorScreen title={t("profile.edit")} submitPlacement="footer" dirty={dirty}
138
+ cancel={{ label: t("common.close"), onAction: closeEditor }}
139
+ submit={{ label: t("profile.save"), onAction: save, pending: saving, disabled: !dirty || !draft.name.trim() }}
140
+ discard={{ mode: "confirm", tone: "danger", title: t("profile.discard.title"), description: t("profile.discard.body"),
141
+ confirmLabel: t("profile.discard.confirm"), cancelLabel: t("profile.discard.cancel"), fallbackErrorMessage: t("common.retryLater") }}>
142
+ <Stack gap="xl">
143
+ {faceRow}
144
+ <TextField label={t("profile.name")} value={draft.name} onValueChange={(name) => setDraft({ ...draft, name })}
145
+ {...(!draft.name.trim() ? { error: t("profile.nameRequired") } : {})} />
146
+ </Stack>
147
+ </EditorScreen> : <ProfileScreen
148
+ title={user.handle}
149
+ actions={<IconButton label={t("profile.accountMenu")} tone="ghost" onClick={openAccountMenu}>{menuIcon}</IconButton>}
150
+ summary={<ProfileSummary user={user} />}
151
+ edit={{ label: t("profile.edit"), onAction: openEditor }}
152
+ state={user ? { kind: "ready" } : { kind: "loading", title: t("profile.loading") }}
153
+ >
154
+ <MyPostsSection />
155
+ </ProfileScreen>
156
+ ```
157
+
158
+ ```tsx
159
+ // Native
160
+ import { ProfileScreen, EditorScreen } from "@hjmds/react-native/screen-flows";
161
+ import { IconButton, Button } from "@hjmds/react-native/actions";
162
+ import { TextField } from "@hjmds/react-native/inputs";
163
+ import { Stack } from "@hjmds/react-native/primitives";
164
+ import { Avatar } from "@hjmds/react-native/data-display";
165
+ import { createBlobatarFallback } from "@hjmds/react-native/avatar-blobatar";
166
+
167
+ const faceRow = <Stack axis="inline" wrap gap="sm">{faces.map((face) =>
168
+ <Button key={face.seed} tone="ghost" selected={draft.seed === face.seed} accessibilityLabel={t(face.labelKey)}
169
+ onPress={() => setDraft({ ...draft, seed: face.seed })}>
170
+ <Avatar name={t(face.labelKey)} decorative size={40} renderFallback={createBlobatarFallback({ seed: face.seed })} />
171
+ </Button>)}</Stack>;
172
+
173
+ editing ? <EditorScreen title={t("profile.edit")} submitPlacement="footer" dirty={dirty}
174
+ cancel={{ label: t("common.close"), onAction: closeEditor }}
175
+ submit={{ label: t("profile.save"), onAction: save, pending: saving, disabled: !dirty || !draft.name.trim() }}
176
+ discard={{ mode: "confirm", tone: "danger", title: t("profile.discard.title"), description: t("profile.discard.body"),
177
+ confirmLabel: t("profile.discard.confirm"), cancelLabel: t("profile.discard.cancel"), fallbackErrorMessage: t("common.retryLater") }}>
178
+ <Stack gap="xl">
179
+ {faceRow}
180
+ <TextField label={t("profile.name")} value={draft.name} onValueChange={(name) => setDraft({ ...draft, name })}
181
+ {...(!draft.name.trim() ? { error: t("profile.nameRequired") } : {})} />
182
+ </Stack>
183
+ </EditorScreen> : <ProfileScreen
184
+ title={user.handle}
185
+ actions={<IconButton label={t("profile.accountMenu")} tone="ghost" onPress={openAccountMenu}>{menuIcon}</IconButton>}
186
+ summary={<ProfileSummary user={user} />}
187
+ edit={{ label: t("profile.edit"), onAction: openEditor }}
188
+ state={user ? { kind: "ready" } : { kind: "loading", title: t("profile.loading") }}
189
+ >
190
+ <MyPostsSection />
191
+ </ProfileScreen>
192
+ ```
193
+
194
+ `ProfileSummary`·`MyPostsSection`·`faces`·`openAccountMenu`(계정 Sheet와 로그아웃 AlertDialog)는 제품이 공급한다.
195
+ `save`가 실패하면 `editing`을 유지하고 초안을 지우지 않는다.
196
+
197
+ ## 큰 글자·다크·좁은 폭
198
+
199
+ | 조건 | 바뀌는 것 |
200
+ | --- | --- |
201
+ | 큰 글자 | 요약의 수치 줄과 얼굴 줄이 감긴다. 수정 화면 footer는 고정이라 필드만 스크롤된다. 제목 열이 모자라면 계정 메뉴가 다음 줄로 내려간다 |
202
+ | 다크 | semantic 색으로 내용과 표면을 함께 전환한다. Blobatar 얼굴은 seed가 정하고 테마와 무관하다 |
203
+ | 좁은 폭 | 320부터 한 열, 게시물 격자는 3열 그대로 칸이 작아진다 |
204
+ | 키보드 | 수정 화면에서 Native host가 safe area와 키보드를 한 번 처리한다. Web은 포커스된 필드가 footer에 가리지 않는지 확인 |
205
+
206
+ ## 플랫폼 차이
207
+
208
+ | 항목 | Web | Native |
209
+ | --- | --- | --- |
210
+ | 얼굴 선택지 | Avatar `size="medium"`(40) `alt=""`, Button `aria-label` | Avatar `size={40}` `decorative`, Button `accessibilityLabel` |
211
+ | 소개 입력 | TextArea `onChange` | TextArea `onValueChange` |
212
+ | 저장 실패 발표 | Notice `danger`가 `role="alert"`로 읽힌다 | Notice `announcement="assertive"`를 줘야 읽힌다 |
213
+ | `기본 얼굴 고르기` 스토리 | 옛 직접 조립 화면(두 열, showcase CSS `profile-studio.css`) | 옛 직접 조립 화면(한 열, `View` style) |
214
+
215
+ ## 함정
216
+
217
+ - 수정 실패는 이전 프로필과 초안 모두 보존한다. 저장 성공 전에 요약을 바꾸지 않는다.
218
+ - ProfileScreen에는 footer가 없다. 로그아웃·탈퇴를 footer에 두려 하지 말고 계정 메뉴(Sheet) 또는 본문 끝 `accountActions`에 둔다.
219
+ - 현재 `기본 얼굴 고르기` 스토리는 ProfileScreen·EditorScreen이 아닌 옛 직접 조립 화면이다. `hjm-profile-studio*` 클래스는 공개 스타일이 아니므로 제품에서 가져다 쓰지 않는다. 제품은 위 코드처럼 수정 화면 안에 얼굴 줄을 둔다.
220
+ - `createBlobatarFallback`은 seed가 바뀔 때만 다시 만들도록 `useMemo`로 묶는다.
221
+ - Storybook은 실제 서버·세션·라우터 연동 증거가 아니다. 기본·어두운 테마·큰 글자와 실패와 복구를 각각 확인한다.