feat: production deployment setup with Gitea Actions CI/CD
Some checks failed
Deploy to Production / deploy (push) Failing after 46s
Some checks failed
Deploy to Production / deploy (push) Failing after 46s
- Remove nginx from docker-compose.prod.yml (NPM handles reverse proxy) - Add Next.js rewrites to proxy /api/* to backend (backend fully hidden) - Bind frontend to 127.0.0.1:3000 only (NPM proxies externally) - Replace hardcoded localhost:8000 in history page with api client - Make CORS origins configurable via environment variable - Restrict CORS methods to GET/POST/PUT/DELETE - Add Gitea Actions deploy workflow with secrets-based env management - Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy) - Add BACKEND_URL build arg to frontend Dockerfile for standalone builds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
642514b227
commit
39d2226d95
22
.env.prod.example
Normal file
22
.env.prod.example
Normal file
@ -0,0 +1,22 @@
|
||||
# Galaxis-Po Production Environment Variables
|
||||
# These values should be set as Gitea Secrets, not in this file.
|
||||
# This file is a reference template only.
|
||||
|
||||
# Database
|
||||
DB_USER=galaxy
|
||||
DB_PASSWORD=your_strong_password_here
|
||||
DB_NAME=galaxy_po
|
||||
|
||||
# JWT Authentication (generate with: openssl rand -hex 32)
|
||||
JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters
|
||||
|
||||
# Korea Investment & Securities OpenAPI
|
||||
KIS_APP_KEY=your_kis_app_key
|
||||
KIS_APP_SECRET=your_kis_app_secret
|
||||
KIS_ACCOUNT_NO=your_account_number
|
||||
|
||||
# DART OpenAPI (Financial Statements)
|
||||
DART_API_KEY=your_dart_api_key
|
||||
|
||||
# CORS (comma-separated origins, used by backend)
|
||||
CORS_ORIGINS=https://your-domain.com
|
||||
56
.gitea/workflows/deploy.yml
Normal file
56
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,56 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create .env.prod from secrets
|
||||
run: |
|
||||
cat <<EOF > .env.prod
|
||||
DB_USER=${{ secrets.DB_USER }}
|
||||
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
|
||||
DB_NAME=${{ secrets.DB_NAME }}
|
||||
JWT_SECRET=${{ secrets.JWT_SECRET }}
|
||||
KIS_APP_KEY=${{ secrets.KIS_APP_KEY }}
|
||||
KIS_APP_SECRET=${{ secrets.KIS_APP_SECRET }}
|
||||
KIS_ACCOUNT_NO=${{ secrets.KIS_ACCOUNT_NO }}
|
||||
DART_API_KEY=${{ secrets.DART_API_KEY }}
|
||||
CORS_ORIGINS=${{ secrets.CORS_ORIGINS }}
|
||||
EOF
|
||||
|
||||
- name: Deploy with Docker Compose
|
||||
run: |
|
||||
DEPLOY_DIR=~/docker/galaxis-po
|
||||
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
rsync -av --delete \
|
||||
--exclude '.git' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '.venv' \
|
||||
--exclude '.next' \
|
||||
./ "$DEPLOY_DIR/"
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
docker compose -f docker-compose.prod.yml build
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "Waiting for services to start..."
|
||||
sleep 15
|
||||
docker compose -f ~/docker/galaxis-po/docker-compose.prod.yml ps
|
||||
curl -sf http://127.0.0.1:3000 > /dev/null || { echo "Frontend: FAILED"; exit 1; }
|
||||
echo "Frontend: OK"
|
||||
docker exec galaxis-po-backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || { echo "Backend: FAILED"; exit 1; }
|
||||
echo "Backend: OK"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,6 +40,7 @@ yarn-error.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.prod
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ class Settings(BaseSettings):
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 60 * 24 # 24 hours
|
||||
|
||||
# CORS
|
||||
cors_origins: str = "http://localhost:3000"
|
||||
|
||||
# External APIs
|
||||
kis_app_key: str = ""
|
||||
kis_app_secret: str = ""
|
||||
|
||||
@ -54,11 +54,15 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
_settings = get_settings()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"],
|
||||
allow_origins=[origin.strip() for origin in _settings.cors_origins.split(",") if origin.strip()],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Production Docker Compose
|
||||
# Usage: docker-compose -f docker-compose.prod.yml up -d
|
||||
# Usage: docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@ -33,6 +33,12 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
start_period: 10s
|
||||
retries: 3
|
||||
restart: always
|
||||
networks:
|
||||
- galaxy-net
|
||||
@ -44,7 +50,9 @@ services:
|
||||
target: production
|
||||
container_name: galaxis-po-frontend
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: ${API_URL}
|
||||
BACKEND_URL: http://backend:8000
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
@ -52,22 +60,6 @@ services:
|
||||
networks:
|
||||
- galaxy-net
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: galaxis-po-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
restart: always
|
||||
networks:
|
||||
- galaxy-net
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
@ -52,7 +52,7 @@ services:
|
||||
target: development
|
||||
container_name: galaxis-po-frontend
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:8000
|
||||
BACKEND_URL: http://backend:8000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
301
docs/plans/2026-02-07-production-deployment-design.md
Normal file
301
docs/plans/2026-02-07-production-deployment-design.md
Normal file
@ -0,0 +1,301 @@
|
||||
# Galaxis-Po 프로덕션 배포 설계
|
||||
|
||||
## 개요
|
||||
|
||||
Galaxis-Po를 클라우드 VM 서버에 프로덕션 배포하기 위한 종합 설계.
|
||||
Gitea와 동일 서버에 배포하며, 외부 노출 포트는 56978 단일 포트(NPM에서 관리).
|
||||
|
||||
## 환경 정보
|
||||
|
||||
- **서버**: 클라우드 VM (Gitea와 동일 서버)
|
||||
- **리버스 프록시**: Nginx Proxy Manager (jlesage/nginx-proxy-manager, Docker)
|
||||
- **CI/CD**: Gitea Actions (Docker 컨테이너 Runner, Docker Socket 마운트)
|
||||
- **도메인**: SSL 적용 (NPM에서 Let's Encrypt)
|
||||
- **사용 범위**: 개인 전용
|
||||
- **배포 경로**: `~/docker/galaxis-po`
|
||||
|
||||
## 1. 아키텍처
|
||||
|
||||
```
|
||||
인터넷
|
||||
│
|
||||
▼ (포트 56978, SSL)
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Nginx Proxy Manager (기존) │
|
||||
│ domain.com → 127.0.0.1:3000 │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│
|
||||
┌──────▼───────────────────────────────────┐
|
||||
│ docker-compose (galaxis-po) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ frontend (Next.js) :3000 │ │
|
||||
│ │ ┌───────────────────────┐ │ │
|
||||
│ │ │ /api/* → rewrites to │──┼──┐ │
|
||||
│ │ │ backend:8000 │ │ │ │
|
||||
│ │ └───────────────────────┘ │ │ │
|
||||
│ └──────────────┬──────────────┘ │ │
|
||||
│ 127.0.0.1:3000 (호스트 노출) │ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────┐ │ │
|
||||
│ │ backend (FastAPI) :8000 │◄──┘ │
|
||||
│ │ (내부 네트워크만) │ │
|
||||
│ └──────────┬─────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────────────┐ │
|
||||
│ │ postgres :5432 │ │
|
||||
│ │ (내부 네트워크만) │ │
|
||||
│ └────────────────────────────┘ │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 핵심 설계 결정
|
||||
|
||||
- **Nginx 컨테이너 제거**: NPM이 리버스 프록시 + SSL 종료 담당
|
||||
- **Next.js rewrites로 API 프록시**: 브라우저 → frontend → backend. Backend 완전 은닉.
|
||||
- **호스트 포트 노출**: frontend의 3000번만 `127.0.0.1:3000`으로 노출 (NPM이 프록시)
|
||||
- **Backend/DB**: Docker 내부 네트워크만 사용, 외부 포트 노출 없음
|
||||
|
||||
## 2. docker-compose.prod.yml 변경
|
||||
|
||||
### 변경 사항
|
||||
1. **nginx 서비스 제거** (NPM 사용)
|
||||
2. **frontend**: `ports: "127.0.0.1:3000:3000"` 추가
|
||||
3. **backend**: 포트 비노출 유지
|
||||
4. **postgres**: 포트 비노출 유지
|
||||
5. **env_file**: Gitea Secrets에서 생성
|
||||
6. **restart**: `always` 유지
|
||||
|
||||
### 수정된 docker-compose.prod.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
container_name: galaxis-po-db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: always
|
||||
networks:
|
||||
- galaxy-net
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: galaxis-po-backend
|
||||
env_file:
|
||||
- .env.prod
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
|
||||
PYTHONPATH: /app
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
networks:
|
||||
- galaxy-net
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
container_name: galaxis-po-frontend
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
networks:
|
||||
- galaxy-net
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
galaxy-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
## 3. Next.js API 프록시 설정
|
||||
|
||||
### next.config.ts
|
||||
|
||||
```typescript
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: "http://backend:8000/api/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
headers: [
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
### api.ts 변경
|
||||
|
||||
```typescript
|
||||
// 변경 전
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
// 변경 후
|
||||
const API_URL = ''; // 같은 도메인의 /api/* 로 요청 (Next.js rewrites 사용)
|
||||
```
|
||||
|
||||
### history/page.tsx 하드코딩 제거
|
||||
|
||||
`http://localhost:8000` 하드코딩을 `api` 클라이언트 사용으로 변경.
|
||||
|
||||
## 4. Gitea Actions CI/CD
|
||||
|
||||
### .gitea/workflows/deploy.yml
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Create .env.prod from secrets
|
||||
run: |
|
||||
cat <<EOF > .env.prod
|
||||
DB_USER=${{ secrets.DB_USER }}
|
||||
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
|
||||
DB_NAME=${{ secrets.DB_NAME }}
|
||||
JWT_SECRET=${{ secrets.JWT_SECRET }}
|
||||
KIS_APP_KEY=${{ secrets.KIS_APP_KEY }}
|
||||
KIS_APP_SECRET=${{ secrets.KIS_APP_SECRET }}
|
||||
KIS_ACCOUNT_NO=${{ secrets.KIS_ACCOUNT_NO }}
|
||||
DART_API_KEY=${{ secrets.DART_API_KEY }}
|
||||
EOF
|
||||
|
||||
- name: Deploy with Docker Compose
|
||||
run: |
|
||||
# 배포 디렉토리로 코드 동기화
|
||||
rsync -av --delete \
|
||||
--exclude '.git' \
|
||||
--exclude 'node_modules' \
|
||||
--exclude '__pycache__' \
|
||||
./ ~/docker/galaxis-po/
|
||||
|
||||
cd ~/docker/galaxis-po
|
||||
|
||||
# 빌드 및 배포
|
||||
docker compose -f docker-compose.prod.yml build
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
sleep 15
|
||||
docker compose -f ~/docker/galaxis-po/docker-compose.prod.yml ps
|
||||
# Frontend 헬스체크
|
||||
curl -f http://127.0.0.1:3000 || echo "Frontend health check failed"
|
||||
```
|
||||
|
||||
### Gitea Secrets 설정 필요 항목
|
||||
|
||||
Gitea 웹 UI → Settings → Secrets 에서 다음 항목 등록:
|
||||
|
||||
| Secret Name | 설명 |
|
||||
|-------------|------|
|
||||
| `DB_USER` | PostgreSQL 사용자명 |
|
||||
| `DB_PASSWORD` | PostgreSQL 비밀번호 (강력한 비밀번호) |
|
||||
| `DB_NAME` | 데이터베이스명 |
|
||||
| `JWT_SECRET` | JWT 서명 키 (최소 32자 랜덤 문자열) |
|
||||
| `KIS_APP_KEY` | 한국투자증권 API 키 |
|
||||
| `KIS_APP_SECRET` | 한국투자증권 API 시크릿 |
|
||||
| `KIS_ACCOUNT_NO` | 한국투자증권 계좌번호 |
|
||||
| `DART_API_KEY` | DART 공시 API 키 |
|
||||
|
||||
## 5. 보안 취약점 수정
|
||||
|
||||
### P0 - 반드시 수정 (배포 전)
|
||||
|
||||
| # | 항목 | 현재 상태 | 수정 내용 |
|
||||
|---|------|-----------|-----------|
|
||||
| 1 | `.env` git 노출 | DB 비밀번호가 git에 커밋됨 | `.gitignore`에 추가, git history에서 제거, 비밀번호 변경 |
|
||||
| 2 | nginx 서비스 제거 | docker-compose.prod.yml이 참조하지만 nginx.conf 없음 | Nginx 서비스 제거 (NPM 사용) |
|
||||
| 3 | CORS 하드코딩 | `localhost:3000`만 허용 | Next.js 프록시 사용으로 CORS 이슈 해소, 환경변수로 관리 |
|
||||
| 4 | `history/page.tsx` | `http://localhost:8000` 직접 참조 | api 클라이언트 사용으로 변경 |
|
||||
| 5 | `next.config.ts` | 비어있음 | rewrites + 보안 헤더 추가 |
|
||||
| 6 | `api.ts` API_URL | `NEXT_PUBLIC_`으로 백엔드 URL 노출 | 상대 경로(`''`)로 변경 |
|
||||
| 7 | `alembic.ini` | dev 비밀번호 하드코딩 | env.py에서 환경변수 사용 확인 |
|
||||
|
||||
### P1 - 권장 수정 (배포 후 조기)
|
||||
|
||||
| # | 항목 | 설명 |
|
||||
|---|------|------|
|
||||
| 1 | 회원가입 비활성화 | 개인 전용이므로 `/register` 엔드포인트 비활성화 |
|
||||
| 2 | `config.py` 기본값 | 프로덕션에서 필수값 미설정 시 에러 발생하도록 변경 |
|
||||
| 3 | CORS methods | `allow_methods=["*"]` → 필요한 메서드만 명시 |
|
||||
|
||||
### P2 - 나중에 고려
|
||||
|
||||
| # | 항목 | 설명 |
|
||||
|---|------|------|
|
||||
| 1 | JWT HttpOnly 쿠키 | localStorage XSS 취약점 (개인용이므로 낮은 위험) |
|
||||
| 2 | Rate limiting | NPM 뒤에 있으므로 급하지 않음 |
|
||||
| 3 | RBAC | 사용자 1명이므로 불필요 |
|
||||
|
||||
## 6. NPM 설정 가이드
|
||||
|
||||
Nginx Proxy Manager 웹 UI에서:
|
||||
|
||||
1. **Proxy Hosts → Add Proxy Host**
|
||||
2. **Domain Names**: `your-domain.com`
|
||||
3. **Scheme**: `http`
|
||||
4. **Forward Hostname/IP**: `127.0.0.1` (또는 호스트 IP)
|
||||
5. **Forward Port**: `3000`
|
||||
6. **SSL**: Request a new SSL Certificate → Force SSL → HSTS 활성화
|
||||
7. **Advanced**: 필요 시 WebSocket 지원 활성화
|
||||
|
||||
## 7. 구현 순서
|
||||
|
||||
1. P0 보안 취약점 수정 (코드 변경)
|
||||
2. docker-compose.prod.yml 수정
|
||||
3. next.config.ts 수정 (rewrites + 보안 헤더)
|
||||
4. api.ts + history/page.tsx 수정
|
||||
5. .gitea/workflows/deploy.yml 생성
|
||||
6. .env.prod.example 생성
|
||||
7. .gitignore 업데이트
|
||||
8. Gitea Secrets 등록 (서버에서)
|
||||
9. NPM Proxy Host 설정 (서버에서)
|
||||
10. 배포 테스트
|
||||
@ -19,6 +19,8 @@ CMD ["npm", "run", "dev"]
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ARG BACKEND_URL=http://backend:8000
|
||||
ENV BACKEND_URL=${BACKEND_URL}
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
|
||||
@ -1,7 +1,27 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: `${process.env.BACKEND_URL || "http://localhost:8000"}/api/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
headers: [
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||
import { DashboardLayout } from '@/components/layout/dashboard-layout';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface SnapshotItem {
|
||||
id: number;
|
||||
@ -59,31 +60,18 @@ export default function PortfolioHistoryPage() {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
|
||||
const [snapshotsRes, returnsRes] = await Promise.all([
|
||||
fetch(`http://localhost:8000/api/portfolios/${portfolioId}/snapshots`, { headers }),
|
||||
fetch(`http://localhost:8000/api/portfolios/${portfolioId}/returns`, { headers }),
|
||||
]);
|
||||
|
||||
if (!snapshotsRes.ok || !returnsRes.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
}
|
||||
|
||||
const [snapshotsData, returnsData] = await Promise.all([
|
||||
snapshotsRes.json(),
|
||||
returnsRes.json(),
|
||||
api.get<SnapshotItem[]>(`/api/portfolios/${portfolioId}/snapshots`),
|
||||
api.get<ReturnsData>(`/api/portfolios/${portfolioId}/returns`),
|
||||
]);
|
||||
|
||||
setSnapshots(snapshotsData);
|
||||
setReturns(returnsData);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === 'API request failed') {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -100,23 +88,7 @@ export default function PortfolioHistoryPage() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(
|
||||
`http://localhost:8000/api/portfolios/${portfolioId}/snapshots`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.detail || 'Failed to create snapshot');
|
||||
}
|
||||
|
||||
await api.post(`/api/portfolios/${portfolioId}/snapshots`);
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
@ -127,19 +99,7 @@ export default function PortfolioHistoryPage() {
|
||||
|
||||
const handleViewSnapshot = async (snapshotId: number) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(
|
||||
`http://localhost:8000/api/portfolios/${portfolioId}/snapshots/${snapshotId}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch snapshot');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await api.get<SnapshotDetail>(`/api/portfolios/${portfolioId}/snapshots/${snapshotId}`);
|
||||
setSelectedSnapshot(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
@ -150,19 +110,7 @@ export default function PortfolioHistoryPage() {
|
||||
if (!confirm('이 스냅샷을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(
|
||||
`http://localhost:8000/api/portfolios/${portfolioId}/snapshots/${snapshotId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to delete snapshot');
|
||||
}
|
||||
|
||||
await api.delete(`/api/portfolios/${portfolioId}/snapshots/${snapshotId}`);
|
||||
setSelectedSnapshot(null);
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user