create-web-0to1 0.1.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 (193) hide show
  1. package/README.md +68 -0
  2. package/internal/engine/create-feature-crud-script.ts +134 -0
  3. package/internal/engine/create-feature-crud.template.mjs +601 -0
  4. package/internal/engine/create-feature-script.ts +142 -0
  5. package/internal/engine/generator-engine.ts +546 -0
  6. package/internal/engine/standalone-feature-preset.ts +34 -0
  7. package/internal/meta/preset-plan.ts +41 -0
  8. package/internal/meta/runtime-copy-plan.ts +220 -0
  9. package/internal/meta/runtime-layout.ts +262 -0
  10. package/internal/meta/scaffold-manifest.ts +169 -0
  11. package/internal/meta/standalone-dependency-manifest.ts +75 -0
  12. package/package.json +45 -0
  13. package/scripts/create-app.mjs +1612 -0
  14. package/source/core/auth/auth-events.ts +13 -0
  15. package/source/core/error/app-error.ts +85 -0
  16. package/source/core/error/handle-app-error.client.ts +35 -0
  17. package/source/core/lib/dayjs.ts +25 -0
  18. package/source/core/query/query-client.ts +126 -0
  19. package/source/core/request/request-core.ts +210 -0
  20. package/source/core/routes/route-paths.ts +4 -0
  21. package/source/core/ui/button.tsx +24 -0
  22. package/source/core/ui/modal-store.ts +32 -0
  23. package/source/core/ui/text-input-field.tsx +36 -0
  24. package/source/core/utils/build-query-string.ts +30 -0
  25. package/source/core/utils/format/date.ts +41 -0
  26. package/source/core/utils/format/index.ts +3 -0
  27. package/source/core/utils/format/number.ts +13 -0
  28. package/source/core/utils/format/text.ts +15 -0
  29. package/source/core/utils/schema-utils.ts +27 -0
  30. package/source/wrappers/monorepo/core/internal.ts +21 -0
  31. package/source/wrappers/monorepo/core/src/index.ts +4 -0
  32. package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
  33. package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
  34. package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
  35. package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
  36. package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
  37. package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
  38. package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
  39. package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
  40. package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
  41. package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
  42. package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
  43. package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
  44. package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
  45. package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
  46. package/source/wrappers/monorepo/request/core-request.ts +93 -0
  47. package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
  48. package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
  49. package/source/wrappers/next/query/hydration.tsx +20 -0
  50. package/source/wrappers/next/query/providers.tsx +35 -0
  51. package/source/wrappers/next/request/request.client.ts +24 -0
  52. package/source/wrappers/next/request/request.server.ts +64 -0
  53. package/source/wrappers/next/request/request.ts +52 -0
  54. package/source/wrappers/next/ui/global-modal.tsx +29 -0
  55. package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
  56. package/source/wrappers/react/query/providers.tsx +31 -0
  57. package/source/wrappers/react/request/request.client.ts +24 -0
  58. package/source/wrappers/react/request/request.ts +51 -0
  59. package/source/wrappers/react/ui/global-modal.tsx +27 -0
  60. package/templates/monorepo/.dockerignore +38 -0
  61. package/templates/monorepo/README.md +292 -0
  62. package/templates/monorepo/_gitignore +38 -0
  63. package/templates/monorepo/_npmrc +1 -0
  64. package/templates/monorepo/apps/project/Dockerfile +32 -0
  65. package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
  66. package/templates/monorepo/apps/project/index.html +14 -0
  67. package/templates/monorepo/apps/project/index.ts +15 -0
  68. package/templates/monorepo/apps/project/package.json +21 -0
  69. package/templates/monorepo/apps/project/tsconfig.json +9 -0
  70. package/templates/monorepo/apps/project/vite.config.ts +6 -0
  71. package/templates/monorepo/apps/web/Dockerfile +43 -0
  72. package/templates/monorepo/apps/web/README.md +111 -0
  73. package/templates/monorepo/apps/web/_gitignore +36 -0
  74. package/templates/monorepo/apps/web/app/favicon.ico +0 -0
  75. package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
  76. package/templates/monorepo/apps/web/app/globals.css +0 -0
  77. package/templates/monorepo/apps/web/app/layout.tsx +28 -0
  78. package/templates/monorepo/apps/web/app/page.tsx +7 -0
  79. package/templates/monorepo/apps/web/app/providers.tsx +25 -0
  80. package/templates/monorepo/apps/web/eslint.config.js +4 -0
  81. package/templates/monorepo/apps/web/next-env.d.ts +6 -0
  82. package/templates/monorepo/apps/web/next.config.js +4 -0
  83. package/templates/monorepo/apps/web/package.json +31 -0
  84. package/templates/monorepo/apps/web/public/file-text.svg +3 -0
  85. package/templates/monorepo/apps/web/public/globe.svg +10 -0
  86. package/templates/monorepo/apps/web/public/next.svg +1 -0
  87. package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
  88. package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
  89. package/templates/monorepo/apps/web/public/vercel.svg +10 -0
  90. package/templates/monorepo/apps/web/public/window.svg +3 -0
  91. package/templates/monorepo/apps/web/tsconfig.json +20 -0
  92. package/templates/monorepo/package.json +24 -0
  93. package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
  94. package/templates/monorepo/packages/core/package.json +32 -0
  95. package/templates/monorepo/packages/core/tsconfig.json +8 -0
  96. package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
  97. package/templates/monorepo/packages/core-next/package.json +43 -0
  98. package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
  99. package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
  100. package/templates/monorepo/packages/core-react/package.json +34 -0
  101. package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
  102. package/templates/monorepo/packages/eslint-config/README.md +3 -0
  103. package/templates/monorepo/packages/eslint-config/base.js +57 -0
  104. package/templates/monorepo/packages/eslint-config/next.js +22 -0
  105. package/templates/monorepo/packages/eslint-config/package.json +25 -0
  106. package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
  107. package/templates/monorepo/packages/typescript-config/base.json +19 -0
  108. package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
  109. package/templates/monorepo/packages/typescript-config/package.json +9 -0
  110. package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
  111. package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
  112. package/templates/monorepo/packages/ui/package.json +26 -0
  113. package/templates/monorepo/packages/ui/src/button.tsx +20 -0
  114. package/templates/monorepo/packages/ui/src/card.tsx +27 -0
  115. package/templates/monorepo/packages/ui/src/code.tsx +11 -0
  116. package/templates/monorepo/packages/ui/tsconfig.json +8 -0
  117. package/templates/monorepo/pnpm-workspace.yaml +9 -0
  118. package/templates/monorepo/turbo/generators/config.js +1336 -0
  119. package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
  120. package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
  121. package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
  122. package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
  123. package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
  124. package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
  125. package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
  126. package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
  127. package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
  128. package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
  129. package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
  130. package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
  131. package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
  132. package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
  133. package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
  134. package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
  135. package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
  136. package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
  137. package/templates/monorepo/turbo.json +28 -0
  138. package/templates/next/.env.example +2 -0
  139. package/templates/next/.prettierignore +9 -0
  140. package/templates/next/.prettierrc.json +9 -0
  141. package/templates/next/README.md +246 -0
  142. package/templates/next/_gitignore +44 -0
  143. package/templates/next/eslint.config.mjs +51 -0
  144. package/templates/next/next.config.ts +7 -0
  145. package/templates/next/package.json +24 -0
  146. package/templates/next/postcss.config.mjs +7 -0
  147. package/templates/next/scripts/create-feature-crud.mjs +5 -0
  148. package/templates/next/scripts/create-feature.mjs +5 -0
  149. package/templates/next/src/app/error.tsx +33 -0
  150. package/templates/next/src/app/globals.css +35 -0
  151. package/templates/next/src/app/layout.tsx +39 -0
  152. package/templates/next/src/app/login/page.tsx +17 -0
  153. package/templates/next/src/app/page.tsx +32 -0
  154. package/templates/next/src/app/providers.tsx +20 -0
  155. package/templates/next/tsconfig.json +34 -0
  156. package/templates/react/.env.example +1 -0
  157. package/templates/react/.prettierignore +10 -0
  158. package/templates/react/.prettierrc.json +9 -0
  159. package/templates/react/README.md +250 -0
  160. package/templates/react/_gitignore +31 -0
  161. package/templates/react/eslint.config.mjs +64 -0
  162. package/templates/react/package.json +19 -0
  163. package/templates/react/scripts/create-feature-crud.mjs +5 -0
  164. package/templates/react/scripts/create-feature.mjs +5 -0
  165. package/templates/react/src/app/app.tsx +15 -0
  166. package/templates/react/src/app/error-boundary.tsx +59 -0
  167. package/templates/react/src/app/frame.tsx +32 -0
  168. package/templates/react/src/app/globals.css +43 -0
  169. package/templates/react/src/app/not-found-page.tsx +23 -0
  170. package/templates/react/src/app/providers.tsx +16 -0
  171. package/templates/react/src/app/router.tsx +62 -0
  172. package/templates/react/src/main.tsx +12 -0
  173. package/templates/react/src/pages/index/page.tsx +36 -0
  174. package/templates/react/src/pages/login/page.tsx +18 -0
  175. package/templates/react/tsconfig.app.json +30 -0
  176. package/templates/react/tsconfig.json +4 -0
  177. package/templates/react/tsconfig.node.json +24 -0
  178. package/templates/react/vite.config.ts +14 -0
  179. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/00-/354/240/204/353/260/230/354/240/201/354/235/270-/355/217/264/353/215/224/352/265/254/354/241/260.md +150 -0
  180. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/01-/352/265/254/354/241/260/354/231/200-/353/235/274/354/232/260/355/214/205.md +186 -0
  181. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/02-/354/204/234/353/262/204/354/231/200-/355/201/264/353/235/274/354/235/264/354/226/270/355/212/270.md +86 -0
  182. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/03-/354/203/201/355/203/234/352/264/200/353/246/254.md +84 -0
  183. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/04-API/354/231/200-/353/215/260/354/235/264/355/204/260.md +199 -0
  184. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/05-/354/227/220/353/237/254/354/231/200-UI-/354/203/201/355/203/234.md +159 -0
  185. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
  186. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/07-/354/212/244/355/203/200/354/235/274/353/247/201/352/263/274-/354/240/221/352/267/274/354/204/261.md +73 -0
  187. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/08-/353/204/244/354/235/264/353/260/215-/354/204/244/354/240/225-/355/217/254/353/247/267/355/214/205.md +98 -0
  188. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/09-/353/235/274/354/232/260/355/212/270-/354/240/225/354/235/230.md +169 -0
  189. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/10-/354/273/244/353/260/213-/354/273/250/353/262/244/354/205/230.md +64 -0
  190. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/11-/352/270/260/355/203/200-/354/233/220/354/271/231.md +187 -0
  191. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/12-/354/213/244/353/254/264-/353/215/260/354/235/264/355/204/260-/355/214/250/355/204/264.md +302 -0
  192. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/13-/354/204/261/353/212/245-/354/233/220/354/271/231.md +175 -0
  193. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/README.md +39 -0
@@ -0,0 +1,169 @@
1
+ # 라우트 정의
2
+
3
+ ## 원칙
4
+
5
+ - route, menu, breadcrumb는 가능하면 한 파일에서 같이 관리합니다.
6
+ - 경로 문자열을 여러 곳에 다시 쓰지 않고, 공통 정의를 참조하게 만듭니다.
7
+ - 정적 경로는 문자열, 동적 경로는 함수로 둡니다.
8
+ - breadcrumb는 `usePathname()`으로 현재 경로를 읽고, 공통 함수로 items를 계산합니다.
9
+ - 다만 규모가 커지면 `menu`와 `breadcrumb`는 별도 파일로 분리해도 됩니다.
10
+ - 중요한 것은 분리 여부보다 공통 route 정의를 기준으로 참조하는 구조를 유지하는 것입니다.
11
+
12
+ ## Why?
13
+
14
+ - 메뉴와 breadcrumb는 결국 같은 경로 정보를 여러 방식으로 보여주는 경우가 많습니다.
15
+ - 경로, 메뉴, breadcrumb가 따로 놀기 시작하면 수정 지점이 늘고 누락도 생깁니다.
16
+ - 페이지와 컴포넌트는 “어디로 가는지”보다 “무엇을 렌더하는지”에 더 집중하는 편이 좋습니다.
17
+ - 처음에는 한 파일로 두는 쪽이 단순하지만, 커졌을 때 `menu-config.ts`, `breadcrumb-config.ts`로 나누는 것은 자연스러운 확장입니다.
18
+
19
+ ## 예시 코드
20
+
21
+ ```ts
22
+ // src/shared/routes/route.ts
23
+ type BreadcrumbItem = {
24
+ label: string;
25
+ href?: string;
26
+ };
27
+
28
+ const isPath =
29
+ (path: string) =>
30
+ (pathname: string) =>
31
+ pathname === path;
32
+
33
+ const isMatch =
34
+ (pattern: RegExp) =>
35
+ (pathname: string) =>
36
+ pattern.test(pathname);
37
+
38
+ export const routes = {
39
+ home: {
40
+ label: '홈',
41
+ href: '/',
42
+ test: isPath('/'),
43
+ },
44
+ posts: {
45
+ label: '게시글',
46
+ href: '/posts',
47
+ test: isPath('/posts'),
48
+ },
49
+ postCreate: {
50
+ label: '글쓰기',
51
+ href: '/posts/new',
52
+ test: isPath('/posts/new'),
53
+ },
54
+ postDetail: {
55
+ label: '상세',
56
+ href: (id: string) => `/posts/${id}`,
57
+ test: isMatch(/^\/posts\/[^/]+$/),
58
+ },
59
+ } as const;
60
+
61
+ export const menuItems = [
62
+ { label: routes.home.label, href: routes.home.href },
63
+ { label: routes.posts.label, href: routes.posts.href },
64
+ { label: routes.postCreate.label, href: routes.postCreate.href },
65
+ ];
66
+
67
+ const breadcrumbConfigs: {
68
+ test: (pathname: string) => boolean;
69
+ items: BreadcrumbItem[];
70
+ }[] = [
71
+ {
72
+ test: routes.home.test,
73
+ items: [{ label: routes.home.label }],
74
+ },
75
+ {
76
+ test: routes.posts.test,
77
+ items: [
78
+ { label: routes.home.label, href: routes.home.href },
79
+ { label: routes.posts.label },
80
+ ],
81
+ },
82
+ {
83
+ test: routes.postCreate.test,
84
+ items: [
85
+ { label: routes.home.label, href: routes.home.href },
86
+ { label: routes.posts.label, href: routes.posts.href },
87
+ { label: routes.postCreate.label },
88
+ ],
89
+ },
90
+ {
91
+ test: routes.postDetail.test,
92
+ items: [
93
+ { label: routes.home.label, href: routes.home.href },
94
+ { label: routes.posts.label, href: routes.posts.href },
95
+ { label: routes.postDetail.label },
96
+ ],
97
+ },
98
+ ];
99
+
100
+ export function getBreadcrumbItems(pathname: string) {
101
+ return breadcrumbConfigs.find((config) => config.test(pathname))?.items ?? [];
102
+ }
103
+ ```
104
+
105
+ ```tsx
106
+ // src/features/posts/components/post-actions.tsx
107
+ 'use client';
108
+
109
+ import Link from 'next/link';
110
+ import { useRouter } from 'next/navigation';
111
+
112
+ import { routes } from '@/shared/routes/route';
113
+
114
+ export function PostActions({ id }: { id: string }) {
115
+ const router = useRouter();
116
+
117
+ return (
118
+ <div className="flex gap-3">
119
+ <Link href={routes.posts.href}>목록</Link>
120
+ <Link href={routes.postDetail.href(id)}>상세</Link>
121
+ <button type="button" onClick={() => router.push(routes.postCreate.href)}>
122
+ 글쓰기
123
+ </button>
124
+ </div>
125
+ );
126
+ }
127
+ ```
128
+
129
+ ```tsx
130
+ // src/shared/ui/breadcrumb.tsx
131
+ 'use client';
132
+
133
+ import Link from 'next/link';
134
+ import { usePathname } from 'next/navigation';
135
+
136
+ import { getBreadcrumbItems } from '@/shared/routes/route';
137
+
138
+ export function Breadcrumb() {
139
+ const pathname = usePathname();
140
+ const items = getBreadcrumbItems(pathname);
141
+
142
+ if (items.length === 0) return null;
143
+
144
+ return (
145
+ <nav aria-label="breadcrumb">
146
+ <ol className="flex items-center gap-2 text-sm text-zinc-500">
147
+ {items.map((item, index) => {
148
+ const isLast = index === items.length - 1;
149
+
150
+ return (
151
+ <li key={`${item.label}-${index}`} className="flex items-center gap-2">
152
+ {item.href && !isLast ? (
153
+ <Link href={item.href}>{item.label}</Link>
154
+ ) : (
155
+ <span>{item.label}</span>
156
+ )}
157
+ {!isLast ? <span>/</span> : null}
158
+ </li>
159
+ );
160
+ })}
161
+ </ol>
162
+ </nav>
163
+ );
164
+ }
165
+ ```
166
+
167
+ ## 한 줄 요약
168
+
169
+ route, menu, breadcrumb는 공통 정의를 중심으로 한 파일에서 관리하면 가장 덜 흩어집니다.
@@ -0,0 +1,64 @@
1
+ # 커밋 컨벤션
2
+
3
+ ## 원칙
4
+
5
+ - 한 커밋에는 가능한 한 한 가지 목적만 담습니다.
6
+ - 커밋 메시지는 `type: summary` 형식을 기본으로 사용합니다.
7
+ - summary는 무엇이 바뀌는지 바로 보이게 짧고 명확하게 씁니다.
8
+ - 의미 없는 메시지보다 변경 내용을 드러내는 메시지를 우선합니다.
9
+
10
+ ## Why?
11
+
12
+ - 커밋 히스토리를 보면 어떤 종류의 변경인지 빠르게 파악할 수 있습니다.
13
+ - 리뷰, 되돌리기, cherry-pick 같은 작업이 쉬워집니다.
14
+ - 여러 목적이 섞인 커밋보다 맥락이 분명한 커밋이 유지보수에 유리합니다.
15
+
16
+ ## 기본 형식
17
+
18
+ ```text
19
+ type: summary
20
+ ```
21
+
22
+ 필요하면 scope를 붙일 수 있습니다.
23
+
24
+ ```text
25
+ type(scope): summary
26
+ ```
27
+
28
+ ## 타입 기준
29
+
30
+ - `feat`: 사용자 기능 추가 또는 의미 있는 기능 변경
31
+ - `fix`: 버그 수정
32
+ - `refactor`: 동작 변화 없이 구조 개선
33
+ - `docs`: 문서 추가 및 수정
34
+ - `chore`: 설정, 스크립트, 의존성, 빌드 관련 작업
35
+ - `test`: 테스트 추가 및 수정
36
+ - `style`: 포맷팅, 정렬, 주석 같은 비기능 수정
37
+
38
+ ## 작성 기준
39
+
40
+ - summary는 너무 길게 쓰지 않습니다.
41
+ - `update`, `change`, `fix stuff` 같은 모호한 표현은 피합니다.
42
+ - 마침표는 보통 붙이지 않습니다.
43
+ - 여러 목적이 섞이면 커밋을 나누는 쪽을 먼저 검토합니다.
44
+
45
+ ## 예시 코드
46
+
47
+ ```text
48
+ feat: 게시글 검색 필터 추가
49
+ fix: 로그인 만료 시 무한 리다이렉트 수정
50
+ refactor: posts query key 구조 정리
51
+ docs: 라우트 정의 문서 추가
52
+ chore: eslint 설정 정리
53
+ test: post list query 테스트 추가
54
+ ```
55
+
56
+ ```text
57
+ feat(posts): 게시글 작성 페이지 추가
58
+ fix(auth): 토큰 만료 처리 수정
59
+ refactor(shared): text input field 구조 정리
60
+ ```
61
+
62
+ ## 한 줄 요약
63
+
64
+ 커밋은 한 가지 목적만 담고, 메시지는 `type: summary` 형식으로 짧고 명확하게 씁니다.
@@ -0,0 +1,187 @@
1
+ # 기타 원칙
2
+
3
+ ## Layout 반응형 처리
4
+
5
+ ### 원칙
6
+
7
+ - layout의 반응형 전환은 가능한 한 CSS와 responsive class로 처리합니다.
8
+ - 화면 크기 자체를 JS state로 들고 있지 않습니다.
9
+ - 사용자 인터랙션이 필요한 상태만 client state로 둡니다.
10
+ - JS에서 브레이크포인트 판단이 꼭 필요하면 `resize` 이벤트를 직접 다루기보다 `useMediaQuery`나 `window.matchMedia` 기반 방식을 우선합니다.
11
+
12
+ ### Why?
13
+
14
+ - 레이아웃 전환은 CSS가 더 단순하고 안정적으로 처리합니다.
15
+ - `window.innerWidth`나 `resize` 이벤트를 직접 다루기 시작하면 코드가 불필요하게 무거워집니다.
16
+ - 브레이크포인트 판단과 열림/닫힘 상태를 분리하면 책임이 깔끔해집니다.
17
+ - media query 기반 방식은 의도가 더 분명하고, 구독/해제 로직도 직접 관리할 일이 줄어듭니다.
18
+
19
+ ### 예시 코드
20
+
21
+ ```tsx
22
+ // src/app/(dashboard)/layout.tsx
23
+ export default function DashboardLayout({
24
+ children,
25
+ }: {
26
+ children: React.ReactNode;
27
+ }) {
28
+ return (
29
+ <div className="min-h-screen lg:grid lg:grid-cols-[240px_1fr]">
30
+ <aside className="hidden border-r border-zinc-200 bg-white lg:block">
31
+ Sidebar
32
+ </aside>
33
+
34
+ <div className="min-w-0">
35
+ <header className="border-b border-zinc-200 bg-white px-4 py-3 lg:hidden">
36
+ Mobile Header
37
+ </header>
38
+ <main className="p-4 lg:p-8">{children}</main>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ```tsx
46
+ // JS로 브레이크포인트 판단이 꼭 필요할 때
47
+ const isDesktop = useMediaQuery('(min-width: 1024px)');
48
+ ```
49
+
50
+ 직접 `resize` 이벤트를 붙여 width를 상태로 관리하는 것보다, 이런 식의 media query 기반 접근이 보통 더 단순합니다.
51
+
52
+ ## Layout State 범위
53
+
54
+ ### 원칙
55
+
56
+ - layout에는 공통 shell 제어 상태만 둡니다.
57
+ - 모바일 사이드바 열림, 전역 검색창 열림 같은 상태는 둘 수 있습니다.
58
+ - 페이지 전용 폼 상태나 feature 데이터는 layout에 두지 않습니다.
59
+
60
+ ### Why?
61
+
62
+ - layout state가 많아지면 여러 페이지가 불필요하게 묶입니다.
63
+ - 공통 껍데기 상태와 페이지 비즈니스 상태가 섞이면 수정 범위가 커집니다.
64
+ - layout은 구조와 진입 제어에 집중할수록 유지보수가 쉽습니다.
65
+
66
+ ### 예시 코드
67
+
68
+ ```tsx
69
+ // src/shared/ui/dashboard-shell.tsx
70
+ 'use client';
71
+
72
+ import { useState } from 'react';
73
+
74
+ export function DashboardShell({
75
+ children,
76
+ }: {
77
+ children: React.ReactNode;
78
+ }) {
79
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
80
+
81
+ return (
82
+ <div className="min-h-screen">
83
+ <button
84
+ type="button"
85
+ className="lg:hidden"
86
+ onClick={() => setIsSidebarOpen((prev) => !prev)}
87
+ >
88
+ 메뉴
89
+ </button>
90
+
91
+ {isSidebarOpen ? <aside>Mobile Sidebar</aside> : null}
92
+ <main>{children}</main>
93
+ </div>
94
+ );
95
+ }
96
+ ```
97
+
98
+ ## 영역별 Provider 주입
99
+
100
+ ### 원칙
101
+
102
+ - provider는 무조건 앱 루트에 올리지 않습니다.
103
+ - 해당 라우트 그룹 전체에서 필요한 provider만 그 layout에서 주입합니다.
104
+ - 전역 provider와 영역 전용 provider를 구분합니다.
105
+
106
+ ### Why?
107
+
108
+ - 모든 provider를 루트에 올리면 앱 전체가 불필요하게 무거워집니다.
109
+ - 어떤 영역에서만 필요한 컨텍스트인지 드러나야 코드 탐색이 쉬워집니다.
110
+ - 공통 조건과 공통 주입을 layout에 모으면 페이지 코드가 단순해집니다.
111
+
112
+ ### 예시 코드
113
+
114
+ ```tsx
115
+ // src/app/(dashboard)/layout.tsx
116
+ import { redirect } from 'next/navigation';
117
+
118
+ import { getCurrentUser } from '@/features/auth/api/get-current-user.server';
119
+ import { DashboardShell } from '@/shared/ui/dashboard-shell';
120
+ import { DashboardProvider } from '@/features/dashboard/providers/dashboard-provider';
121
+
122
+ export default async function DashboardLayout({
123
+ children,
124
+ }: {
125
+ children: React.ReactNode;
126
+ }) {
127
+ const user = await getCurrentUser();
128
+
129
+ if (!user) {
130
+ redirect('/login');
131
+ }
132
+
133
+ return (
134
+ <DashboardProvider>
135
+ <DashboardShell>{children}</DashboardShell>
136
+ </DashboardProvider>
137
+ );
138
+ }
139
+ ```
140
+
141
+ ## Props Drilling 기준
142
+
143
+ ### 원칙
144
+
145
+ - props drilling은 무조건 나쁜 것으로 보지 않습니다.
146
+ - 2~3단계 정도의 명확한 전달은 그냥 props로 두는 편이 더 단순합니다.
147
+ - 중간 컴포넌트가 값을 쓰지 않는데 계속 전달만 한다면 구조를 다시 봅니다.
148
+
149
+ ### Why?
150
+
151
+ - 얕고 명확한 props 전달은 가장 읽기 쉽고 추적하기 쉽습니다.
152
+ - 작은 전달까지 모두 context나 store로 올리면 오히려 구조가 무거워질 수 있습니다.
153
+ - 문제는 props 전달 자체보다 깊이와 불필요한 중계입니다.
154
+
155
+ ### 예시 코드
156
+
157
+ ```tsx
158
+ // src/features/posts/components/post-page.tsx
159
+ export function PostPage({
160
+ posts,
161
+ selectedPostId,
162
+ }: {
163
+ posts: { id: string; title: string }[];
164
+ selectedPostId?: string;
165
+ }) {
166
+ return <PostListSection posts={posts} selectedPostId={selectedPostId} />;
167
+ }
168
+ ```
169
+
170
+ ```tsx
171
+ // src/features/posts/components/post-list-section.tsx
172
+ import { PostList } from '@/features/posts/components/post-list';
173
+
174
+ export function PostListSection({
175
+ posts,
176
+ selectedPostId,
177
+ }: {
178
+ posts: { id: string; title: string }[];
179
+ selectedPostId?: string;
180
+ }) {
181
+ return <PostList posts={posts} selectedPostId={selectedPostId} />;
182
+ }
183
+ ```
184
+
185
+ ## 한 줄 요약
186
+
187
+ layout은 반응형은 CSS로, state는 shell 범위로, provider는 필요한 영역에서만 주입하고, 얕은 props 전달은 과하게 두려워하지 않는 쪽이 가장 깔끔합니다.