-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 튜토리얼 페이지 제작 #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <ChartTutorialModal isOpen={isOpen} onClose={() => setIsOpen(false)} /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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]); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. localStorage 접근 시 예외 처리 필요 localStorage 접근 시 발생할 수 있는 예외 상황(private 모드 등)에 대한 처리가 필요합니다. const handleComplete = useCallback(() => {
if (typeof window !== "undefined") {
+ try {
localStorage.setItem("stock-tutorial-completed", "true");
+ } catch (error) {
+ console.error("Failed to save tutorial status:", error);
+ }
}
onClose();
}, [onClose]);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <BaseModal isOpen={isOpen} onClose={onClose}> | ||||||||||||||||||||||||||||||||||
| <div className="shadow-xl rounded-lg bg-white px-26 py-20"> | ||||||||||||||||||||||||||||||||||
| <div className="mb-15 flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||
| <h2 className="text-24-600 text-gray-900">주식 거래 시작하기</h2> | ||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||||||||||||||
| className="rounded-full p-1 hover:bg-gray-100" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <X className="size-25 text-gray-500" /> | ||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 접근성 개선 필요 버튼 요소들에 대한 접근성 속성이 누락되어 있습니다. 스크린 리더 사용자를 위한 aria 속성 추가가 필요합니다. <button
type="button"
onClick={onClose}
+ aria-label="튜토리얼 닫기"
className="rounded-full p-1 hover:bg-gray-100"
>
<X className="size-25 text-gray-500" />
</button>
<button
type="button"
onClick={handlePrev}
+ aria-label="이전 단계로 이동"
className="flex items-center gap-4 rounded-md border border-gray-300 px-8 py-4 text-16-600 text-gray-700 hover:bg-gray-50"
>Also applies to: 56-64, 66-74, 75-82 |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <TutorialContent step={TUTORIAL_STEPS[currentStep]} /> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||
| <div className="text-16-600 text-gray-500"> | ||||||||||||||||||||||||||||||||||
| {currentStep + 1} / {TUTORIAL_STEPS.length} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| <div className="flex gap-10"> | ||||||||||||||||||||||||||||||||||
| {currentStep > 0 && ( | ||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||
| onClick={handlePrev} | ||||||||||||||||||||||||||||||||||
| className="flex items-center gap-4 rounded-md border border-gray-300 px-8 py-4 text-16-600 text-gray-700 hover:bg-gray-50" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <ChevronLeft className="size-16" /> | ||||||||||||||||||||||||||||||||||
| 이전 | ||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| {currentStep < TUTORIAL_STEPS.length - 1 ? ( | ||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||
| onClick={handleNext} | ||||||||||||||||||||||||||||||||||
| className="flex items-center gap-4 rounded-md bg-green-500 px-8 py-4 text-16-600 text-white hover:bg-green-600" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| 다음 | ||||||||||||||||||||||||||||||||||
| <ChevronRight className="size-16" /> | ||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||
| onClick={handleComplete} | ||||||||||||||||||||||||||||||||||
| className="rounded-md bg-green-500 px-8 py-4 text-16-600 text-white hover:bg-green-600" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| 시작하기 | ||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </BaseModal> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,9 @@ | ||||||||||||||||||||||||||
| export interface TutorialContentItem { | ||||||||||||||||||||||||||
| subtitle: string; | ||||||||||||||||||||||||||
| description: string; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export interface TutorialStep { | ||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||
| content: TutorialContentItem[]; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+1
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 중복된 타입 정의 제거 필요 동일한 인터페이스가
다음과 같이 수정하는 것을 제안합니다: -export interface TutorialContentItem {
- subtitle: string;
- description: string;
-}
-
-export interface TutorialStep {
- title: string;
- content: TutorialContentItem[];
-}
+import type { TutorialContentItem, TutorialStep } from '@/components/common/tutorial/types';
+
+export type { TutorialContentItem, TutorialStep };📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TutorialModal from "./tutorial-modal"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function TutorialContainer() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isOpen, setIsOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasCompletedTutorial = localStorage.getItem("nav-tutorial-completed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!hasCompletedTutorial) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 튜토리얼을 본 것으로 표시 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem("nav-tutorial-completed", "true"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. localStorage 접근 및 useEffect 개선 필요 localStorage 접근 시 발생할 수 있는 예외 처리와 useEffect의 안전한 구현이 필요합니다. 다음과 같이 수정하는 것을 제안합니다: +const TUTORIAL_COMPLETED_KEY = 'nav-tutorial-completed';
+
export default function TutorialContainer() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
- const hasCompletedTutorial = localStorage.getItem("nav-tutorial-completed");
- if (!hasCompletedTutorial) {
- setIsOpen(true);
- // 튜토리얼을 본 것으로 표시
- localStorage.setItem("nav-tutorial-completed", "true");
+ try {
+ const hasCompletedTutorial = localStorage.getItem(TUTORIAL_COMPLETED_KEY);
+ if (!hasCompletedTutorial) {
+ setIsOpen(true);
+ localStorage.setItem(TUTORIAL_COMPLETED_KEY, 'true');
+ }
+ } catch (error) {
+ console.error('튜토리얼 상태 저장 중 오류 발생:', error);
}
+
+ return () => {
+ // cleanup 로직이 필요한 경우 여기에 추가
+ };
}, []);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <TutorialModal isOpen={isOpen} onClose={() => setIsOpen(false)} />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* eslint-disable */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TutorialStep } from "../types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface TutorialContentProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| step: TutorialStep; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function TutorialContent({ step }: TutorialContentProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-20"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3 className="text-20-500 mb-10 font-semibold text-gray-800"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {step.title} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-20"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {step.content.map((item, idx) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={`${step.title}-content-${idx}`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h4 className="text-16-600 text-gray-700 mb-10">{item.subtitle}</h4> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-14-500 text-gray-600">{item.description}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 접근성 및 시맨틱 마크업 개선 필요 현재 구조에 다음과 같은 개선이 필요합니다:
다음과 같이 개선해보세요: - <div className="mb-20">
+ <section className="mb-20" aria-labelledby="tutorial-title">
- <h3 className="text-20-500 mb-10 font-semibold text-gray-800">
+ <h3 id="tutorial-title" className="text-20-500 mb-10 font-semibold text-gray-800">
{step.title}
</h3>
- <div className="space-y-20">
+ <ul className="space-y-20 list-none" role="list">
{step.content.map((item, idx) => (
- <div key={`${step.title}-content-${idx}`}>
+ <li key={`${step.title}-${item.subtitle}-${idx}`}>
<h4 className="text-16-600 text-gray-700 mb-10">{item.subtitle}</h4>
<p className="text-14-500 text-gray-600">{item.description}</p>
- </div>
+ </li>
))}
- </div>
- </div>
+ </ul>
+ </section>📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 "/"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 라우팅 로직 개선 필요 현재 구현된 다음과 같이 개선해보세요: + const STEP_PATHS = {
+ 0: "/",
+ 1: "/search",
+ 2: "/my-account",
+ 3: "/portfolio"
+ } as const;
const getPathForStep = (step: number) => {
- switch (step) {
- case 0:
- return "/";
- case 1:
- return "/search";
- case 2:
- return "/my-account";
- case 3:
- return "/portfolio";
- default:
- return "/";
- }
+ return STEP_PATHS[step as keyof typeof STEP_PATHS] || "/";
};📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. localStorage 접근 에러 처리 개선 필요 localStorage 접근 시 발생할 수 있는 예외 상황(예: 사용자가 쿠키를 차단한 경우)에 대한 처리가 필요합니다. 다음과 같이 개선해보세요: const handleComplete = useCallback(() => {
if (typeof window !== "undefined") {
+ try {
localStorage.setItem("nav-tutorial-completed", "true");
+ } catch (error) {
+ console.error("Failed to save tutorial state:", error);
+ }
}
onClose();
}, [onClose]);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BaseModal isOpen={isOpen} onClose={onClose}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="shadow-xl rounded-lg bg-white px-26 py-20"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-15 flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="text-24-600 text-gray-900">GrowFoilo 둘러보기</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="rounded-full p-3 hover:bg-gray-100" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <X className="size-25 text-gray-500" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <TutorialContent step={TUTORIAL_STEPS[currentStep]} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-16-600 text-gray-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {currentStep + 1} / {TUTORIAL_STEPS.length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex gap-10"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {currentStep > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handlePrev} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="flex items-center gap-4 rounded-md border border-gray-300 px-8 py-4 text-16-600 text-gray-700 hover:bg-gray-50" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ChevronLeft className="size-16" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 이전 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {currentStep < TUTORIAL_STEPS.length - 1 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleNext} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="flex items-center gap-4 rounded-md bg-green-500 px-8 py-4 text-16-600 text-white hover:bg-green-600" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 다음 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ChevronRight className="size-16" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleComplete} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="rounded-md bg-green-500 px-8 py-4 text-16-600 text-white hover:bg-green-600" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 시작하기 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </BaseModal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 접근성 개선 필요 모달 컴포넌트의 접근성이 부족합니다. 키보드 네비게이션과 스크린 리더 지원을 추가해야 합니다. 다음 개선사항을 적용해주세요:
- <div className="shadow-xl rounded-lg bg-white px-26 py-20">
+ <div
+ className="shadow-xl rounded-lg bg-white px-26 py-20"
+ role="dialog"
+ aria-labelledby="tutorial-title"
+ aria-describedby="tutorial-desc"
+ >
<button
type="button"
onClick={handleNext}
+ onKeyDown={(e) => e.key === 'Enter' && handleNext()}
className="flex items-center gap-4 rounded-md bg-green-500 px-8 py-4 text-16-600 text-white hover:bg-green-600"
>
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
localStorage 접근 및 상태 관리 개선 필요
다음과 같은 개선사항을 고려해주세요:
다음과 같이 개선해보세요:
useEffect(() => { - const hasCompletedTutorial = localStorage.getItem( - "chart-tutorial-completed", - ); - if (!hasCompletedTutorial) { - setIsOpen(true); - localStorage.setItem("chart-tutorial-completed", "true"); - } + try { + const hasCompletedTutorial = localStorage.getItem( + "chart-tutorial-completed", + ); + if (!hasCompletedTutorial) { + setIsOpen(true); + } + } catch (error) { + console.error('로컬 스토리지 접근 중 오류 발생:', error); + } }, []);