주식 매매 api 및 화면 오류 수정
This commit is contained in:
31
src/api.js
31
src/api.js
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user