From b01206e9e3d91fc8ffeaab4d9e249a9902381aa9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=EC=84=B1=EC=9C=A4?= <1112csy@naver.com>
Date: Wed, 3 Sep 2025 21:46:43 +0900
Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8:=20=EB=A7=88=EC=9D=B4=ED=8E=98?=
=?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?=
=?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/layout/Header.tsx | 14 +++++++++++++-
src/pages/myPage/index.tsx | 29 ++++++++++++++++++++++++++---
2 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index bad8298..5ae666d 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import BALLFiNLogo from "../../assets/BALLFiN.svg";
import { Link, useLocation } from "react-router-dom";
import { Menu, X, LogOut, User, Settings } from "lucide-react";
@@ -40,6 +40,7 @@ const Header = () => {
message: string;
type: "success" | "error";
}>({ show: false, message: "", type: "success" });
+ const wasLoggedInRef = useRef(false);
useEffect(() => {
// 로그인 상태 및 사용자 정보 확인
@@ -47,6 +48,15 @@ const Header = () => {
const token = localStorage.getItem("access_token");
if (token) {
+ // 로그인 전환 토스트
+ if (!wasLoggedInRef.current) {
+ setToast({
+ show: true,
+ message: "로그인되었습니다.",
+ type: "success",
+ });
+ }
+ wasLoggedInRef.current = true;
setIsLoggedIn(true);
// 먼저 localStorage에서 사용자 정보 확인
@@ -81,11 +91,13 @@ const Header = () => {
setIsLoggedIn(false);
setUserName("");
setUserEmail("");
+ wasLoggedInRef.current = false;
}
} else {
setIsLoggedIn(false);
setUserName("");
setUserEmail("");
+ wasLoggedInRef.current = false;
}
};
diff --git a/src/pages/myPage/index.tsx b/src/pages/myPage/index.tsx
index 31a268f..6f5bf18 100644
--- a/src/pages/myPage/index.tsx
+++ b/src/pages/myPage/index.tsx
@@ -11,6 +11,7 @@ import {
LogOut,
Edit3,
} from "lucide-react";
+import Toast from "@/components/common/Toast";
export default function MyPage() {
const [user] = useState({
@@ -21,6 +22,10 @@ export default function MyPage() {
membership: "프리미엄",
joinDate: "2024년 1월",
});
+ const [toast, setToast] = useState<{
+ message: string;
+ type: "success" | "error";
+ } | null>(null);
const menuItems = [
{
@@ -61,13 +66,31 @@ export default function MyPage() {
},
];
- const handleLogout = () => {
- // 로그아웃 로직
- console.log("로그아웃");
+ const handleLogout = async () => {
+ const { logout } = await import("@/api/auth/logoutApi");
+ try {
+ await logout();
+ setToast({ message: "로그아웃 되었습니다.", type: "success" });
+ } catch (_) {
+ setToast({ message: "로그아웃 중 오류가 발생했습니다.", type: "error" });
+ } finally {
+ localStorage.removeItem("access_token");
+ setTimeout(() => {
+ window.location.href = "/";
+ }, 800);
+ }
};
return (
+ {toast && (
+
setToast(null)}
+ duration={1500}
+ />
+ )}
{/* 프로필 카드 */}
Date: Wed, 3 Sep 2025 22:00:59 +0900
Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8:=20=ED=97=A4=EB=8D=94=20=EB=A1=9C?=
=?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EA=B8=B0?=
=?UTF-8?q?=ED=9A=8C=201=EC=9C=BC=EB=A1=9C=20=EC=A0=9C=ED=95=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/auth/loginApi.ts | 3 +++
src/components/layout/Header.tsx | 6 ++++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/api/auth/loginApi.ts b/src/api/auth/loginApi.ts
index 7686e89..001b6d3 100644
--- a/src/api/auth/loginApi.ts
+++ b/src/api/auth/loginApi.ts
@@ -68,6 +68,9 @@ export const login = async (
? responseData.access_token
: `Bearer ${responseData.access_token}`;
+ // 헤더에서 첫 로그인만 토스트를 띄우기 위한 플래그
+ sessionStorage.setItem("just_logged_in", "1");
+
return {
token,
user: responseData.user,
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 5ae666d..619f93f 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -48,13 +48,15 @@ const Header = () => {
const token = localStorage.getItem("access_token");
if (token) {
- // 로그인 전환 토스트
- if (!wasLoggedInRef.current) {
+ // 첫 로그인 플래그 확인
+ const justLoggedIn = sessionStorage.getItem("just_logged_in");
+ if (justLoggedIn) {
setToast({
show: true,
message: "로그인되었습니다.",
type: "success",
});
+ sessionStorage.removeItem("just_logged_in");
}
wasLoggedInRef.current = true;
setIsLoggedIn(true);
From d304084f76ff48d5a31c790106088467ee257035 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=EC=84=B1=EC=9C=A4?= <1112csy@naver.com>
Date: Thu, 4 Sep 2025 03:05:15 +0900
Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8:=20=EA=B8=B0=EC=88=A0=EC=A0=81?=
=?UTF-8?q?=EB=B6=84=EC=84=9D,=20=EC=9E=AC=EB=AC=B4=EB=B6=84=EC=84=9D=20ll?=
=?UTF-8?q?m=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=9C=EB=B9=99=20api=20?=
=?UTF-8?q?=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/stock/detail.ts | 21 ++++
.../stockDetail/FinancialStatement.tsx | 7 +-
.../stockDetail/TechnicalAnalysis.tsx | 103 +++++++++---------
src/lib/axiosInstance.ts | 2 +-
src/pages/stock/StockDetailPage.tsx | 85 ++++++++++++---
5 files changed, 147 insertions(+), 71 deletions(-)
diff --git a/src/api/stock/detail.ts b/src/api/stock/detail.ts
index 7eb88ba..ffe7b1b 100644
--- a/src/api/stock/detail.ts
+++ b/src/api/stock/detail.ts
@@ -40,3 +40,24 @@ export async function getStockChart(
);
return data;
}
+
+// Total analysis (LLM) API
+export interface TotalAnalysisResponse {
+ main_analysis?: string;
+ volatility_analysis?: string;
+ volume_analysis?: string;
+ fin_total_analysis?: string;
+ company_analysis?: string;
+ total_analysis?: string;
+ combined_technical_analysis?: string;
+}
+
+export async function getTotalAnalysis(
+ stockCode: string
+): Promise {
+ const { data } = await axiosInstance.get(
+ `/api/info/total_analysis/${encodeURIComponent(stockCode)}`,
+ { timeout: 60000 } // LLM 분석은 60초 타임아웃
+ );
+ return data;
+}
diff --git a/src/components/stockDetail/FinancialStatement.tsx b/src/components/stockDetail/FinancialStatement.tsx
index 5938328..e930314 100644
--- a/src/components/stockDetail/FinancialStatement.tsx
+++ b/src/components/stockDetail/FinancialStatement.tsx
@@ -33,7 +33,7 @@ export default function FinancialStatement({
}: FinancialStatementProps) {
const [hoveredIndicator, setHoveredIndicator] = useState(null);
- const company = analysis?.company_analysis;
+ const company = analysis?.company_data; // 재무 데이터
const isAnalysisLoading = !company;
// 간단 스켈레톤 텍스트
@@ -303,7 +303,10 @@ export default function FinancialStatement({
) : (
- {safeText(company?.total_analysis)}
+ {safeText(
+ analysis?.company_analysis ??
+ "재무 분석 정보를 불러오고 있습니다."
+ )}
)}
diff --git a/src/components/stockDetail/TechnicalAnalysis.tsx b/src/components/stockDetail/TechnicalAnalysis.tsx
index 1bcb525..736b924 100644
--- a/src/components/stockDetail/TechnicalAnalysis.tsx
+++ b/src/components/stockDetail/TechnicalAnalysis.tsx
@@ -68,26 +68,34 @@ export default function TechnicalAnalysis({
return "text-blue-600";
};
- const main = analysis?.main_analysis;
- const vola = analysis?.volatility_analysis;
- const vol = analysis?.volume_analysis;
+ // API 응답이 문자열이므로 직접 사용
+ const mainAnalysisText = analysis?.main_analysis;
+ const volatilityAnalysisText = analysis?.volatility_analysis;
+ const volumeAnalysisText = analysis?.volume_analysis;
+ const combinedAnalysisText = analysis?.combined_technical_analysis;
+
+ // 기존 기술적 지표 데이터
+ const mainData = analysis?.main_analysis_data;
+ const volaData = analysis?.volatility_analysis_data;
+ const volData = analysis?.volume_analysis_data;
const isAnalysisLoading = !analysis;
- // 기술적 지표 값 (없으면 기본값)
- const rsi = main?.rsi?.value ?? 28.4;
- const dailyRange = vola?.volatility?.value?.volatility_percent ?? 2.8;
- const avgVolatility = vola?.volatility?.value?.avg_volatility_percent ?? 2.8;
- const currentVolume = toNum(vol?.volume?.value?.volume) || 15234567;
- const avgVolume = toNum(vol?.volume?.value?.avg_volume_20) || 12456789;
+ // 기술적 지표 값 (실제 데이터 사용)
+ const rsi = mainData?.rsi?.value ?? 28.4;
+ const dailyRange = volaData?.volatility?.value?.volatility_percent ?? 2.8;
+ const avgVolatility =
+ volaData?.volatility?.value?.avg_volatility_percent ?? 2.8;
+ const currentVolume = toNum(volData?.volume?.value?.volume) || 15234567;
+ const avgVolume = toNum(volData?.volume?.value?.avg_volume_20) || 12456789;
const volumeRatio = (
((currentVolume - avgVolume) / (avgVolume || 1)) *
100
).toFixed(1);
const mfiValue =
- typeof vol?.mfi?.value === "number" ? vol.mfi.value : undefined;
- const obvValue = toNum(vol?.obv?.value?.obv);
- const obvMa20Value = toNum(vol?.obv?.value?.obv_ma20);
+ typeof volData?.mfi?.value === "number" ? volData.mfi.value : undefined;
+ const obvValue = toNum(volData?.obv?.value?.obv);
+ const obvMa20Value = toNum(volData?.obv?.value?.obv_ma20);
const tabs = [
{ id: "summary", label: "주요 지표", icon: Target },
@@ -113,20 +121,21 @@ export default function TechnicalAnalysis({
{isAnalysisLoading ? (
) : (
- (main?.moving_average?.arrangement?.status ?? "정보 없음")
+ (mainData?.moving_average?.arrangement?.status ??
+ "정보 없음")
)}
{hoveredKey === "summary_ma" && (
- {main?.moving_average?.arrangement?.description ??
- main?.moving_average?.price_vs_ma20?.description ??
- ""}
+ {mainData?.moving_average?.arrangement?.description ??
+ mainData?.moving_average?.price_vs_ma20?.description ??
+ "이동평균선 분석 정보"}
@@ -144,17 +153,19 @@ export default function TechnicalAnalysis({
{isAnalysisLoading ? (
) : (
- (main?.stochastic?.status ?? "정보 없음")
+ (mainData?.stochastic?.status ?? "정보 없음")
)}
{hoveredKey === "summary_stoch" && (
-
{main?.stochastic?.analysis ?? ""}
+
+ {mainData?.stochastic?.analysis ?? "스토캐스틱 분석 정보"}
+
)}
@@ -169,12 +180,12 @@ export default function TechnicalAnalysis({
RSI
{isAnalysisLoading ? (
) : (
- (main?.rsi?.status ?? "중립")
+ (mainData?.rsi?.status ?? "중립")
)}
{hoveredKey === "summary_rsi" && (
@@ -196,17 +207,17 @@ export default function TechnicalAnalysis({
{isAnalysisLoading ? (
) : (
- (main?.rsi?.status ?? "중립")
+ (mainData?.macd?.status ?? "중립")
)}
{hoveredKey === "summary_total" && (
-
{main?.macd?.analysis ?? ""}
+
{mainData?.macd?.analysis ?? "종합 신호 분석"}
)}
@@ -225,7 +236,7 @@ export default function TechnicalAnalysis({
) : (
- {main?.total_analysis ?? "분석 정보를 불러오고 있습니다."}
+ {mainAnalysisText ?? "분석 정보를 불러오고 있습니다."}
)}
@@ -287,13 +298,7 @@ export default function TechnicalAnalysis({
{hoveredKey === "vol_avg" && (
-
- RVI:{" "}
- {typeof vola?.rvi?.value?.rvi === "number"
- ? vola.rvi.value.rvi.toFixed(4)
- : "-"}{" "}
- | ATR: {vola?.atr?.value?.atr ?? "-"}
-
+
RVI: - | ATR: -
)}
@@ -311,17 +316,17 @@ export default function TechnicalAnalysis({
ATR
{isAnalysisLoading ? (
) : (
- (vola?.atr?.status ?? "정보 없음")
+ (volaData?.atr?.status ?? "정보 없음")
)}
{hoveredKey === "vol_atr" && (
-
{vola?.atr?.analysis ?? ""}
+
{volaData?.atr?.analysis ?? "ATR 분석 정보"}
)}
@@ -336,17 +341,17 @@ export default function TechnicalAnalysis({
RVI
{isAnalysisLoading ? (
) : (
- (vola?.rvi?.status ?? "정보 없음")
+ (volaData?.rvi?.status ?? "정보 없음")
)}
{hoveredKey === "vol_rvi" && (
-
{vola?.rvi?.analysis ?? ""}
+
{volaData?.rvi?.analysis ?? "RVI 분석 정보"}
)}
@@ -366,8 +371,7 @@ export default function TechnicalAnalysis({
) : (
- {vola?.total_analysis ??
- vola?.volatility?.analysis ??
+ {volatilityAnalysisText ??
"변동성 분석 정보를 불러오고 있습니다."}
)}
@@ -429,12 +433,12 @@ export default function TechnicalAnalysis({
MFI
{isAnalysisLoading ? (
) : (
- (vol?.mfi?.status ?? "정보 없음")
+ (volData?.mfi?.status ?? "정보 없음")
)}
{hoveredKey === "vol_mfi_card" && (
@@ -444,7 +448,7 @@ export default function TechnicalAnalysis({
? `값 ${mfiValue.toFixed(2)}`
: ""}
- {vol?.mfi?.analysis ?? ""}
+ {volData?.mfi?.analysis ?? "MFI 분석 정보"}
)}
@@ -459,12 +463,12 @@ export default function TechnicalAnalysis({
OBV
{isAnalysisLoading ? (
) : (
- (vol?.obv?.status ?? "정보 없음")
+ (volData?.obv?.status ?? "정보 없음")
)}
{hoveredKey === "vol_obv_card" && (
@@ -473,7 +477,7 @@ export default function TechnicalAnalysis({
{obvValue ? obvValue.toLocaleString() : "-"} / MA20{" "}
{obvMa20Value ? obvMa20Value.toLocaleString() : "-"}
- {vol?.obv?.analysis ?? ""}
+ {volData?.obv?.analysis ?? "OBV 분석 정보"}
)}
@@ -493,8 +497,7 @@ export default function TechnicalAnalysis({
) : (
- {vol?.total_analysis ??
- vol?.volume?.analysis ??
+ {volumeAnalysisText ??
`평균 대비 ${volumeRatio}% 수준의 거래량입니다.`}
)}
@@ -519,7 +522,7 @@ export default function TechnicalAnalysis({
) : (
- {analysis?.fin_total_analysis ??
+ {combinedAnalysisText ??
"종합 분석 정보를 불러오고 있습니다."}
)}
diff --git a/src/lib/axiosInstance.ts b/src/lib/axiosInstance.ts
index b3d75d0..7151fc8 100644
--- a/src/lib/axiosInstance.ts
+++ b/src/lib/axiosInstance.ts
@@ -7,7 +7,7 @@ const baseURL = import.meta.env.DEV
export const axiosInstance = axios.create({
baseURL,
- timeout: 15000,
+ timeout: 30000, // 30초로 증가
headers: {
"Content-Type": "application/json",
},
diff --git a/src/pages/stock/StockDetailPage.tsx b/src/pages/stock/StockDetailPage.tsx
index 6e120c8..7ca8622 100644
--- a/src/pages/stock/StockDetailPage.tsx
+++ b/src/pages/stock/StockDetailPage.tsx
@@ -10,6 +10,7 @@ import {
getStockInfoByCode,
getCompanyInfoByCode,
getStockChart,
+ getTotalAnalysis,
} from "@/api/stock";
import { getNewsByCompany } from "@/api/news";
@@ -185,24 +186,74 @@ export default function StockDetailPage() {
if (!cancelled) setStock(mapped);
- // 기술적/재무 데이터 매핑
- if (!cancelled && companyInfo) {
- // setTechSummary(companyInfo.main_analysis ?? null);
- setCompanyAnalysis(companyInfo);
- setFinancialData({
- revenue:
- (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
- netIncome:
- (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
- debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
- roe: companyInfo.company_analysis?.ROE ?? 0,
- per: companyInfo.company_analysis?.PER ?? 0,
- pbr: companyInfo.company_analysis?.PBR ?? 0,
- dividendYield: 0,
- });
+ // LLM 종합 분석 요청
+ try {
+ const totalAnalysis = await getTotalAnalysis(code);
+ console.log("Total Analysis Response:", totalAnalysis);
+
+ // 기술적/재무 데이터 매핑
+ if (!cancelled && companyInfo) {
+ const mergedAnalysis = {
+ ...totalAnalysis,
+ company_analysis: totalAnalysis.company_analysis, // API에서 받은 재무 분석 텍스트
+ company_data: companyInfo.company_analysis ?? companyInfo, // 기업 재무 데이터
+ main_analysis: totalAnalysis.main_analysis,
+ volume_analysis: totalAnalysis.volume_analysis,
+ volatility_analysis: totalAnalysis.volatility_analysis,
+ combined_technical_analysis:
+ totalAnalysis.combined_technical_analysis,
+ fin_total_analysis: totalAnalysis.fin_total_analysis,
+ // 기존 기술적 지표 데이터도 포함
+ main_analysis_data: companyInfo.main_analysis,
+ volatility_analysis_data: companyInfo.volatility_analysis,
+ volume_analysis_data: companyInfo.volume_analysis,
+ };
+ setCompanyAnalysis(mergedAnalysis);
+ setFinancialData({
+ revenue:
+ (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
+ netIncome:
+ (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
+ debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
+ roe: companyInfo.company_analysis?.ROE ?? 0,
+ per: companyInfo.company_analysis?.PER ?? 0,
+ pbr: companyInfo.company_analysis?.PBR ?? 0,
+ dividendYield: 0,
+ });
+ }
+ } catch (error) {
+ console.error("Total Analysis API Error:", error);
+ // API 실패 시에도 기본 분석 데이터 설정
+ if (!cancelled && companyInfo) {
+ const fallbackAnalysis = {
+ company_analysis: "재무 분석 정보를 불러오고 있습니다.",
+ company_data: companyInfo.company_analysis ?? companyInfo,
+ main_analysis: "분석 정보를 불러오고 있습니다.",
+ volume_analysis: "분석 정보를 불러오고 있습니다.",
+ volatility_analysis: "분석 정보를 불러오고 있습니다.",
+ combined_technical_analysis: "분석 정보를 불러오고 있습니다.",
+ fin_total_analysis: "분석 정보를 불러오고 있습니다.",
+ // 기존 기술적 지표 데이터도 포함
+ main_analysis_data: companyInfo.main_analysis,
+ volatility_analysis_data: companyInfo.volatility_analysis,
+ volume_analysis_data: companyInfo.volume_analysis,
+ };
+ setCompanyAnalysis(fallbackAnalysis);
+ setFinancialData({
+ revenue:
+ (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
+ netIncome:
+ (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
+ debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
+ roe: companyInfo.company_analysis?.ROE ?? 0,
+ per: companyInfo.company_analysis?.PER ?? 0,
+ pbr: companyInfo.company_analysis?.PBR ?? 0,
+ dividendYield: 0,
+ });
+ }
}
- // 차트 데이터 API 연동 (일봉 180개 기본)
+ // 차트, 뉴스
const chartRes = await getStockChart(code, "D", 180);
const chartData: HistoricalData[] = chartRes.candles.map((c) => ({
date: c.date,
@@ -213,7 +264,6 @@ export default function StockDetailPage() {
volume: c.volume,
}));
- // 기업 뉴스 API 연동
const companyNews = await getNewsByCompany(code, 10);
const mappedNews: NewsListItem[] = companyNews.map((n) => ({
id: n.id,
@@ -237,7 +287,6 @@ export default function StockDetailPage() {
setHistoricalData(chartData);
setNews(mappedNews);
setIsChartLoading(false);
- // financialData는 company API에서 세팅됨 (실패 시에만 목데이터 유지)
if (!financialData) setFinancialData(mockFinancialData);
}
} catch {
From acaeed092499d58b8361a3e05e612893257608ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=EC=84=B1=EC=9C=A4?= <1112csy@naver.com>
Date: Thu, 4 Sep 2025 03:11:07 +0900
Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8:=20=EC=A3=BC=EC=8B=9D=EC=84=B8?=
=?UTF-8?q?=EB=B6=80=20api=20=EB=A1=9C=EB=94=A9=20=EC=88=9C=EC=84=9C=20?=
=?UTF-8?q?=EC=A7=80=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../stockDetail/FinancialStatement.tsx | 4 +-
.../stockDetail/TechnicalAnalysis.tsx | 28 +--
src/pages/stock/StockDetailPage.tsx | 178 ++++++++++--------
3 files changed, 116 insertions(+), 94 deletions(-)
diff --git a/src/components/stockDetail/FinancialStatement.tsx b/src/components/stockDetail/FinancialStatement.tsx
index e930314..319c9f1 100644
--- a/src/components/stockDetail/FinancialStatement.tsx
+++ b/src/components/stockDetail/FinancialStatement.tsx
@@ -14,6 +14,7 @@ interface FinancialData {
interface FinancialStatementProps {
data: FinancialData;
analysis?: any; // /info/company/{code} 응답 객체 (company_analysis 포함)
+ isAnalysisLoading?: boolean; // LLM 분석 로딩 상태
}
interface FinancialIndicator {
@@ -30,6 +31,7 @@ interface FinancialIndicator {
export default function FinancialStatement({
data,
analysis,
+ isAnalysisLoading: _isAnalysisLoading = false,
}: FinancialStatementProps) {
const [hoveredIndicator, setHoveredIndicator] = useState(null);
@@ -296,7 +298,7 @@ export default function FinancialStatement({
종합 해석
- {isAnalysisLoading ? (
+ {_isAnalysisLoading ? (
diff --git a/src/components/stockDetail/TechnicalAnalysis.tsx b/src/components/stockDetail/TechnicalAnalysis.tsx
index 736b924..2640924 100644
--- a/src/components/stockDetail/TechnicalAnalysis.tsx
+++ b/src/components/stockDetail/TechnicalAnalysis.tsx
@@ -34,6 +34,8 @@ interface TechnicalAnalysisProps {
stock: StockDetail;
historicalData: HistoricalData[];
analysis?: any; // /info/company/{code} 응답 전체 또는 필요한 섹션 포함 객체
+ isAnalysisLoading?: boolean; // LLM 분석 로딩 상태
+ isTechnicalLoading?: boolean; // 기술적 분석 데이터 로딩 상태
}
type TabType = "summary" | "volatility" | "volume" | "overview";
@@ -42,6 +44,8 @@ export default function TechnicalAnalysis({
stock: _stock,
historicalData: _historicalData,
analysis,
+ isAnalysisLoading: _isAnalysisLoading = false,
+ isTechnicalLoading: _isTechnicalLoading = true,
}: TechnicalAnalysisProps) {
const [activeTab, setActiveTab] = useState
("summary");
const [hoveredKey, setHoveredKey] = useState(null);
@@ -123,7 +127,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(mainData?.moving_average?.arrangement?.status ??
@@ -155,7 +159,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(mainData?.stochastic?.status ?? "정보 없음")
@@ -182,7 +186,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(mainData?.rsi?.status ?? "중립")
@@ -209,7 +213,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(mainData?.macd?.status ?? "중립")
@@ -229,7 +233,7 @@ export default function TechnicalAnalysis({
주요지표 분석
- {isAnalysisLoading ? (
+ {_isAnalysisLoading ? (
@@ -318,7 +322,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(volaData?.atr?.status ?? "정보 없음")
@@ -343,7 +347,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(volaData?.rvi?.status ?? "정보 없음")
@@ -364,7 +368,7 @@ export default function TechnicalAnalysis({
변동성 분석
- {isAnalysisLoading ? (
+ {_isAnalysisLoading ? (
@@ -435,7 +439,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(volData?.mfi?.status ?? "정보 없음")
@@ -465,7 +469,7 @@ export default function TechnicalAnalysis({
- {isAnalysisLoading ? (
+ {_isTechnicalLoading ? (
) : (
(volData?.obv?.status ?? "정보 없음")
@@ -490,7 +494,7 @@ export default function TechnicalAnalysis({
거래량 분석
- {isAnalysisLoading ? (
+ {_isAnalysisLoading ? (
@@ -515,7 +519,7 @@ export default function TechnicalAnalysis({
기술적 분석 종합 해석
- {isAnalysisLoading ? (
+ {_isAnalysisLoading ? (
diff --git a/src/pages/stock/StockDetailPage.tsx b/src/pages/stock/StockDetailPage.tsx
index 7ca8622..66fb2dc 100644
--- a/src/pages/stock/StockDetailPage.tsx
+++ b/src/pages/stock/StockDetailPage.tsx
@@ -89,6 +89,7 @@ export default function StockDetailPage() {
null
);
const [companyAnalysis, setCompanyAnalysis] = useState
(null);
+ const [isAnalysisLoading, setIsAnalysisLoading] = useState(true);
// const [techSummary, setTechSummary] = useState(null);
// 기술적 분석을 로딩 중에도 렌더링하기 위한 기본 스톡 값
@@ -106,7 +107,9 @@ export default function StockDetailPage() {
};
// 섹션별 로딩 상태
+ const [isHeaderLoading, setIsHeaderLoading] = useState(true);
const [isChartLoading, setIsChartLoading] = useState(true);
+ const [isTechnicalLoading, setIsTechnicalLoading] = useState(true);
const isNewsLoading = news.length === 0;
const isFinancialLoading = financialData == null;
@@ -127,6 +130,9 @@ export default function StockDetailPage() {
const run = async () => {
try {
if (!code) return;
+
+ // 1단계: Stock Header 데이터 로딩
+ console.log("1단계: Stock Header 로딩 시작");
const [priceInfo, companyInfo] = await Promise.all([
getStockInfoByCode(code),
getCompanyInfoByCode(code),
@@ -184,76 +190,14 @@ export default function StockDetailPage() {
prediction: { targetPrice: 0, confidence: 0, recommendation: "hold" },
};
- if (!cancelled) setStock(mapped);
-
- // LLM 종합 분석 요청
- try {
- const totalAnalysis = await getTotalAnalysis(code);
- console.log("Total Analysis Response:", totalAnalysis);
-
- // 기술적/재무 데이터 매핑
- if (!cancelled && companyInfo) {
- const mergedAnalysis = {
- ...totalAnalysis,
- company_analysis: totalAnalysis.company_analysis, // API에서 받은 재무 분석 텍스트
- company_data: companyInfo.company_analysis ?? companyInfo, // 기업 재무 데이터
- main_analysis: totalAnalysis.main_analysis,
- volume_analysis: totalAnalysis.volume_analysis,
- volatility_analysis: totalAnalysis.volatility_analysis,
- combined_technical_analysis:
- totalAnalysis.combined_technical_analysis,
- fin_total_analysis: totalAnalysis.fin_total_analysis,
- // 기존 기술적 지표 데이터도 포함
- main_analysis_data: companyInfo.main_analysis,
- volatility_analysis_data: companyInfo.volatility_analysis,
- volume_analysis_data: companyInfo.volume_analysis,
- };
- setCompanyAnalysis(mergedAnalysis);
- setFinancialData({
- revenue:
- (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
- netIncome:
- (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
- debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
- roe: companyInfo.company_analysis?.ROE ?? 0,
- per: companyInfo.company_analysis?.PER ?? 0,
- pbr: companyInfo.company_analysis?.PBR ?? 0,
- dividendYield: 0,
- });
- }
- } catch (error) {
- console.error("Total Analysis API Error:", error);
- // API 실패 시에도 기본 분석 데이터 설정
- if (!cancelled && companyInfo) {
- const fallbackAnalysis = {
- company_analysis: "재무 분석 정보를 불러오고 있습니다.",
- company_data: companyInfo.company_analysis ?? companyInfo,
- main_analysis: "분석 정보를 불러오고 있습니다.",
- volume_analysis: "분석 정보를 불러오고 있습니다.",
- volatility_analysis: "분석 정보를 불러오고 있습니다.",
- combined_technical_analysis: "분석 정보를 불러오고 있습니다.",
- fin_total_analysis: "분석 정보를 불러오고 있습니다.",
- // 기존 기술적 지표 데이터도 포함
- main_analysis_data: companyInfo.main_analysis,
- volatility_analysis_data: companyInfo.volatility_analysis,
- volume_analysis_data: companyInfo.volume_analysis,
- };
- setCompanyAnalysis(fallbackAnalysis);
- setFinancialData({
- revenue:
- (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
- netIncome:
- (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
- debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
- roe: companyInfo.company_analysis?.ROE ?? 0,
- per: companyInfo.company_analysis?.PER ?? 0,
- pbr: companyInfo.company_analysis?.PBR ?? 0,
- dividendYield: 0,
- });
- }
+ if (!cancelled) {
+ setStock(mapped);
+ setIsHeaderLoading(false);
+ console.log("1단계: Stock Header 로딩 완료");
}
- // 차트, 뉴스
+ // 2단계: 차트 데이터 로딩
+ console.log("2단계: 차트 데이터 로딩 시작");
const chartRes = await getStockChart(code, "D", 180);
const chartData: HistoricalData[] = chartRes.candles.map((c) => ({
date: c.date,
@@ -264,6 +208,46 @@ export default function StockDetailPage() {
volume: c.volume,
}));
+ if (!cancelled) {
+ setHistoricalData(chartData);
+ setIsChartLoading(false);
+ console.log("2단계: 차트 데이터 로딩 완료");
+ }
+
+ // 3단계: 기술적 분석 데이터 로딩
+ console.log("3단계: 기술적 분석 데이터 로딩 시작");
+ if (!cancelled && companyInfo) {
+ const initialAnalysis = {
+ company_analysis: "재무 분석 정보를 불러오고 있습니다.",
+ company_data: companyInfo.company_analysis ?? companyInfo,
+ main_analysis: "분석 정보를 불러오고 있습니다.",
+ volume_analysis: "분석 정보를 불러오고 있습니다.",
+ volatility_analysis: "분석 정보를 불러오고 있습니다.",
+ combined_technical_analysis: "분석 정보를 불러오고 있습니다.",
+ fin_total_analysis: "분석 정보를 불러오고 있습니다.",
+ // 기존 기술적 지표 데이터도 포함
+ main_analysis_data: companyInfo.main_analysis,
+ volatility_analysis_data: companyInfo.volatility_analysis,
+ volume_analysis_data: companyInfo.volume_analysis,
+ };
+ setCompanyAnalysis(initialAnalysis);
+ setFinancialData({
+ revenue:
+ (companyInfo.company_analysis?.["매출액"] ?? 0) * 1_000_000_000,
+ netIncome:
+ (companyInfo.company_analysis?.["순이익"] ?? 0) * 1_000_000_000,
+ debtRatio: companyInfo.company_analysis?.["부채비율"] ?? 0,
+ roe: companyInfo.company_analysis?.ROE ?? 0,
+ per: companyInfo.company_analysis?.PER ?? 0,
+ pbr: companyInfo.company_analysis?.PBR ?? 0,
+ dividendYield: 0,
+ });
+ setIsTechnicalLoading(false);
+ console.log("3단계: 기술적 분석 데이터 로딩 완료");
+ }
+
+ // 4단계: 뉴스 데이터 로딩 (백그라운드)
+ console.log("4단계: 뉴스 데이터 로딩 시작");
const companyNews = await getNewsByCompany(code, 10);
const mappedNews: NewsListItem[] = companyNews.map((n) => ({
id: n.id,
@@ -273,22 +257,49 @@ export default function StockDetailPage() {
sentiment: (n.impact as any) ?? "neutral",
}));
- const mockFinancialData: FinancialData = {
- revenue: 279600000000000,
- netIncome: 15400000000000,
- debtRatio: 23.4,
- roe: 15.8,
- per: 12.3,
- pbr: 1.2,
- dividendYield: 2.1,
- };
-
if (!cancelled) {
- setHistoricalData(chartData);
setNews(mappedNews);
- setIsChartLoading(false);
- if (!financialData) setFinancialData(mockFinancialData);
+ console.log("4단계: 뉴스 데이터 로딩 완료");
}
+
+ // 5단계: LLM 종합 분석 요청 (비동기로 별도 처리)
+ const loadAnalysis = async () => {
+ try {
+ console.log("5단계: LLM 분석 로딩 시작");
+ const totalAnalysis = await getTotalAnalysis(code);
+ console.log("Total Analysis Response:", totalAnalysis);
+
+ if (!cancelled && companyInfo) {
+ const mergedAnalysis = {
+ ...totalAnalysis,
+ company_analysis: totalAnalysis.company_analysis, // API에서 받은 재무 분석 텍스트
+ company_data: companyInfo.company_analysis ?? companyInfo, // 기업 재무 데이터
+ main_analysis: totalAnalysis.main_analysis,
+ volume_analysis: totalAnalysis.volume_analysis,
+ volatility_analysis: totalAnalysis.volatility_analysis,
+ combined_technical_analysis:
+ totalAnalysis.combined_technical_analysis,
+ fin_total_analysis: totalAnalysis.fin_total_analysis,
+ // 기존 기술적 지표 데이터도 포함
+ main_analysis_data: companyInfo.main_analysis,
+ volatility_analysis_data: companyInfo.volatility_analysis,
+ volume_analysis_data: companyInfo.volume_analysis,
+ };
+ setCompanyAnalysis(mergedAnalysis);
+ console.log("5단계: LLM 분석 로딩 완료");
+ }
+ } catch (error) {
+ console.error("Total Analysis API Error:", error);
+ // API 실패 시에도 기본 분석 데이터는 유지
+ } finally {
+ if (!cancelled) {
+ setIsAnalysisLoading(false);
+ }
+ }
+ };
+
+ // LLM 분석을 별도로 실행
+ loadAnalysis();
} catch {
// 실패 시에도 목데이터로 모든 섹션 채워서 UI가 비지 않도록 처리
if (!cancelled) {
@@ -309,6 +320,7 @@ export default function StockDetailPage() {
},
} as StockDetail;
setStock(fallback);
+ setIsHeaderLoading(false);
const mockHistoricalData: HistoricalData[] = Array.from(
{ length: 30 },
@@ -356,6 +368,7 @@ export default function StockDetailPage() {
dividendYield: 2.1,
};
setFinancialData(mockFinancialData);
+ setIsTechnicalLoading(false);
}
}
};
@@ -409,6 +422,8 @@ export default function StockDetailPage() {
stock={stock ?? placeholderStock}
historicalData={historicalData}
analysis={companyAnalysis}
+ isAnalysisLoading={isAnalysisLoading}
+ isTechnicalLoading={isTechnicalLoading}
/>
@@ -474,6 +489,7 @@ export default function StockDetailPage() {
)}
From 7e56e5c7532756b2f280f8d7b024585d5c83377c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=EC=84=B1=EC=9C=A4?= <1112csy@naver.com>
Date: Thu, 4 Sep 2025 13:48:28 +0900
Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=8E=A8:=20=20=EC=B0=A8=ED=8A=B8=20?=
=?UTF-8?q?=EA=B0=84=20=ED=95=84=ED=84=B0=EB=A7=81=20=EB=B3=80=ED=99=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/stock/detail.ts | 3 +-
.../stockDetail/chart/PriceVolumeChart.tsx | 143 +++++++++++++-----
.../stockDetail/chart/StockChartHeader.tsx | 59 +++++---
src/components/stockDetail/chart/index.tsx | 86 ++++++++---
src/pages/stock/StockDetailPage.tsx | 47 +++---
5 files changed, 235 insertions(+), 103 deletions(-)
diff --git a/src/api/stock/detail.ts b/src/api/stock/detail.ts
index ffe7b1b..4115980 100644
--- a/src/api/stock/detail.ts
+++ b/src/api/stock/detail.ts
@@ -56,8 +56,7 @@ export async function getTotalAnalysis(
stockCode: string
): Promise
{
const { data } = await axiosInstance.get(
- `/api/info/total_analysis/${encodeURIComponent(stockCode)}`,
- { timeout: 60000 } // LLM 분석은 60초 타임아웃
+ `/api/info/total_analysis/${encodeURIComponent(stockCode)}`
);
return data;
}
diff --git a/src/components/stockDetail/chart/PriceVolumeChart.tsx b/src/components/stockDetail/chart/PriceVolumeChart.tsx
index dea63a2..1e3c1ca 100644
--- a/src/components/stockDetail/chart/PriceVolumeChart.tsx
+++ b/src/components/stockDetail/chart/PriceVolumeChart.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, memo } from "react";
import Highcharts from "highcharts/highstock";
import HighchartsReact from "highcharts-react-official";
import { TimeRangePT } from ".";
@@ -12,38 +12,40 @@ export interface PriceVolumeChartProps {
showMA: Record<"ma5" | "ma20" | "ma60" | "ma120", boolean>;
}
-export default function PriceVolumeChart({
+const PriceVolumeChart = memo(function PriceVolumeChart({
data,
timeRange,
showMA,
}: PriceVolumeChartProps) {
- // 초기부터 렌더링하여 차트가 안 보이는 문제 방지
- const [ready, setReady] = useState(true);
+ // TradingView 차트 우선 사용
+ const [ready, setReady] = useState(false);
const tvContainerRef = useRef(null);
const [tvReady, setTvReady] = useState(false);
+ const [tvFailed, setTvFailed] = useState(false);
- // Annotations 모듈 안전 로딩 (환경별 export 방식 대응)
+ // TradingView 실패 시에만 Highcharts 모듈 로딩
useEffect(() => {
- (async () => {
- try {
- const mod: any = await import("highcharts/modules/annotations");
- const initFn =
- typeof mod === "function"
- ? mod
- : typeof mod?.default === "function"
- ? mod.default
- : null;
- if (initFn) {
- initFn(Highcharts);
+ if (tvFailed) {
+ (async () => {
+ try {
+ const mod: any = await import("highcharts/modules/annotations");
+ const initFn =
+ typeof mod === "function"
+ ? mod
+ : typeof mod?.default === "function"
+ ? mod.default
+ : null;
+ if (initFn) {
+ initFn(Highcharts);
+ }
+ } catch (_) {
+ // 실패해도 치명적이지 않음
+ } finally {
+ setReady(true);
}
- } catch (_) {
- // 실패해도 치명적이지 않음
- } finally {
- // 모듈 로딩 실패/성공과 무관하게 이미 렌더링 중
- setReady(true);
- }
- })();
- }, []);
+ })();
+ }
+ }, [tvFailed]);
// TradingView Lightweight Charts 렌더링 (가능하면 우선 사용)
useEffect(() => {
@@ -55,6 +57,14 @@ export default function PriceVolumeChart({
let ma60Series: any | null = null;
let ma120Series: any | null = null;
+ // TradingView 로딩 타임아웃 (3초)
+ const timeoutId = setTimeout(() => {
+ if (!tvReady && !tvFailed) {
+ console.log("TradingView 차트 로딩 타임아웃, Highcharts로 폴백");
+ setTvFailed(true);
+ }
+ }, 3000);
+
(async () => {
if (!tvContainerRef.current) return;
try {
@@ -161,19 +171,25 @@ export default function PriceVolumeChart({
chart.timeScale().fitContent();
setTvReady(true);
- } catch {
+ setTvFailed(false);
+ clearTimeout(timeoutId); // 성공 시 타임아웃 클리어
+ } catch (error) {
+ console.error("TradingView 차트 로딩 실패:", error);
setTvReady(false);
+ setTvFailed(true);
+ clearTimeout(timeoutId); // 실패 시 타임아웃 클리어
}
})();
return () => {
+ clearTimeout(timeoutId); // 컴포넌트 언마운트 시 타임아웃 클리어
if (chart && tvContainerRef.current) {
try {
chart.remove?.();
} catch {}
}
};
- }, [data, showMA]);
+ }, [data, showMA, tvReady, tvFailed]);
// timeRange 에 따른 데이터 필터링 및 Point 생성
const {
@@ -274,28 +290,36 @@ export default function PriceVolumeChart({
};
}, [data, timeRange]);
- // 3) 차트 옵션
+ // 3) 차트 옵션 (최적화)
const options: Highcharts.Options = useMemo(() => {
return {
chart: {
zoomType: "x",
backgroundColor: "#ffffff",
height: 500,
+ animation: {
+ duration: 500, // 부드러운 전환을 위한 애니메이션
+ easing: "easeInOutCubic",
+ },
+ reflow: false, // 리플로우 비활성화
},
accessibility: { enabled: false },
title: { text: "" },
xAxis: {
type: "datetime",
- crosshair: true,
+ crosshair: false, // 크로스헤어 비활성화로 성능 향상
gridLineWidth: 1,
gridLineColor: "#f1f5f9",
+ labels: {
+ step: Math.max(1, Math.floor(ohlcData.length / 10)), // 라벨 수 줄이기
+ },
},
yAxis: [
{
title: { text: "" },
height: "65%",
lineWidth: 2,
- crosshair: true,
+ crosshair: false, // 크로스헤어 비활성화
opposite: true,
gridLineColor: "#eef2f7",
labels: { style: { color: "#475569" } },
@@ -314,6 +338,7 @@ export default function PriceVolumeChart({
tooltip: {
shared: false,
split: true,
+ animation: false, // 툴팁 애니메이션 비활성화
},
series: [
{
@@ -328,6 +353,11 @@ export default function PriceVolumeChart({
upLineColor: "#16a34a",
pointPadding: 0,
dataGrouping: { enabled: false },
+ animation: {
+ duration: 500,
+ easing: "easeInOutCubic",
+ }, // 시리즈 애니메이션 활성화
+ enableMouseTracking: true,
},
{
type: "column",
@@ -393,18 +423,49 @@ export default function PriceVolumeChart({
lastClose,
]);
- if (!ready) {
- return ;
+ // TradingView 차트 우선 렌더링
+ if (tvReady) {
+ return (
+
+ );
}
- return tvReady ? (
-
- ) : (
-
+ // TradingView 실패 시 Highcharts 사용
+ if (tvFailed && ready) {
+ return (
+
+
+
+ );
+ }
+
+ // TradingView 로딩 중이지만 데이터가 있으면 Highcharts로 폴백
+ if (data.length > 0 && !tvReady && !tvFailed) {
+ return (
+
+
+
+ );
+ }
+
+ // 로딩 중
+ return (
+
);
-}
+});
+
+export default PriceVolumeChart;
diff --git a/src/components/stockDetail/chart/StockChartHeader.tsx b/src/components/stockDetail/chart/StockChartHeader.tsx
index 7ddcfdb..9838448 100644
--- a/src/components/stockDetail/chart/StockChartHeader.tsx
+++ b/src/components/stockDetail/chart/StockChartHeader.tsx
@@ -1,13 +1,18 @@
-import { dayTable, miniteTable } from '@/config/chart';
-import { TimeRangePT } from '.';
+import { dayTable, miniteTable } from "@/config/chart";
+import { TimeRangePT } from ".";
interface StockChartHeader {
timeRange: TimeRangePT;
onTimeRangeChange: (r: TimeRangePT) => void;
- showMA: Record<'ma5' | 'ma20' | 'ma60' | 'ma120', boolean>;
- onToggleMA: (maType: 'ma5' | 'ma20' | 'ma60' | 'ma120') => void;
+ showMA: Record<"ma5" | "ma20" | "ma60" | "ma120", boolean>;
+ onToggleMA: (maType: "ma5" | "ma20" | "ma60" | "ma120") => void;
}
-export default function StockChartHeader({ showMA, timeRange, onTimeRangeChange, onToggleMA }: StockChartHeader) {
+export default function StockChartHeader({
+ showMA,
+ timeRange,
+ onTimeRangeChange,
+ onToggleMA,
+}: StockChartHeader) {
return (
@@ -17,33 +22,41 @@ export default function StockChartHeader({ showMA, timeRange, onTimeRangeChange,
{/* 이동평균선 토글 버튼들 */}
+ {/* A/B 테스트 결과 섹션 */}
+
+
+
+
+ 📊 LLM 기반 금융 분석 모델 A/B 테스트 결과
+
+
+ CoT/RAG 방식의 우수성을 과학적으로 검증했습니다
+
+
+ {/* 기술 스택 비교 */}
+
+
+
+
+ Ours
+
+
+
+ GPT-4o
+
+
+ + BarbellAI 기술
+
+
+ Chain of Thought + RAG
+
+
+
+
+
+
+
+ {/* 종합 분석 평가 */}
+
+
+ 🎯 종합 분석 평가 (깊이+근거+통찰력 평균)
+
+
+
+
+ 6.92
+
+
CoT/RAG 평균 점수
+
+
+
+ 5.25
+
+
+ Baseline 평균 점수
+
+
+
+
+
+ +31.79%
+
+
성능 향상률
+
+ P-value: 0.0000 (p < 0.0001)
+
+
+
+
+ {/* 개별 항목 평가 */}
+
+ {/* 분석의 깊이 */}
+
+
+ 🔍 분석의 깊이
+
+
+
+ CoT/RAG
+ 7.53
+
+
+ Baseline
+ 5.28
+
+
+
+
+
+ {/* 논리적 근거 */}
+
+
+ 🧠 논리적 근거
+
+
+
+ CoT/RAG
+ 6.61
+
+
+ Baseline
+ 5.59
+
+
+
+
+
+ {/* 통찰력 */}
+
+
+ 💡 통찰력
+
+
+
+ CoT/RAG
+ 6.63
+
+
+ Baseline
+ 4.89
+
+
+
+
+
+
+ {/* 결론 */}
+
+
+
🏆 결론
+
+ CoT/RAG 방식이 모든 평가 항목에서 통계적으로 유의미하게 우수한
+ 성능을 보여주었습니다.
+
+ p < 0.001 수준에서
+ 통계적 유의성을 확인했습니다.
+
+
+
+
+
+
+
{/* 시작하기 섹션 */}
From cace83c2d5f676b4d3d11ab0194ced84453ccfaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=EC=84=B1=EC=9C=A4?= <1112csy@naver.com>
Date: Thu, 4 Sep 2025 14:04:04 +0900
Subject: [PATCH 7/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20A/B=ED=85=8C=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/Intro/ABTestResults.tsx | 233 +++++++++++++++++++++++++
src/pages/introPage.tsx | 231 +-----------------------
2 files changed, 235 insertions(+), 229 deletions(-)
create mode 100644 src/components/Intro/ABTestResults.tsx
diff --git a/src/components/Intro/ABTestResults.tsx b/src/components/Intro/ABTestResults.tsx
new file mode 100644
index 0000000..f540b18
--- /dev/null
+++ b/src/components/Intro/ABTestResults.tsx
@@ -0,0 +1,233 @@
+import { motion } from "framer-motion";
+
+export default function ABTestResults() {
+ return (
+
+
+
+
+ 📊 LLM 기반 금융 분석 모델 A/B 테스트 결과
+
+
+ CoT/RAG 방식의 우수성을 과학적으로 검증했습니다
+
+
+ {/* 기술 스택 비교 */}
+
+
+
+
+ Ours
+
+
+
+ GPT-4o
+
+
+ + BarbellAI 기술
+
+
+ Chain of Thought + RAG
+
+
+
+
+
+
+
+ {/* 종합 분석 평가 */}
+
+
+ 🎯 종합 분석 평가 (깊이+근거+통찰력 평균)
+
+
+
+
+ 6.92
+
+
CoT/RAG 평균 점수
+
+
+
+ 5.25
+
+
Baseline 평균 점수
+
+
+
+
+ +31.79%
+
+
성능 향상률
+
+ P-value: 0.0000 (p < 0.0001)
+
+
+
+
+ {/* 개별 항목 평가 */}
+
+ {/* 분석의 깊이 */}
+
+
+ 🔍 분석의 깊이
+
+
+
+ CoT/RAG
+ 7.53
+
+
+ Baseline
+ 5.28
+
+
+
+
+
+ {/* 논리적 근거 */}
+
+
+ 🧠 논리적 근거
+
+
+
+ CoT/RAG
+ 6.61
+
+
+ Baseline
+ 5.59
+
+
+
+
+
+ {/* 통찰력 */}
+
+
+ 💡 통찰력
+
+
+
+ CoT/RAG
+ 6.63
+
+
+ Baseline
+ 4.89
+
+
+
+
+
+
+ {/* 결론 */}
+
+
+
🏆 결론
+
+ CoT/RAG 방식이 모든 평가 항목에서 통계적으로 유의미하게 우수한
+ 성능을 보여주었습니다.
+
+ p < 0.001 수준에서
+ 통계적 유의성을 확인했습니다.
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/introPage.tsx b/src/pages/introPage.tsx
index 40a46c3..3f6327d 100644
--- a/src/pages/introPage.tsx
+++ b/src/pages/introPage.tsx
@@ -1,5 +1,6 @@
import BALLFiNLogo from "../assets/BALLFiN.svg";
import { FeatureCard } from "../components/Intro/FeatureCard";
+import ABTestResults from "../components/Intro/ABTestResults";
import { motion } from "framer-motion";
import { useNavigate } from "react-router-dom";
import cheon from "../assets/member/cheon.png";
@@ -133,235 +134,7 @@ export default function Intro() {
{/* A/B 테스트 결과 섹션 */}
-
-
-
-
- 📊 LLM 기반 금융 분석 모델 A/B 테스트 결과
-
-
- CoT/RAG 방식의 우수성을 과학적으로 검증했습니다
-
-
- {/* 기술 스택 비교 */}
-
-
-
-
- Ours
-
-
-
- GPT-4o
-
-
- + BarbellAI 기술
-
-
- Chain of Thought + RAG
-
-
-
-
-
-
-
- {/* 종합 분석 평가 */}
-
-
- 🎯 종합 분석 평가 (깊이+근거+통찰력 평균)
-
-
-
-
- 6.92
-
-
CoT/RAG 평균 점수
-
-
-
- 5.25
-
-
- Baseline 평균 점수
-
-
-
-
-
- +31.79%
-
-
성능 향상률
-
- P-value: 0.0000 (p < 0.0001)
-
-
-
-
- {/* 개별 항목 평가 */}
-
- {/* 분석의 깊이 */}
-
-
- 🔍 분석의 깊이
-
-
-
- CoT/RAG
- 7.53
-
-
- Baseline
- 5.28
-
-
-
-
-
- {/* 논리적 근거 */}
-
-
- 🧠 논리적 근거
-
-
-
- CoT/RAG
- 6.61
-
-
- Baseline
- 5.59
-
-
-
-
-
- {/* 통찰력 */}
-
-
- 💡 통찰력
-
-
-
- CoT/RAG
- 6.63
-
-
- Baseline
- 4.89
-
-
-
-
-
-
- {/* 결론 */}
-
-
-
🏆 결론
-
- CoT/RAG 방식이 모든 평가 항목에서 통계적으로 유의미하게 우수한
- 성능을 보여주었습니다.
-
- p < 0.001 수준에서
- 통계적 유의성을 확인했습니다.
-
-
-
-
-
-
+
{/* 시작하기 섹션 */}