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:
2026-04-08 10:43:41 +09:00
committed by jinseok
parent d3efb9e2d2
commit d1fdae63ac
18 changed files with 4085 additions and 2 deletions

16
web-app/.editorconfig Normal file
View 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
View 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
View 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"]

View File

@@ -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
View 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
View 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
View 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;

View File

@@ -0,0 +1,3 @@
export function clientLoader() {
throw new Response("Not Found", { status: 404 });
}

View File

@@ -0,0 +1,7 @@
export default function Home() {
return (
<div>
Home
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Users() {
return (
<div className="px-3">
</div>
);
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
web-app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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
View 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
View 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,
},
});