// src/api.js // 프론트와 API가 동일 도메인(nginx 프록시)이므로 항상 상대 경로 사용. // 절대 URL(VITE_API_BASE)은 Mixed Content를 유발하므로 사용하지 않음. const toApiUrl = (path) => path; export async function apiGet(path) { const res = await fetch(toApiUrl(path), { headers: { "Accept": "application/json" }, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); } return res.json(); } export async function apiDelete(path) { const res = await fetch(toApiUrl(path), { method: "DELETE" }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); } return res.json(); } export async function apiPost(path, body) { const res = await fetch(toApiUrl(path), { method: "POST", headers: { "Accept": "application/json", ...(body ? { "Content-Type": "application/json" } : {}), }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); } return res.json(); } export async function apiPut(path, body) { const res = await fetch(toApiUrl(path), { method: "PUT", headers: { "Accept": "application/json", ...(body ? { "Content-Type": "application/json" } : {}), }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); } return res.json(); } export function getLatest() { return apiGet("/api/lotto/latest"); } export function getStats() { return apiGet("/api/lotto/stats"); } export function recommend(params) { const qs = new URLSearchParams({ recent_window: String(params.recent_window), recent_weight: String(params.recent_weight), avoid_recent_k: String(params.avoid_recent_k), }); return apiGet(`/api/lotto/recommend?${qs.toString()}`); } export function getHistory(limit = 30, offset = 0) { return apiGet(`/api/history?limit=${limit}&offset=${offset}`); } export function deleteHistory(id) { return apiDelete(`/api/history/${id}`); } // ── 시뮬레이션 관련 API ────────────────────────────────────────────────────── export function getBestPicks(limit = 20) { return apiGet(`/api/lotto/best?limit=${limit}`); } export function getAnalysis() { return apiGet('/api/lotto/analysis'); } export function triggerSimulate(nCandidates = 20000, topK = 100, bestN = 20) { const qs = new URLSearchParams({ n_candidates: String(nCandidates), top_k: String(topK), best_n: String(bestN), }); return apiPost(`/api/admin/simulate?${qs.toString()}`); } export function getStockNews(limit = 20, category) { const qs = new URLSearchParams({ limit: String(limit) }); if (category) { qs.set("category", category); } return apiGet(`/api/stock/news?${qs.toString()}`); } export function getStockIndices() { return apiGet("/api/stock/indices"); } export function getTradeBalance() { return apiGet("/api/trade/balance"); } export function createTradeOrder(payload) { return apiPost("/api/trade/order", payload); } // ── 포트폴리오 (수동 입력) API ────────────────────────────────────────────── export function getPortfolio() { return apiGet("/api/portfolio"); } export function addPortfolio(item) { return apiPost("/api/portfolio", item); } export function updatePortfolio(id, fields) { return apiPut(`/api/portfolio/${id}`, fields); } export function deletePortfolio(id) { return apiDelete(`/api/portfolio/${id}`); } // ── 자산 스냅샷 API ────────────────────────────────────────────────────────── // 장 마감 시점 총 자산을 기록하고, 기간별 추이를 조회합니다. // GET /api/portfolio/snapshot/history?days=N // response: { history: [{ date: "2026-03-07", total_assets: 12345678 }, ...] } export function getAssetHistory(days = 30) { const qs = days ? `?days=${days}` : ''; return apiGet(`/api/portfolio/snapshot/history${qs}`); } // POST /api/portfolio/snapshot (body 없이 호출 — 서버가 현재 total_assets 계산해서 저장) // 또는 body: { total_assets: number } 로 직접 지정 가능 export function saveAssetSnapshot(total_assets) { return apiPost('/api/portfolio/snapshot', total_assets != null ? { total_assets } : undefined); } // ── 예수금 API ─────────────────────────────────────────────────────────────── export function upsertCash(broker, cash) { return apiPut('/api/portfolio/cash', { broker, cash }); } export function deleteCash(broker) { return apiDelete(`/api/portfolio/cash/${encodeURIComponent(broker)}`); } // ── 시장 심리 지표 API ──────────────────────────────────────────────────────── // CNN Fear & Greed Index (개발: vite proxy /ext/feargreed, 프로덕션: nginx proxy 필요) export async function getFearAndGreed() { const res = await fetch('/ext/feargreed', { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } // Yahoo Finance chart API 공통 파서 async function fetchYahooPrice(extPath) { const res = await fetch(extPath, { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const meta = data?.chart?.result?.[0]?.meta; const price = meta?.regularMarketPrice; const prevClose = meta?.previousClose ?? meta?.chartPreviousClose; if (price == null) throw new Error('데이터 없음'); const rounded = Math.round(price * 100) / 100; const change = prevClose != null ? Math.round((price - prevClose) * 100) / 100 : null; const changePercent = prevClose ? Math.round(((price - prevClose) / prevClose) * 10000) / 100 : null; return { value: rounded, change, changePercent }; } // VIX 지수 (Yahoo Finance 공개 API) export function getVix() { return fetchYahooPrice('/ext/vix'); } // 미국 10년물 국채 금리 (^TNX) export function getTreasury10Y() { return fetchYahooPrice('/ext/treasury'); } // WTI 원유 선물 (CL=F) export function getWTI() { return fetchYahooPrice('/ext/wti'); } // Brent 원유 선물 (BZ=F) export function getBrent() { return fetchYahooPrice('/ext/brent'); } // ── TODO API ───────────────────────────────────────────────────────────────── export function getTodos() { return apiGet('/api/todos'); } export function addTodo(data) { return apiPost('/api/todos', data); } export function updateTodo(id, data) { return apiPut(`/api/todos/${id}`, data); } export function deleteTodo(id) { return apiDelete(`/api/todos/${id}`); } export function clearTodos() { return apiDelete('/api/todos/done'); } // ── 실현손익 내역 API ───────────────────────────────────────────────────────── // GET /api/portfolio/sell-history?broker=X&days=N → { records: [...] } // POST /api/portfolio/sell-history → 저장된 레코드 반환 // DELETE /api/portfolio/sell-history/:id → { ok: true } export function getSellHistory({ broker, days } = {}) { const qs = new URLSearchParams(); if (broker && broker !== 'ALL') qs.set('broker', broker); if (days) qs.set('days', String(days)); const q = qs.toString(); return apiGet(`/api/portfolio/sell-history${q ? '?' + q : ''}`); } export function addSellHistory(record) { return apiPost('/api/portfolio/sell-history', record); } export function updateSellHistory(id, record) { return apiPut(`/api/portfolio/sell-history/${id}`, record); } export function deleteSellHistory(id) { return apiDelete(`/api/portfolio/sell-history/${id}`); } // ── AI 음악 생성 API ────────────────────────────────────────────────────────── // GET /api/music/providers → { providers: [{ id, name, description, features }] } export function getMusicProviders() { return apiGet('/api/music/providers'); } // POST /api/music/generate // body: { provider, genre, moods, instruments, duration_sec, bpm, key, scale, prompt, lyrics, instrumental } // → { task_id: string, provider: string } export function generateMusic(payload) { return apiPost('/api/music/generate', payload); } // GET /api/music/status/:task_id // → { status, progress, message, audio_url?, error?, provider?, track? } export function getMusicStatus(taskId) { return apiGet(`/api/music/status/${encodeURIComponent(taskId)}`); } // POST /api/music/lyrics body: { prompt } // → { id, status, text } (Suno 가사 생성) export function generateMusicLyrics(prompt) { return apiPost('/api/music/lyrics', { prompt }); } // GET /api/music/library // → { tracks: [{ id, title, genre, ..., provider, lyrics, image_url, suno_id }] } export function getMusicLibrary() { return apiGet('/api/music/library'); } // POST /api/music/library body: track object // → saved track with id export function saveMusicTrack(data) { return apiPost('/api/music/library', data); } // DELETE /api/music/library/:id // → { ok: true } export function deleteMusicTrack(id) { return apiDelete(`/api/music/library/${id}`); } // GET /api/music/models → { models: [{ id, name, max_duration, description }] } export function getMusicModels() { return apiGet('/api/music/models'); } // GET /api/music/credits → { remaining, total, ... } export function getMusicCredits() { return apiGet('/api/music/credits'); } // POST /api/music/extend body: { suno_id, continue_at, prompt, style, title, model } // → { task_id, provider } export function extendMusicTrack(payload) { return apiPost('/api/music/extend', payload); } // POST /api/music/vocal-removal body: { suno_id, title } // → { task_id, provider } export function removeVocals(payload) { return apiPost('/api/music/vocal-removal', payload); } // ── 저장된 가사 CRUD ───────────────────────────────────────────────────────── // GET /api/music/lyrics/library → { lyrics: [{ id, title, text, prompt, created_at, updated_at }] } export function getSavedLyrics() { return apiGet('/api/music/lyrics/library'); } // POST /api/music/lyrics/library body: { title, text, prompt } export function saveLyrics(data) { return apiPost('/api/music/lyrics/library', data); } // PUT /api/music/lyrics/library/:id body: { title?, text?, prompt? } export function updateLyrics(id, data) { return apiPut(`/api/music/lyrics/library/${id}`, data); } // DELETE /api/music/lyrics/library/:id export function deleteLyrics(id) { return apiDelete(`/api/music/lyrics/library/${id}`); } // ── Phase 1: 커버 이미지 ──────────────────────────────────────────────────── // POST /api/music/cover-image body: { suno_task_id, track_id } export function generateCoverImage(payload) { return apiPost('/api/music/cover-image', payload); } // ── Phase 2 API ───────────────────────────────────────────────────────────── // POST /api/music/wav body: { suno_task_id, suno_id, track_id } export function convertToWav(payload) { return apiPost('/api/music/wav', payload); } // POST /api/music/stem-split body: { suno_task_id, suno_id, track_id } export function splitStems(payload) { return apiPost('/api/music/stem-split', payload); } // GET /api/music/timestamped-lyrics?task_id=...&suno_id=... export function getTimestampedLyrics(taskId, sunoId) { return apiGet(`/api/music/timestamped-lyrics?task_id=${encodeURIComponent(taskId)}&suno_id=${encodeURIComponent(sunoId)}`); } // POST /api/music/style-boost body: { content } export function generateStyleBoost(content) { return apiPost('/api/music/style-boost', { content }); } // ── Phase 3 API ───────────────────────────────────────────────────────────── // POST /api/music/upload-cover export function uploadAndCover(payload) { return apiPost('/api/music/upload-cover', payload); } // POST /api/music/upload-extend export function uploadAndExtend(payload) { return apiPost('/api/music/upload-extend', payload); } // POST /api/music/add-vocals export function addVocals(payload) { return apiPost('/api/music/add-vocals', payload); } // POST /api/music/add-instrumental export function addInstrumental(payload) { return apiPost('/api/music/add-instrumental', payload); } // POST /api/music/video export function generateVideo(payload) { return apiPost('/api/music/video', payload); } // ── 로또 고도화 API ──────────────────────────────────────────────────────────── // GET /api/lotto/stats/performance export function getPerformanceStats() { return apiGet('/api/lotto/stats/performance'); } // GET /api/lotto/report/latest export function getLatestReport() { return apiGet('/api/lotto/report/latest'); } // GET /api/lotto/report/:drw_no export function getReport(drwNo) { return apiGet(`/api/lotto/report/${drwNo}`); } // GET /api/lotto/report/history?limit=N export function getReportHistory(limit = 10) { return apiGet(`/api/lotto/report/history?limit=${limit}`); } // GET /api/lotto/analysis/personal export function getPersonalAnalysis() { return apiGet('/api/lotto/analysis/personal'); } // ── 종합 추론 추천 ────────────────────────────────────────────────────────── // GET /api/lotto/recommend/combined export function getCombinedRecommend() { return apiGet('/api/lotto/recommend/combined'); } // GET /api/lotto/recommend/combined/history export function getCombinedHistory(limit = 30) { return apiGet(`/api/lotto/recommend/combined/history?limit=${limit}`); } // GET /api/lotto/purchase?draw_no=N&days=N export function getPurchases({ draw_no, days } = {}) { const qs = new URLSearchParams(); if (draw_no) qs.set('draw_no', String(draw_no)); if (days) qs.set('days', String(days)); const q = qs.toString(); return apiGet(`/api/lotto/purchase${q ? '?' + q : ''}`); } // GET /api/lotto/purchase/stats export function getPurchaseStats() { return apiGet('/api/lotto/purchase/stats'); } // POST /api/lotto/purchase export function addPurchase(data) { return apiPost('/api/lotto/purchase', data); } // PUT /api/lotto/purchase/:id export function updatePurchase(id, data) { return apiPut(`/api/lotto/purchase/${id}`, data); } // DELETE /api/lotto/purchase/:id export function deletePurchase(id) { return apiDelete(`/api/lotto/purchase/${id}`); } // ── 블로그 API ──────────────────────────────────────────────────────────────── // GET /api/blog/posts → { posts: [{id, title, tags, body, date, excerpt}] } // POST /api/blog/posts → 새 글 생성 // PUT /api/blog/posts/:id → 글 수정 // DELETE /api/blog/posts/:id → 글 삭제 export function getBlogPostsApi() { return apiGet('/api/blog/posts'); } export function createBlogPost(data) { return apiPost('/api/blog/posts', data); } export function updateBlogPost(id, data) { return apiPut(`/api/blog/posts/${id}`, data); } export function deleteBlogPost(id) { return apiDelete(`/api/blog/posts/${id}`); } // ── 블로그 마케팅 API ──────────────────────────────────────────────────────── export function getBlogMarketingStatus() { return apiGet('/api/blog-marketing/status'); } export function startResearch(keyword) { return apiPost('/api/blog-marketing/research', { keyword }); } export function getResearchHistory(limit = 30) { return apiGet(`/api/blog-marketing/research/history?limit=${limit}`); } export function getResearchDetail(id) { return apiGet(`/api/blog-marketing/research/${id}`); } export function deleteResearch(id) { return apiDelete(`/api/blog-marketing/research/${id}`); } export function getBlogMarketingTask(taskId) { return apiGet(`/api/blog-marketing/task/${encodeURIComponent(taskId)}`); } export function startGenerate(keywordId) { return apiPost('/api/blog-marketing/generate', { keyword_id: keywordId }); } export function startReview(postId) { return apiPost(`/api/blog-marketing/review/${postId}`); } export function startRegenerate(postId) { return apiPost(`/api/blog-marketing/regenerate/${postId}`); } export function getBlogMarketingPosts(status, limit = 50) { const qs = new URLSearchParams(); if (status) qs.set('status', status); if (limit) qs.set('limit', String(limit)); const q = qs.toString(); return apiGet(`/api/blog-marketing/posts${q ? '?' + q : ''}`); } export function getBlogMarketingPost(id) { return apiGet(`/api/blog-marketing/posts/${id}`); } export function updateBlogMarketingPost(id, data) { return apiPut(`/api/blog-marketing/posts/${id}`, data); } export function deleteBlogMarketingPost(id) { return apiDelete(`/api/blog-marketing/posts/${id}`); } export function publishBlogMarketingPost(id, naverUrl) { return apiPost(`/api/blog-marketing/posts/${id}/publish`, { naver_url: naverUrl || '' }); } export function getBlogMarketingCommissions(postId) { const qs = postId ? `?post_id=${postId}` : ''; return apiGet(`/api/blog-marketing/commissions${qs}`); } export function addBlogMarketingCommission(data) { return apiPost('/api/blog-marketing/commissions', data); } export function updateBlogMarketingCommission(id, data) { return apiPut(`/api/blog-marketing/commissions/${id}`, data); } export function deleteBlogMarketingCommission(id) { return apiDelete(`/api/blog-marketing/commissions/${id}`); } export function getBlogMarketingDashboard() { return apiGet('/api/blog-marketing/dashboard'); } // 마케터 단계 export function startMarket(postId) { return apiPost(`/api/blog-marketing/market/${postId}`); } // 브랜드커넥트 링크 CRUD export function getBrandLinks(params = {}) { const qs = new URLSearchParams(); if (params.post_id) qs.set('post_id', String(params.post_id)); if (params.keyword_id) qs.set('keyword_id', String(params.keyword_id)); const q = qs.toString(); return apiGet(`/api/blog-marketing/links${q ? '?' + q : ''}`); } export function createBrandLink(data) { return apiPost('/api/blog-marketing/links', data); } export function updateBrandLink(id, data) { return apiPut(`/api/blog-marketing/links/${id}`, data); } export function deleteBrandLink(id) { return apiDelete(`/api/blog-marketing/links/${id}`); }