first commit
This commit is contained in:
112
.gitignore
vendored
Normal file
112
.gitignore
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
############################################
|
||||
# Logs
|
||||
############################################
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
############################################
|
||||
# Build output
|
||||
############################################
|
||||
/dist
|
||||
/dist-ssr
|
||||
*.local
|
||||
|
||||
############################################
|
||||
# Node modules
|
||||
############################################
|
||||
/node_modules/
|
||||
|
||||
############################################
|
||||
# Environment files
|
||||
############################################
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.backup
|
||||
|
||||
############################################
|
||||
# IDE & Editor settings
|
||||
############################################
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
.cursor/
|
||||
.fleet/
|
||||
.zed/
|
||||
|
||||
############################################
|
||||
# OS & 임시 파일
|
||||
############################################
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
desktop.ini
|
||||
*.swp
|
||||
*.swo
|
||||
*.tmp
|
||||
*.bak
|
||||
*.old
|
||||
*.orig
|
||||
|
||||
############################################
|
||||
# Test / Coverage / Cache
|
||||
############################################
|
||||
*.cache
|
||||
*.coverage
|
||||
*.out
|
||||
*.pid
|
||||
*.seed
|
||||
*.seed.php
|
||||
|
||||
############################################
|
||||
# 업로드 / 백업 / 데이터
|
||||
############################################
|
||||
/backup/
|
||||
/backups/
|
||||
**/data/*
|
||||
!**/data/
|
||||
!**/data/.gitkeep
|
||||
|
||||
############################################
|
||||
# 맵 파일 (소스맵)
|
||||
############################################
|
||||
*.map
|
||||
|
||||
############################################
|
||||
# 문서/이미지/미디어 파일 제외 (업로드 방지)
|
||||
############################################
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.png
|
||||
*.gif
|
||||
*.bmp
|
||||
*.svg
|
||||
*.webp
|
||||
*.ico
|
||||
|
||||
*.pdf
|
||||
*.doc
|
||||
*.docx
|
||||
*.xls
|
||||
*.xlsx
|
||||
*.ppt
|
||||
*.pptx
|
||||
*.hwp
|
||||
|
||||
*.mp3
|
||||
*.wav
|
||||
*.ogg
|
||||
*.mp4
|
||||
*.avi
|
||||
*.mov
|
||||
*.wmv
|
||||
*.mkv
|
||||
474
CURRENT_WORKS.md
Normal file
474
CURRENT_WORKS.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# React 프론트엔드 작업 현황
|
||||
|
||||
## 2025-10-13 (일) - React 프론트엔드 프로젝트 초기 셋팅 완료
|
||||
|
||||
### ✅ 완료된 작업
|
||||
|
||||
#### 1. 프로젝트 초기화 및 의존성 설치
|
||||
- Vite + React 19 + TypeScript 5 프로젝트 생성
|
||||
- 모든 필수 의존성 설치 완료 (242개 패키지, 0 vulnerabilities)
|
||||
|
||||
**설치된 주요 패키지:**
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 프로젝트 구조 생성
|
||||
완성된 디렉토리 구조:
|
||||
```
|
||||
react/
|
||||
├── src/
|
||||
│ ├── components/ # 재사용 가능한 UI 컴포넌트
|
||||
│ ├── pages/ # 라우트별 페이지 컴포넌트
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ ├── services/ # API 서비스 레이어
|
||||
│ │ └── api.ts # Auth, Tenant API + CRUD 헬퍼
|
||||
│ ├── stores/ # Zustand 스토어
|
||||
│ │ └── auth.ts # 인증 스토어 (persist 포함)
|
||||
│ ├── utils/ # 유틸리티 함수
|
||||
│ ├── types/ # TypeScript 타입 정의
|
||||
│ │ └── api.ts # API 응답 타입
|
||||
│ ├── lib/ # 라이브러리 설정
|
||||
│ │ ├── utils.ts # cn(), formatDate(), getEnv()
|
||||
│ │ ├── axios.ts # Axios 인스턴스 + 인터셉터
|
||||
│ │ └── query-client.ts # React Query 설정
|
||||
│ ├── App.tsx # 메인 앱 컴포넌트
|
||||
│ ├── main.tsx # 엔트리 포인트
|
||||
│ └── index.css # Tailwind CSS + 테마
|
||||
├── public/ # 정적 파일
|
||||
├── .env.local # 환경변수 (VITE_API_BASE_URL 등)
|
||||
├── vite.config.ts # Vite 설정 (path aliases 포함)
|
||||
├── tailwind.config.js # Tailwind CSS v4 설정
|
||||
├── tsconfig.json # TypeScript 설정 (프로젝트 참조)
|
||||
├── tsconfig.app.json # 앱 TypeScript 설정 (path aliases)
|
||||
├── package.json # 의존성 정의
|
||||
└── README.md # 프로젝트 문서
|
||||
```
|
||||
|
||||
#### 3. Tailwind CSS 4.x 설정
|
||||
- PostCSS 플러그인 설치: `@tailwindcss/postcss`
|
||||
- Tailwind CSS v4 방식으로 `@theme` 설정
|
||||
- Light/Dark 모드 지원 (CSS 변수 기반)
|
||||
- shadcn/ui 호환 색상 시스템
|
||||
|
||||
**주요 설정 파일:**
|
||||
- `index.css`: `@import "tailwindcss"` + `@theme` 블록
|
||||
- `postcss.config.js`: `@tailwindcss/postcss` 플러그인
|
||||
- `tailwind.config.js`: content paths 정의
|
||||
|
||||
#### 4. Path Aliases 설정
|
||||
TypeScript 및 Vite에서 `@/` 경로 별칭 사용 가능:
|
||||
|
||||
**tsconfig.app.json:**
|
||||
```json
|
||||
{
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/pages/*": ["./src/pages/*"],
|
||||
"@/hooks/*": ["./src/hooks/*"],
|
||||
"@/services/*": ["./src/services/*"],
|
||||
"@/stores/*": ["./src/stores/*"],
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/types/*": ["./src/types/*"],
|
||||
"@/lib/*": ["./src/lib/*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**vite.config.ts:**
|
||||
```typescript
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/components': path.resolve(__dirname, './src/components'),
|
||||
// ... 기타 aliases
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Axios + API 인터셉터 구현
|
||||
**파일: `src/lib/axios.ts`**
|
||||
- API Key 헤더 자동 추가: `X-API-Key`
|
||||
- Bearer Token 자동 추가: `Authorization: Bearer {token}`
|
||||
- 401 Unauthorized 시 자동 로그아웃 + 리다이렉트
|
||||
- 403/404/500 에러 핸들링
|
||||
|
||||
**API 서비스 구조 (`src/services/api.ts`):**
|
||||
```typescript
|
||||
// 인증 API
|
||||
export const authApi = {
|
||||
login: (email, password) => ApiResponse<LoginResponse>
|
||||
logout: () => ApiResponse<null>
|
||||
me: () => ApiResponse<any>
|
||||
}
|
||||
|
||||
// 테넌트 API
|
||||
export const tenantApi = {
|
||||
switch: (tenantId) => ApiResponse<any>
|
||||
list: () => ApiResponse<any>
|
||||
}
|
||||
|
||||
// 제네릭 CRUD 헬퍼
|
||||
export const createCrudApi = <T>(basePath: string) => {
|
||||
list, get, create, update, delete
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. React Query 설정
|
||||
**파일: `src/lib/query-client.ts`**
|
||||
```typescript
|
||||
{
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5분
|
||||
gcTime: 10 * 60 * 1000 // 10분
|
||||
},
|
||||
mutations: {
|
||||
retry: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Zustand 인증 스토어 구현
|
||||
**파일: `src/stores/auth.ts`**
|
||||
- LocalStorage persist 적용
|
||||
- 사용자 정보, 토큰, 현재 테넌트 관리
|
||||
- `setAuth`, `setCurrentTenant`, `logout`, `clearAuth` 액션
|
||||
|
||||
**사용 예시:**
|
||||
```typescript
|
||||
const { user, isAuthenticated, setAuth, logout } = useAuthStore()
|
||||
```
|
||||
|
||||
#### 8. TypeScript 타입 정의
|
||||
**파일: `src/types/api.ts`**
|
||||
- `ApiResponse<T>`: 표준 API 응답 구조
|
||||
- `PaginatedResponse<T>`: 페이지네이션 응답
|
||||
- `User`, `Tenant`: 기본 엔티티 타입
|
||||
- `LoginResponse`, `AuthUser`: 인증 관련 타입
|
||||
|
||||
#### 9. 유틸리티 함수
|
||||
**파일: `src/lib/utils.ts`**
|
||||
- `cn()`: Tailwind 클래스 병합 (clsx + tailwind-merge)
|
||||
- `formatDate()`: 날짜 포맷팅 (TODO: date-fns 적용 예정)
|
||||
- `delay()`: 비동기 딜레이
|
||||
- `getEnv()`: 타입 안전 환경변수 접근
|
||||
|
||||
#### 10. Docker 설정 완료
|
||||
**docker-compose.yml에 React 서비스 추가:**
|
||||
```yaml
|
||||
react:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ../docker/react/Dockerfile
|
||||
volumes:
|
||||
- ../react:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- VITE_API_BASE_URL=http://api.sam.kr
|
||||
- VITE_API_KEY=${VITE_API_KEY:-}
|
||||
- VITE_APP_NAME=SAM
|
||||
- VITE_APP_ENV=development
|
||||
networks:
|
||||
- samnet
|
||||
working_dir: /app
|
||||
```
|
||||
|
||||
**nginx.conf에 dev.sam.kr 설정 추가:**
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name dev.sam.kr;
|
||||
|
||||
location / {
|
||||
proxy_pass http://react:5173;
|
||||
# WebSocket support for Vite HMR
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dockerfile (`docker/react/Dockerfile`):**
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY ../../react/package*.json ./
|
||||
RUN npm ci
|
||||
COPY ../../react .
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
```
|
||||
|
||||
#### 11. 환경변수 설정
|
||||
**파일: `.env.local`**
|
||||
```env
|
||||
VITE_API_BASE_URL=http://api.sam.kr
|
||||
VITE_API_KEY=your-api-key-here
|
||||
VITE_APP_NAME=SAM
|
||||
VITE_APP_ENV=development
|
||||
```
|
||||
|
||||
#### 12. 빌드 검증 완료
|
||||
```bash
|
||||
npm run build
|
||||
# ✅ Output:
|
||||
# dist/index.html 0.45 kB │ gzip: 0.29 kB
|
||||
# dist/assets/index-Uh-4EJ_4.css 6.56 kB │ gzip: 2.02 kB
|
||||
# dist/assets/index-DmPu9Lzh.js 250.04 kB │ gzip: 79.38 kB
|
||||
# ✓ built in 580ms
|
||||
```
|
||||
|
||||
### 📋 주요 의사 결정
|
||||
|
||||
1. **Tailwind CSS v4 채택**
|
||||
- 최신 버전 사용으로 향후 호환성 보장
|
||||
- `@theme` 방식 사용 (shadcn/ui 호환)
|
||||
- PostCSS 플러그인: `@tailwindcss/postcss`
|
||||
|
||||
2. **API 인증 전략**
|
||||
- 2단계 인증: API Key + Sanctum Bearer Token
|
||||
- 인터셉터를 통한 자동 헤더 추가
|
||||
- 401 에러 시 자동 로그아웃 처리
|
||||
|
||||
3. **상태 관리 분리**
|
||||
- 전역 상태: Zustand (인증, UI 상태)
|
||||
- 서버 상태: React Query (API 데이터)
|
||||
- LocalStorage persist로 새로고침 대응
|
||||
|
||||
4. **Path Aliases 전략**
|
||||
- `@/` 기본 경로로 통일
|
||||
- 각 주요 디렉토리별 개별 alias 제공
|
||||
- TypeScript + Vite 양쪽 설정 동기화
|
||||
|
||||
5. **Docker 개발 환경**
|
||||
- Hot Reload 지원 (Vite HMR + WebSocket)
|
||||
- Volume mount로 실시간 코드 반영
|
||||
- node_modules 별도 volume으로 성능 최적화
|
||||
|
||||
### 🎯 다음 단계 (향후 작업)
|
||||
|
||||
1. **UI 컴포넌트 개발**
|
||||
- shadcn/ui 컴포넌트 추가 설치 필요시
|
||||
- 공통 컴포넌트 제작 (Button, Input, Modal 등)
|
||||
|
||||
2. **페이지 구현**
|
||||
- 로그인 페이지
|
||||
- 대시보드
|
||||
- 주요 기능 페이지들
|
||||
|
||||
3. **라우팅 설정**
|
||||
- React Router 라우트 정의
|
||||
- Protected Routes (인증 필요 페이지)
|
||||
- 권한 기반 라우팅
|
||||
|
||||
4. **API 연동 확장**
|
||||
- 각 도메인별 API 서비스 추가
|
||||
- React Query hooks 작성
|
||||
- 에러 핸들링 강화
|
||||
|
||||
5. **테스트 환경 구축**
|
||||
- Docker Compose로 전체 환경 테스트
|
||||
- dev.sam.kr 도메인 접속 확인
|
||||
|
||||
### 🔧 개발 명령어
|
||||
|
||||
```bash
|
||||
# 개발 서버 실행
|
||||
npm run dev
|
||||
|
||||
# 프로덕션 빌드
|
||||
npm run build
|
||||
|
||||
# 빌드 결과 미리보기
|
||||
npm run preview
|
||||
|
||||
# Docker로 실행 (전체 환경)
|
||||
cd ../docker
|
||||
docker-compose up -d react
|
||||
```
|
||||
|
||||
### 📌 참고사항
|
||||
|
||||
1. **TypeScript 엄격 모드**
|
||||
- `verbatimModuleSyntax` 활성화
|
||||
- 타입 import 시 `type` 키워드 필수: `import type { ... }`
|
||||
|
||||
2. **Tailwind CSS v4 변경사항**
|
||||
- `@apply` 대신 `@theme` 사용
|
||||
- CSS 변수 직접 정의
|
||||
- 플러그인: `@tailwindcss/postcss` 필수
|
||||
|
||||
3. **API 응답 구조**
|
||||
- 모든 API는 `{ success, message, data }` 구조
|
||||
- 메시지는 i18n 키 사용 (예: `message.created`)
|
||||
|
||||
4. **환경변수 접근**
|
||||
- Vite에서는 `import.meta.env.VITE_*` 형식만 사용 가능
|
||||
- `getEnv()` 헬퍼 함수로 타입 안전 접근
|
||||
|
||||
### ✅ 검증 완료 항목
|
||||
|
||||
- [x] npm install 성공 (0 vulnerabilities)
|
||||
- [x] npm run build 성공
|
||||
- [x] TypeScript 컴파일 에러 없음
|
||||
- [x] Path aliases 정상 작동
|
||||
- [x] Tailwind CSS 빌드 성공
|
||||
- [x] Docker 설정 파일 생성
|
||||
- [x] Nginx 프록시 설정 완료
|
||||
- [x] 환경변수 파일 생성
|
||||
- [x] **Docker 컨테이너 빌드 및 실행 완료**
|
||||
- [x] **dev.sam.kr 도메인 설정 완료**
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-13 (일) - Docker 환경 실행 완료 + Vite 프록시 설정 수정
|
||||
|
||||
### Docker 컨테이너 실행
|
||||
|
||||
#### 문제 해결 과정 #1: Docker Build Context
|
||||
**문제**: 초기 docker-compose.yml 설정에서 빌드 context가 잘못 설정됨
|
||||
```yaml
|
||||
# 문제 있는 설정
|
||||
react:
|
||||
build:
|
||||
context: . # /docker 디렉토리를 context로 사용
|
||||
dockerfile: ../docker/react/Dockerfile
|
||||
```
|
||||
|
||||
**해결**: Docker build context를 상위 디렉토리로 변경
|
||||
```yaml
|
||||
# 수정된 설정
|
||||
react:
|
||||
build:
|
||||
context: .. # /SAM 디렉토리를 context로 사용
|
||||
dockerfile: docker/react/Dockerfile
|
||||
```
|
||||
|
||||
#### 실행 결과
|
||||
```bash
|
||||
docker-compose up -d --build react
|
||||
# ✅ 빌드 성공 (2.5초)
|
||||
# ✅ 컨테이너 시작 완료
|
||||
|
||||
docker ps --filter "name=react"
|
||||
# CONTAINER ID IMAGE STATUS PORTS NAMES
|
||||
# 515b94586161 sam-react Up 7 seconds 5173/tcp sam-react-1
|
||||
|
||||
docker-compose restart nginx
|
||||
# ✅ Nginx 재시작 완료 (dev.sam.kr 설정 반영)
|
||||
```
|
||||
|
||||
#### 문제 해결 과정 #2: Vite allowedHosts 설정
|
||||
**오류 메시지**:
|
||||
```
|
||||
Blocked request. This host ("dev.sam.kr") is not allowed.
|
||||
To allow this host, add "dev.sam.kr" to `server.allowedHosts` in vite.config.js.
|
||||
```
|
||||
|
||||
**근본 원인 분석** (devops-architect 페르소나 사용):
|
||||
1. **Vite 보안 메커니즘**: Host Header를 검증하여 허용되지 않은 도메인 요청 차단
|
||||
2. **Docker 환경**: 컨테이너 내부에서 실행되는 Vite가 Nginx의 `Host: dev.sam.kr` 헤더를 받음
|
||||
3. **기본 설정**: `allowedHosts`가 설정되지 않아 프록시를 통한 접근 차단
|
||||
|
||||
**해결 방법**:
|
||||
`vite.config.ts`에 다음 설정 추가:
|
||||
```typescript
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
// Nginx 리버스 프록시 환경을 위한 설정
|
||||
hmr: {
|
||||
clientPort: 80, // HTTP 환경 (nginx port 80)
|
||||
protocol: 'ws', // WebSocket (HTTP용)
|
||||
host: 'dev.sam.kr', // HMR 연결 호스트
|
||||
},
|
||||
watch: {
|
||||
usePolling: true, // Docker 파일 감시 필수
|
||||
interval: 100,
|
||||
},
|
||||
cors: true,
|
||||
allowedHosts: [
|
||||
'dev.sam.kr', // 프로덕션 도메인
|
||||
'localhost', // 로컬 개발
|
||||
'127.0.0.1', // 로컬 IP
|
||||
'.sam.kr', // 서브도메인 와일드카드
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**주요 변경사항**:
|
||||
- ✅ `allowedHosts` 추가: dev.sam.kr 및 와일드카드 도메인 허용
|
||||
- ✅ `hmr.clientPort`: 80 (HTTP 환경에 맞춤)
|
||||
- ✅ `hmr.protocol`: 'ws' (HTTPS가 아닌 HTTP 사용)
|
||||
- ✅ `watch.usePolling`: Docker 환경에서 파일 변경 감지
|
||||
|
||||
**재시작 결과**:
|
||||
```bash
|
||||
docker-compose restart react
|
||||
# ✅ Vite 서버 정상 재시작 (88ms)
|
||||
# ✅ Network: http://172.18.0.6:5173/ 리스닝 중
|
||||
```
|
||||
|
||||
### 접속 정보
|
||||
- **URL**: http://dev.sam.kr
|
||||
- **포트**: 80 (Nginx 프록시) → 5173 (Vite 개발 서버)
|
||||
- **HMR**: WebSocket을 통한 Hot Module Replacement 지원 (ws://dev.sam.kr)
|
||||
|
||||
### 추가 검증 항목
|
||||
- [x] Docker 이미지 빌드 성공 (263 packages, 0 vulnerabilities)
|
||||
- [x] React 컨테이너 정상 실행 중
|
||||
- [x] Nginx 프록시 설정 반영
|
||||
- [x] Vite HMR WebSocket 연결 지원
|
||||
- [x] **Vite allowedHosts 설정 완료**
|
||||
- [x] **dev.sam.kr 접속 허용 설정 완료**
|
||||
|
||||
### 🔍 사용된 도구 및 방법론
|
||||
- **SuperClaude 페르소나**: devops-architect (Docker + Nginx + Vite 통합 분석)
|
||||
- **MCP**: 없음 (devops-architect Task 에이전트 사용)
|
||||
- **Native Tools**: Edit (vite.config.ts 수정), Bash (Docker 재시작)
|
||||
|
||||
### 📚 학습 포인트
|
||||
1. **Docker 프록시 환경에서는 반드시 `allowedHosts` 설정 필요**
|
||||
2. **HMR 설정은 프로토콜(HTTP/HTTPS)에 따라 다르게 구성**
|
||||
3. **복잡한 통합 작업은 SuperClaude 페르소나 활용이 필수**
|
||||
4. **단순한 설정으로 보이는 작업도 전문가 분석이 오류 예방에 중요**
|
||||
|
||||
---
|
||||
|
||||
**작업 완료 시간**: 약 30분 (설정) + 5분 (Docker 실행) + 10분 (프록시 설정 수정)
|
||||
**생성된 파일 수**: 25개
|
||||
**수정된 파일 수**: 1개 (vite.config.ts)
|
||||
**작성된 코드 라인 수**: 약 800줄
|
||||
**사용된 도구**: Vite, npm, Tailwind CSS v4, Docker, Docker Compose, devops-architect 페르소나
|
||||
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# SAM React Frontend
|
||||
|
||||
React 기반 SAM 프론트엔드 애플리케이션입니다.
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **Build Tool**: Vite 7.x
|
||||
- **Framework**: React 19.x
|
||||
- **Language**: TypeScript 5.x
|
||||
- **Styling**: Tailwind CSS 4.x
|
||||
- **State Management**:
|
||||
- Zustand (전역 상태)
|
||||
- React Query / TanStack Query v5 (서버 상태)
|
||||
- **Routing**: React Router v7
|
||||
- **Form**: React Hook Form + Zod
|
||||
- **HTTP Client**: Axios
|
||||
- **UI Components**: shadcn/ui (Radix UI 기반)
|
||||
- **Icons**: Lucide React
|
||||
- **Date**: date-fns
|
||||
- **Table**: TanStack Table
|
||||
|
||||
## 개발 환경 설정
|
||||
|
||||
### 환경변수 설정
|
||||
|
||||
`.env.local` 파일을 생성하고 다음 내용을 설정하세요:
|
||||
|
||||
```env
|
||||
VITE_API_BASE_URL=http://api.sam.kr
|
||||
VITE_API_KEY=your-api-key-here
|
||||
```
|
||||
|
||||
### 의존성 설치
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 개발 서버 실행
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
개발 서버는 `http://localhost:5173`에서 실행됩니다.
|
||||
|
||||
## API 연동
|
||||
|
||||
SAM API는 2단계 인증을 사용합니다:
|
||||
1. **API Key**: `X-API-Key` 헤더 (모든 요청)
|
||||
2. **Bearer Token**: `Authorization: Bearer {token}` 헤더 (로그인 후)
|
||||
|
||||
자세한 내용은 프로젝트 문서를 참고하세요.
|
||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>react</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4751
package-lock.json
generated
Normal file
4751
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
42
src/App.css
Normal file
42
src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
37
src/App.tsx
Normal file
37
src/App.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { queryClient } from '@/lib/query-client'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container mx-auto p-8">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-4">
|
||||
SAM React Frontend
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
React 프로젝트가 성공적으로 설정되었습니다.
|
||||
</p>
|
||||
<div className="mt-8 p-6 bg-card rounded-lg border">
|
||||
<h2 className="text-2xl font-semibold mb-2">설정 완료</h2>
|
||||
<ul className="list-disc list-inside space-y-1 text-card-foreground">
|
||||
<li>Vite + React + TypeScript</li>
|
||||
<li>Tailwind CSS with shadcn/ui theming</li>
|
||||
<li>React Query for server state</li>
|
||||
<li>Zustand for global state</li>
|
||||
<li>Axios with interceptors</li>
|
||||
<li>React Router for routing</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
52
src/index.css
Normal file
52
src/index.css
Normal file
@@ -0,0 +1,52 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Colors */
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #0a0a0a;
|
||||
--color-card: #ffffff;
|
||||
--color-card-foreground: #0a0a0a;
|
||||
--color-popover: #ffffff;
|
||||
--color-popover-foreground: #0a0a0a;
|
||||
--color-primary: #18181b;
|
||||
--color-primary-foreground: #fafafa;
|
||||
--color-secondary: #f4f4f5;
|
||||
--color-secondary-foreground: #18181b;
|
||||
--color-muted: #f4f4f5;
|
||||
--color-muted-foreground: #71717a;
|
||||
--color-accent: #f4f4f5;
|
||||
--color-accent-foreground: #18181b;
|
||||
--color-destructive: #ef4444;
|
||||
--color-destructive-foreground: #fafafa;
|
||||
--color-border: #e4e4e7;
|
||||
--color-input: #e4e4e7;
|
||||
--color-ring: #18181b;
|
||||
|
||||
/* Border radius */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dark mode colors (optional) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
--color-background: #0a0a0a;
|
||||
--color-foreground: #fafafa;
|
||||
--color-card: #0a0a0a;
|
||||
--color-card-foreground: #fafafa;
|
||||
--color-popover: #0a0a0a;
|
||||
--color-popover-foreground: #fafafa;
|
||||
--color-primary: #fafafa;
|
||||
--color-primary-foreground: #18181b;
|
||||
--color-secondary: #27272a;
|
||||
--color-secondary-foreground: #fafafa;
|
||||
--color-muted: #27272a;
|
||||
--color-muted-foreground: #a1a1aa;
|
||||
--color-accent: #27272a;
|
||||
--color-accent-foreground: #fafafa;
|
||||
--color-destructive: #7f1d1d;
|
||||
--color-destructive-foreground: #fafafa;
|
||||
--color-border: #27272a;
|
||||
--color-input: #27272a;
|
||||
--color-ring: #d4d4d8;
|
||||
}
|
||||
}
|
||||
73
src/lib/axios.ts
Normal file
73
src/lib/axios.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { API_BASE_URL, API_KEY } from './utils'
|
||||
|
||||
/**
|
||||
* Axios instance with default configuration
|
||||
*/
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Request interceptor - Add API Key and Bearer token
|
||||
*/
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// Add API Key header
|
||||
if (API_KEY) {
|
||||
config.headers['X-API-Key'] = API_KEY
|
||||
}
|
||||
|
||||
// Add Bearer token if available
|
||||
const token = localStorage.getItem('auth_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Response interceptor - Handle common errors
|
||||
*/
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// Handle 401 Unauthorized - redirect to login
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
// Handle 403 Forbidden
|
||||
if (error.response?.status === 403) {
|
||||
console.error('Access forbidden:', error.response.data)
|
||||
}
|
||||
|
||||
// Handle 404 Not Found
|
||||
if (error.response?.status === 404) {
|
||||
console.error('Resource not found:', error.config?.url)
|
||||
}
|
||||
|
||||
// Handle 500 Internal Server Error
|
||||
if (error.response?.status === 500) {
|
||||
console.error('Server error:', error.response.data)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default axiosInstance
|
||||
18
src/lib/query-client.ts
Normal file
18
src/lib/query-client.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
/**
|
||||
* React Query client configuration
|
||||
*/
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
|
||||
},
|
||||
mutations: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
43
src/lib/utils.ts
Normal file
43
src/lib/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Merge Tailwind CSS classes with conflict resolution
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date using date-fns
|
||||
*/
|
||||
export function formatDate(date: Date | string, _format: string = 'yyyy-MM-dd'): string {
|
||||
// TODO: Implement using date-fns when needed
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay execution (useful for testing loading states)
|
||||
*/
|
||||
export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
/**
|
||||
* Get environment variable with type safety
|
||||
*/
|
||||
export function getEnv(key: string, defaultValue?: string): string {
|
||||
const value = import.meta.env[key]
|
||||
if (value === undefined && defaultValue === undefined) {
|
||||
throw new Error(`Environment variable ${key} is not defined`)
|
||||
}
|
||||
return value ?? defaultValue ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* API base URL
|
||||
*/
|
||||
export const API_BASE_URL = getEnv('VITE_API_BASE_URL', 'http://api.sam.kr')
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
export const API_KEY = getEnv('VITE_API_KEY', '')
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
85
src/services/api.ts
Normal file
85
src/services/api.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import axiosInstance from '@/lib/axios'
|
||||
import type { ApiResponse, LoginResponse } from '@/types/api'
|
||||
|
||||
/**
|
||||
* Auth API Service
|
||||
*/
|
||||
export const authApi = {
|
||||
/**
|
||||
* Login with email and password
|
||||
*/
|
||||
login: async (email: string, password: string): Promise<ApiResponse<LoginResponse>> => {
|
||||
const response = await axiosInstance.post<ApiResponse<LoginResponse>>('/v1/login', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout current user
|
||||
*/
|
||||
logout: async (): Promise<ApiResponse<null>> => {
|
||||
const response = await axiosInstance.post<ApiResponse<null>>('/v1/logout')
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
me: async (): Promise<ApiResponse<any>> => {
|
||||
const response = await axiosInstance.get<ApiResponse<any>>('/v1/users/me')
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant API Service
|
||||
*/
|
||||
export const tenantApi = {
|
||||
/**
|
||||
* Switch to different tenant
|
||||
*/
|
||||
switch: async (tenantId: number): Promise<ApiResponse<any>> => {
|
||||
const response = await axiosInstance.post<ApiResponse<any>>(`/v1/tenants/${tenantId}/switch`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of tenants for current user
|
||||
*/
|
||||
list: async (): Promise<ApiResponse<any>> => {
|
||||
const response = await axiosInstance.get<ApiResponse<any>>('/v1/users/me/tenants')
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic API helper for CRUD operations
|
||||
*/
|
||||
export const createCrudApi = <T>(basePath: string) => ({
|
||||
list: async (params?: Record<string, any>): Promise<ApiResponse<T[]>> => {
|
||||
const response = await axiosInstance.get<ApiResponse<T[]>>(basePath, { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
get: async (id: number | string): Promise<ApiResponse<T>> => {
|
||||
const response = await axiosInstance.get<ApiResponse<T>>(`${basePath}/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
create: async (data: Partial<T>): Promise<ApiResponse<T>> => {
|
||||
const response = await axiosInstance.post<ApiResponse<T>>(basePath, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
update: async (id: number | string, data: Partial<T>): Promise<ApiResponse<T>> => {
|
||||
const response = await axiosInstance.put<ApiResponse<T>>(`${basePath}/${id}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
delete: async (id: number | string): Promise<ApiResponse<null>> => {
|
||||
const response = await axiosInstance.delete<ApiResponse<null>>(`${basePath}/${id}`)
|
||||
return response.data
|
||||
},
|
||||
})
|
||||
83
src/stores/auth.ts
Normal file
83
src/stores/auth.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import type { AuthUser, Tenant } from '@/types/api'
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUser | null
|
||||
token: string | null
|
||||
isAuthenticated: boolean
|
||||
currentTenant: Tenant | null
|
||||
}
|
||||
|
||||
interface AuthActions {
|
||||
setAuth: (user: AuthUser, token: string) => void
|
||||
setCurrentTenant: (tenant: Tenant) => void
|
||||
logout: () => void
|
||||
clearAuth: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication store using Zustand with persistence
|
||||
*/
|
||||
export const useAuthStore = create<AuthState & AuthActions>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
currentTenant: null,
|
||||
|
||||
// Actions
|
||||
setAuth: (user, token) => {
|
||||
localStorage.setItem('auth_token', token)
|
||||
set({
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: true,
|
||||
currentTenant: user.current_tenant,
|
||||
})
|
||||
},
|
||||
|
||||
setCurrentTenant: (tenant) => {
|
||||
set((state) => ({
|
||||
currentTenant: tenant,
|
||||
user: state.user
|
||||
? { ...state.user, current_tenant: tenant }
|
||||
: null,
|
||||
}))
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
currentTenant: null,
|
||||
})
|
||||
},
|
||||
|
||||
clearAuth: () => {
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
currentTenant: null,
|
||||
})
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
token: state.token,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
currentTenant: state.currentTenant,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
85
src/types/api.ts
Normal file
85
src/types/api.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Standard API response structure (from SAM API)
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
/**
|
||||
* API error response
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
success: false
|
||||
message: string
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination metadata
|
||||
*/
|
||||
export interface PaginationMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated API response
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
meta: PaginationMeta
|
||||
links: {
|
||||
first: string
|
||||
last: string
|
||||
prev: string | null
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User type
|
||||
*/
|
||||
export interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant type
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth login response
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
tenants: Tenant[]
|
||||
current_tenant: Tenant | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth user with tenant info
|
||||
*/
|
||||
export interface AuthUser extends User {
|
||||
current_tenant: Tenant | null
|
||||
tenants: Tenant[]
|
||||
}
|
||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
plugins: [
|
||||
// Note: Tailwind v4 has built-in support for forms and typography
|
||||
// These plugins are kept for compatibility
|
||||
],
|
||||
}
|
||||
42
tsconfig.app.json
Normal file
42
tsconfig.app.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/pages/*": ["./src/pages/*"],
|
||||
"@/hooks/*": ["./src/hooks/*"],
|
||||
"@/services/*": ["./src/services/*"],
|
||||
"@/stores/*": ["./src/stores/*"],
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/types/*": ["./src/types/*"],
|
||||
"@/lib/*": ["./src/lib/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
46
vite.config.ts
Normal file
46
vite.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/components': path.resolve(__dirname, './src/components'),
|
||||
'@/pages': path.resolve(__dirname, './src/pages'),
|
||||
'@/hooks': path.resolve(__dirname, './src/hooks'),
|
||||
'@/services': path.resolve(__dirname, './src/services'),
|
||||
'@/stores': path.resolve(__dirname, './src/stores'),
|
||||
'@/utils': path.resolve(__dirname, './src/utils'),
|
||||
'@/types': path.resolve(__dirname, './src/types'),
|
||||
'@/lib': path.resolve(__dirname, './src/lib'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // Docker 컨테이너 내 모든 네트워크 인터페이스에서 접근 허용
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
// Nginx 리버스 프록시를 통한 도메인 접근 허용
|
||||
hmr: {
|
||||
clientPort: 80, // HTTP 사용 (nginx가 port 80에서 리스닝)
|
||||
protocol: 'ws', // HTTP 환경에서는 WebSocket (ws) 사용
|
||||
host: 'dev.sam.kr', // HMR 연결 시 사용할 호스트
|
||||
},
|
||||
// 파일 감시 설정 (Docker 환경에서 필수)
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 100, // 폴링 간격 (ms)
|
||||
},
|
||||
// CORS 설정 (필요한 경우)
|
||||
cors: true,
|
||||
// 리버스 프록시를 통한 도메인 접근 허용
|
||||
allowedHosts: [
|
||||
'dev.sam.kr',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'.sam.kr', // 서브도메인 와일드카드
|
||||
],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user