diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1c6a335..0ad45bc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { dehydrate, QueryClient } from "@tanstack/react-query"; import type { Metadata } from "next"; import Toast from "@/components/common/toast/index"; +import TutorialContainer from "@/components/common/tutorial/_components/tutorial-container"; import MainContent from "@/components/main-content"; import NavBar from "@/components/nav-bar"; import AuthInitializer from "@/provider/AuthInitializer"; @@ -52,6 +53,7 @@ export default async function RootLayout({ + {children} diff --git a/src/app/search/[id]/_components/tutorial/constant/index.ts b/src/app/search/[id]/_components/tutorial/constant/index.ts new file mode 100644 index 0000000..ea633ce --- /dev/null +++ b/src/app/search/[id]/_components/tutorial/constant/index.ts @@ -0,0 +1,45 @@ +import { TutorialStep } from "../types"; + +const TUTORIAL_STEPS: TutorialStep[] = [ + { + title: "차트 보기", + content: [ + { + subtitle: "기간 설정", + description: "일/주/월 버튼으로 원하는 기간의 차트를 확인하세요.", + }, + { + subtitle: "이동평균선(MA)", + description: "MA 버튼으로 주가의 평균 추세선을 표시할 수 있어요.", + }, + ], + }, + { + title: "거래하기", + content: [ + { + subtitle: "매수/매도", + description: "원하는 거래 유형을 선택하고 수량과 가격을 입력하세요.", + }, + { + subtitle: "실시간 호가", + description: "현재 매수/매도 호가를 실시간으로 확인할 수 있어요.", + }, + ], + }, + { + title: "주문 관리", + content: [ + { + subtitle: "체결 내역", + description: "체결내역 탭에서 완료된 거래를 확인하세요.", + }, + { + subtitle: "미체결 주문", + description: "정정/취소 탭에서 아직 체결되지 않은 주문을 관리하세요.", + }, + ], + }, +]; + +export default TUTORIAL_STEPS; diff --git a/src/app/search/[id]/_components/tutorial/tutorial-container.tsx b/src/app/search/[id]/_components/tutorial/tutorial-container.tsx new file mode 100644 index 0000000..d0923a9 --- /dev/null +++ b/src/app/search/[id]/_components/tutorial/tutorial-container.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import ChartTutorialModal from "./tutorial-modal"; + +export default function ChartTutorialContainer() { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const hasCompletedTutorial = localStorage.getItem( + "chart-tutorial-completed", + ); + if (!hasCompletedTutorial) { + setIsOpen(true); + localStorage.setItem("chart-tutorial-completed", "true"); + } + }, []); + + return ( + setIsOpen(false)} /> + ); +} diff --git a/src/app/search/[id]/_components/tutorial/tutorial-modal.tsx b/src/app/search/[id]/_components/tutorial/tutorial-modal.tsx new file mode 100644 index 0000000..f762415 --- /dev/null +++ b/src/app/search/[id]/_components/tutorial/tutorial-modal.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import { useCallback, useState } from "react"; + +import BaseModal from "@/components/common/modal"; +import TutorialContent from "@/components/common/tutorial/_components/tutorial-contents"; + +import TUTORIAL_STEPS from "./constant"; + +interface TutorialModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function TutorialModal({ isOpen, onClose }: TutorialModalProps) { + const [currentStep, setCurrentStep] = useState(0); + + const handleNext = useCallback(() => { + setCurrentStep((prev) => Math.min(prev + 1, TUTORIAL_STEPS.length - 1)); + }, []); + + const handlePrev = useCallback(() => { + setCurrentStep((prev) => Math.max(prev - 1, 0)); + }, []); + + const handleComplete = useCallback(() => { + if (typeof window !== "undefined") { + localStorage.setItem("stock-tutorial-completed", "true"); + } + onClose(); + }, [onClose]); + + return ( + +
+
+

주식 거래 시작하기

+ +
+ + + +
+
+ {currentStep + 1} / {TUTORIAL_STEPS.length} +
+
+ {currentStep > 0 && ( + + )} + {currentStep < TUTORIAL_STEPS.length - 1 ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/src/app/search/[id]/_components/tutorial/types/index.ts b/src/app/search/[id]/_components/tutorial/types/index.ts new file mode 100644 index 0000000..7e62ac8 --- /dev/null +++ b/src/app/search/[id]/_components/tutorial/types/index.ts @@ -0,0 +1,9 @@ +export interface TutorialContentItem { + subtitle: string; + description: string; +} + +export interface TutorialStep { + title: string; + content: TutorialContentItem[]; +} diff --git a/src/app/search/[id]/page.tsx b/src/app/search/[id]/page.tsx index 6056f2f..0c4dcf7 100644 --- a/src/app/search/[id]/page.tsx +++ b/src/app/search/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import CandlestickChartContainer from "./_components/candle-chart-container"; import StockHeader from "./_components/stock-header"; import TransactionForm from "./_components/transaction-form"; +import TutorialContainer from "./_components/tutorial/tutorial-container"; import { ChartResponse, StockInfo, VolumeResponse } from "./types"; async function getInitialData(id: string) { @@ -59,6 +60,7 @@ export default async function StockPage({ const stockName = decodeURIComponent(params.id); return (
+ { + const hasCompletedTutorial = localStorage.getItem("nav-tutorial-completed"); + if (!hasCompletedTutorial) { + setIsOpen(true); + // 튜토리얼을 본 것으로 표시 + localStorage.setItem("nav-tutorial-completed", "true"); + } + }, []); + + return setIsOpen(false)} />; +} diff --git a/src/components/common/tutorial/_components/tutorial-contents.tsx b/src/components/common/tutorial/_components/tutorial-contents.tsx new file mode 100644 index 0000000..6540643 --- /dev/null +++ b/src/components/common/tutorial/_components/tutorial-contents.tsx @@ -0,0 +1,26 @@ +/* eslint-disable */ + +import { TutorialStep } from "../types"; + +interface TutorialContentProps { + step: TutorialStep; +} + +export default function TutorialContent({ step }: TutorialContentProps) { + return ( +
+

+ {step.title} +

+ +
+ {step.content.map((item, idx) => ( +
+

{item.subtitle}

+

{item.description}

+
+ ))} +
+
+ ); +} diff --git a/src/components/common/tutorial/_components/tutorial-modal.tsx b/src/components/common/tutorial/_components/tutorial-modal.tsx new file mode 100644 index 0000000..bbc001a --- /dev/null +++ b/src/components/common/tutorial/_components/tutorial-modal.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; + +import BaseModal from "@/components/common/modal"; + +import TUTORIAL_STEPS from "../constant"; +import TutorialContent from "./tutorial-contents"; + +interface TutorialModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function TutorialModal({ isOpen, onClose }: TutorialModalProps) { + const [currentStep, setCurrentStep] = useState(0); + const router = useRouter(); + + // 스텝에 따른 경로 반환 + const getPathForStep = (step: number) => { + switch (step) { + case 0: + return "/"; + case 1: + return "/search"; + case 2: + return "/my-account"; + case 3: + return "/portfolio"; + default: + return "/"; + } + }; + + const handleNext = useCallback(() => { + setCurrentStep((prev) => Math.min(prev + 1, TUTORIAL_STEPS.length - 1)); + // 페이지 이동 + router.push(getPathForStep(currentStep + 1)); + }, [currentStep, router]); + + const handlePrev = useCallback(() => { + setCurrentStep((prev) => Math.max(prev - 1, 0)); + // 페이지 이동 + router.push(getPathForStep(currentStep - 1)); + }, [currentStep, router]); + + const handleComplete = useCallback(() => { + if (typeof window !== "undefined") { + localStorage.setItem("nav-tutorial-completed", "true"); + } + onClose(); + }, [onClose]); + + return ( + +
+
+

GrowFoilo 둘러보기

+ +
+ + + +
+
+ {currentStep + 1} / {TUTORIAL_STEPS.length} +
+
+ {currentStep > 0 && ( + + )} + {currentStep < TUTORIAL_STEPS.length - 1 ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/src/components/common/tutorial/constant/index.ts b/src/components/common/tutorial/constant/index.ts new file mode 100644 index 0000000..0780777 --- /dev/null +++ b/src/components/common/tutorial/constant/index.ts @@ -0,0 +1,60 @@ +import { TutorialStep } from "../types"; + +const TUTORIAL_STEPS: TutorialStep[] = [ + { + title: "홈", + content: [ + { + subtitle: "투자 현황", + description: + "전체 계좌의 수익률과 포트폴리오 현황을 한눈에 확인하세요.", + }, + { + subtitle: "실시간 인기 종목", + description: "거래량이 많은 인기 종목들을 실시간으로 확인할 수 있어요.", + }, + ], + }, + { + title: "주식 조회", + content: [ + { + subtitle: "종목 검색", + description: "원하는 주식 종목을 검색하고 상세 정보를 확인하세요.", + }, + { + subtitle: "차트 분석", + description: + "주식을 클릭하면 일별/주별/월별 주가 변동과 거래량을 분석할 수 있어요.", + }, + ], + }, + { + title: "내 계좌", + content: [ + { + subtitle: "계좌 현황", + description: "보유 중인 주식과 예수금을 실시간으로 확인하세요.", + }, + { + subtitle: "거래 내역", + description: "매수/매도 거래 내역을 확인할수 있어요.", + }, + ], + }, + { + title: "포트폴리오", + content: [ + { + subtitle: "자산 배분", + description: "투자 종목별 비중과 섹터별 분포를 한눈에 보세요.", + }, + { + subtitle: "분석 보고서", + description: "내게 필요한 투자 정보를 확인할 수 있어요.", + }, + ], + }, +]; + +export default TUTORIAL_STEPS; diff --git a/src/components/common/tutorial/types/index.ts b/src/components/common/tutorial/types/index.ts new file mode 100644 index 0000000..7e62ac8 --- /dev/null +++ b/src/components/common/tutorial/types/index.ts @@ -0,0 +1,9 @@ +export interface TutorialContentItem { + subtitle: string; + description: string; +} + +export interface TutorialStep { + title: string; + content: TutorialContentItem[]; +}