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,250 @@
1
+ # Vite React Frontend Template
2
+
3
+ 실무에서 반복되는 구조를 바로 가져다 쓸 수 있도록 만든 `Vite + React + TypeScript` 템플릿이다.
4
+
5
+ ## Defaults
6
+
7
+ - `feature-first` 구조
8
+ - `react-router-dom` 기본 포함
9
+ - `src/pages/**/page.tsx` 기반 자동 라우팅
10
+ - 공통 `request` 코어 + 클라이언트 wrapper 분리
11
+ - `TanStack Query` 중심 서버 상태 관리
12
+ - `Zustand`는 전역 UI 상태에만 사용
13
+ - `React Hook Form + Zod` 예제는 CRUD feature 생성 시 추가
14
+ - 날짜, 포맷팅, query string, 에러 처리 같은 공통 로직 분리
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ npm install
20
+ cp .env.example .env.local
21
+ npm run dev
22
+ ```
23
+
24
+ 필수 환경 변수:
25
+
26
+ ```bash
27
+ VITE_API_BASE_URL=http://localhost:4000
28
+ ```
29
+
30
+ - 클라이언트 요청은 기본적으로 `VITE_API_BASE_URL`을 사용한다.
31
+ - feature API 함수는 `features/<name>/api` 아래에 둔다.
32
+
33
+ ## Structure
34
+
35
+ ```txt
36
+ src/
37
+ app/
38
+ pages/
39
+ features/
40
+ shared/
41
+ api/
42
+ auth/
43
+ config/
44
+ error/
45
+ lib/
46
+ query/
47
+ routes/
48
+ ui/
49
+ utils/
50
+ ```
51
+
52
+ - `app`
53
+ - 앱 엔트리, router, provider, 공통 frame을 둔다.
54
+ - `pages`
55
+ - route component를 둔다. `src/pages/**/page.tsx`는 자동으로 React Router 경로가 된다.
56
+ - `features`
57
+ - 특정 도메인에서만 쓰는 API, hook, component, lib를 둔다.
58
+ - `shared`
59
+ - 두 개 이상 feature 또는 앱 전역에서 재사용하는 공통 코드만 둔다.
60
+
61
+ 라우트 예시:
62
+
63
+ - `src/pages/index/page.tsx` -> `/`
64
+ - `src/pages/login/page.tsx` -> `/login`
65
+ - `src/pages/products/page.tsx` -> `/products`
66
+ - `src/pages/products/new/page.tsx` -> `/products/new`
67
+
68
+ ## Request And Error Flow
69
+
70
+ 요청 함수 선택 기준:
71
+
72
+ - 일반 요청: `requestClient`
73
+ - 인증 쿠키 포함 요청: `requestClientAuth`
74
+
75
+ 기준:
76
+
77
+ - `request.ts`는 공통 타입과 실행 함수 정의를 두고, 실제 사용은 wrapper를 통해 한다.
78
+ - 상대 경로 요청은 `VITE_API_BASE_URL`을 기준으로 실행된다.
79
+ - `request`는 정규화된 `AppError`를 throw한다.
80
+
81
+ 예시:
82
+
83
+ ```ts
84
+ import { requestClient } from '@/shared/api/request.client';
85
+
86
+ export function getProducts(search?: string) {
87
+ return requestClient<Product[]>({
88
+ method: 'GET',
89
+ url: '/products',
90
+ params: { search },
91
+ });
92
+ }
93
+ ```
94
+
95
+ 옵션 예제:
96
+
97
+ - `src/shared/error/handle-app-error.client.ts`
98
+ - `src/shared/auth/auth-error-listener.tsx`
99
+ - `src/shared/ui/global-modal.tsx`
100
+ - 기본 템플릿에서는 연결하지 않으며, 필요하면 `src/app/providers.tsx`, `src/app/frame.tsx`에서 수동으로 연결한다.
101
+
102
+ ## Scripts
103
+
104
+ ```bash
105
+ npm run dev
106
+ npm run build
107
+ npm run preview
108
+ npm run lint
109
+ npm run lint:fix
110
+ npm run format
111
+ npm run format:check
112
+ npm run pack:dry-run
113
+ npm run release:check
114
+ npm run create:feature -- products
115
+ npm run create:feature:crud -- products
116
+ ```
117
+
118
+ ## Create Feature
119
+
120
+ ```bash
121
+ npm run create:feature -- products
122
+ ```
123
+
124
+ 생성 대상:
125
+
126
+ - `src/features/<name>/api/`
127
+ - `src/features/<name>/components/`
128
+ - `src/features/<name>/hooks/`
129
+ - `src/features/<name>/lib/`
130
+ - `src/pages/<name>/page.tsx`
131
+
132
+ 주의:
133
+
134
+ - 최소 폴더 구조와 route page만 만든다.
135
+ - route는 `src/pages/**/page.tsx` 규칙으로 자동 연결된다.
136
+ - CRUD 예제가 필요하면 아래 명령을 사용한다.
137
+
138
+ ```bash
139
+ npm run create:feature:crud -- products
140
+ ```
141
+
142
+ CRUD 예시 생성 대상:
143
+
144
+ - `src/features/<name>/api`
145
+ - `src/features/<name>/components`
146
+ - `src/features/<name>/hooks`
147
+ - `src/features/<name>/lib`
148
+ - `src/pages/<name>/page.tsx`
149
+ - `src/pages/<name>/new/page.tsx`
150
+
151
+ 주의:
152
+
153
+ - 생성된 API 함수는 외부 백엔드 연동을 전제로 한다.
154
+ - same-origin mock route는 생성하지 않는다.
155
+ - `routePaths`는 자동 병합하지 않는다.
156
+ - 필요하면 생성 후 `src/shared/routes/route-paths.ts`에 직접 추가한다.
157
+
158
+ 관련 스크립트:
159
+
160
+ - `scripts/create-app.mjs`
161
+ - `scripts/create-feature.mjs`
162
+ - `scripts/create-feature-crud.mjs`
163
+
164
+ ## Coding Defaults
165
+
166
+ - 앱 내부 타입 선언은 기본적으로 `.ts` / `.tsx`에서 `type`을 우선 사용한다.
167
+ - `interface`는 declaration merging이나 외부 타입 확장처럼 확장 계약이 필요할 때만 사용한다.
168
+ - query key는 feature별 파일에서 관리하고, `GET`은 query, `POST/PUT/PATCH/DELETE`는 mutation으로 분리한다.
169
+ - URL로 표현 가능한 상태는 URL에 두고, 전역 store는 여러 컴포넌트가 함께 알아야 하는 UI 상태에만 사용한다.
170
+ - 날짜와 숫자 포맷팅은 공통 함수로 관리한다.
171
+ - 클릭 가능한 요소는 `button`, 이동은 `Link`를 사용하고, input은 `label`과 연결한다.
172
+
173
+ ## Create App
174
+
175
+ 배포 전에 템플릿 로컬 검증이 필요하면 이 저장소에서 바로 실행할 수 있다.
176
+
177
+ ```bash
178
+ node ./scripts/create-app.mjs my-app
179
+ ```
180
+
181
+ `commitlint`까지 같이 확인하고 싶으면:
182
+
183
+ ```bash
184
+ node ./scripts/create-app.mjs my-app --with-commitlint
185
+ ```
186
+
187
+ 스타일 없이 시작하는 버전으로 확인하고 싶으면:
188
+
189
+ ```bash
190
+ node ./scripts/create-app.mjs my-app --no-style
191
+ ```
192
+
193
+ 패키지를 npm에 배포한 뒤에는 아래처럼 사용할 수 있다.
194
+
195
+ ```bash
196
+ npm create react-0to1@latest my-app
197
+ ```
198
+
199
+ 또는:
200
+
201
+ ```bash
202
+ bun create react-0to1 my-app
203
+ ```
204
+
205
+ 기본 실행 시에는 `create-vite`의 원래 인터랙션이 그대로 열린다.
206
+
207
+ 이 템플릿은 React 계열 Vite 템플릿만 지원한다.
208
+ - 예: `react-ts`, `react-compiler-ts`
209
+ - `vue`, `svelte`, `solid` 같은 비-React 템플릿을 고르면 생성 결과를 정리한 뒤 안내 메시지와 함께 중단한다.
210
+
211
+ 인터랙션 없이 바로 만들고 싶으면:
212
+
213
+ ```bash
214
+ npm create react-0to1@latest my-app -- --template react-ts
215
+ ```
216
+
217
+ React Compiler 템플릿을 바로 쓰고 싶으면:
218
+
219
+ ```bash
220
+ npm create react-0to1@latest my-app -- --template react-compiler-ts
221
+ ```
222
+
223
+ 배포 후 `commitlint`까지 같이 넣고 싶으면:
224
+
225
+ ```bash
226
+ npm create react-0to1@latest my-app -- --with-commitlint
227
+ ```
228
+
229
+ 스타일 파일 없이 시작하고 싶으면:
230
+
231
+ ```bash
232
+ npm create react-0to1@latest my-app -- --no-style
233
+ ```
234
+
235
+ `--with-commitlint`를 켜면 `commit-msg` hook과 `commitlint` 설정이 추가되고, `feat: 내용` 같은 Conventional Commits 형식을 검사한다.
236
+ `--no-style`을 켜면 `@tailwindcss/vite`, `tailwindcss`, 전역 CSS import를 제외하고 기본 화면과 feature 스캐폴드도 스타일 없이 생성한다.
237
+
238
+ ## Release Flow
239
+
240
+ 첫 공개 릴리스 전에는 아래 순서를 권장한다.
241
+
242
+ 1. `npm run release:check`
243
+ 2. `npm publish`
244
+ 3. `npm view create-react-0to1 version`
245
+ 4. 배포 확인 후 README의 `npm create react-0to1@latest my-app` 예시를 사용한다.
246
+
247
+ 버전을 올려서 배포할 때는 필요 시 먼저 `package.json`과 `package-lock.json`의 버전을 함께 갱신한다.
248
+
249
+ 이 템플릿은 `TypeScript + React Router + ESLint + Vite` 전제를 가진다.
250
+ Husky는 기본 설치하며, `pre-commit`에서 `lint`만 실행한다.
@@ -0,0 +1,31 @@
1
+ # dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.*
5
+ .yarn/*
6
+ !.yarn/patches
7
+ !.yarn/plugins
8
+ !.yarn/releases
9
+ !.yarn/versions
10
+
11
+ # build
12
+ /dist
13
+ /coverage
14
+
15
+ # misc
16
+ .DS_Store
17
+
18
+ # debug
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+ .pnpm-debug.log*
23
+
24
+ # env files
25
+ .env*
26
+ !.env.example
27
+
28
+ # typescript
29
+ *.tsbuildinfo
30
+
31
+ .idea
@@ -0,0 +1,64 @@
1
+ import js from '@eslint/js';
2
+ import { defineConfig, globalIgnores } from 'eslint/config';
3
+ import prettier from 'eslint-config-prettier/flat';
4
+ import globals from 'globals';
5
+ import reactHooks from 'eslint-plugin-react-hooks';
6
+ import reactRefresh from 'eslint-plugin-react-refresh';
7
+ import simpleImportSort from 'eslint-plugin-simple-import-sort';
8
+ import unusedImports from 'eslint-plugin-unused-imports';
9
+ import tseslint from 'typescript-eslint';
10
+
11
+ export default defineConfig([
12
+ globalIgnores(['dist/**', 'coverage/**']),
13
+ {
14
+ files: ['**/*.{ts,tsx}'],
15
+ extends: [
16
+ js.configs.recommended,
17
+ ...tseslint.configs.recommended,
18
+ reactHooks.configs.flat.recommended,
19
+ reactRefresh.configs.vite,
20
+ ],
21
+ languageOptions: {
22
+ ecmaVersion: 2020,
23
+ globals: {
24
+ ...globals.browser,
25
+ ...globals.node,
26
+ },
27
+ },
28
+ plugins: {
29
+ 'simple-import-sort': simpleImportSort,
30
+ 'unused-imports': unusedImports,
31
+ },
32
+ rules: {
33
+ 'no-var': 'error',
34
+ 'prefer-const': 'error',
35
+ curly: ['error', 'all'],
36
+ eqeqeq: ['error', 'always'],
37
+ 'object-shorthand': ['error', 'always'],
38
+ 'no-unused-vars': 'off',
39
+ '@typescript-eslint/no-unused-vars': 'off',
40
+ 'unused-imports/no-unused-imports': 'error',
41
+ 'unused-imports/no-unused-vars': [
42
+ 'warn',
43
+ {
44
+ args: 'after-used',
45
+ argsIgnorePattern: '^_',
46
+ ignoreRestSiblings: true,
47
+ vars: 'all',
48
+ varsIgnorePattern: '^_',
49
+ },
50
+ ],
51
+ '@typescript-eslint/consistent-type-imports': [
52
+ 'error',
53
+ {
54
+ disallowTypeAnnotations: false,
55
+ fixStyle: 'inline-type-imports',
56
+ prefer: 'type-imports',
57
+ },
58
+ ],
59
+ 'simple-import-sort/imports': 'error',
60
+ 'simple-import-sort/exports': 'error',
61
+ },
62
+ },
63
+ prettier,
64
+ ]);
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint --fix .",
11
+ "format": "prettier --write .",
12
+ "format:check": "prettier --check .",
13
+ "pack:dry-run": "npm pack --dry-run",
14
+ "release:check": "npm run lint && npm run build && npm run pack:dry-run",
15
+ "preview": "vite preview",
16
+ "create:feature": "node ./scripts/create-feature.mjs",
17
+ "create:feature:crud": "node ./scripts/create-feature-crud.mjs"
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.error('이 파일은 generator 템플릿 placeholder입니다.');
4
+ console.error('실제 생성 결과물에서는 engine이 공통 create-feature-crud 스크립트로 대체합니다.');
5
+ process.exit(1);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.error('이 파일은 generator 템플릿 placeholder입니다.');
4
+ console.error('실제 생성 결과물에서는 engine이 공통 create-feature 스크립트로 대체합니다.');
5
+ process.exit(1);
@@ -0,0 +1,15 @@
1
+ import { RouterProvider } from 'react-router-dom';
2
+
3
+ import { AppErrorBoundary } from '@/app/error-boundary';
4
+ import { AppProviders } from '@/app/providers';
5
+ import { router } from '@/app/router';
6
+
7
+ export default function App() {
8
+ return (
9
+ <AppErrorBoundary>
10
+ <AppProviders>
11
+ <RouterProvider router={router} />
12
+ </AppProviders>
13
+ </AppErrorBoundary>
14
+ );
15
+ }
@@ -0,0 +1,59 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+
3
+ type AppErrorBoundaryProps = {
4
+ children: ReactNode;
5
+ };
6
+
7
+ type AppErrorBoundaryState = {
8
+ error: Error | null;
9
+ retryKey: number;
10
+ };
11
+
12
+ export class AppErrorBoundary extends Component<AppErrorBoundaryProps, AppErrorBoundaryState> {
13
+ state: AppErrorBoundaryState = {
14
+ error: null,
15
+ retryKey: 0,
16
+ };
17
+
18
+ static getDerivedStateFromError(error: Error): Partial<AppErrorBoundaryState> {
19
+ return {
20
+ error,
21
+ };
22
+ }
23
+
24
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
25
+ console.error(error, errorInfo);
26
+ }
27
+
28
+ handleRetry = () => {
29
+ this.setState((prevState) => ({
30
+ error: null,
31
+ retryKey: prevState.retryKey + 1,
32
+ }));
33
+ };
34
+
35
+ render() {
36
+ if (this.state.error) {
37
+ return (
38
+ <main className="mx-auto flex min-h-screen w-full max-w-xl items-center px-6 py-16">
39
+ <section className="w-full rounded-3xl border border-zinc-200 bg-white p-8 shadow-sm">
40
+ <p className="text-sm font-medium uppercase tracking-[0.2em] text-zinc-500">오류</p>
41
+ <h1 className="mt-3 text-3xl font-semibold text-zinc-950">문제가 발생했습니다.</h1>
42
+ <p className="mt-3 text-sm leading-6 text-zinc-600">
43
+ 요청을 처리하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
44
+ </p>
45
+ <button
46
+ type="button"
47
+ onClick={this.handleRetry}
48
+ className="mt-6 rounded-lg bg-zinc-950 px-4 py-2 text-sm font-medium text-white"
49
+ >
50
+ 다시 시도
51
+ </button>
52
+ </section>
53
+ </main>
54
+ );
55
+ }
56
+
57
+ return <div key={this.state.retryKey}>{this.props.children}</div>;
58
+ }
59
+ }
@@ -0,0 +1,32 @@
1
+ import { Outlet, useNavigation } from 'react-router-dom';
2
+
3
+ /**
4
+ * 앱 공통 레이아웃 예시입니다.
5
+ * 공통 헤더, 인증 리스너, 전역 모달 연결이 필요하면 여기서 추가해서 사용하세요.
6
+ */
7
+ export function AppFrame() {
8
+ const navigation = useNavigation();
9
+ const isRoutePending = navigation.state !== 'idle';
10
+
11
+ return (
12
+ <div className="min-h-screen bg-zinc-50 text-zinc-950">
13
+ {isRoutePending ? (
14
+ <>
15
+ <div className="fixed inset-x-0 top-0 z-50 h-1 overflow-hidden bg-zinc-200/80">
16
+ <div className="h-full w-1/3 animate-pulse rounded-full bg-zinc-950" />
17
+ </div>
18
+ <div className="pointer-events-none fixed inset-x-0 top-5 z-50 flex justify-center px-4">
19
+ <div
20
+ role="status"
21
+ aria-live="polite"
22
+ className="rounded-full border border-zinc-200 bg-white/95 px-3 py-1 text-xs font-medium tracking-[0.12em] text-zinc-600 shadow-sm backdrop-blur"
23
+ >
24
+ 페이지를 불러오는 중입니다
25
+ </div>
26
+ </div>
27
+ </>
28
+ ) : null}
29
+ <Outlet />
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,43 @@
1
+ @import 'tailwindcss';
2
+
3
+ :root {
4
+ --background: #fafaf9;
5
+ --foreground: #18181b;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans:
12
+ 'Pretendard Variable', 'Pretendard', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
13
+ --font-mono: 'SFMono-Regular', ui-monospace, monospace;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ html,
21
+ body,
22
+ #root {
23
+ min-height: 100%;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ background: var(--background);
29
+ color: var(--foreground);
30
+ font-family: var(--font-sans);
31
+ }
32
+
33
+ button,
34
+ input,
35
+ textarea,
36
+ select {
37
+ font: inherit;
38
+ }
39
+
40
+ a {
41
+ color: inherit;
42
+ text-decoration: none;
43
+ }
@@ -0,0 +1,23 @@
1
+ import { Link } from 'react-router-dom';
2
+
3
+ import { routePaths } from '@/shared/routes/route-paths';
4
+
5
+ export function NotFoundPage() {
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">404</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
+ </p>
14
+ <Link
15
+ to={routePaths.home}
16
+ className="mt-6 inline-flex rounded-lg bg-zinc-950 px-4 py-2 text-sm font-medium text-white"
17
+ >
18
+ 홈으로 이동
19
+ </Link>
20
+ </section>
21
+ </main>
22
+ );
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { QueryProviders } from '@/shared/query/providers';
4
+
5
+ type AppProvidersProps = {
6
+ children: ReactNode;
7
+ };
8
+
9
+ /**
10
+ * 앱 전역 provider 연결 지점입니다.
11
+ * 전역 query 에러 처리가 필요하면
12
+ * <QueryProviders onGlobalError={handleClientAppError}> 처럼 연결해서 사용하세요.
13
+ */
14
+ export function AppProviders({ children }: AppProvidersProps) {
15
+ return <QueryProviders>{children}</QueryProviders>;
16
+ }
@@ -0,0 +1,62 @@
1
+ import type { ComponentType } from 'react';
2
+ import { createBrowserRouter, type RouteObject } from 'react-router-dom';
3
+
4
+ import { AppFrame } from '@/app/frame';
5
+ import { NotFoundPage } from '@/app/not-found-page';
6
+
7
+ type PageModule = {
8
+ default: ComponentType;
9
+ };
10
+
11
+ const pageModules = import.meta.glob<PageModule>('../pages/**/page.tsx', {
12
+ eager: false,
13
+ });
14
+
15
+ function toRoutePath(filePath: string) {
16
+ const relativePath = filePath.replace('../pages/', '').replace('/page.tsx', '');
17
+ const segments = relativePath
18
+ .split('/')
19
+ .filter(Boolean)
20
+ .filter((segment) => segment !== 'index');
21
+
22
+ return segments.join('/');
23
+ }
24
+
25
+ function createPageRoutes(): RouteObject[] {
26
+ return Object.entries(pageModules).map(([filePath, loadPageModule]) => {
27
+ const routePath = toRoutePath(filePath);
28
+ const lazy = async () => {
29
+ const module = await loadPageModule();
30
+
31
+ return {
32
+ Component: module.default,
33
+ };
34
+ };
35
+
36
+ if (!routePath) {
37
+ return {
38
+ index: true,
39
+ lazy,
40
+ };
41
+ }
42
+
43
+ return {
44
+ path: routePath,
45
+ lazy,
46
+ };
47
+ });
48
+ }
49
+
50
+ export const router = createBrowserRouter([
51
+ {
52
+ path: '/',
53
+ element: <AppFrame />,
54
+ children: [
55
+ ...createPageRoutes(),
56
+ {
57
+ path: '*',
58
+ element: <NotFoundPage />,
59
+ },
60
+ ],
61
+ },
62
+ ]);
@@ -0,0 +1,12 @@
1
+ import '@/app/globals.css';
2
+
3
+ import { StrictMode } from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+
6
+ import App from '@/app/app';
7
+
8
+ createRoot(document.getElementById('root')!).render(
9
+ <StrictMode>
10
+ <App />
11
+ </StrictMode>,
12
+ );
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 템플릿 첫 화면 예시입니다.
3
+ * 프로젝트 소개나 대시보드 시작 화면으로 교체해서 사용하세요.
4
+ */
5
+ export default function HomePage() {
6
+ return (
7
+ <main className="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-8 px-6 py-16">
8
+ <section className="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-4xl font-semibold tracking-tight text-zinc-950">
11
+ 필요한 기본값과 필수 전역 처리를 갖춘 Vite React 프론트엔드 스타터
12
+ </h1>
13
+ <p className="mt-4 max-w-3xl text-base leading-7 text-zinc-600">
14
+ 이 템플릿은 feature-first 구조, React Router, TanStack Query, request helper, shared
15
+ utilities처럼 초기에 바로 필요한 것만 포함합니다.
16
+ </p>
17
+ </section>
18
+
19
+ <section className="rounded-3xl border border-dashed border-zinc-300 bg-zinc-50 p-8">
20
+ <h2 className="text-lg font-semibold text-zinc-950">기본 라우팅 흐름까지 바로 시작</h2>
21
+ <ul className="mt-3 grid gap-2 text-sm leading-6 text-zinc-600">
22
+ <li>
23
+ <code>src/pages/**/page.tsx</code> 파일은 자동으로 React Router 경로가 됩니다.
24
+ </li>
25
+ <li>
26
+ <code>react-router-dom</code>, Query, request helper를 기본으로 포함했습니다.
27
+ </li>
28
+ <li>
29
+ 폼/목록 예제가 필요하면 <code>npm run create:feature -- products</code>로 생성할 수
30
+ 있습니다.
31
+ </li>
32
+ </ul>
33
+ </section>
34
+ </main>
35
+ );
36
+ }