import { defineConfig, createLogger } from 'vite'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer'; import { readFileSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); function readAppVersionForBuild(): string { const fromEnv = process.env.VITE_APP_VERSION?.trim() || process.env.FRONTEND_APP_VERSION?.trim() || process.env.DESKTOP_APP_VERSION?.trim(); if (fromEnv) return fromEnv; try { const latestPath = path.resolve(__dirname, 'public/desktop/updates/latest.json'); const latest = JSON.parse(readFileSync(latestPath, 'utf8')) as { version?: string }; const fromLatest = latest.version?.trim(); if (fromLatest) return fromLatest; } catch { /* optional */ } try { const confPath = path.resolve(repoRoot, 'desktop/src-tauri/tauri.conf.json'); const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string }; return String(conf.version ?? '').trim(); } catch { return ''; } } const appVersion = readAppVersionForBuild(); // WebSocket proxy 재연결/종료 시 발생하는 ECONNRESET·EPIPE 는 정상 TCP 이벤트이므로 로그 억제 const SUPPRESS = ['ECONNRESET', 'EPIPE', 'ws proxy socket error']; const logger = createLogger(); const _origError = logger.error.bind(logger); logger.error = (msg, opts) => { if (SUPPRESS.some(s => msg.includes(s))) return; _origError(msg, opts); }; export default defineConfig({ customLogger: logger, define: { 'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion), }, plugins: [ react(), visualizer({ filename: 'dist/stats.html', open: false, gzipSize: true, brotliSize: true }), ], resolve: { dedupe: ['lightweight-charts'], }, optimizeDeps: { include: ['react', 'react-dom'], }, build: { rollupOptions: { output: { manualChunks(id) { if (id.includes('node_modules/lightweight-charts')) return 'vendor-charts'; if (id.includes('node_modules/@xyflow')) return 'vendor-flow'; if (id.includes('node_modules/@stomp') || id.includes('node_modules/sockjs-client')) return 'vendor-stomp'; if (id.includes('node_modules/@tanstack/react-virtual')) return 'vendor-virtual'; // 차트 워크스페이스 전용 청크 — lazy import로 분리된 코드 if ( id.includes('/chart/ChartWorkspaceRoot') || id.includes('/chart/useChartWorkspace') || id.includes('/chart/ChartWorkspaceView') || id.includes('/utils/ChartManager') ) return 'chart-workspace'; }, }, }, }, server: { proxy: { // ── 모바일 APK 정보/다운로드 → exdev (로컬 QR·PC 다운로드가 exdev APK 사용) ── '/api/mobile-app': { target: process.env.VITE_MOBILE_APP_PROXY_TARGET || 'https://stock.exdev.co.kr', changeOrigin: true, secure: true, }, // ── GoldenChart Backend API 프록시 ─────────────────────────── '/api': { target: 'http://localhost:8080', changeOrigin: true, ws: true, // STOMP SockJS (/api/ws/trading) }, // ── REST API 프록시 (CORS 우회) ────────────────────────────── '/upbit-api': { target: 'https://api.upbit.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/upbit-api/, ''), headers: { Accept: 'application/json' }, }, // ── WebSocket 프록시 (Origin 차단 우회) ────────────────────── // 브라우저 → ws://localhost:PORT/upbit-ws → wss://api.upbit.com/websocket/v1 '/upbit-ws': { target: 'wss://api.upbit.com', changeOrigin: true, ws: true, rewrite: () => '/websocket/v1', // ECONNRESET 은 Upbit 서버가 연결을 정상 종료할 때 발생하는 TCP 이벤트 // (페이지 새로고침/재연결 시 정상 현상) → 로그 억제 configure: (proxy) => { proxy.on('error', (err: Error & { code?: string }) => { if (err.code === 'ECONNRESET' || err.code === 'EPIPE') return; // 정상 종료 → 무시 console.error('[upbit-ws proxy]', err.message); }); }, }, }, }, });