주식 매매 api 및 화면 오류 수정

This commit is contained in:
2026-01-27 03:27:01 +09:00
parent 9ab45b64b6
commit 7d01c72e58
4 changed files with 148 additions and 36 deletions

View File

@@ -1,6 +1,31 @@
// src/api.js // 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) { 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) { if (!res.ok) {
const text = await res.text().catch(() => ""); const text = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
@@ -9,7 +34,7 @@ export async function apiGet(path) {
} }
export async function apiDelete(path) { export async function apiDelete(path) {
const res = await fetch(path, { method: "DELETE" }); const res = await fetch(toApiUrl(path), { method: "DELETE" });
if (!res.ok) { if (!res.ok) {
const text = await res.text().catch(() => ""); const text = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`); throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
@@ -18,7 +43,7 @@ export async function apiDelete(path) {
} }
export async function apiPost(path, body) { export async function apiPost(path, body) {
const res = await fetch(path, { const res = await fetch(toApiUrl(path), {
method: "POST", method: "POST",
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",

View File

@@ -83,7 +83,8 @@ const inferDateFromSlug = (slug) => {
export const getBlogPosts = () => { export const getBlogPosts = () => {
const modules = import.meta.glob('/src/content/blog/**/*.md', { const modules = import.meta.glob('/src/content/blog/**/*.md', {
as: 'raw', query: '?raw',
import: 'default',
eager: true, eager: true,
}); });

View File

@@ -26,18 +26,46 @@ const getLatestBy = (items, key) => {
}; };
const normalizeIndices = (data) => { const normalizeIndices = (data) => {
if (!data || typeof data !== 'object' || Array.isArray(data)) return []; if (!data) return [];
return Object.entries(data)
.filter(([, value]) => value && typeof value === 'object') if (Array.isArray(data)) {
.map(([name, value]) => ({ return data.map((item) => ({
name, name: item?.name ?? '-',
value: value?.value ?? '-', value: item?.value ?? '-',
change: value?.change ?? '', change: item?.change_value ?? item?.change ?? '',
percent: value?.percent ?? '', 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) => const pick = (value) =>
value === undefined || value === null || value === '' ? null : value; value === undefined || value === null || value === '' ? null : value;
const raw = pick(change) ?? pick(percent); const raw = pick(change) ?? pick(percent);
@@ -209,7 +237,8 @@ const Stock = () => {
sortedIndices.map((item) => { sortedIndices.map((item) => {
const direction = getDirection( const direction = getDirection(
item.change, item.change,
item.percent item.percent,
item.direction
); );
const changeText = [ const changeText = [
item.change, item.change,

View File

@@ -18,6 +18,40 @@ const formatPercent = (value) => {
return `${numeric.toFixed(2)}%`; 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 StockTrade = () => {
const [balance, setBalance] = useState(null); const [balance, setBalance] = useState(null);
const [balanceLoading, setBalanceLoading] = useState(false); const [balanceLoading, setBalanceLoading] = useState(false);
@@ -46,7 +80,7 @@ const StockTrade = () => {
try { try {
const result = await requestAutoTrade(); const result = await requestAutoTrade();
setAutoResult(result); setAutoResult(result);
if (result?.status === 'success') { if (result?.status === 'success' || result?.status === 'completed') {
await loadBalance(); await loadBalance();
} }
} catch (err) { } catch (err) {
@@ -60,14 +94,33 @@ const StockTrade = () => {
loadBalance(); loadBalance();
}, []); }, []);
const holdings = useMemo( const holdings = useMemo(() => {
() => (Array.isArray(balance?.holdings) ? balance.holdings : []), if (!balance) return [];
[balance] 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 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 autoStatus = autoResult?.status ?? '';
const decision = autoResult?.decision ?? null; const decision = autoResult?.decision ?? autoResult?.ai_response ?? null;
const tradeResult = autoResult?.trade_result ?? 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 ( return (
<div className="stock"> <div className="stock">
@@ -90,11 +143,11 @@ const StockTrade = () => {
<div className="stock-status"> <div className="stock-status">
<div> <div>
<span> 평가금액</span> <span> 평가금액</span>
<strong>{formatNumber(summary.total_eval)}</strong> <strong>{formatNumber(totalEval)}</strong>
</div> </div>
<div> <div>
<span>예수금</span> <span>예수금</span>
<strong>{formatNumber(summary.deposit)}</strong> <strong>{formatNumber(deposit)}</strong>
</div> </div>
<div> <div>
<span>보유 종목</span> <span>보유 종목</span>
@@ -137,11 +190,11 @@ const StockTrade = () => {
{[ {[
{ {
label: '총 평가', label: '총 평가',
value: summary.total_eval, value: totalEval,
}, },
{ {
label: '예수금', label: '예수금',
value: summary.deposit, value: deposit,
}, },
].map((item) => ( ].map((item) => (
<div <div
@@ -171,25 +224,25 @@ const StockTrade = () => {
<div> <div>
<span>수량</span> <span>수량</span>
<strong> <strong>
{formatNumber(item.qty)} {formatNumber(getQty(item))}
</strong> </strong>
</div> </div>
<div> <div>
<span>매입가</span> <span>매입가</span>
<strong> <strong>
{formatNumber(item.buy_price)} {formatNumber(getBuyPrice(item))}
</strong> </strong>
</div> </div>
<div> <div>
<span>현재가</span> <span>현재가</span>
<strong> <strong>
{formatNumber(item.current_price)} {formatNumber(getCurrentPrice(item))}
</strong> </strong>
</div> </div>
<div> <div>
<span>수익률</span> <span>수익률</span>
<strong> <strong>
{formatPercent(item.profit_rate)} {formatPercent(getProfitRate(item))}
</strong> </strong>
</div> </div>
</div> </div>
@@ -244,7 +297,11 @@ const StockTrade = () => {
<div className="stock-status"> <div className="stock-status">
<div> <div>
<span>액션</span> <span>액션</span>
<strong>{decision?.action ?? '-'}</strong> <strong>
{decision?.action ??
decision?.decision ??
'-'}
</strong>
</div> </div>
<div> <div>
<span>종목코드</span> <span>종목코드</span>
@@ -268,14 +325,14 @@ const StockTrade = () => {
<div className="stock-status"> <div className="stock-status">
<div> <div>
<span>상태</span> <span>상태</span>
<strong> <strong>{statusLabel}</strong>
{tradeResult?.success === true
? '성공'
: tradeResult?.success === false
? '실패'
: '대기'}
</strong>
</div> </div>
{execution ? (
<div>
<span>실행</span>
<strong>{execution}</strong>
</div>
) : null}
<div> <div>
<span>주문번호</span> <span>주문번호</span>
<strong> <strong>