React + FastAPI 풀 마이그레이션 — Streamlit 제거
- backend/ — FastAPI + JWT + 모든 REST 엔드포인트 - frontend/ — Next.js 14 + Tailwind + 7페이지 (대시보드/트레이드/거래소/자동매매/설정/내정보/로그인) - core_logic.py — 신호계산/알림 로직 분리 (기존 app_streamlit.py 에서 추출) - users_db.py + bcrypt 인증, exchange_keys.py + Fernet 암호화 - trades_db.py — 진입/청산 lifecycle 추적, signal_events raw 로그 - settings_db.py — 모든 운영 파라미터 DB 영속 저장 (RSI/거래량/펀딩비 임계값 포함) - docker-compose: frontend / backend / postgres + Traefik 라우팅 - assets/logo.svg — JUNGGOMOA 그라디언트 로고 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// 클라이언트 API wrapper. JWT 는 localStorage 에 보관.
|
||||
const TOKEN_KEY = 'jm_token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
export function setToken(t: string) { localStorage.setItem(TOKEN_KEY, t); }
|
||||
export function clearToken() { localStorage.removeItem(TOKEN_KEY); }
|
||||
|
||||
async function request<T = any>(path: string, opts: RequestInit = {}): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(opts.headers as any),
|
||||
};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const res = await fetch(path, { ...opts, headers });
|
||||
if (res.status === 401) {
|
||||
clearToken();
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let msg = text;
|
||||
try { msg = JSON.parse(text).detail || text; } catch {}
|
||||
throw new Error(msg || `${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return undefined as any;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T = any>(p: string) => request<T>(p),
|
||||
post: <T = any>(p: string, body: any) => request<T>(p, { method: 'POST', body: JSON.stringify(body) }),
|
||||
put: <T = any>(p: string, body: any) => request<T>(p, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
delete: <T = any>(p: string) => request<T>(p, { method: 'DELETE' }),
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { api, getToken, setToken, clearToken } from './api';
|
||||
|
||||
interface AuthUser { id?: number; username: string; role: string; created_at?: string; last_login_at?: string; }
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUser | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
login: (u: string, p: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
fetchMe: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useAuth = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
async login(u, p) {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const r = await api.post<{ access_token: string; user: AuthUser }>('/api/auth/login', { username: u, password: p });
|
||||
setToken(r.access_token);
|
||||
set({ user: r.user, loading: false });
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
set({ error: e?.message || '로그인 실패', loading: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
clearToken();
|
||||
set({ user: null });
|
||||
if (typeof window !== 'undefined') window.location.href = '/login';
|
||||
},
|
||||
async fetchMe() {
|
||||
if (!getToken()) { set({ user: null }); return; }
|
||||
try {
|
||||
const me = await api.get<AuthUser>('/api/auth/me');
|
||||
set({ user: me });
|
||||
} catch {
|
||||
clearToken();
|
||||
set({ user: null });
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,3 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
|
||||
Reference in New Issue
Block a user