feat: init (#1)
## webpack vs vite vite는 최근 webpack 대신 많이 채택되고 있는 빌드 도구. 내부는 rollup이나 dev에선 esbuild, native esm을 사용하여 속도가 빠름. 반면 webpack은 관련된 모든 파일을 번들링 해야하기 때문에 개발에서 빌드 속도 차이가 수 초 이상 발생하게됨 ## react-router vs tanstack router react router는 리액트 초창기부터 사용되어져왔고, tanstack router는 비교적 최근에 생겨났는데, 타입 안정성에 신경을 쓰다보니 라우트를 위해 신경써야할 장치들이 있고, export 해야할 데이터가 달라 처음 사용하는 사람은 혼란이 있을 수 있음. 또한 그에 따른 러닝커브가 존재하여 react-router를 선택 Reviewed-on: #1 Co-authored-by: Jinseok (심진석) <jinseok.sim@tf.dabeeo.com> Co-committed-by: Jinseok (심진석) <jinseok.sim@tf.dabeeo.com>
This commit was merged in pull request #1.
This commit is contained in:
16
web-app/.editorconfig
Normal file
16
web-app/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,jsx,ts,tsx}]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
49
web-app/.gitignore
vendored
Normal file
49
web-app/.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build outputs
|
||||
build/
|
||||
dist/
|
||||
.output/
|
||||
|
||||
# React Router
|
||||
.react-router/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
26
web-app/Dockerfile
Normal file
26
web-app/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM node:24-alpine AS development-dependencies-env
|
||||
RUN npm install -g corepack && corepack enable
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM node:24-alpine AS production-dependencies-env
|
||||
RUN npm install -g corepack && corepack enable
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
WORKDIR /app
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
FROM node:24-alpine AS build-env
|
||||
RUN npm install -g corepack && corepack enable
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN pnpm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
RUN npm install -g corepack && corepack enable
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["pnpm", "run", "start"]
|
||||
@@ -1,3 +1,102 @@
|
||||
### 다비오 변화 탐지 시스템
|
||||
# 다비오 변화 탐지 시스템 - Web Application
|
||||
|
||||
## API application
|
||||
React Router 7 기반 SPA 웹 애플리케이션
|
||||
|
||||
## 기술 스택
|
||||
|
||||
| 분류 | 기술 |
|
||||
|------|------|
|
||||
| Framework | React 19 + React Router 7 (SPA) |
|
||||
| Language | TypeScript 5.9 (strict) |
|
||||
| Styling | Tailwind CSS 4.2 |
|
||||
| Build Tool | Vite 8 |
|
||||
| Package Manager | pnpm (corepack) |
|
||||
| Code Quality | ESLint 10 + Prettier |
|
||||
| Container | Docker + Docker Compose |
|
||||
|
||||
## 사전 요구사항
|
||||
|
||||
- **Node.js** 24+
|
||||
- **corepack** 활성화 (`corepack enable` 실행 시 pnpm 자동 설치)
|
||||
- **Docker & Docker Compose** (선택, 컨테이너 환경 사용 시)
|
||||
|
||||
## 시작하기
|
||||
|
||||
### 로컬 개발
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
개발 서버가 `http://localhost:5173`에서 실행됩니다.
|
||||
|
||||
### Docker 개발
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
소스 코드가 볼륨 마운트되어 HMR이 동작합니다. `http://localhost:5173`에서 접근 가능합니다.
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
```
|
||||
web-app/
|
||||
├── app/
|
||||
│ ├── root.tsx # Root layout, ErrorBoundary
|
||||
│ ├── app.css # Tailwind 글로벌 스타일
|
||||
│ ├── routes.ts # 라우트 정의
|
||||
│ ├── routes/
|
||||
│ │ ├── home.tsx # 메인 페이지 (/)
|
||||
│ │ ├── users.tsx # 유저 목록 (/users)
|
||||
│ │ └── catch-all.tsx # 404 처리
|
||||
│ └── welcome/ # Welcome 컴포넌트
|
||||
├── public/ # 정적 파일
|
||||
├── Dockerfile # 프로덕션 빌드 (multi-stage)
|
||||
├── docker-compose.yml # 개발 환경
|
||||
├── vite.config.ts # Vite 설정
|
||||
├── react-router.config.ts # React Router 설정
|
||||
├── tsconfig.json # TypeScript 설정
|
||||
└── eslint.config.js # ESLint 설정
|
||||
```
|
||||
|
||||
> **경로 별칭**: `~/`는 `./app/`을 가리킵니다. (`tsconfig.json`에서 설정)
|
||||
|
||||
## 스크립트
|
||||
|
||||
| 명령어 | 설명 |
|
||||
|--------|------|
|
||||
| `pnpm dev` | 개발 서버 실행 (Vite HMR) |
|
||||
| `pnpm build` | 프로덕션 빌드 |
|
||||
| `pnpm start` | 프로덕션 서버 실행 |
|
||||
| `pnpm typecheck` | TypeScript 타입 검사 |
|
||||
| `pnpm lint` | ESLint 검사 |
|
||||
| `pnpm lint:fix` | ESLint 자동 수정 |
|
||||
|
||||
## Docker
|
||||
|
||||
### 개발 환경 (Docker Compose)
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- 소스 코드를 컨테이너에 볼륨 마운트하여 실시간 반영
|
||||
- `node_modules`는 별도 named volume으로 관리
|
||||
- 포트: `5173`
|
||||
|
||||
### 프로덕션 빌드 (Dockerfile)
|
||||
|
||||
```bash
|
||||
docker build -t dabeeo-web .
|
||||
docker run -p 3000:3000 dabeeo-web
|
||||
```
|
||||
|
||||
Multi-stage 빌드로 최적화된 이미지를 생성합니다:
|
||||
|
||||
1. **development-dependencies-env** - 전체 의존성 설치 (빌드 도구 포함)
|
||||
2. **production-dependencies-env** - 프로덕션 의존성만 설치
|
||||
3. **build-env** - 애플리케이션 빌드
|
||||
4. **final** - 프로덕션 의존성 + 빌드 결과물만 포함하여 실행
|
||||
|
||||
12
web-app/app/app.css
Normal file
12
web-app/app/app.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
@import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Pretendard Variable", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
|
||||
}
|
||||
65
web-app/app/root.tsx
Normal file
65
web-app/app/root.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Route } from './+types/root';
|
||||
|
||||
import './app.css';
|
||||
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from 'react-router';
|
||||
|
||||
export const links: Route.LinksFunction = () => [];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = 'Oops!';
|
||||
let details = 'An unexpected error occurred.';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? '404' : 'Error';
|
||||
details
|
||||
= error.status === 404
|
||||
? 'The requested page could not be found.'
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
9
web-app/app/routes.ts
Normal file
9
web-app/app/routes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RouteConfig } from '@react-router/dev/routes';
|
||||
|
||||
import { index, route } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('routes/home.tsx'),
|
||||
route('users', './routes/users.tsx'),
|
||||
route('*', './routes/catch-all.tsx'),
|
||||
] satisfies RouteConfig;
|
||||
3
web-app/app/routes/catch-all.tsx
Normal file
3
web-app/app/routes/catch-all.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function clientLoader() {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
7
web-app/app/routes/home.tsx
Normal file
7
web-app/app/routes/home.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
Home
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
web-app/app/routes/users.tsx
Normal file
7
web-app/app/routes/users.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Users() {
|
||||
return (
|
||||
<div className="px-3">
|
||||
유저목록
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
web-app/docker-compose.yml
Normal file
15
web-app/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
web:
|
||||
image: node:24-alpine
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
- node_modules:/app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
command: sh -c "npm install -g corepack && corepack enable && pnpm install && pnpm run dev --host"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
21
web-app/eslint.config.js
Normal file
21
web-app/eslint.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['node_modules/', 'dist/', 'build/', '.react-router/'] },
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
stylistic.configs.customize({
|
||||
indent: 2,
|
||||
semi: true,
|
||||
jsx: true,
|
||||
braceStyle: '1tbs',
|
||||
commaDangle: 'always-multiline',
|
||||
quoteProps: 'as-needed',
|
||||
arrowParens: true,
|
||||
}),
|
||||
reactHooks.configs.flat.recommended,
|
||||
);
|
||||
37
web-app/package.json
Normal file
37
web-app/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "dabeeo-detection",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-router/node": "7.14.0",
|
||||
"@react-router/serve": "7.14.0",
|
||||
"isbot": "^5.1.37",
|
||||
"pretendard": "^1.3.9",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "7.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@react-router/dev": "7.14.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
3674
web-app/pnpm-lock.yaml
generated
Normal file
3674
web-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
web-app/public/favicon.ico
Normal file
BIN
web-app/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
web-app/react-router.config.ts
Normal file
7
web-app/react-router.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: false,
|
||||
} satisfies Config;
|
||||
26
web-app/tsconfig.json
Normal file
26
web-app/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
10
web-app/vite.config.ts
Normal file
10
web-app/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), reactRouter()],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user