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,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,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에 둡니다.
|