Skip to content
13 changes: 1 addition & 12 deletions src/_BacktestingPage/utils/backtestFormSchema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { z } from "zod";
import { differenceInMonths, differenceInYears } from "date-fns";
import { differenceInMonths } from "date-fns";

const TODAY = new Date();
const MIN_DATE = new Date("1990-01-01"); // 1990년 1월 1일
const MIN_MONTHS_DIFF = 3; // 최소 3개월
const MAX_YEARS_DIFF = 10; // 최대 10년

export const backtestFormSchema = z
.object({
Expand Down Expand Up @@ -33,15 +32,5 @@ export const backtestFormSchema = z
message: `시작일은 종료일보다 최소 ${MIN_MONTHS_DIFF}개월 전이어야 합니다.`,
path: ["startDate"],
}
)
.refine(
(data) => {
const yearsDiff = differenceInYears(data.endDate, data.startDate);
return yearsDiff <= MAX_YEARS_DIFF;
},
{
message: `시작일은 종료일보다 최대 ${MAX_YEARS_DIFF}년 전까지 가능합니다.`,
path: ["startDate"],
}
);
export type BacktestFormSchema = z.infer<typeof backtestFormSchema>;
36 changes: 25 additions & 11 deletions src/_MainPage/components/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,45 @@ import { Link } from "react-router-dom";

export default function HeroSection() {
return (
<div className="flex flex-col justify-center items-center py-20 sm:py-32 text-center">
<h1 className="font-bold text-white text-4xl sm:text-6xl tracking-tight">
<div className="flex flex-col justify-center items-center px-4 py-10 sm:py-20 md:py-32 text-center">
<h1 className="px-4 font-bold text-white text-3xl sm:text-4xl md:text-6xl tracking-tight">
{HERO_CONTENT.title.split("\n").map((line, i) => (
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
</h1>
<p className="mt-6 max-w-2xl text-gray-300 text-lg">
{HERO_CONTENT.description.split("\n").map((line, i) => (
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
<p className="mt-4 sm:mt-6 px-4 max-w-2xl text-gray-300 text-base sm:text-lg">
{/* 모바일 환경에서만 표시되는 텍스트 */}
<span className="sm:hidden block">
{"과거 시장 데이터를 기반으로\n당신의 투자 전략을 시뮬레이션하여\n수익률과 안정성을 미리 예측할 수 있습니다."
.split("\n")
.map((line, i) => (
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
</span>
{/* 태블릿 이상 환경에서 표시되는 텍스트 */}
<span className="hidden sm:block">
{HERO_CONTENT.description.split("\n").map((line, i) => (
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
</span>
</p>
<Button
asChild
size="lg"
className="bg-blue-500 hover:bg-blue-600 mt-10 px-9 py-6 rounded-full text-[1rem] text-white"
className="bg-blue-500 hover:bg-blue-600 mt-6 sm:mt-10 px-6 sm:px-9 py-4 sm:py-6 rounded-full text-white md:text-[1rem] text-sm sm:text-base"
>
<Link to="/backtest">
{HERO_CONTENT.cta}
<ArrowRight className="ml-1 w-5 h-5" />
<ArrowRight className="ml-1 w-4 sm:w-5 h-4 sm:h-5" />
</Link>
</Button>
</div>
Expand Down
66 changes: 39 additions & 27 deletions src/_MainPage/components/MarketIndexCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIn
// 로딩 상태
if (isLoading) {
return (
<Card className="bg-[#0A194E] border-gray-700 text-white">
<CardHeader>
<CardTitle className="font-medium text-gray-300 text-lg">{marketType}</CardTitle>
<Card className="gap-1 sm:gap-6 bg-[#0A194E] py-3 sm:py-6 border-gray-700 text-white">
<CardHeader className="gap-1 sm:gap-1.5 px-4 sm:px-6">
<CardTitle className="font-medium text-gray-300 text-base sm:text-lg">
{marketType}
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-4 sm:px-6">
<div className="flex justify-center items-center h-48">
<Spinner className="size-12" />
<Spinner className="size-8 sm:size-12" />
</div>
</CardContent>
</Card>
Expand All @@ -50,13 +52,15 @@ const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIn
axiosError.response?.data?.detail || "데이터를 불러오는 중 오류가 발생했습니다.";

return (
<Card className="bg-[#0A194E] border-gray-700 text-white">
<CardHeader>
<CardTitle className="font-medium text-gray-300 text-lg">{marketType}</CardTitle>
<Card className="gap-1 sm:gap-6 bg-[#0A194E] py-3 sm:py-6 border-gray-700 text-white">
<CardHeader className="gap-1 sm:gap-1.5 px-4 sm:px-6">
<CardTitle className="font-medium text-gray-300 text-base sm:text-lg">
{marketType}
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-4 sm:px-6">
<div className="flex justify-center items-center h-48">
<p className="text-red-500 text-center">{errorMessage}</p>
<p className="text-red-500 text-sm sm:text-base text-center">{errorMessage}</p>
</div>
</CardContent>
</Card>
Expand All @@ -66,13 +70,15 @@ const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIn
// 데이터가 없는 경우
if (!marketIndex) {
return (
<Card className="bg-[#0A194E] border-gray-700 text-white">
<CardHeader>
<CardTitle className="font-medium text-gray-300 text-lg">{marketType}</CardTitle>
<Card className="gap-1 sm:gap-6 bg-[#0A194E] py-3 sm:py-6 border-gray-700 text-white">
<CardHeader className="gap-1 sm:gap-1.5 px-4 sm:px-6">
<CardTitle className="font-medium text-gray-300 text-base sm:text-lg">
{marketType}
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-4 sm:px-6">
<div className="flex justify-center items-center h-48">
<p className="text-gray-400">데이터가 없습니다.</p>
<p className="text-gray-400 text-sm sm:text-base">데이터가 없습니다.</p>
</div>
</CardContent>
</Card>
Expand All @@ -84,13 +90,15 @@ const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIn

if (!latestData) {
return (
<Card className="bg-[#0A194E] border-gray-700 text-white">
<CardHeader>
<CardTitle className="font-medium text-gray-300 text-lg">{marketType}</CardTitle>
<Card className="gap-1 sm:gap-6 bg-[#0A194E] py-3 sm:py-6 border-gray-700 text-white">
<CardHeader className="gap-1 sm:gap-1.5 px-4 sm:px-6">
<CardTitle className="font-medium text-gray-300 text-base sm:text-lg">
{marketType}
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="px-4 sm:px-6">
<div className="flex justify-center items-center h-48">
<p className="text-gray-400">데이터가 없습니다.</p>
<p className="text-gray-400 text-sm sm:text-base">데이터가 없습니다.</p>
</div>
</CardContent>
</Card>
Expand All @@ -108,18 +116,22 @@ const MarketIndexCard = ({ marketType, marketIndex, isLoading, error }: MarketIn
}));

return (
<Card className="bg-[#0A194E] border-gray-700 text-white">
<CardHeader>
<CardTitle className="font-medium text-gray-300 text-lg">{marketType}</CardTitle>
<Card className="gap-1 sm:gap-6 bg-[#0A194E] py-2 sm:py-6 border-gray-700 text-white">
<CardHeader className="gap-1 sm:gap-1.5 px-4 sm:px-6 pt-2">
<CardTitle className="font-medium text-gray-300 text-base sm:text-lg">
{marketType}
</CardTitle>
</CardHeader>

<CardContent>
<p className="font-bold text-4xl">{latestData.closePrice.toLocaleString()}</p>
<p className={`mt-2 text-lg font-semibold ${getChangeColor(latestData.changeRate)}`}>
<CardContent className="px-4 sm:px-6">
<p className="font-bold text-2xl sm:text-4xl">{latestData.closePrice.toLocaleString()}</p>
<p
className={`mt-1.5 sm:mt-2 text-sm sm:text-lg font-semibold ${getChangeColor(latestData.changeRate)}`}
>
{arrow} {Math.abs(latestData.changeAmount).toFixed(2)} ({sign}
{latestData.changeRate.toFixed(2)}%)
</p>
<div className="mt-4 h-24">
<div className="mt-3 sm:mt-4 h-24">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<YAxis domain={["dataMin - 10", "dataMax + 10"]} hide />
Expand Down
6 changes: 4 additions & 2 deletions src/components/Navbar/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { Link } from "react-router-dom";

const Logo = () => {
return (
<div className="flex items-center justify-center bg-transparent font-lalezar text-[6.25rem]">
<Link to="/">STPT</Link>
<div className="flex justify-center items-center bg-transparent pt-2 font-lalezar text-[4rem] md:text-[6.25rem]">
<Link to="/" className="pt-2">
STPT
</Link>
</div>
);
};
Expand Down
81 changes: 73 additions & 8 deletions src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,82 @@
import Logo from "@/components/Navbar/Logo";
import NavItem from "@/components/Navbar/NavItem";
import SearchBar from "@/components/Navbar/SearchBar";
import SideBarButton from "@/components/Navbar/SideBarButton";
import { Link } from "react-router-dom";
import { useState, useEffect, useRef } from "react";

const Navbar = () => {
const [isSideBarOpen, setIsSideBarOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null);

// 외부 클릭 시 모달 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (
modalRef.current &&
!modalRef.current.contains(target) &&
buttonRef.current &&
!buttonRef.current.contains(target)
) {
setIsSideBarOpen(false);
}
};

if (isSideBarOpen) {
document.addEventListener("mousedown", handleClickOutside);
}

return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isSideBarOpen]);

return (
<nav className="relative flex gap-16 bg-transparent mr-[3.25rem] ml-[2.6875rem] w-auto h-40">
<Logo />
<ul className="relative flex gap-[3.75rem] p-0 w-full">
<NavItem to="portfolio" label="Portfolios" />
<NavItem to="markets" label="Markets" />
</ul>
<SearchBar />
</nav>
<>
<nav className="z-50 relative flex justify-between items-center gap-16 bg-transparent mx-[2rem] sm:mx-[3rem] w-auto md:h-40">
<Logo />
<div className="relative" ref={buttonRef}>
<SideBarButton isOpen={isSideBarOpen} onClick={() => setIsSideBarOpen(!isSideBarOpen)} />

{/* 드롭다운 - 사이드바 버튼 아래에 표시 */}
{isSideBarOpen && (
<div
ref={modalRef}
className="md:hidden top-full right-0 z-50 absolute bg-[#0A194E] slide-in-from-top-2 shadow-lg mt-2 border border-gray-700 rounded-lg min-w-[150px] animate-in duration-200 fade-in"
>
<div className="p-4">
<p className="opacity-45 mb-2 text-white text-sm">Menu</p>
<div className="flex flex-col gap-">
<Link
to="/portfolio"
className="hover:opacity-100 py-1 font-bold text-white text-base transition-opacity"
onClick={() => setIsSideBarOpen(false)}
>
Portfolio
</Link>
<Link
to="/markets"
className="hover:opacity-100 py-1 font-bold text-white text-base transition-opacity"
onClick={() => setIsSideBarOpen(false)}
>
Markets
</Link>
</div>
</div>
</div>
)}
</div>
<div className="hidden md:flex md:flex-row md:justify-between md:items-center md:gap-16 md:w-full">
<ul className="relative flex gap-[3.75rem] p-0 w-full">
<NavItem to="portfolio" label="Portfolios" />
<NavItem to="markets" label="Markets" />
</ul>
<SearchBar />
</div>
</nav>
</>
);
};

Expand Down
34 changes: 34 additions & 0 deletions src/components/Navbar/SideBarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Button } from "@/components/ui/button";

interface SideBarButtonProps {
isOpen: boolean;
onClick: () => void;
}

export default function SideBarButton({ isOpen, onClick }: SideBarButtonProps) {
return (
<Button
variant="ghost"
className="md:hidden relative flex flex-col justify-center items-center gap-1.5 bg-transparent p-2 border border-white/20 rounded-xl w-12 h-12 transition-colors hover:transparent"
onClick={onClick}
>
<span
className={`bg-white rounded-full w-5 h-0.5 transition-all duration-300 ease-in-out ${
isOpen ? "rotate-45 translate-y-2" : ""
}`}
></span>

<span
className={`bg-white rounded-full w-5 h-0.5 transition-all duration-300 ease-in-out ${
isOpen ? "opacity-0" : "opacity-100"
}`}
></span>

<span
className={`bg-white rounded-full w-5 h-0.5 transition-all duration-300 ease-in-out ${
isOpen ? "-rotate-45 -translate-y-2" : ""
}`}
></span>
</Button>
);
}