@farmzone/fz-template-react 0.0.1
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 +108 -0
- package/bin/create.js +102 -0
- package/package.json +24 -0
- package/template/.prettierrc +9 -0
- package/template/eslint.config.js +23 -0
- package/template/gitignore +32 -0
- package/template/index.css +19 -0
- package/template/index.html +19 -0
- package/template/package.json +41 -0
- package/template/src/app/App.tsx +24 -0
- package/template/src/app/layout/Layout.tsx +16 -0
- package/template/src/app/layout/Sidebar.tsx +53 -0
- package/template/src/app/layout/menu.ts +27 -0
- package/template/src/app/router/Router.tsx +28 -0
- package/template/src/index.tsx +12 -0
- package/template/src/pages/dashboard/index.tsx +8 -0
- package/template/src/pages/error/Error.tsx +17 -0
- package/template/src/pages/error/NotFound.tsx +17 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @farmzone/fz-template-react
|
|
2
|
+
|
|
3
|
+
Farmzone React 프로젝트 보일러플레이트 생성 CLI.
|
|
4
|
+
|
|
5
|
+
## 사용법
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @farmzone/fz-template-react <프로젝트명>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
예시:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @farmzone/fz-template-react my-app
|
|
15
|
+
cd my-app
|
|
16
|
+
npm install
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
프로젝트명을 생략하면 대화형으로 입력을 받습니다.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @farmzone/fz-template-react
|
|
24
|
+
# 프로젝트 이름을 입력하세요: my-app
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 생성되는 프로젝트 구조
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
my-app/
|
|
31
|
+
├── src/
|
|
32
|
+
│ ├── app/
|
|
33
|
+
│ │ ├── layout/
|
|
34
|
+
│ │ │ ├── Layout.tsx # 사이드바 + Outlet 레이아웃
|
|
35
|
+
│ │ │ ├── Sidebar.tsx # 사이드바 컴포넌트
|
|
36
|
+
│ │ │ └── menu.ts # 메뉴 설정
|
|
37
|
+
│ │ ├── router/
|
|
38
|
+
│ │ │ └── Router.tsx
|
|
39
|
+
│ │ └── App.tsx # QueryClientProvider 루트
|
|
40
|
+
│ ├── pages/
|
|
41
|
+
│ │ ├── dashboard/
|
|
42
|
+
│ │ │ └── index.tsx
|
|
43
|
+
│ │ └── error/
|
|
44
|
+
│ │ ├── Error.tsx
|
|
45
|
+
│ │ └── NotFound.tsx
|
|
46
|
+
│ └── index.tsx
|
|
47
|
+
├── .gitignore
|
|
48
|
+
├── index.html
|
|
49
|
+
├── index.css
|
|
50
|
+
├── vite.config.ts
|
|
51
|
+
├── tsconfig.json
|
|
52
|
+
└── package.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 포함된 기술 스택
|
|
56
|
+
|
|
57
|
+
| 항목 | 버전 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| React | ^19 |
|
|
60
|
+
| Vite | ^7 |
|
|
61
|
+
| TypeScript | ~5.9 |
|
|
62
|
+
| Tailwind CSS | v4 |
|
|
63
|
+
| React Router | ^7 |
|
|
64
|
+
| TanStack Query | ^5 |
|
|
65
|
+
| @farmzone/fz-react-ui | latest |
|
|
66
|
+
| react-hook-form + zod | 최신 |
|
|
67
|
+
|
|
68
|
+
## 메뉴 추가
|
|
69
|
+
|
|
70
|
+
`src/app/layout/menu.ts` 수정:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { LayoutDashboard, Users, ScrollText } from "lucide-react";
|
|
74
|
+
|
|
75
|
+
export const MENU_SECTIONS: MenuSection[] = [
|
|
76
|
+
{
|
|
77
|
+
items: [
|
|
78
|
+
{ icon: LayoutDashboard, label: "대시보드", path: "/" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: "관리",
|
|
83
|
+
items: [
|
|
84
|
+
{ icon: Users, label: "사용자 관리", path: "/user" },
|
|
85
|
+
{ icon: ScrollText, label: "로그", path: "/log" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 라우트 추가
|
|
92
|
+
|
|
93
|
+
`src/app/router/Router.tsx`의 `children` 배열에 추가:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
children: [
|
|
97
|
+
{ index: true, element: <DashboardPage /> },
|
|
98
|
+
{ path: "user", element: <UserListPage /> },
|
|
99
|
+
{ path: "user/:id", element: <UserDetailPage /> },
|
|
100
|
+
{ path: "log", element: <LogPage /> },
|
|
101
|
+
],
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## npm 배포
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm publish --access public
|
|
108
|
+
```
|
package/bin/create.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
copyFileSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
} from "fs";
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
function prompt(question) {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.trim());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function copyDir(src, dest) {
|
|
30
|
+
mkdirSync(dest, { recursive: true });
|
|
31
|
+
for (const entry of readdirSync(src)) {
|
|
32
|
+
const srcPath = join(src, entry);
|
|
33
|
+
const destPath = join(dest, entry);
|
|
34
|
+
if (statSync(srcPath).isDirectory()) {
|
|
35
|
+
copyDir(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
copyFileSync(srcPath, destPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function replaceInFile(filePath, search, replacement) {
|
|
43
|
+
if (!existsSync(filePath)) return;
|
|
44
|
+
const content = readFileSync(filePath, "utf-8");
|
|
45
|
+
writeFileSync(filePath, content.replaceAll(search, replacement));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
console.log("\n🌱 Farmzone React Template\n");
|
|
50
|
+
|
|
51
|
+
let projectName = process.argv[2];
|
|
52
|
+
|
|
53
|
+
if (!projectName) {
|
|
54
|
+
projectName = await prompt("프로젝트 이름을 입력하세요: ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!projectName) {
|
|
58
|
+
console.error("❌ 프로젝트 이름을 입력해야 합니다.");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(projectName)) {
|
|
63
|
+
console.error("❌ 프로젝트 이름은 소문자, 숫자, 하이픈(-)만 사용 가능하며 하이픈으로 시작하거나 끝날 수 없습니다.");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const targetDir = join(process.cwd(), projectName);
|
|
68
|
+
|
|
69
|
+
if (existsSync(targetDir)) {
|
|
70
|
+
console.error(`❌ '${projectName}' 디렉터리가 이미 존재합니다.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const templateDir = join(__dirname, "..", "template");
|
|
75
|
+
|
|
76
|
+
console.log(`\n🚀 '${projectName}' 프로젝트를 생성합니다...\n`);
|
|
77
|
+
|
|
78
|
+
// 템플릿 복사
|
|
79
|
+
copyDir(templateDir, targetDir);
|
|
80
|
+
|
|
81
|
+
// gitignore -> .gitignore 이름 변경 (npm publish 시 .gitignore는 자동으로 제외됨)
|
|
82
|
+
const gitignoreSrc = join(targetDir, "gitignore");
|
|
83
|
+
const gitignoreDest = join(targetDir, ".gitignore");
|
|
84
|
+
if (existsSync(gitignoreSrc)) renameSync(gitignoreSrc, gitignoreDest);
|
|
85
|
+
|
|
86
|
+
// __PROJECT_NAME__ 치환
|
|
87
|
+
replaceInFile(join(targetDir, "package.json"), "__PROJECT_NAME__", projectName);
|
|
88
|
+
replaceInFile(join(targetDir, "index.html"), "__PROJECT_NAME__", projectName);
|
|
89
|
+
|
|
90
|
+
console.log("✅ 프로젝트 생성 완료!\n");
|
|
91
|
+
console.log("──────────────────────────────");
|
|
92
|
+
console.log("다음 명령어로 시작하세요:\n");
|
|
93
|
+
console.log(` cd ${projectName}`);
|
|
94
|
+
console.log(` npm install`);
|
|
95
|
+
console.log(` npm run dev`);
|
|
96
|
+
console.log("──────────────────────────────\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((err) => {
|
|
100
|
+
console.error(err);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farmzone/fz-template-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Farmzone React 프로젝트 보일러플레이트 생성 CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fz-template-react": "./bin/create.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"farmzone",
|
|
15
|
+
"react",
|
|
16
|
+
"template",
|
|
17
|
+
"boilerplate",
|
|
18
|
+
"vite"
|
|
19
|
+
],
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(["dist"]),
|
|
10
|
+
{
|
|
11
|
+
files: ["**/*.{ts,tsx}"],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 로그
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
|
|
9
|
+
# 의존성
|
|
10
|
+
node_modules/
|
|
11
|
+
.pnp
|
|
12
|
+
.pnp.js
|
|
13
|
+
|
|
14
|
+
# 빌드 결과물
|
|
15
|
+
dist/
|
|
16
|
+
dist-ssr/
|
|
17
|
+
|
|
18
|
+
# 환경 변수
|
|
19
|
+
.env
|
|
20
|
+
.env.local
|
|
21
|
+
.env.*.local
|
|
22
|
+
|
|
23
|
+
# 에디터
|
|
24
|
+
.vscode/*
|
|
25
|
+
!.vscode/extensions.json
|
|
26
|
+
.idea
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.suo
|
|
29
|
+
*.ntvs*
|
|
30
|
+
*.njsproj
|
|
31
|
+
*.sln
|
|
32
|
+
*.sw?
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "@farmzone/fz-react-ui/styles";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@tailwind utilities;
|
|
8
|
+
|
|
9
|
+
@theme inline {
|
|
10
|
+
--color-main: #3b82f6;
|
|
11
|
+
--color-sub-lightgray: #f3f4f6;
|
|
12
|
+
--color-sub-darkgray: #6b7280;
|
|
13
|
+
--color-sub-darkgray-2: #374151;
|
|
14
|
+
--color-basic-gray: #9ca3af;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
|
6
|
+
<link
|
|
7
|
+
rel="stylesheet"
|
|
8
|
+
as="style"
|
|
9
|
+
crossorigin
|
|
10
|
+
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css"
|
|
11
|
+
/>
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
|
+
<title>__PROJECT_NAME__</title>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --host 0.0.0.0 --port 3000",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@farmzone/fz-react-ui": "latest",
|
|
14
|
+
"@hookform/resolvers": "^5.2.2",
|
|
15
|
+
"@tanstack/react-query": "^5.90.0",
|
|
16
|
+
"axios": "^1.13.0",
|
|
17
|
+
"lucide-react": "^1.16.0",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0",
|
|
20
|
+
"react-hook-form": "^7.68.0",
|
|
21
|
+
"react-router": "^7.11.0",
|
|
22
|
+
"zod": "^3.25.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "^9.0.0",
|
|
26
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@types/react-dom": "^19.0.0",
|
|
30
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
31
|
+
"eslint": "^9.0.0",
|
|
32
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
33
|
+
"eslint-plugin-react-refresh": "^0.4.0",
|
|
34
|
+
"globals": "^16.0.0",
|
|
35
|
+
"tailwindcss": "^4.0.0",
|
|
36
|
+
"tw-animate-css": "^1.0.0",
|
|
37
|
+
"typescript": "~5.9.0",
|
|
38
|
+
"typescript-eslint": "^8.0.0",
|
|
39
|
+
"vite": "^7.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import Router from "@/app/router/Router";
|
|
4
|
+
|
|
5
|
+
const queryClient = new QueryClient({
|
|
6
|
+
defaultOptions: {
|
|
7
|
+
queries: {
|
|
8
|
+
staleTime: 0,
|
|
9
|
+
gcTime: 5 * 60 * 1000,
|
|
10
|
+
refetchOnMount: true,
|
|
11
|
+
refetchOnReconnect: false,
|
|
12
|
+
refetchOnWindowFocus: false,
|
|
13
|
+
retry: 1,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
return (
|
|
20
|
+
<QueryClientProvider client={queryClient}>
|
|
21
|
+
<Router />
|
|
22
|
+
</QueryClientProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Outlet } from "react-router";
|
|
2
|
+
import { ToastProvider } from "@farmzone/fz-react-ui";
|
|
3
|
+
|
|
4
|
+
import Sidebar from "./Sidebar";
|
|
5
|
+
|
|
6
|
+
export default function Layout() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex h-screen bg-gray-50">
|
|
9
|
+
<ToastProvider />
|
|
10
|
+
<Sidebar />
|
|
11
|
+
<main className="flex-1 overflow-y-auto">
|
|
12
|
+
<Outlet />
|
|
13
|
+
</main>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NavLink, useNavigate } from "react-router";
|
|
2
|
+
import { cn } from "@farmzone/fz-react-ui";
|
|
3
|
+
|
|
4
|
+
import { MENU_SECTIONS } from "./menu";
|
|
5
|
+
|
|
6
|
+
export default function Sidebar() {
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<aside className="flex h-screen w-56 shrink-0 flex-col border-r border-gray-200 bg-white">
|
|
11
|
+
{/* 로고 영역 */}
|
|
12
|
+
<div className="flex h-14 items-center border-b border-gray-200 px-5">
|
|
13
|
+
<button
|
|
14
|
+
onClick={() => navigate("/")}
|
|
15
|
+
className="text-base font-bold text-gray-900 transition-colors hover:text-blue-600"
|
|
16
|
+
>
|
|
17
|
+
PROJECT
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{/* 메뉴 영역 */}
|
|
22
|
+
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
|
23
|
+
{MENU_SECTIONS.map((section, i) => (
|
|
24
|
+
<div key={i} className="mb-4">
|
|
25
|
+
{section.title && (
|
|
26
|
+
<p className="mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
|
27
|
+
{section.title}
|
|
28
|
+
</p>
|
|
29
|
+
)}
|
|
30
|
+
{section.items.map((item) => (
|
|
31
|
+
<NavLink
|
|
32
|
+
key={item.path}
|
|
33
|
+
to={item.path}
|
|
34
|
+
end={item.path === "/"}
|
|
35
|
+
className={({ isActive }) =>
|
|
36
|
+
cn(
|
|
37
|
+
"mb-0.5 flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
38
|
+
isActive
|
|
39
|
+
? "bg-blue-50 text-blue-600"
|
|
40
|
+
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<item.icon size={18} strokeWidth={1.8} />
|
|
45
|
+
{item.label}
|
|
46
|
+
</NavLink>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</nav>
|
|
51
|
+
</aside>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LayoutDashboard } from "lucide-react";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export interface MenuItem {
|
|
5
|
+
icon: LucideIcon;
|
|
6
|
+
label: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MenuSection {
|
|
11
|
+
title?: string;
|
|
12
|
+
items: MenuItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MENU_SECTIONS: MenuSection[] = [
|
|
16
|
+
{
|
|
17
|
+
items: [{ icon: LayoutDashboard, label: "대시보드", path: "/" }],
|
|
18
|
+
},
|
|
19
|
+
// 메뉴 섹션 추가 예시:
|
|
20
|
+
// {
|
|
21
|
+
// title: "관리",
|
|
22
|
+
// items: [
|
|
23
|
+
// { icon: Users, label: "사용자 관리", path: "/user" },
|
|
24
|
+
// { icon: ScrollText, label: "로그", path: "/log" },
|
|
25
|
+
// ],
|
|
26
|
+
// },
|
|
27
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { createBrowserRouter, RouterProvider } from "react-router";
|
|
3
|
+
|
|
4
|
+
import Layout from "@/app/layout/Layout";
|
|
5
|
+
import DashboardPage from "@/pages/dashboard";
|
|
6
|
+
import ErrorPage from "@/pages/error/Error";
|
|
7
|
+
import NotFoundPage from "@/pages/error/NotFound";
|
|
8
|
+
|
|
9
|
+
export default function Router() {
|
|
10
|
+
const router = useMemo(
|
|
11
|
+
() =>
|
|
12
|
+
createBrowserRouter([
|
|
13
|
+
{
|
|
14
|
+
path: "/",
|
|
15
|
+
element: <Layout />,
|
|
16
|
+
errorElement: <ErrorPage />,
|
|
17
|
+
children: [
|
|
18
|
+
{ index: true, element: <DashboardPage /> },
|
|
19
|
+
// 여기에 라우트를 추가하세요
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{ path: "*", element: <NotFoundPage /> },
|
|
23
|
+
]),
|
|
24
|
+
[],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return <RouterProvider router={router} />;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useRouteError, useNavigate } from "react-router";
|
|
2
|
+
import { Button } from "@farmzone/fz-react-ui";
|
|
3
|
+
|
|
4
|
+
export default function ErrorPage() {
|
|
5
|
+
const error = useRouteError() as { statusText?: string; message?: string };
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex h-screen flex-col items-center justify-center gap-4 bg-gray-50">
|
|
10
|
+
<h1 className="text-4xl font-bold text-gray-900">오류가 발생했습니다</h1>
|
|
11
|
+
<p className="text-gray-500">{error?.statusText ?? error?.message ?? "알 수 없는 오류"}</p>
|
|
12
|
+
<Button variant="outline" onClick={() => navigate("/")}>
|
|
13
|
+
홈으로 돌아가기
|
|
14
|
+
</Button>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useNavigate } from "react-router";
|
|
2
|
+
import { Button } from "@farmzone/fz-react-ui";
|
|
3
|
+
|
|
4
|
+
export default function NotFoundPage() {
|
|
5
|
+
const navigate = useNavigate();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex h-screen flex-col items-center justify-center gap-3 bg-gray-50">
|
|
9
|
+
<p className="text-8xl font-bold text-gray-200">404</p>
|
|
10
|
+
<h1 className="text-xl font-semibold text-gray-700">페이지를 찾을 수 없습니다</h1>
|
|
11
|
+
<p className="text-sm text-gray-400">요청하신 페이지가 존재하지 않거나 이동되었습니다.</p>
|
|
12
|
+
<Button className="mt-2" onClick={() => navigate("/")}>
|
|
13
|
+
홈으로 돌아가기
|
|
14
|
+
</Button>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true,
|
|
26
|
+
"baseUrl": ".",
|
|
27
|
+
"paths": {
|
|
28
|
+
"@/*": ["src/*"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": ["src"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@": path.resolve(__dirname, "src"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|