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,18 @@
1
+ /**
2
+ * 인증 흐름 예시를 위한 로그인 플레이스홀더 화면입니다.
3
+ * 프로젝트의 실제 로그인 페이지로 교체해서 사용하세요.
4
+ */
5
+ export default function LoginPage() {
6
+ return (
7
+ <main className="mx-auto flex min-h-screen w-full max-w-xl items-center px-6 py-16">
8
+ <section className="w-full rounded-3xl border border-zinc-200 bg-white p-8 shadow-sm">
9
+ <p className="text-sm font-medium uppercase tracking-[0.2em] text-zinc-500">로그인</p>
10
+ <h1 className="mt-3 text-3xl font-semibold text-zinc-950">로그인이 필요합니다</h1>
11
+ <p className="mt-3 text-sm leading-6 text-zinc-600">
12
+ 기본 템플릿은 인증 오류가 발생하면 이 경로로 이동할 수 있습니다. 프로젝트 로그인 화면으로
13
+ 교체하세요.
14
+ </p>
15
+ </section>
16
+ </main>
17
+ );
18
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["./src/*"]
20
+ },
21
+
22
+ "strict": true,
23
+ "noUnusedLocals": true,
24
+ "noUnusedParameters": true,
25
+ "erasableSyntaxOnly": true,
26
+ "noFallthroughCasesInSwitch": true,
27
+ "noUncheckedSideEffectImports": true
28
+ },
29
+ "include": ["src"]
30
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "files": [],
3
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
4
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts", "scripts/**/*.mjs"]
24
+ }
@@ -0,0 +1,14 @@
1
+ import path from 'node:path';
2
+
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import react from '@vitejs/plugin-react';
5
+ import { defineConfig } from 'vite';
6
+
7
+ export default defineConfig({
8
+ plugins: [react(), tailwindcss()],
9
+ resolve: {
10
+ alias: {
11
+ '@': path.resolve(__dirname, './src'),
12
+ },
13
+ },
14
+ });
@@ -0,0 +1,150 @@
1
+ # 전반적인 폴더 구조
2
+
3
+ ## 원칙
4
+
5
+ - 폴더 구조는 기술 기준보다 기능 기준으로 나눕니다.
6
+ - 먼저 `feature 안에 두는 것`을 기본으로 생각합니다.
7
+ - `shared/ui`는 앱 전반에서 재사용되는 진짜 공통 UI만 둡니다.
8
+ - `2~3개 feature`가 같이 쓰는 준공통 컴포넌트는 `shared/components/[공유-맥락-이름]`으로 둘 수 있습니다.
9
+ - 공통화는 단계적으로 올리고, 처음부터 너무 위로 올리지 않습니다.
10
+
11
+ ## Why?
12
+
13
+ - 모든 걸 `shared`로 올리기 시작하면 재사용 경계가 흐려집니다.
14
+ - 반대로 전부 feature 안에만 두면 비슷한 코드가 여러 군데서 반복됩니다.
15
+ - 그래서 `feature 전용`, `준공통`, `진짜 공통`을 나눠서 보는 편이 가장 실무적입니다.
16
+
17
+ ## 권장 구조
18
+
19
+ ```text
20
+ app/
21
+ (dashboard)/
22
+ layout.tsx
23
+ posts/page.tsx
24
+ comments/page.tsx
25
+
26
+ features/
27
+ posts/
28
+ api/
29
+ components/
30
+ hooks/
31
+ lib/
32
+ types/
33
+ comments/
34
+ api/
35
+ components/
36
+ hooks/
37
+ lib/
38
+ types/
39
+ users/
40
+ api/
41
+ components/
42
+ hooks/
43
+ types/
44
+
45
+ shared/
46
+ api/
47
+ config/
48
+ lib/
49
+ ui/
50
+ button.tsx
51
+ global-modal.tsx
52
+ text-input-field.tsx
53
+ components/
54
+ content-meta/
55
+ author-badge.tsx
56
+ content-meta.tsx
57
+ ```
58
+
59
+ ## 폴더별 역할
60
+
61
+ ### `app/`
62
+
63
+ - Next.js 라우트 엔트리
64
+ - 페이지, layout, loading, error 같은 라우팅 단위 파일
65
+ - 가능한 한 조립과 진입 제어에 집중합니다.
66
+
67
+ ### `features/<domain>/`
68
+
69
+ - 해당 기능의 UI, API, hooks, types, 유틸을 둡니다.
70
+ - 기본값은 여기입니다.
71
+ - 특정 도메인 용어와 규칙이 들어가면 우선 feature 안에 둡니다.
72
+
73
+ ### `shared/ui/`
74
+
75
+ - 진짜 전역 공통 UI만 둡니다.
76
+ - 도메인을 몰라도 설명 가능한 컴포넌트가 들어갑니다.
77
+ - 예: `Button`, `TextInputField`, `GlobalModal`
78
+
79
+ ### `shared/components/[공유-맥락-이름]/`
80
+
81
+ - 2~3개 feature가 함께 쓰는 준공통 컴포넌트를 둡니다.
82
+ - 완전히 전역 공통은 아니지만, 한 feature 안에만 두기엔 애매할 때 사용합니다.
83
+ - 폴더명은 여러 feature가 공유하는 맥락이나 책임을 드러내게 짓습니다.
84
+ - 글자를 억지로 이어붙이기보다, 겹치는 개념을 하나의 이름으로 표현하는 편이 좋습니다.
85
+ - 예: `content-meta`, `billing`, `access-control`
86
+
87
+ ## 어디에 둘지 빠른 기준
88
+
89
+ - 한 feature에서만 쓴다: `features/<domain>/components`
90
+ - 2~3개 feature가 함께 쓰지만 공통 맥락이 있다: `shared/components/[공유-맥락-이름]`
91
+ - 앱 어디서든 써도 되는 도메인 중립 UI다: `shared/ui`
92
+
93
+ ## 예시 코드
94
+
95
+ ```tsx
96
+ // src/features/posts/components/post-publish-button.tsx
97
+ type PostPublishButtonProps = {
98
+ postId: string;
99
+ isPublished: boolean;
100
+ onPublish: (postId: string) => void;
101
+ };
102
+
103
+ export function PostPublishButton({
104
+ postId,
105
+ isPublished,
106
+ onPublish,
107
+ }: PostPublishButtonProps) {
108
+ return (
109
+ <button disabled={isPublished} onClick={() => onPublish(postId)}>
110
+ {isPublished ? "발행 완료" : "발행하기"}
111
+ </button>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ```tsx
117
+ // src/shared/components/content-meta/author-badge.tsx
118
+ type AuthorBadgeProps = {
119
+ name: string;
120
+ role?: string;
121
+ };
122
+
123
+ export function AuthorBadge({ name, role }: AuthorBadgeProps) {
124
+ return (
125
+ <div>
126
+ <strong>{name}</strong>
127
+ {role ? <span>{role}</span> : null}
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ```tsx
134
+ // src/shared/ui/global-modal.tsx
135
+ 'use client';
136
+
137
+ import { useModalStore } from '@/shared/ui/modal-store';
138
+
139
+ export function GlobalModal() {
140
+ const { isOpen } = useModalStore();
141
+
142
+ if (!isOpen) return null;
143
+
144
+ return <div>공통 모달</div>;
145
+ }
146
+ ```
147
+
148
+ ## 한 줄 요약
149
+
150
+ 기본은 feature 안에 두고, 2~3개 feature 공통은 `shared/components/[공유-맥락-이름]`, 진짜 전역 공통만 `shared/ui`에 둡니다.
@@ -0,0 +1,186 @@
1
+ # 구조와 라우팅
2
+
3
+ ## 원칙
4
+
5
+ - 코드는 기술 기준보다 기능 기준으로 나눕니다.
6
+ - 기본 구조는 `features/<domain>` + `shared`를 우선합니다.
7
+ - `shared/ui`는 앱 전반에서 재사용되는 진짜 공통 UI만 둡니다.
8
+ - `2~3개 feature`가 함께 쓰는 준공통 컴포넌트는 `shared/components/[공유-맥락-이름]`으로 둘 수 있습니다.
9
+ - `layout`은 공통 화면 틀, 인증/권한 체크, provider 주입을 담당합니다.
10
+ - `layout`에는 페이지 전용 비즈니스 로직을 넣지 않습니다.
11
+ - 컴포넌트는 크기보다 책임 경계로 나눕니다.
12
+ - 너무 이른 공통화는 피합니다.
13
+
14
+ ## Why?
15
+
16
+ - 기능 단위로 묶어두면 수정 범위가 좁아지고 찾기 쉬워집니다.
17
+ - 공통 UI와 기능 전용 UI를 섞어두면 재사용 경계가 흐려집니다.
18
+ - 전역 공통과 준공통을 같은 폴더에 몰아넣으면 `shared`가 금방 비대해집니다.
19
+ - `layout`이 인증, provider, 공통 화면 틀을 맡으면 페이지 코드가 가벼워집니다.
20
+
21
+ ## 공통 계층을 나누는 기준
22
+
23
+ ### `features/<domain>/components`
24
+
25
+ - 한 feature 안에서만 쓰는 컴포넌트
26
+ - 해당 도메인 용어와 규칙이 그대로 남아 있는 컴포넌트
27
+
28
+ ### `shared/components/[공유-맥락-이름]`
29
+
30
+ - 2~3개 feature가 함께 쓰는 준공통 컴포넌트
31
+ - 전역 공통이라고 보긴 어렵지만, 특정 feature 하나에 두기 애매한 컴포넌트
32
+ - 폴더명은 여러 feature를 억지로 이어붙이기보다, 겹치는 책임과 맥락을 한 개념으로 표현합니다.
33
+ - 예: `shared/components/content-meta`, `shared/components/billing`
34
+
35
+ ### `shared/ui`
36
+
37
+ - 앱 어디서든 재사용 가능한 진짜 공통 UI
38
+ - 도메인 중립적인 primitive 또는 base component
39
+ - 예: `Button`, `TextInputField`, `GlobalModal`
40
+
41
+ ## 공통 컴포넌트 판단 기준
42
+
43
+ - 한 feature에서만 쓰면 `features/<domain>/components`에 둡니다.
44
+ - `2~3개 독립 feature`가 비슷한 구조와 동작으로 쓰고, 공통 맥락이 남아 있으면 `shared/components/[공유-맥락-이름]`으로 올립니다.
45
+ - 앱 어디서든 쓸 수 있는 도메인 중립 UI면 `shared/ui`로 올립니다.
46
+ - 공통화는 마크업만이 아니라 동작과 props까지 비슷할 때 합니다.
47
+ - 공통화 때문에 `variant`, `mode`, `type` 같은 예외 props가 계속 늘면 멈추고 feature에 둡니다.
48
+ - `Button`, `TextInputField`, `GlobalModal` 같은 base UI는 `2개 feature`만 보여도 `shared/ui` 후보가 될 수 있습니다.
49
+
50
+ 여기서 feature는 `posts`, `comments`, `users`처럼 도메인이 다른 단위를 뜻합니다.
51
+ 같은 feature 안의 여러 페이지는 보통 "여러 화면"으로 봅니다.
52
+
53
+ ## 예시 구조
54
+
55
+ ```text
56
+ app/
57
+ (dashboard)/
58
+ layout.tsx
59
+ posts/page.tsx
60
+ features/
61
+ posts/
62
+ api/
63
+ get-post-list.server.ts
64
+ get-post-list.client.ts
65
+ components/
66
+ post-list.tsx
67
+ post-list-client.tsx
68
+ hooks/
69
+ query-keys.ts
70
+ use-post-list-query.ts
71
+ use-create-post-mutation.ts
72
+ types/
73
+ post.ts
74
+ shared/
75
+ components/
76
+ content-meta/
77
+ author-badge.tsx
78
+ ui/
79
+ button.tsx
80
+ global-modal.tsx
81
+ text-input-field.tsx
82
+ ```
83
+
84
+ ## 예시 코드
85
+
86
+ ```tsx
87
+ // src/features/posts/components/post-publish-button.tsx
88
+ type PostPublishButtonProps = {
89
+ postId: string;
90
+ isPublished: boolean;
91
+ onPublish: (postId: string) => void;
92
+ };
93
+
94
+ export function PostPublishButton({
95
+ postId,
96
+ isPublished,
97
+ onPublish,
98
+ }: PostPublishButtonProps) {
99
+ return (
100
+ <button disabled={isPublished} onClick={() => onPublish(postId)}>
101
+ {isPublished ? "발행 완료" : "발행하기"}
102
+ </button>
103
+ );
104
+ }
105
+ ```
106
+
107
+ 위 컴포넌트는 `postId`, `isPublished`, `onPublish`처럼 게시글 도메인에 강하게 묶여 있으므로 `shared`보다 `features/posts`에 두는 편이 자연스럽습니다.
108
+
109
+ ```tsx
110
+ // src/shared/components/content-meta/author-badge.tsx
111
+ type AuthorBadgeProps = {
112
+ name: string;
113
+ role?: string;
114
+ };
115
+
116
+ export function AuthorBadge({ name, role }: AuthorBadgeProps) {
117
+ return (
118
+ <div>
119
+ <strong>{name}</strong>
120
+ {role ? <span>{role}</span> : null}
121
+ </div>
122
+ );
123
+ }
124
+ ```
125
+
126
+ 위 컴포넌트는 게시글과 댓글처럼 몇 개 feature에서 함께 쓸 수 있지만, 완전히 전역 공통 primitive라고 보긴 어려워서 `shared/components/content-meta` 같은 위치가 더 자연스럽습니다.
127
+
128
+ 여기서 이름은 `post-comment`처럼 글자를 이어붙이는 방식보다, 두 feature가 공유하는 맥락을 표현하는 `content-meta` 같은 이름이 더 좋습니다.
129
+
130
+ ```tsx
131
+ // src/shared/ui/text-input-field.tsx
132
+ import type { InputHTMLAttributes } from 'react';
133
+
134
+ type TextInputFieldProps = {
135
+ label: string;
136
+ error?: string;
137
+ } & InputHTMLAttributes<HTMLInputElement>;
138
+
139
+ export function TextInputField({ label, error, id, ...props }: TextInputFieldProps) {
140
+ const inputId = id ?? props.name;
141
+
142
+ return (
143
+ <div>
144
+ <label htmlFor={inputId}>{label}</label>
145
+ <input id={inputId} {...props} />
146
+ {error ? <p className="text-sm text-rose-600">{error}</p> : null}
147
+ </div>
148
+ );
149
+ }
150
+ ```
151
+
152
+ 위 컴포넌트는 특정 도메인을 모르고, 여러 feature에서 같은 방식으로 쓸 수 있어서 `shared/ui`로 올리기 좋습니다.
153
+
154
+ ```tsx
155
+ // src/app/layout.tsx
156
+ import './globals.css';
157
+
158
+ import { AppProviders } from '@/app/providers';
159
+
160
+ export default function RootLayout({
161
+ children,
162
+ }: Readonly<{
163
+ children: React.ReactNode;
164
+ }>) {
165
+ return (
166
+ <html lang="ko">
167
+ <body>
168
+ <AppProviders>{children}</AppProviders>
169
+ </body>
170
+ </html>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ## 체크 포인트
176
+
177
+ - 이 코드가 특정 기능에만 쓰이는가: feature 안에 둡니다.
178
+ - 2~3개 feature가 같이 쓰지만 공유되는 맥락을 한 이름으로 설명할 수 있는가: `shared/components/[공유-맥락-이름]` 후보입니다.
179
+ - 앱 어디서든 쓸 수 있을 정도로 도메인 중립적인가: `shared/ui` 후보입니다.
180
+ - 공통화하려면 예외 props가 계속 늘어나는가: 그렇다면 아직 feature에 둡니다.
181
+ - 이 컴포넌트가 도메인 용어 없이도 설명 가능한가: 가능하면 `shared` 후보입니다.
182
+ - 페이지 여러 곳이 같은 화면 틀을 쓰는가: `layout`으로 올립니다.
183
+
184
+ ## 한 줄 요약
185
+
186
+ 구조는 기능 기준으로 나누고, 공통 계층은 정말 공통인 것만 올립니다.
@@ -0,0 +1,86 @@
1
+ # 서버와 클라이언트
2
+
3
+ ## 원칙
4
+
5
+ - Next.js에서는 서버 컴포넌트를 기본값으로 생각합니다.
6
+ - 서버 컴포넌트는 초기 데이터 조회, 접근 제어, `redirect`, `notFound` 같은 라우팅 판단을 맡습니다.
7
+ - 클라이언트 컴포넌트는 인터랙션, 브라우저 상태, 이벤트 처리를 맡습니다.
8
+ - `use client`는 꼭 필요한 경우에만 붙입니다.
9
+
10
+ ## Why?
11
+
12
+ - 서버에서 끝낼 수 있는 일을 클라이언트로 내리면 번들 크기와 복잡도가 커집니다.
13
+ - 초기 데이터와 권한 체크를 서버에서 처리하면 화면 진입 흐름이 더 단순해집니다.
14
+ - 반대로 입력, 드래그, 브라우저 API처럼 사용자 상호작용이 중심인 것은 클라이언트가 더 자연스럽습니다.
15
+
16
+ ## 서버에서 처리하기 좋은 것
17
+
18
+ - 초기 데이터 조회
19
+ - 인증/권한 체크
20
+ - 페이지 진입 시 분기
21
+ - 서버에서 끝낼 수 있는 가공
22
+
23
+ ## 클라이언트에서 처리하기 좋은 것
24
+
25
+ - 클릭, 입력, 드래그 같은 상호작용
26
+ - 브라우저 API 사용
27
+ - 로컬 UI 상태 관리
28
+ - 폼 제어
29
+
30
+ ## 판단 기준
31
+
32
+ - 브라우저 API가 필요한가
33
+ - 사용자 이벤트에 따라 즉시 반응해야 하는가
34
+ - local state가 필요한가
35
+ - 아니면 서버에서 미리 처리 가능한가
36
+
37
+ 위 질문에 대부분 `아니오`라면 서버에 두는 쪽이 기본입니다.
38
+
39
+ ## 예시 코드
40
+
41
+ ```tsx
42
+ // src/app/posts/page.tsx
43
+ import { getPostListServer } from '@/features/posts/api/get-post-list.server';
44
+ import { PostFilter } from '@/features/posts/components/post-filter';
45
+ import { PostList } from '@/features/posts/components/post-list';
46
+
47
+ export default async function PostsPage() {
48
+ const posts = await getPostListServer();
49
+
50
+ return (
51
+ <section>
52
+ <h1>게시글</h1>
53
+ <PostFilter />
54
+ <PostList posts={posts} />
55
+ </section>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ```tsx
61
+ // src/features/posts/components/post-filter.tsx
62
+ 'use client';
63
+
64
+ import { useState } from 'react';
65
+
66
+ import { TextInputField } from '@/shared/ui/text-input-field';
67
+
68
+ export function PostFilter() {
69
+ const [keyword, setKeyword] = useState('');
70
+
71
+ return (
72
+ <TextInputField
73
+ label="검색"
74
+ value={keyword}
75
+ onChange={(event) => setKeyword(event.target.value)}
76
+ placeholder="검색어를 입력하세요"
77
+ />
78
+ );
79
+ }
80
+ ```
81
+
82
+ 위 예시에서는 목록 데이터는 서버에서 가져오고, 입력 상호작용만 클라이언트로 분리했습니다.
83
+
84
+ ## 한 줄 요약
85
+
86
+ 서버는 초기 데이터와 접근 제어를 맡고, 클라이언트는 인터랙션과 브라우저 상태를 맡습니다.
@@ -0,0 +1,84 @@
1
+ # 상태관리
2
+
3
+ ## 원칙
4
+
5
+ - 상태는 한 군데 store에 몰아넣지 않습니다.
6
+ - 상태의 성격에 맞는 계층에 둡니다.
7
+
8
+ ## Why?
9
+
10
+ - 상태를 성격별로 나누면 중복 저장과 책임 충돌이 줄어듭니다.
11
+ - 서버 데이터와 로컬 UI 상태를 같은 곳에 섞어두면 갱신 기준이 흐려집니다.
12
+ - URL로 표현 가능한 상태를 URL에 두면 공유, 새로고침, 뒤로가기 동작이 자연스러워집니다.
13
+
14
+ ## 어디에 둘지 기준
15
+
16
+ - URL로 표현 가능하고 공유/새로고침이 필요한 상태: URL
17
+ - 서버에서 가져온 데이터: `TanStack Query`
18
+ - 입력 중인 폼 값: `React Hook Form`
19
+ - 특정 컴포넌트 안에서만 쓰는 UI 상태: local state
20
+ - 앱 여러 곳이 동시에 알아야 하는 상태: global store
21
+
22
+ ## 전역 store 원칙
23
+
24
+ - 인증 상태
25
+ - 전역 모달
26
+ - 앱 전역 UI 상태
27
+
28
+ 위처럼 정말 전역 공유가 필요한 것만 넣습니다.
29
+
30
+ ## 피할 것
31
+
32
+ - 서버 데이터를 전역 store에 복제하기
33
+ - URL에 둘 수 있는 상태를 store에만 두기
34
+ - 폼 값을 전역 상태로 관리하기
35
+
36
+ ## 예시 코드
37
+
38
+ ```tsx
39
+ // src/app/posts/page.tsx
40
+ const params = useSearchParams();
41
+ const page = Number(params.get('page') ?? '1');
42
+ ```
43
+
44
+ ```tsx
45
+ // src/features/posts/components/post-list-client.tsx
46
+ const { data: posts = [], isPending } = usePostListQuery();
47
+ ```
48
+
49
+ ```tsx
50
+ // src/features/posts/components/create-post-form.tsx
51
+ const form = useForm<CreatePostFormValues>({
52
+ resolver: zodResolver(createPostSchema),
53
+ });
54
+ ```
55
+
56
+ ```tsx
57
+ // src/features/posts/components/post-filter.tsx
58
+ const [keyword, setKeyword] = useState('');
59
+ ```
60
+
61
+ ```ts
62
+ // src/shared/ui/modal-store.ts
63
+ export const useModalStore = create<ModalState>((set) => ({
64
+ isOpen: false,
65
+ title: '',
66
+ description: '',
67
+ openModal: ({ title, description }) =>
68
+ set({
69
+ isOpen: true,
70
+ title,
71
+ description,
72
+ }),
73
+ closeModal: () =>
74
+ set({
75
+ isOpen: false,
76
+ title: '',
77
+ description: '',
78
+ }),
79
+ }));
80
+ ```
81
+
82
+ ## 한 줄 요약
83
+
84
+ URL은 URL에, 서버 데이터는 Query에, 폼은 Form에, 지역 UI는 local state에 둡니다.