From 7d01c72e586c6028bec00493f8e2a162612c6fe9 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 27 Jan 2026 03:27:01 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EC=8B=9D=20=EB=A7=A4=EB=A7=A4=20api?= =?UTF-8?q?=20=EB=B0=8F=20=ED=99=94=EB=A9=B4=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.js | 31 +++++++++- src/data/blog.js | 3 +- src/pages/stock/Stock.jsx | 49 ++++++++++++---- src/pages/stock/StockTrade.jsx | 101 ++++++++++++++++++++++++++------- 4 files changed, 148 insertions(+), 36 deletions(-) diff --git a/src/api.js b/src/api.js index 7599cf3..a357b85 100644 --- a/src/api.js +++ b/src/api.js @@ -1,6 +1,31 @@ // src/api.js +const API_BASE = import.meta.env.VITE_API_BASE || ""; + +const toApiUrl = (path) => { + if (!API_BASE) return path; + + const baseClean = API_BASE.replace(/\/+$/, ""); + const baseForJoin = `${baseClean}/`; + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + let pathForJoin = normalizedPath; + + if (baseClean.endsWith("/api") && normalizedPath.startsWith("api/")) { + pathForJoin = normalizedPath.slice(4); + } + + try { + const baseUrl = new URL(baseForJoin, window.location.origin); + return new URL(pathForJoin, baseUrl).toString(); + } catch (error) { + console.warn("Invalid VITE_API_BASE, falling back to relative URL.", error); + return path; + } +}; + export async function apiGet(path) { - const res = await fetch(path, { headers: { "Accept": "application/json" } }); + 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}`); @@ -9,7 +34,7 @@ export async function apiGet(path) { } export async function apiDelete(path) { - const res = await fetch(path, { method: "DELETE" }); + 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}`); @@ -18,7 +43,7 @@ export async function apiDelete(path) { } export async function apiPost(path, body) { - const res = await fetch(path, { + const res = await fetch(toApiUrl(path), { method: "POST", headers: { "Accept": "application/json", diff --git a/src/data/blog.js b/src/data/blog.js index 5ee2ae1..a11a801 100644 --- a/src/data/blog.js +++ b/src/data/blog.js @@ -83,7 +83,8 @@ const inferDateFromSlug = (slug) => { export const getBlogPosts = () => { const modules = import.meta.glob('/src/content/blog/**/*.md', { - as: 'raw', + query: '?raw', + import: 'default', eager: true, }); diff --git a/src/pages/stock/Stock.jsx b/src/pages/stock/Stock.jsx index 348550b..bbaa271 100644 --- a/src/pages/stock/Stock.jsx +++ b/src/pages/stock/Stock.jsx @@ -26,18 +26,46 @@ const getLatestBy = (items, key) => { }; const normalizeIndices = (data) => { - if (!data || typeof data !== 'object' || Array.isArray(data)) return []; - return Object.entries(data) - .filter(([, value]) => value && typeof value === 'object') - .map(([name, value]) => ({ - name, - value: value?.value ?? '-', - change: value?.change ?? '', - percent: value?.percent ?? '', + if (!data) return []; + + if (Array.isArray(data)) { + return data.map((item) => ({ + name: item?.name ?? '-', + value: item?.value ?? '-', + change: item?.change_value ?? item?.change ?? '', + percent: item?.change_percent ?? item?.percent ?? '', + direction: item?.direction ?? '', })); + } + + if (Array.isArray(data?.indices)) { + return data.indices.map((item) => ({ + name: item?.name ?? '-', + value: item?.value ?? '-', + change: item?.change_value ?? item?.change ?? '', + percent: item?.change_percent ?? item?.percent ?? '', + direction: item?.direction ?? '', + })); + } + + if (typeof data === 'object') { + return Object.entries(data) + .filter(([, value]) => value && typeof value === 'object') + .map(([name, value]) => ({ + name, + value: value?.value ?? '-', + change: value?.change ?? '', + percent: value?.percent ?? '', + direction: value?.direction ?? '', + })); + } + + return []; }; -const getDirection = (change, percent) => { +const getDirection = (change, percent, direction) => { + if (direction === 'red') return 'up'; + if (direction === 'blue') return 'down'; const pick = (value) => value === undefined || value === null || value === '' ? null : value; const raw = pick(change) ?? pick(percent); @@ -209,7 +237,8 @@ const Stock = () => { sortedIndices.map((item) => { const direction = getDirection( item.change, - item.percent + item.percent, + item.direction ); const changeText = [ item.change, diff --git a/src/pages/stock/StockTrade.jsx b/src/pages/stock/StockTrade.jsx index 40b0e79..f4da5b8 100644 --- a/src/pages/stock/StockTrade.jsx +++ b/src/pages/stock/StockTrade.jsx @@ -18,6 +18,40 @@ const formatPercent = (value) => { return `${numeric.toFixed(2)}%`; }; +const pickFirst = (...values) => + values.find((value) => value !== undefined && value !== null && value !== ''); + +const getQty = (item) => + pickFirst(item?.qty, item?.quantity, item?.holding, item?.hold_qty); + +const getBuyPrice = (item) => + pickFirst( + item?.buy_price, + item?.avg_price, + item?.avg, + item?.buyPrice, + item?.price + ); + +const getCurrentPrice = (item) => + pickFirst( + item?.current_price, + item?.current, + item?.cur_price, + item?.now_price, + item?.market_price + ); + +const getProfitRate = (item) => + pickFirst( + item?.profit_rate, + item?.profitRate, + item?.profit_pct, + item?.profitPercent, + item?.pnl_rate, + item?.return_rate + ); + const StockTrade = () => { const [balance, setBalance] = useState(null); const [balanceLoading, setBalanceLoading] = useState(false); @@ -46,7 +80,7 @@ const StockTrade = () => { try { const result = await requestAutoTrade(); setAutoResult(result); - if (result?.status === 'success') { + if (result?.status === 'success' || result?.status === 'completed') { await loadBalance(); } } catch (err) { @@ -60,14 +94,33 @@ const StockTrade = () => { loadBalance(); }, []); - const holdings = useMemo( - () => (Array.isArray(balance?.holdings) ? balance.holdings : []), - [balance] - ); + const holdings = useMemo(() => { + if (!balance) return []; + if (Array.isArray(balance.holdings)) return balance.holdings; + if (Array.isArray(balance.positions)) return balance.positions; + if (Array.isArray(balance.items)) return balance.items; + return []; + }, [balance]); const summary = balance?.summary ?? {}; + const totalEval = + summary.total_eval ?? balance?.total_eval ?? balance?.total_value; + const deposit = summary.deposit ?? balance?.deposit ?? balance?.available_cash; const autoStatus = autoResult?.status ?? ''; - const decision = autoResult?.decision ?? null; + const decision = autoResult?.decision ?? autoResult?.ai_response ?? null; const tradeResult = autoResult?.trade_result ?? null; + const execution = autoResult?.execution ?? tradeResult?.execution ?? ''; + const statusLabel = + tradeResult?.success === true + ? '성공' + : tradeResult?.success === false + ? '실패' + : autoStatus === 'completed' + ? '완료' + : autoStatus === 'success' + ? '성공' + : autoStatus + ? autoStatus + : '대기'; return (
@@ -90,11 +143,11 @@ const StockTrade = () => {
총 평가금액 - {formatNumber(summary.total_eval)} + {formatNumber(totalEval)}
예수금 - {formatNumber(summary.deposit)} + {formatNumber(deposit)}
보유 종목 @@ -137,11 +190,11 @@ const StockTrade = () => { {[ { label: '총 평가', - value: summary.total_eval, + value: totalEval, }, { label: '예수금', - value: summary.deposit, + value: deposit, }, ].map((item) => (
{
수량 - {formatNumber(item.qty)} + {formatNumber(getQty(item))}
매입가 - {formatNumber(item.buy_price)} + {formatNumber(getBuyPrice(item))}
현재가 - {formatNumber(item.current_price)} + {formatNumber(getCurrentPrice(item))}
수익률 - {formatPercent(item.profit_rate)} + {formatPercent(getProfitRate(item))}
@@ -244,7 +297,11 @@ const StockTrade = () => {
액션 - {decision?.action ?? '-'} + + {decision?.action ?? + decision?.decision ?? + '-'} +
종목코드 @@ -268,14 +325,14 @@ const StockTrade = () => {
상태 - - {tradeResult?.success === true - ? '성공' - : tradeResult?.success === false - ? '실패' - : '대기'} - + {statusLabel}
+ {execution ? ( +
+ 실행 + {execution} +
+ ) : null}
주문번호