Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/_BacktestingPage/components/SaveBacktestButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Save } from "lucide-react";
import { useAuth } from "@/lib/hooks/useAuth";
import { useSaveBacktest } from "@/lib/hooks/useBacktestStorage";
import type { BacktestRequest, BacktestResult } from "@/_BacktestingPage/types/backtestFormType";
import { toast } from "sonner";

interface SaveBacktestButtonProps {
request: BacktestRequest;
result: BacktestResult;
}

const SaveBacktestButton = ({ request, result }: SaveBacktestButtonProps) => {
const { user } = useAuth();
const { mutate: save, isPending } = useSaveBacktest();
const [showTooltip, setShowTooltip] = useState(false);
const [saved, setSaved] = useState(false);

const handleSave = () => {
if (!user) return;

const count = Date.now().toString(36).slice(-4);
const title = `${user.id.slice(0, 8)}의 포트폴리오 ${count}`;

save(
{ title, request, result },
{
onSuccess: () => {
toast.success("백테스트 결과가 저장되었습니다.");
setSaved(true);
},
onError: () => {
toast.error("저장에 실패했습니다.");
},
}
);
};

return (
<div className="relative flex justify-center mt-8">
<div
onMouseEnter={() => !user && setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<Button
onClick={handleSave}
disabled={!user || isPending || saved}
className="flex items-center gap-2 bg-white hover:brightness-85 px-8 py-4 text-navy cursor-pointer disabled:cursor-not-allowed"
>
<Save className="w-5 h-5" />
{saved ? "저장 완료" : isPending ? "저장 중..." : "포트폴리오에 저장"}
</Button>
</div>

{showTooltip && !user && (
<div className="bottom-full left-1/2 absolute bg-gray-800 mb-2 px-3 py-2 rounded-lg text-sm text-white whitespace-nowrap -translate-x-1/2">
로그인하면 백테스트 결과를 저장할 수 있어요
<div className="top-full left-1/2 absolute border-t-gray-800 border-x-transparent border-x-4 border-t-4 -translate-x-1/2" />
</div>
)}
</div>
);
};

export default SaveBacktestButton;
119 changes: 70 additions & 49 deletions src/_MainPage/components/MarketIndexSection.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,84 @@
/* eslint-disable no-console */
import { getIndexData } from "@/lib/apis/getIndex";
// --- 실제 API 모드 (백엔드 연결 시 주석 해제하고 아래 목데이터 모드 제거) ---
// /* eslint-disable no-console */
// import { getIndexData } from "@/lib/apis/getIndex";
// import MarketIndexCard from "@/_MainPage/components/MarketIndexCard";
// import { useQuery } from "@tanstack/react-query";
// import type { AxiosError } from "axios";
// import type { ApiErrorResponse } from "@/lib/apis/types";
// import { __DEV__ } from "@/utils/instance";
//
// export default function MarketIndexSection() {
// const {
// data: kospiData,
// isLoading: isKospiLoading,
// error: kospiError,
// } = useQuery({
// queryKey: ["indexData", "KOSPI"],
// queryFn: () => getIndexData("KOSPI"),
// });
//
// const {
// data: kosdaqData,
// isLoading: isKosdaqLoading,
// error: kosdaqError,
// } = useQuery({
// queryKey: ["indexData", "KOSDAQ"],
// queryFn: () => getIndexData("KOSDAQ"),
// });
//
// if (__DEV__ && kospiError) {
// const error = kospiError as AxiosError<ApiErrorResponse>;
// if (error.response?.data) {
// console.error("KOSPI 데이터 조회 에러:", error.response.data.detail);
// }
// }
//
// if (__DEV__ && kosdaqError) {
// const error = kosdaqError as AxiosError<ApiErrorResponse>;
// if (error.response?.data) {
// console.error("KOSDAQ 데이터 조회 에러:", error.response.data.detail);
// }
// }
//
// return (
// <div className="mx-auto px-6 lg:px-8 py-16 max-w-7xl">
// <div className="gap-8 grid grid-cols-1 md:grid-cols-2">
// <MarketIndexCard
// marketType="KOSPI"
// marketIndex={kospiData ?? null}
// isLoading={isKospiLoading}
// error={kospiError}
// />
// <MarketIndexCard
// marketType="KOSDAQ"
// marketIndex={kosdaqData ?? null}
// isLoading={isKosdaqLoading}
// error={kosdaqError}
// />
// </div>
// </div>
// );
// }

// --- 목데이터 모드 ---
import MarketIndexCard from "@/_MainPage/components/MarketIndexCard";
import { useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import type { ApiErrorResponse } from "@/lib/apis/types";
import { __DEV__ } from "@/utils/instance";
import { MOCK_KOSPI, MOCK_KOSDAQ } from "@/_MainPage/mocks/marketIndexMock";

export default function MarketIndexSection() {
// KOSPI 데이터 조회 - 독립적으로 실행
const {
data: kospiData,
isLoading: isKospiLoading,
error: kospiError,
} = useQuery({
queryKey: ["indexData", "KOSPI"],
queryFn: () => getIndexData("KOSPI"),
});

// KOSDAQ 데이터 조회 - 독립적으로 실행
const {
data: kosdaqData,
isLoading: isKosdaqLoading,
error: kosdaqError,
} = useQuery({
queryKey: ["indexData", "KOSDAQ"],
queryFn: () => getIndexData("KOSDAQ"),
});

// 에러 처리 (개발 환경에서만 로깅)
if (__DEV__ && kospiError) {
const error = kospiError as AxiosError<ApiErrorResponse>;
if (error.response?.data) {
console.error("KOSPI 데이터 조회 에러:", error.response.data.detail);
}
}

if (__DEV__ && kosdaqError) {
const error = kosdaqError as AxiosError<ApiErrorResponse>;
if (error.response?.data) {
console.error("KOSDAQ 데이터 조회 에러:", error.response.data.detail);
}
}

return (
<div className="mx-auto px-6 lg:px-8 py-16 max-w-7xl">
<div className="gap-8 grid grid-cols-1 md:grid-cols-2">
{/* KOSPI 카드 - 독립적인 로딩/에러 상태 */}
<MarketIndexCard
marketType="KOSPI"
marketIndex={kospiData ?? null}
isLoading={isKospiLoading}
error={kospiError}
marketIndex={MOCK_KOSPI}
isLoading={false}
error={null}
/>
{/* KOSDAQ 카드 - 독립적인 로딩/에러 상태 */}
<MarketIndexCard
marketType="KOSDAQ"
marketIndex={kosdaqData ?? null}
isLoading={isKosdaqLoading}
error={kosdaqError}
marketIndex={MOCK_KOSDAQ}
isLoading={false}
error={null}
/>
</div>
</div>
Expand Down
157 changes: 157 additions & 0 deletions src/_MainPage/mocks/marketIndexMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { MarketIndex } from "@/_MainPage/types/MarketIndexType";

export const MOCK_KOSPI: MarketIndex = {
marketType: "KOSPI",
startDate: "2026-03-01",
endDate: "2026-03-08",
data: [
{
marketType: "KOSPI",
baseDate: "2026-03-02",
openPrice: 5320.15,
closePrice: 5345.82,
highPrice: 5368.4,
lowPrice: 5310.2,
changeAmount: 25.67,
changeRate: 0.48,
},
{
marketType: "KOSPI",
baseDate: "2026-03-03",
openPrice: 5345.82,
closePrice: 5412.3,
highPrice: 5425.1,
lowPrice: 5338.5,
changeAmount: 66.48,
changeRate: 1.24,
},
{
marketType: "KOSPI",
baseDate: "2026-03-04",
openPrice: 5412.3,
closePrice: 5389.75,
highPrice: 5430.0,
lowPrice: 5375.2,
changeAmount: -22.55,
changeRate: -0.42,
},
{
marketType: "KOSPI",
baseDate: "2026-03-05",
openPrice: 5389.75,
closePrice: 5456.2,
highPrice: 5470.8,
lowPrice: 5382.1,
changeAmount: 66.45,
changeRate: 1.23,
},
{
marketType: "KOSPI",
baseDate: "2026-03-06",
openPrice: 5456.2,
closePrice: 5523.48,
highPrice: 5540.3,
lowPrice: 5448.9,
changeAmount: 67.28,
changeRate: 1.23,
},
{
marketType: "KOSPI",
baseDate: "2026-03-07",
openPrice: 5523.48,
closePrice: 5498.12,
highPrice: 5545.0,
lowPrice: 5480.5,
changeAmount: -25.36,
changeRate: -0.46,
},
{
marketType: "KOSPI",
baseDate: "2026-03-08",
openPrice: 5498.12,
closePrice: 5567.35,
highPrice: 5580.2,
lowPrice: 5492.8,
changeAmount: 69.23,
changeRate: 1.26,
},
],
};

export const MOCK_KOSDAQ: MarketIndex = {
marketType: "KOSDAQ",
startDate: "2026-03-01",
endDate: "2026-03-08",
data: [
{
marketType: "KOSDAQ",
baseDate: "2026-03-02",
openPrice: 1085.3,
closePrice: 1092.45,
highPrice: 1098.7,
lowPrice: 1080.1,
changeAmount: 7.15,
changeRate: 0.66,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-03",
openPrice: 1092.45,
closePrice: 1108.2,
highPrice: 1115.3,
lowPrice: 1090.8,
changeAmount: 15.75,
changeRate: 1.44,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-04",
openPrice: 1108.2,
closePrice: 1095.6,
highPrice: 1112.4,
lowPrice: 1088.3,
changeAmount: -12.6,
changeRate: -1.14,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-05",
openPrice: 1095.6,
closePrice: 1118.35,
highPrice: 1125.0,
lowPrice: 1093.2,
changeAmount: 22.75,
changeRate: 2.08,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-06",
openPrice: 1118.35,
closePrice: 1135.8,
highPrice: 1142.5,
lowPrice: 1115.4,
changeAmount: 17.45,
changeRate: 1.56,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-07",
openPrice: 1135.8,
closePrice: 1128.92,
highPrice: 1140.2,
lowPrice: 1122.6,
changeAmount: -6.88,
changeRate: -0.61,
},
{
marketType: "KOSDAQ",
baseDate: "2026-03-08",
openPrice: 1128.92,
closePrice: 1148.5,
highPrice: 1155.3,
lowPrice: 1126.4,
changeAmount: 19.58,
changeRate: 1.73,
},
],
};
Loading