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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
Expand Down
79 changes: 79 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 64 additions & 37 deletions src/_BacktestingPage/components/AssetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react";
import type { SearchResult, Asset } from "@/_BacktestingPage/types/backtestFormType";
import { useGetSearchAssets } from "@/lib/hooks/useGetSearchAssets";
import { useDebounce } from "@/lib/hooks/useDebounce";
import { Spinner } from "@/components/ui/spinner";
type AssetItemProps = {
AssetIndex: number;
asset: Asset;
Expand All @@ -15,10 +16,11 @@ const AssetItem = ({ AssetIndex, asset, onUpdate, onDelete }: AssetItemProps) =>
const wrapperRef = useRef<HTMLDivElement>(null);
const skipSearchRef = useRef(false);
const hasClearedRef = useRef(false);
const isSelectedRef = useRef(false);

const debouncedQuery = useDebounce(query, 500);

const { data: searchAssets } = useGetSearchAssets(debouncedQuery);
const { data: searchAssets, isLoading } = useGetSearchAssets(debouncedQuery);

const searchResults: SearchResult[] = searchAssets
? searchAssets.map((item) => ({
Expand All @@ -27,23 +29,40 @@ const AssetItem = ({ AssetIndex, asset, onUpdate, onDelete }: AssetItemProps) =>
}))
: [];

// 검색 결과가 있을 때 드롭다운 열기
// 검색 결과가 있을 때 드롭다운 열기, 값이 없으면 닫기
useEffect(() => {
// 종목 선택 후에는 드롭다운을 열지 않음
if (isSelectedRef.current) {
isSelectedRef.current = false;
setIsDropdownOpen(false);
return;
}

if (skipSearchRef.current) {
skipSearchRef.current = false;
return;
}
if (searchResults.length > 0 && debouncedQuery) {

// 선택된 종목 표시 형식 (이름 (티커))인 경우 드롭다운을 열지 않음
if (query.includes("(") && query.includes(")")) {
setIsDropdownOpen(false);
return;
}

if (isLoading && debouncedQuery) {
setIsDropdownOpen(true);
} else if (searchResults.length > 0 && debouncedQuery) {
setIsDropdownOpen(true);
} else if (!debouncedQuery) {
} else if (!debouncedQuery || (!isLoading && searchResults.length === 0)) {
setIsDropdownOpen(false);
}
}, [searchResults, debouncedQuery]);
}, [searchResults, debouncedQuery, isLoading, query]);

const handleSelect = (selected: SearchResult) => {
const displayValue = `${selected.name} (${selected.ticker})`;
onUpdate({ ...asset, name: selected.name, ticker: selected.ticker });
skipSearchRef.current = true;
isSelectedRef.current = true;
setQuery(displayValue);
setIsDropdownOpen(false);
hasClearedRef.current = false;
Expand All @@ -65,39 +84,47 @@ const AssetItem = ({ AssetIndex, asset, onUpdate, onDelete }: AssetItemProps) =>
<div ref={wrapperRef} className="relative flex items-center gap-4">
<div className="w-20 font-medium text-gray-300 text-sm">자산 {AssetIndex + 1}</div>

<input
className="flex-1 bg-white/10 focus:bg-white/15 px-3 py-2 border border-white/20 focus:border-white/30 rounded-lg focus:outline-none h-10 text-white placeholder:text-gray-500 transition-colors"
value={query}
onChange={(e) => {
setQuery(e.target.value);
hasClearedRef.current = true; // 입력 중엔 다시 초기화 안되게
}}
placeholder="종목명 입력"
onFocus={() => {
if (!hasClearedRef.current && query.includes("(")) {
setQuery("");
setIsDropdownOpen(true);
hasClearedRef.current = true;
} else {
if (searchResults.length > 0) setIsDropdownOpen(true);
}
}}
/>
<div className="relative flex-1">
<input
className="bg-white/10 focus:bg-white/15 px-3 py-2 border border-white/20 focus:border-white/30 rounded-lg focus:outline-none w-full h-10 text-white placeholder:text-gray-500 transition-colors"
value={query}
onChange={(e) => {
setQuery(e.target.value);
hasClearedRef.current = true; // 입력 중엔 다시 초기화 안되게
}}
placeholder="종목명 입력"
onFocus={() => {
if (!hasClearedRef.current && query.includes("(")) {
setQuery("");
setIsDropdownOpen(true);
hasClearedRef.current = true;
} else {
if (searchResults.length > 0 || isLoading) setIsDropdownOpen(true);
}
}}
/>

{isDropdownOpen && searchResults.length > 0 && (
<div className="top-full left-20 z-20 absolute flex flex-col bg-white/95 shadow-xl backdrop-blur-sm mt-2 border border-white/20 rounded-lg w-[calc(100%-5rem)] max-h-[240px] overflow-y-auto">
{searchResults.map((item) => (
<div
key={item.ticker}
className="flex hover:bg-white/10 px-4 py-2 transition-colors cursor-pointer"
onClick={() => handleSelect(item)}
>
<span className="text-gray-900">{item.name}</span>
<span className="ml-2 text-gray-600">({item.ticker})</span>
</div>
))}
</div>
)}
{isDropdownOpen && (isLoading || searchResults.length > 0) && (
<div className="top-full left-0 z-20 absolute flex flex-col bg-white/5 shadow-xl backdrop-blur-sm mt-2 border border-white/20 rounded-lg w-full max-h-[240px] overflow-y-auto">
{isLoading && searchResults.length === 0 ? (
<div className="flex justify-center items-center py-8">
<Spinner className="size-6 text-white" />
</div>
) : (
searchResults.map((item) => (
<div
key={item.ticker}
className="flex hover:bg-white/10 px-4 py-2 text-white transition-colors cursor-pointer"
onClick={() => handleSelect(item)}
>
<span className="text-white">{item.name}</span>
<span className="ml-2 text-gray-300">({item.ticker})</span>
</div>
))
)}
</div>
)}
</div>
<input
type="number"
className="bg-white/10 focus:bg-white/15 px-3 py-2 border border-white/20 focus:border-white/30 rounded-lg focus:outline-none w-24 h-10 text-white placeholder:text-gray-500 transition-colors"
Expand Down
58 changes: 58 additions & 0 deletions src/_BacktestingPage/hooks/useProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState, useEffect } from "react";

interface UseProgressProps {
isPending: boolean;
data: unknown;
error: Error | null;
}

export const useProgress = ({ isPending, data, error }: UseProgressProps) => {
const [progress, setProgress] = useState(0);
const [showResult, setShowResult] = useState(false);

// Progress 진행률 관리
useEffect(() => {
let timer1: NodeJS.Timeout | null = null;
let timer2: NodeJS.Timeout | null = null;
let timer3: NodeJS.Timeout | null = null;
let timer4: NodeJS.Timeout | null = null;

if (isPending) {
// 초기화
setProgress(0);
setShowResult(false);

// 33%까지 300ms 동안 진행
timer1 = setTimeout(() => {
setProgress(33);
}, 100);

// 66%까지 진행 (응답 대기)
timer2 = setTimeout(() => {
setProgress(66);
}, 500);
} else if (data && !error) {
// 응답이 오면 100%로 진행
timer3 = setTimeout(() => {
setProgress(100);
// 100%가 된 후 데이터 렌더링
timer4 = setTimeout(() => {
setShowResult(true);
}, 350);
}, 100);
} else if (error) {
// 에러 발생 시 progress 초기화
setProgress(0);
setShowResult(false);
}

return () => {
if (timer1) clearTimeout(timer1);
if (timer2) clearTimeout(timer2);
if (timer3) clearTimeout(timer3);
if (timer4) clearTimeout(timer4);
};
}, [isPending, data, error]);

return { progress, showResult };
};
16 changes: 6 additions & 10 deletions src/_BacktestingPage/types/backtestFormType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
export type BacktestFormValues = {
start_date: string; // "2010-01-01"
end_date: string; // "2020-12-31"
rebalance_frequency: "매년" | "분기별" | "매월";
initial_amount: number;
};

export type Asset = {
id: string;
ticker: string;
Expand All @@ -13,12 +6,15 @@ export type Asset = {
};

export type AssetRequest = {
name: string;
ticker: string;
stockCd: string;
weight: number;
};

export type BacktestRequest = BacktestFormValues & {
export type BacktestRequest = {
startDate: string; // "2017-11-18"
endDate: string; // "2025-11-18"
initialCapital: number;
rebalanceCycle: "YEARLY" | "QUARTERLY" | "MONTHLY";
assets: AssetRequest[];
};

Expand Down
10 changes: 0 additions & 10 deletions src/_BacktestingPage/utils/convertAssetsForRequest.ts

This file was deleted.

Loading