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.
- package/README.md +68 -0
- package/internal/engine/create-feature-crud-script.ts +134 -0
- package/internal/engine/create-feature-crud.template.mjs +601 -0
- package/internal/engine/create-feature-script.ts +142 -0
- package/internal/engine/generator-engine.ts +546 -0
- package/internal/engine/standalone-feature-preset.ts +34 -0
- package/internal/meta/preset-plan.ts +41 -0
- package/internal/meta/runtime-copy-plan.ts +220 -0
- package/internal/meta/runtime-layout.ts +262 -0
- package/internal/meta/scaffold-manifest.ts +169 -0
- package/internal/meta/standalone-dependency-manifest.ts +75 -0
- package/package.json +45 -0
- package/scripts/create-app.mjs +1612 -0
- package/source/core/auth/auth-events.ts +13 -0
- package/source/core/error/app-error.ts +85 -0
- package/source/core/error/handle-app-error.client.ts +35 -0
- package/source/core/lib/dayjs.ts +25 -0
- package/source/core/query/query-client.ts +126 -0
- package/source/core/request/request-core.ts +210 -0
- package/source/core/routes/route-paths.ts +4 -0
- package/source/core/ui/button.tsx +24 -0
- package/source/core/ui/modal-store.ts +32 -0
- package/source/core/ui/text-input-field.tsx +36 -0
- package/source/core/utils/build-query-string.ts +30 -0
- package/source/core/utils/format/date.ts +41 -0
- package/source/core/utils/format/index.ts +3 -0
- package/source/core/utils/format/number.ts +13 -0
- package/source/core/utils/format/text.ts +15 -0
- package/source/core/utils/schema-utils.ts +27 -0
- package/source/wrappers/monorepo/core/internal.ts +21 -0
- package/source/wrappers/monorepo/core/src/index.ts +4 -0
- package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
- package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
- package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
- package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
- package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
- package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
- package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
- package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
- package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
- package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
- package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
- package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
- package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
- package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
- package/source/wrappers/monorepo/request/core-request.ts +93 -0
- package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
- package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
- package/source/wrappers/next/query/hydration.tsx +20 -0
- package/source/wrappers/next/query/providers.tsx +35 -0
- package/source/wrappers/next/request/request.client.ts +24 -0
- package/source/wrappers/next/request/request.server.ts +64 -0
- package/source/wrappers/next/request/request.ts +52 -0
- package/source/wrappers/next/ui/global-modal.tsx +29 -0
- package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
- package/source/wrappers/react/query/providers.tsx +31 -0
- package/source/wrappers/react/request/request.client.ts +24 -0
- package/source/wrappers/react/request/request.ts +51 -0
- package/source/wrappers/react/ui/global-modal.tsx +27 -0
- package/templates/monorepo/.dockerignore +38 -0
- package/templates/monorepo/README.md +292 -0
- package/templates/monorepo/_gitignore +38 -0
- package/templates/monorepo/_npmrc +1 -0
- package/templates/monorepo/apps/project/Dockerfile +32 -0
- package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
- package/templates/monorepo/apps/project/index.html +14 -0
- package/templates/monorepo/apps/project/index.ts +15 -0
- package/templates/monorepo/apps/project/package.json +21 -0
- package/templates/monorepo/apps/project/tsconfig.json +9 -0
- package/templates/monorepo/apps/project/vite.config.ts +6 -0
- package/templates/monorepo/apps/web/Dockerfile +43 -0
- package/templates/monorepo/apps/web/README.md +111 -0
- package/templates/monorepo/apps/web/_gitignore +36 -0
- package/templates/monorepo/apps/web/app/favicon.ico +0 -0
- package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
- package/templates/monorepo/apps/web/app/globals.css +0 -0
- package/templates/monorepo/apps/web/app/layout.tsx +28 -0
- package/templates/monorepo/apps/web/app/page.tsx +7 -0
- package/templates/monorepo/apps/web/app/providers.tsx +25 -0
- package/templates/monorepo/apps/web/eslint.config.js +4 -0
- package/templates/monorepo/apps/web/next-env.d.ts +6 -0
- package/templates/monorepo/apps/web/next.config.js +4 -0
- package/templates/monorepo/apps/web/package.json +31 -0
- package/templates/monorepo/apps/web/public/file-text.svg +3 -0
- package/templates/monorepo/apps/web/public/globe.svg +10 -0
- package/templates/monorepo/apps/web/public/next.svg +1 -0
- package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
- package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
- package/templates/monorepo/apps/web/public/vercel.svg +10 -0
- package/templates/monorepo/apps/web/public/window.svg +3 -0
- package/templates/monorepo/apps/web/tsconfig.json +20 -0
- package/templates/monorepo/package.json +24 -0
- package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/core/package.json +32 -0
- package/templates/monorepo/packages/core/tsconfig.json +8 -0
- package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
- package/templates/monorepo/packages/core-next/package.json +43 -0
- package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
- package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/core-react/package.json +34 -0
- package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
- package/templates/monorepo/packages/eslint-config/README.md +3 -0
- package/templates/monorepo/packages/eslint-config/base.js +57 -0
- package/templates/monorepo/packages/eslint-config/next.js +22 -0
- package/templates/monorepo/packages/eslint-config/package.json +25 -0
- package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
- package/templates/monorepo/packages/typescript-config/base.json +19 -0
- package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
- package/templates/monorepo/packages/typescript-config/package.json +9 -0
- package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
- package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/ui/package.json +26 -0
- package/templates/monorepo/packages/ui/src/button.tsx +20 -0
- package/templates/monorepo/packages/ui/src/card.tsx +27 -0
- package/templates/monorepo/packages/ui/src/code.tsx +11 -0
- package/templates/monorepo/packages/ui/tsconfig.json +8 -0
- package/templates/monorepo/pnpm-workspace.yaml +9 -0
- package/templates/monorepo/turbo/generators/config.js +1336 -0
- package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
- package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
- package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
- package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
- package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
- package/templates/monorepo/turbo.json +28 -0
- package/templates/next/.env.example +2 -0
- package/templates/next/.prettierignore +9 -0
- package/templates/next/.prettierrc.json +9 -0
- package/templates/next/README.md +246 -0
- package/templates/next/_gitignore +44 -0
- package/templates/next/eslint.config.mjs +51 -0
- package/templates/next/next.config.ts +7 -0
- package/templates/next/package.json +24 -0
- package/templates/next/postcss.config.mjs +7 -0
- package/templates/next/scripts/create-feature-crud.mjs +5 -0
- package/templates/next/scripts/create-feature.mjs +5 -0
- package/templates/next/src/app/error.tsx +33 -0
- package/templates/next/src/app/globals.css +35 -0
- package/templates/next/src/app/layout.tsx +39 -0
- package/templates/next/src/app/login/page.tsx +17 -0
- package/templates/next/src/app/page.tsx +32 -0
- package/templates/next/src/app/providers.tsx +20 -0
- package/templates/next/tsconfig.json +34 -0
- package/templates/react/.env.example +1 -0
- package/templates/react/.prettierignore +10 -0
- package/templates/react/.prettierrc.json +9 -0
- package/templates/react/README.md +250 -0
- package/templates/react/_gitignore +31 -0
- package/templates/react/eslint.config.mjs +64 -0
- package/templates/react/package.json +19 -0
- package/templates/react/scripts/create-feature-crud.mjs +5 -0
- package/templates/react/scripts/create-feature.mjs +5 -0
- package/templates/react/src/app/app.tsx +15 -0
- package/templates/react/src/app/error-boundary.tsx +59 -0
- package/templates/react/src/app/frame.tsx +32 -0
- package/templates/react/src/app/globals.css +43 -0
- package/templates/react/src/app/not-found-page.tsx +23 -0
- package/templates/react/src/app/providers.tsx +16 -0
- package/templates/react/src/app/router.tsx +62 -0
- package/templates/react/src/main.tsx +12 -0
- package/templates/react/src/pages/index/page.tsx +36 -0
- package/templates/react/src/pages/login/page.tsx +18 -0
- package/templates/react/tsconfig.app.json +30 -0
- package/templates/react/tsconfig.json +4 -0
- package/templates/react/tsconfig.node.json +24 -0
- package/templates/react/vite.config.ts +14 -0
- 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
- 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
- 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
- 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
- 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
- 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
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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,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,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
|
+
}
|