Repository: AI-Tutor-2024/Web-v3.0
Pull request #45: [Feat] 온보딩 모달 구현
Author: login: ChoiTheCreator
Created: 2025-03-09T10:22:16Z | Merged: 2025-03-18T13:02:30Z

## 🔍 관련 이슈
- close #41 

## 🌏 Summary
처음 서비스를 사용하는 사용자를 위한 튜토리얼 가이드 모달입니다.

디자이너 분과 얘기하여 홈페이지에서 전체적인 기능을 6개의 섹션으로 나눈 캐러셀과 함께 활용하기로 했습니다.

모달을 닫은 적이 있다면 이 상태가 로컬스토리지에 기록되어, 한번 모달을 본 사용자는 일단 자동으로 다음부턴 모달이 표시되지 않습니다.


## 📸 스크린샷
<img width="1879" alt="스크린샷 2025-03-09 오후 7 10 42" src="https://github.com/user-attachments/assets/ed808df4-3277-4d5c-b57d-5f05c49e7324" />


## 🤔 고민했던 내용

보실때 좋은 순서는
localstorage/index.ts -> useOnboarding.ts -> 모달 컴포넌트,캐러셀 컴포넌트 입니다.


hooks 폴더의 useOnboarding.ts가 온보딩을 띄어줄지 말지 를 판단하는 custom Hook입니다.

-> 이는 로컬스토리지에서의 상태를 가져와서 판단합니다

로컬스트로지에 처음 활용하는 사용자인지 아닌지를 검증하는 함수는 
utils 폴더 내부의 localStorage/Index.ts에 있습니다.
이는 true/false를 set/get 하며 당연히 LS에는 문자열만 들어오기에 true -> 'true' 이런식으로 저장했습니다.

컴포넌트 분리 기준으론 모달 컴포넌트 , 캐러셀 컴포넌트를 분리했습니다.

-> 모달 컴포넌트가 캐러셀 컴포넌트를 품고 있는 구조로, 각 컴포넌트에는 명확한 props 구조를 위하여 interface로 type을 지정한 후에 props를 보냈습니다.

모달을 닫히게 하기 위해 x 버튼과 외부를 아무데나 클릭하면 사용자가 편히 끌수 있게 설계를 했습니다.

캐러셀은 reactSlider로 구현했으나, 화살표와 dots들은 디자이너의 디자인을 통일시키기 위해 커스텀 작업으로 만들었습니다.

또한 SlideData( 부모 컴포넌트인 모달컴포넌트의 슬라이드 배열)을 하나의 객체 배열로 정의했기에 이 객체 속성들을 자식 컴포넌트(캐러셀 컴포넌트)에 입히는(보내는) 작업이 약간 비효율적인가 ? 싶긴 했습니다만  Slide 자체의 타입을 정의하므로써 조금 명확하게 하려고 노력했습니다.


##개선 포인트
캐러셀 반응속도가 이미지 변환 속도보다 약 0.5초 가량 빠릅니다. 이 부분 사용자로써는 조금 거슬릴수도있는데, 이걸 같이 고민해보면 좋을것같아요





<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Introduced an enhanced onboarding experience for first-time users featuring a guided modal with an interactive carousel. Users can navigate through introductory slides with smooth transitions and custom navigation controls, providing an engaging overview of the app. The experience automatically activates based on user status and can be dismissed once the introduction is complete.
	- Added new dependencies to support the onboarding carousel functionality.
- **Bug Fixes**
	- Improved error handling during folder operations to enhance user experience.
- **Documentation**
	- Updated constants and hooks related to onboarding for clarity and usability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #41: [FEAT] 온보딩 모달 생성
## 🌏 Summary
어떤 기능인가요?

프론트 측에서, 로컬스토로지에서 첫 회원인지 아닌지를 판단하여 첫 회원일 경우 ( 한번도 기능을 사용하지 않은 회원일경우)
회원에게 기능을 소개할 온보딩(튜토리얼) 모달을 구현한다.

## ✔ Task & Description
- 피그마에 있는 Frame10에 해당하는 모달을 생성한다.
1. 캐러셀 위에는 이미지로써, 이미지를 그대로활용한다.
2. 캐러셀은 총 3단계로써 피그마를 보고 구현한다.



Diff:
diff --git a/package-lock.json b/package-lock.json
index be61701..d46880e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,12 +23,15 @@
         "react-dom": "^18",
         "react-dropzone": "^14.2.3",
         "react-hot-toast": "^2.5.2",
+        "react-slick": "^0.30.3",
+        "slick-carousel": "^1.8.1",
         "zustand": "^5.0.0-rc.2"
       },
       "devDependencies": {
         "@types/node": "^20",
         "@types/react": "^18",
         "@types/react-dom": "^18",
+        "@types/react-slick": "^0.23.13",
         "eslint": "^8",
         "eslint-config-next": "14.2.3",
         "postcss": "^8",
@@ -906,6 +909,16 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-slick": {
+      "version": "0.23.13",
+      "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
+      "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@typescript-eslint/parser": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
@@ -1624,6 +1637,12 @@
         "node": ">= 6"
       }
     },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
     "node_modules/client-only": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -1979,6 +1998,12 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/enquire.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
+      "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==",
+      "license": "MIT"
+    },
     "node_modules/es-abstract": {
       "version": "1.23.3",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -3712,6 +3737,13 @@
         "url": "https://github.com/sponsors/panva"
       }
     },
+    "node_modules/jquery": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/js-cookie": {
       "version": "3.0.5",
       "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -3755,6 +3787,15 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
       "dev": true
     },
+    "node_modules/json2mq": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+      "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "string-convert": "^0.2.0"
+      }
+    },
     "node_modules/json5": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
@@ -3869,6 +3910,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+      "license": "MIT"
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4849,6 +4896,23 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "node_modules/react-slick": {
+      "version": "0.30.3",
+      "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.3.tgz",
+      "integrity": "sha512-B4x0L9GhkEWUMApeHxr/Ezp2NncpGc+5174R02j+zFiWuYboaq98vmxwlpafZfMjZic1bjdIqqmwLDcQY0QaFA==",
+      "license": "MIT",
+      "dependencies": {
+        "classnames": "^2.2.5",
+        "enquire.js": "^2.1.6",
+        "json2mq": "^0.2.0",
+        "lodash.debounce": "^4.0.8",
+        "resize-observer-polyfill": "^1.5.0"
+      },
+      "peerDependencies": {
+        "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -4922,6 +4986,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
     "node_modules/resolve": {
       "version": "1.22.8",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -5215,6 +5285,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/slick-carousel": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
+      "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "jquery": ">=1.8.0"
+      }
+    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5260,6 +5339,12 @@
         "safe-buffer": "~5.2.0"
       }
     },
+    "node_modules/string-convert": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+      "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+      "license": "MIT"
+    },
     "node_modules/string-width": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
diff --git a/package.json b/package.json
index 0a6610f..0c44d53 100644
--- a/package.json
+++ b/package.json
@@ -24,12 +24,15 @@
     "react-dom": "^18",
     "react-dropzone": "^14.2.3",
     "react-hot-toast": "^2.5.2",
+    "react-slick": "^0.30.3",
+    "slick-carousel": "^1.8.1",
     "zustand": "^5.0.0-rc.2"
   },
   "devDependencies": {
     "@types/node": "^20",
     "@types/react": "^18",
     "@types/react-dom": "^18",
+    "@types/react-slick": "^0.23.13",
     "eslint": "^8",
     "eslint-config-next": "14.2.3",
     "postcss": "^8",
diff --git a/public/IMG/slide1.png b/public/IMG/slide1.png
new file mode 100644
index 0000000..0a8a9f7
Binary files /dev/null and b/public/IMG/slide1.png differ
diff --git a/public/IMG/slide2.png b/public/IMG/slide2.png
new file mode 100644
index 0000000..85c22df
Binary files /dev/null and b/public/IMG/slide2.png differ
diff --git a/public/IMG/slide3.png b/public/IMG/slide3.png
new file mode 100644
index 0000000..5a0070e
Binary files /dev/null and b/public/IMG/slide3.png differ
diff --git a/public/IMG/slide4.png b/public/IMG/slide4.png
new file mode 100644
index 0000000..f1d709c
Binary files /dev/null and b/public/IMG/slide4.png differ
diff --git a/public/IMG/slide5.png b/public/IMG/slide5.png
new file mode 100644
index 0000000..035d49b
Binary files /dev/null and b/public/IMG/slide5.png differ
diff --git a/public/IMG/slide6.png b/public/IMG/slide6.png
new file mode 100644
index 0000000..3492ac1
Binary files /dev/null and b/public/IMG/slide6.png differ
diff --git a/src/app/(routes)/home/page.tsx b/src/app/(routes)/home/page.tsx
index 5201f13..82c24e3 100644
--- a/src/app/(routes)/home/page.tsx
+++ b/src/app/(routes)/home/page.tsx
@@ -1,20 +1,24 @@
-"use client";
+'use client';
 
-import React, { useState } from "react";
-import Button from "@/app/components/atoms/Button";
+import React, { useState } from 'react';
+import Button from '@/app/components/atoms/Button';
 import {
   SectionFolder,
   SectionModal,
   SectionModify,
-} from "@/app/components/molecules/Modal";
-import { useFetchFolders } from "@/app/hooks/folder/useFetchFolders";
-import { useCreateFolder } from "@/app/hooks/folder/useCreateFolder";
-import { useUpdateFolder } from "@/app/hooks/folder/useUpdateFolder";
-import { useDeleteFolder } from "@/app/hooks/folder/useDeleteFolder";
-import { useSession } from "next-auth/react";
-import Image from "next/image";
-import speach_bubble from "../../../../public/speech_bubble.svg";
-import { Folder } from "@/app/types/folder";
+} from '@/app/components/molecules/Modal';
+import { useFetchFolders } from '@/app/hooks/folder/useFetchFolders';
+import { useCreateFolder } from '@/app/hooks/folder/useCreateFolder';
+import { useUpdateFolder } from '@/app/hooks/folder/useUpdateFolder';
+import { useDeleteFolder } from '@/app/hooks/folder/useDeleteFolder';
+import { useSession } from 'next-auth/react';
+import Image from 'next/image';
+import speach_bubble from '../../../../public/speech_bubble.svg';
+import { Folder } from '@/app/types/folder';
+
+import { useOnboarding } from '@/app/hooks/useOnboarding';
+
+import OnBoardingModal from '@/app/components/molecules/OnBoardingModal';
 
 const HomePage = () => {
   const { data: session } = useSession();
@@ -26,18 +30,22 @@ const HomePage = () => {
   const [selectedFolder, setSelectedFolder] = useState<Folder | null>(null);
   const [showModify, setShowModify] = useState<{ [key: string]: boolean }>({});
   const [showModal, setShowModal] = useState(false);
-  const [subject, setSubject] = useState("");
-  const [professor, setProfessor] = useState("");
+  const [subject, setSubject] = useState('');
+  const [professor, setProfessor] = useState('');
   const [isEditMode, setIsEditMode] = useState(false);
 
-  const handleCreateFolder = () => {
-    createFolder.mutate({
-      folderName: subject,
-      professorName: professor,
-    });
-    setShowModal(false);
-    setSubject("");
-    setProfessor("");
+  const handleCreateFolder = async () => {
+    try {
+      await createFolder.mutateAsync({
+        folderName: subject,
+        professorName: professor,
+      });
+      setShowModal(false);
+      setSubject('');
+      setProfessor('');
+    } catch (error) {
+      console.error('폴더 생성 실패:', error);
+    }
   };
 
   const handleUpdateFolder = () => {
@@ -57,7 +65,9 @@ const HomePage = () => {
     deleteFolder.mutate(folderId);
     setSelectedFolder(null);
   };
+  const { showOnboarding, closeOnboarding } = useOnboarding();
 
+  console.log(session?.user.name, 'section');
   return (
     <div className="flex flex-col justify-between h-full w-full">
       <div className="flex flex-col h-full rounded-t-xl rounded-r-ml rounded-l-ml bg-black-90">
@@ -81,8 +91,8 @@ const HomePage = () => {
               variant="create"
               onClick={() => {
                 setIsEditMode(false);
-                setSubject("");
-                setProfessor("");
+                setSubject('');
+                setProfessor('');
                 setShowModal(true);
               }}
             />
@@ -90,8 +100,14 @@ const HomePage = () => {
         </div>
 
         <div className="bg-black-80 rounded-lg rounded-b-none mx-4 h-full">
+          <div className="flex text-center">
+            {showOnboarding && (
+              <OnBoardingModal onClose={closeOnboarding}></OnBoardingModal>
+            )}
+          </div>
           {folders.length === 0 ? (
             <div className="flex flex-col justify-center items-center h-full text-center text-white">
+              <div className="flex items-center justify-center"> </div>
               <p className="text-xl font-bold mb-4">
                 강의 과목 폴더를 만들어 보세요.
               </p>
@@ -102,7 +118,7 @@ const HomePage = () => {
             </div>
           ) : (
             <div className="flex flex-wrap justify-start items-start gap-4 mx-5 py-4">
-              {folders.map((folder) => (
+              {folders.map((folder: any) => (
                 <div
                   key={folder.folderId}
                   className="relative flex flex-col items-center"
diff --git a/src/app/components/molecules/OnBoardingCarousel.tsx b/src/app/components/molecules/OnBoardingCarousel.tsx
new file mode 100644
index 0000000..3e259fb
--- /dev/null
+++ b/src/app/components/molecules/OnBoardingCarousel.tsx
@@ -0,0 +1,106 @@
+import 'slick-carousel/slick/slick.css';
+import 'slick-carousel/slick/slick-theme.css';
+import dynamic from 'next/dynamic';
+import React from 'react';
+
+const Slider = dynamic(() => import('react-slick'), { ssr: false });
+
+const CustomPrevArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❮
+    </div>
+  );
+};
+
+const CustomNextArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❯
+    </div>
+  );
+};
+
+interface SlideData {
+  background: string;
+  title: string;
+  desc: string;
+}
+interface OnBoardingCarouselProps {
+  slides: SlideData[];
+  onSlideChange: (index: number) => void;
+}
+
+const OnBoardingCarousel: React.FC<OnBoardingCarouselProps> = ({
+  slides,
+  onSlideChange,
+}) => {
+  const carouselSetting = {
+    dots: true,
+    customPaging: (i: number) => (
+      <div className="w-1 h-1 rounded-full bg-gray-300" />
+    ),
+    appendDots: (dots: React.ReactNode) => (
+      <ul className="flex justify-center gap-3 list-none absolute w-full top-1">
+        {React.Children.map(dots, (dot) => {
+          const element = dot as React.ReactElement<any>;
+          const isActive =
+            element.props.className &&
+            element.props.className.includes('slick-active');
+          return (
+            <li
+              key={element.key || Math.random()}
+              onClick={element.props.onClick}
+              className="cursor-pointer"
+            >
+              <div
+                className={`w-3 h-3 rounded-full relative bottom-6 ${
+                  isActive ? 'bg-primary' : 'bg-gray-300'
+                }`}
+              ></div>
+            </li>
+          );
+        })}
+      </ul>
+    ),
+    infinite: false,
+    speed: 500,
+    slidesToShow: 1,
+    slidesToScroll: 1,
+    arrows: true,
+    prevArrow: <CustomPrevArrow />,
+    nextArrow: <CustomNextArrow />,
+    afterChange: (current: number) => {
+      if (onSlideChange) onSlideChange(current);
+    },
+    beforeChange: (oldIndex: number, newIndex: number) => {
+      if (onSlideChange) onSlideChange(newIndex);
+    },
+  };
+
+  return (
+    <div className="relative w-full h-[130px] bg-white">
+      <Slider className="w-full h-full" {...carouselSetting}>
+        {slides.map((slide, idx) => (
+          <div
+            key={idx}
+            className="flex flex-col justify-center items-center h-full"
+          >
+            <p className="text-black font-semibold mt-4">{slide.title}</p>
+            <p className="mt-2 text-black">{slide.desc}</p>
+          </div>
+        ))}
+      </Slider>
+    </div>
+  );
+};
+
+export default OnBoardingCarousel;
diff --git a/src/app/components/molecules/OnBoardingModal.tsx b/src/app/components/molecules/OnBoardingModal.tsx
new file mode 100644
index 0000000..b5a25c9
--- /dev/null
+++ b/src/app/components/molecules/OnBoardingModal.tsx
@@ -0,0 +1,56 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+import { slides } from '@/app/constants/\bOnBoardingModalImageList';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+  //carousel IMG Array
+
+  const [slideIndex, setSlideIndex] = useState(0);
+
+  const handleSlideClicked = (index: number) => {
+    setSlideIndex(index);
+  };
+
+  return (
+    <div
+      className="fixed flex flex-col z-50 inset-0 items-center justify-center bg-black bg-opacity-50 backdrop-blur-md"
+      onClick={onClose}
+    >
+      <div
+        onClick={(e) => e.stopPropagation()}
+        className="relative w-[800px] h-[200px] max-w-full bg-transparent -translate-y-20
+      "
+      >
+        <div className="relative w-full">
+          <Image
+            className="w-full object-cover rounded-t-lg"
+            src={slides[slideIndex].background}
+            alt="온보딩 배경"
+            width={600}
+            height={100}
+            priority
+          ></Image>
+        </div>
+        <OnBoardingCarousel
+          slides={slides}
+          onSlideChange={handleSlideClicked}
+        ></OnBoardingCarousel>{' '}
+        <button
+          className="absolute top-4 right-1 bg-transparent text-white px-3 py-1 rounded-md"
+          onClick={onClose}
+        >
+          ✕
+        </button>
+      </div>
+    </div>
+  );
+};
+
+export default OnBoardingModal;
diff --git "a/src/app/constants/\bOnBoardingModalImageList.ts" "b/src/app/constants/\bOnBoardingModalImageList.ts"
new file mode 100644
index 0000000..fb745e9
--- /dev/null
+++ "b/src/app/constants/\bOnBoardingModalImageList.ts"
@@ -0,0 +1,35 @@
+export const slides = [
+  {
+    background: '/IMG/slide1.png',
+    title: '새 과목 폴더를 만들어보세요',
+    desc: '이미 만든 폴더는 내용을 수정하거나 삭제할 수 있어요!',
+  },
+  {
+    background: '/IMG/slide2.png',
+    title:
+      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+  },
+  {
+    background: '/IMG/slide3.png',
+    title:
+      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+  },
+  {
+    background: '/IMG/slide4.png',
+    title: '핵심 키워드와 요구사항을 입력해보세요!',
+    desc: '핵심 키워드를 토대로 요약본을 제공해드립니다.',
+  },
+  {
+    background: '/IMG/slide5.png',
+    title: '요약분을 바탕으로 복습 퀴즈와 하이라이트 영상을 제작해보세요!',
+    desc: '효율적인 학습을 위한 자료를 제공해드립니다',
+  },
+
+  {
+    background: '/IMG/slide6.png',
+    title: '필요한 문제 개수와 원하는 문제 유형을 선택하세요!',
+    desc: '만들기 버튼으로 문제를 직접 만들어보세요!',
+  },
+];
diff --git a/src/app/hooks/useOnboarding.ts b/src/app/hooks/useOnboarding.ts
new file mode 100644
index 0000000..7e8c446
--- /dev/null
+++ b/src/app/hooks/useOnboarding.ts
@@ -0,0 +1,19 @@
+import { getIsFirstTimeUser, setIsFirstTimeUser } from '../utils/localstorage'; //@
+import { useEffect, useState } from 'react';
+
+export const useOnboarding = () => {
+  const [showOnboarding, setShowOnboarding] = useState(false);
+
+  useEffect(() => {
+    if (getIsFirstTimeUser()) {
+      setShowOnboarding(true);
+    }
+  }, []);
+
+  const closeOnboarding = () => {
+    setShowOnboarding(false);
+    setIsFirstTimeUser(false);
+  };
+
+  return { showOnboarding, closeOnboarding };
+};
diff --git a/src/app/utils/localstorage/index.ts b/src/app/utils/localstorage/index.ts
new file mode 100644
index 0000000..55e5abe
--- /dev/null
+++ b/src/app/utils/localstorage/index.ts
@@ -0,0 +1,18 @@
+export const getIsFirstTimeUser = (): boolean => {
+  if (typeof window !== 'undefined') {
+    const value: string | null = localStorage.getItem('isFirstTimeUser'); // "true" or "false"
+    if (value === null) {
+      return true;
+    }
+
+    return value === 'true';
+  }
+
+  return true;
+};
+
+export const setIsFirstTimeUser = (value: boolean): void => {
+  if (typeof window != 'undefined') {
+    localStorage.setItem('isFirstTimeUser', value.toString());
+  }
+};


Reviews:
user: login: ujinsim
body: 고생하셨습니다 !!!! 👍👍
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T11:58:40Z
user: login: ChoiTheCreator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T12:29:35Z
user: login: ChoiTheCreator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T12:32:53Z
user: login: ChoiTheCreator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T12:33:19Z
user: login: ChoiTheCreator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T04:33:25Z
user: login: ChoiTheCreator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T08:05:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (16)</summary><blockquote>

<details>
<summary>src/app/hooks/useOnboarding.ts (1)</summary><blockquote>

`1-1`: **Remove unnecessary comment marker**

The import statement contains a trailing `//@` comment that doesn't appear to serve any purpose and should be removed.


```diff
- import { getIsFirstTimeUser, setIsFirstTimeUser } from '../utils/localstorage'; //@
+ import { getIsFirstTimeUser, setIsFirstTimeUser } from '../utils/localstorage';
```

</blockquote></details>
<details>
<summary>src/app/utils/localstorage/index.ts (1)</summary><blockquote>

`14-18`: **Use consistent equality operators**

There's an inconsistency in the comparison operators used between the two functions:


```diff
- if (typeof window != 'undefined') {
+ if (typeof window !== 'undefined') {
```

While this won't cause functional issues, it's better to consistently use strict equality operators (`!==`) throughout your codebase, as you did in the `getIsFirstTimeUser` function.

</blockquote></details>
<details>
<summary>src/app/(routes)/home/page.tsx (3)</summary><blockquote>

`76-76`: **Remove console.log statement**

This console.log statement appears to be leftover from debugging and should be removed before merging to production.

```diff
-  console.log(session?.user.name, 'section');
```

---

`116-116`: **Consider removing empty div**

This empty div with flex styling doesn't appear to serve a functional purpose. Consider removing it or adding a comment explaining its purpose if it's intentionally empty.

```diff
-              <div className="flex items-center justify-center"> </div>
```

---

`127-127`: **Use specific type instead of 'any'**

Instead of using the 'any' type, consider using the Folder interface that's already imported at line 17.

```diff
-              {folders.map((folder: any) => (
+              {folders.map((folder: Folder) => (
```

</blockquote></details>
<details>
<summary>src/app/components/molecules/OnBoardingModal.tsx (4)</summary><blockquote>

`9-9`: **Remove unnecessary comment**

This comment about potentially adding more props should be removed or replaced with proper JSDoc documentation.

```diff
-  //필요 props 추가할거 있으면 해야할듯 합니다.
```

---

`13-14`: **Remove unnecessary comment**

This comment about carousel IMG Array doesn't provide useful information and should be removed.

```diff
-  //carousel IMG Array
-
```

---

`44-45`: **Remove unnecessary whitespace**

There's a trailing `{' '}` after the closing JSX tag which should be removed.

```diff
-        ></OnBoardingCarousel>{' '}
+        ></OnBoardingCarousel>
```

---

`46-50`: **Improve accessibility of close button**

The close button should have improved accessibility for screen readers.

```diff
-        <button
-          className="absolute top-4 right-1 bg-transparent text-white px-3 py-1 rounded-md"
-          onClick={onClose}
-        >
-          ✕
-        </button>
+        <button
+          className="absolute top-4 right-1 bg-transparent text-white px-3 py-1 rounded-md"
+          onClick={onClose}
+          aria-label="Close onboarding modal"
+        >
+          ✕
+        </button>
```

</blockquote></details>
<details>
<summary>src/app/components/molecules/OnBoardingCarousel.tsx (4)</summary><blockquote>

`8-18`: **Improve typing and accessibility of CustomPrevArrow component**

The component has two issues that should be addressed:
1. The props are typed as `any` which is not type-safe
2. The arrow character should be more accessible for screen readers


```diff
-const CustomPrevArrow = (props: any) => {
+interface ArrowProps {
+  onClick?: () => void;
+  className?: string;
+}
+
+const CustomPrevArrow = (props: ArrowProps) => {
  const { onClick } = props;
  return (
    <div
      onClick={onClick}
      className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+     aria-label="Previous slide"
+     role="button"
+     tabIndex={0}
    >
      ❮
    </div>
  );
};
```

---

`20-30`: **Improve typing and accessibility of CustomNextArrow component**

Similar to the previous arrow component, this one should also have improved typing and accessibility.


```diff
-const CustomNextArrow = (props: any) => {
+const CustomNextArrow = (props: ArrowProps) => {
  const { onClick } = props;
  return (
    <div
      onClick={onClick}
      className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+     aria-label="Next slide"
+     role="button"
+     tabIndex={0}
    >
      ❯
    </div>
  );
};
```

---

`56-58`: **Use optional chaining for className check**

As suggested by the static analysis tool, use optional chaining for a cleaner check of the className property.


```diff
-          const isActive =
-            element.props.className &&
-            element.props.className.includes('slick-active');
+          const isActive = element.props.className?.includes('slick-active');
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 56-58: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`81-86`: **Simplify callback conditions**

The null checks before calling onSlideChange are redundant since TypeScript will ensure the function is provided based on your interface.


```diff
-    afterChange: (current: number) => {
-      if (onSlideChange) onSlideChange(current);
-    },
-    beforeChange: (oldIndex: number, newIndex: number) => {
-      if (onSlideChange) onSlideChange(newIndex);
-    },
+    afterChange: (current: number) => {
+      onSlideChange(current);
+    },
+    beforeChange: (oldIndex: number, newIndex: number) => {
+      onSlideChange(newIndex);
+    },
```

</blockquote></details>
<details>
<summary>src/app/constants/OnBoardingModalImageList.ts (3)</summary><blockquote>

`3-3`: **Consider using lowercase for image directory paths.**

Using uppercase for directory names (`/IMG/`) is uncommon in web projects and might cause issues on case-sensitive file systems like Linux servers. Web standards typically favor lowercase paths.


```diff
-    background: '/IMG/slide1.png',
+    background: '/img/slide1.png',
```

---

`29-29`: **Remove unnecessary empty line.**

There's an empty line at position 29 that doesn't serve any purpose and can be removed for cleaner code.

---

`1-35`: **Consider adding image dimensions or metadata for performance optimization.**

For better performance and to prevent layout shifts during loading, consider adding image dimensions or other metadata to each slide object. This would help the browser allocate space for images before they load.


Example:

```diff
  {
    background: '/IMG/slide1.png',
+    width: 800,
+    height: 600,
    title: '새 과목 폴더를 만들어보세요',
    desc: '이미 만든 폴더는 내용을 수정하거나 삭제할 수 있어요!',
  },
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 91f1b297ddd787897b4d8c4c1789bbb0d7d9f6f1 and 9a8935440a60cae5d9f3ae6bcbc6579a6e27a2da.

</details>

<details>
<summary>⛔ Files ignored due to path filters (7)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`
* `public/IMG/slide1.png` is excluded by `!**/*.png`
* `public/IMG/slide2.png` is excluded by `!**/*.png`
* `public/IMG/slide3.png` is excluded by `!**/*.png`
* `public/IMG/slide4.png` is excluded by `!**/*.png`
* `public/IMG/slide5.png` is excluded by `!**/*.png`
* `public/IMG/slide6.png` is excluded by `!**/*.png`

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `package.json` (1 hunks)
* `src/app/(routes)/home/page.tsx` (7 hunks)
* `src/app/components/molecules/OnBoardingCarousel.tsx` (1 hunks)
* `src/app/components/molecules/OnBoardingModal.tsx` (1 hunks)
* `src/app/constants/OnBoardingModalImageList.ts` (1 hunks)
* `src/app/hooks/useOnboarding.ts` (1 hunks)
* `src/app/utils/localstorage/index.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>src/app/hooks/useOnboarding.ts (1)</summary><blockquote>

<details>
<summary>src/app/utils/localstorage/index.ts (2) (2)</summary>

* `getIsFirstTimeUser` (1:12)
* `setIsFirstTimeUser` (14:18)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/app/components/molecules/OnBoardingCarousel.tsx</summary>

[error] 56-58: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`26-27`: **Dependencies for carousel look good**

The addition of react-slick, slick-carousel, and the corresponding type definitions is appropriate for implementing the onboarding carousel as mentioned in the PR objectives.



Also applies to: 34-34

</details>
<details>
<summary>src/app/hooks/useOnboarding.ts (1)</summary>

`4-19`: **Good implementation of the onboarding hook**

The hook's implementation is clean and follows React best practices:
- Using useState to track the onboarding visibility state
- Using useEffect with an empty dependency array to check first-time status only on mount
- Providing a clear API with showOnboarding state and closeOnboarding function
- Properly updating localStorage when the onboarding is closed

This aligns well with the PR objectives to control the visibility of the onboarding modal based on user status.

</details>
<details>
<summary>src/app/utils/localstorage/index.ts (1)</summary>

`1-12`: **Implementation of getIsFirstTimeUser looks good**

The function correctly:
- Handles server-side rendering with the window check
- Properly retrieves and parses the localStorage value
- Returns appropriate defaults when the value is not set

This ensures new users will see the onboarding modal while returning users won't.

</details>
<details>
<summary>src/app/(routes)/home/page.tsx (1)</summary>

`109-113`: **Good onboarding modal implementation**

The conditional rendering of the onboarding modal is well implemented. The user will only see the modal if `showOnboarding` is true, which is managed by the custom hook.

</details>
<details>
<summary>src/app/components/molecules/OnBoardingModal.tsx (2)</summary>

`12-12`: **Consider alternative to React.FC**

Using React.FC is a stylistic choice that was questioned in a previous review. Consider using a more direct function type definition for better type safety and to avoid implicit children prop inclusion.


```diff
-const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+const OnBoardingModal = ({ onClose }: OnBoardingModalProps) => {
```

---

`23-23`: **Good use of z-index for modal overlay**

The modal's z-index and vertical alignment have been properly implemented as suggested in the previous review.

</details>
<details>
<summary>src/app/components/molecules/OnBoardingCarousel.tsx (1)</summary>

`75-75`: **Speed setting is consistent with OnBoardingModal**

The transition speed of 500ms is intentional as mentioned in the previous review comment. This creates a consistent experience across components.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T08:08:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/app/(routes)/home/page.tsx (3)</summary><blockquote>

`38-49`: **Consider showing user-friendly error messages for folder creation failures.**
Currently, errors are only logged to the console, which doesn't inform the user when the creation fails.

---

`93-93`: **Remove or refine debug logs.**

Console logs can clutter production output. Consider removing them or using a structured logging library with a suitable logging level.

---

`144-144`: **Use a stricter type for folder objects rather than `any`.**
Defining a folder interface or type will enhance readability and reduce runtime errors.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9a8935440a60cae5d9f3ae6bcbc6579a6e27a2da and 6fc0d83fb631dfeb672bcbe0e52b34cf4874cc5f.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `package.json` (1 hunks)
* `src/app/(routes)/home/page.tsx` (5 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* package.json

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>src/app/(routes)/home/page.tsx (1)</summary><blockquote>

<details>
<summary>src/app/api/folders/index.ts (3) (3)</summary>

* `createFolder` (16:26)
* `updateFolder` (28:42)
* `deleteFolder` (44:50)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/app/(routes)/home/page.tsx</summary>

[error] 54-56: Expected a statement but instead found '<<<<<<< feat/onboard-Modal'.

Expected a statement here.


(parse)

---

[error] 56-56: `await` is only allowed within async functions and at the top levels of modules.



(parse)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/app/(routes)/home/page.tsx (5)</summary>

`1-1`: **No issues with these imports or use client directive.**



Also applies to: 3-4, 9-16, 19-19, 21-21

---

`33-34`: **State initialization looks good.**

---

`91-91`: **Hook usage is correct.**

---

`126-130`: **Conditional rendering of OnBoardingModal is straightforward.**

---

`133-133`: **Empty `<div>` placeholder**  
No issue—may be a spacing placeholder.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T12:37:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/app/(routes)/home/page.tsx (3)</summary><blockquote>

`70-70`: **Remove console.log statement in production code.**

Debug logs like this should be removed before merging to production to avoid cluttering the console.

```diff
-  console.log(session?.user.name, 'section');
```

---

`51-62`: **Make handleUpdateFolder function async to properly use await.**

This function appears to be trying to use async/await pattern but is missing the `async` keyword. Although the current implementation uses `mutate` instead of `mutateAsync`, it's good practice to mark the function as async for consistency with `handleCreateFolder` and for future modifications.


```diff
-const handleUpdateFolder = () => {
+const handleUpdateFolder = async () => {
  if (!selectedFolder) return;

-  updateFolder.mutate({
+  try {
+    await updateFolder.mutateAsync({
      folderId: selectedFolder.folderId,
      folderName: subject,
      professorName: professor,
    });
-  };
+    setShowModify((prev) => ({ ...prev, [selectedFolder.folderId]: false }));
+    setShowModal(false);
+  } catch (error) {
+    console.error('폴더 수정 실패:', error);
+  }
};
```

---

`121-121`: **Avoid using 'any' type in TypeScript.**

Using `any` bypasses TypeScript's type checking, which reduces the benefits of using TypeScript. Consider using the `Folder` type that is already imported.


```diff
-  {folders.map((folder: any) => (
+  {folders.map((folder: Folder) => (
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6fc0d83fb631dfeb672bcbe0e52b34cf4874cc5f and a1fe13b4b30bbdc1a5570fb6487b3ff3e1c2ec53.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `package.json` (1 hunks)
* `src/app/(routes)/home/page.tsx` (5 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* package.json

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>src/app/(routes)/home/page.tsx (1)</summary><blockquote>

<details>
<summary>src/app/api/folders/index.ts (1) (1)</summary>

* `createFolder` (16:26)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/app/(routes)/home/page.tsx (3)</summary>

`37-49`: **Good error handling implementation.**

The updated `handleCreateFolder` function with async/await pattern and proper error handling is a good improvement. This will ensure that any errors during folder creation are properly logged and won't cause the application to crash.

---

`103-107`: **Great implementation of the onboarding modal for first-time users.**

The conditional rendering of the `OnBoardingModal` based on the `showOnboarding` state from the custom hook is well implemented. This approach correctly follows the PR objectives of showing the onboarding modal only to first-time users.

---

`68-68`: **Good use of custom hook for onboarding logic.**

Extracting the onboarding logic into a custom hook (`useOnboarding`) follows React's best practices for code organization and reusability. This makes the component cleaner and the onboarding functionality potentially reusable across the application.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T13:02:17Z

Review comments:
diff_hunk: @@ -0,0 +1,103 @@
+import 'slick-carousel/slick/slick.css';
+import 'slick-carousel/slick/slick-theme.css';
+import dynamic from 'next/dynamic';
+import React from 'react';
+
+const Slider = dynamic(() => import('react-slick'), { ssr: false });
+
+const CustomPrevArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❮
+    </div>
+  );
+};
+
+const CustomNextArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❯
+    </div>
+  );
+};
+
+interface SlideData {
+  background: string;
+  title: string;
+  desc: string;
+}
+interface OnBoardingCarouselProps {
+  slides: SlideData[];
+  onSlideChange: (index: number) => void;
+}
+
+const OnBoardingCarousel: React.FC<OnBoardingCarouselProps> = ({
+  slides,
+  onSlideChange,
+}) => {
+  const carouselSetting = {
+    dots: true,
+    customPaging: (i: number) => (
+      <div className="w-1 h-1 rounded-full bg-gray-300" />
+    ),
+    appendDots: (dots: React.ReactNode) => (
+      <ul className="flex justify-center gap-3 list-none absolute w-full top-1">
+        {React.Children.map(dots, (dot) => {
+          const element = dot as React.ReactElement<any>;
+          const isActive =
+            element.props.className &&
+            element.props.className.includes('slick-active');
+          return (
+            <li
+              key={element.key || Math.random()}
+              onClick={element.props.onClick}
+              className="cursor-pointer"
+            >
+              <div
+                className={`w-3 h-3 rounded-full relative bottom-6 ${
+                  isActive ? 'bg-primary' : 'bg-gray-300'
+                }`}
+              ></div>
+            </li>
+          );
+        })}
+      </ul>
+    ),
+    infinite: false,
+    speed: 500,
path: src/app/components/molecules/OnBoardingCarousel.tsx
user: login: ujinsim
body: `onBoardingCarousel`과 `onBoardingModal `모두 Speed가 500인데 의도하신 건지 궁금합니다 !
created_at: 2025-03-10T11:49:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+  //carousel IMG Array
+  const slides = [
+    {
+      background: '/IMG/slide1.png',
+      title: '새 과목 폴더를 만들어보세요',
+      desc: '이미 만든 폴더는 내용을 수정하거나 삭제할 수 있어요!',
+    },
+    {
+      background: '/IMG/slide2.png',
+      title:
+        '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+      desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+    },
+    {
+      background: '/IMG/slide3.png',
+      title:
+        '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+      desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+    },
+    {
+      background: '/IMG/slide4.png',
+      title: '핵심 키워드와 요구사항을 입력해보세요!',
+      desc: '핵심 키워드를 토대로 요약본을 제공해드립니다.',
+    },
+    {
+      background: '/IMG/slide5.png',
+      title: '요약분을 바탕으로 복습 퀴즈와 하이라이트 영상을 제작해보세요!',
+      desc: '효율적인 학습을 위한 자료를 제공해드립니다',
+    },
+
+    {
+      background: '/IMG/slide6.png',
+      title: '필요한 문제 개수와 원하는 문제 유형을 선택하세요!',
+      desc: '만들기 버튼으로 문제를 직접 만들어보세요!',
+    },
+  ];
+
+  const [slideIndex, setSlideIndex] = useState(0);
+
+  const handleSlideClicked = (index: number) => {
+    setSlideIndex(index);
+  };
+
+  return (
+    <div
+      className="fixed flex inset-0 items-center justify-center bg-black bg-opacity-50 backdrop-blur-md"
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ujinsim
body: Modal이기 때문에 `z-index` 값과 `세로 정렬`을 추가해도 좋을 것 같네요 ~~ 
created_at: 2025-03-10T11:50:44Z
author_association: COLLABORATOR
path: public/IMG/onBoardingBg.jpeg
user: login: ujinsim
body: 해당 이미지가 2개 올라와있어서 지워주시면 감사하겠습니다 !
created_at: 2025-03-10T11:51:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ujinsim
body: React.FC 방식을 사용하신 이유가 궁금합니다 !
created_at: 2025-03-10T11:53:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+  //carousel IMG Array
+  const slides = [
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ujinsim
body: slides 는 `constants`로 빼도 좋을 것 같네요 !
created_at: 2025-03-10T11:54:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,103 @@
+import 'slick-carousel/slick/slick.css';
+import 'slick-carousel/slick/slick-theme.css';
+import dynamic from 'next/dynamic';
+import React from 'react';
+
+const Slider = dynamic(() => import('react-slick'), { ssr: false });
+
+const CustomPrevArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❮
+    </div>
+  );
+};
+
+const CustomNextArrow = (props: any) => {
+  const { onClick } = props;
+  return (
+    <div
+      onClick={onClick}
+      className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 text-3xl cursor-pointer z-10"
+    >
+      ❯
+    </div>
+  );
+};
+
+interface SlideData {
+  background: string;
+  title: string;
+  desc: string;
+}
+interface OnBoardingCarouselProps {
+  slides: SlideData[];
+  onSlideChange: (index: number) => void;
+}
+
+const OnBoardingCarousel: React.FC<OnBoardingCarouselProps> = ({
+  slides,
+  onSlideChange,
+}) => {
+  const carouselSetting = {
+    dots: true,
+    customPaging: (i: number) => (
+      <div className="w-1 h-1 rounded-full bg-gray-300" />
+    ),
+    appendDots: (dots: React.ReactNode) => (
+      <ul className="flex justify-center gap-3 list-none absolute w-full top-1">
+        {React.Children.map(dots, (dot) => {
+          const element = dot as React.ReactElement<any>;
+          const isActive =
+            element.props.className &&
+            element.props.className.includes('slick-active');
+          return (
+            <li
+              key={element.key || Math.random()}
+              onClick={element.props.onClick}
+              className="cursor-pointer"
+            >
+              <div
+                className={`w-3 h-3 rounded-full relative bottom-6 ${
+                  isActive ? 'bg-primary' : 'bg-gray-300'
+                }`}
+              ></div>
+            </li>
+          );
+        })}
+      </ul>
+    ),
+    infinite: false,
+    speed: 500,
path: src/app/components/molecules/OnBoardingCarousel.tsx
user: login: ChoiTheCreator
body: speed 500 전환속도가 가장 자연스러웠기에 선정했습니다! 이 부분은 다른 분들과 얘기해서 조절 가능할것같아요
created_at: 2025-03-10T12:29:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+  //carousel IMG Array
+  const slides = [
+    {
+      background: '/IMG/slide1.png',
+      title: '새 과목 폴더를 만들어보세요',
+      desc: '이미 만든 폴더는 내용을 수정하거나 삭제할 수 있어요!',
+    },
+    {
+      background: '/IMG/slide2.png',
+      title:
+        '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+      desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+    },
+    {
+      background: '/IMG/slide3.png',
+      title:
+        '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+      desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+    },
+    {
+      background: '/IMG/slide4.png',
+      title: '핵심 키워드와 요구사항을 입력해보세요!',
+      desc: '핵심 키워드를 토대로 요약본을 제공해드립니다.',
+    },
+    {
+      background: '/IMG/slide5.png',
+      title: '요약분을 바탕으로 복습 퀴즈와 하이라이트 영상을 제작해보세요!',
+      desc: '효율적인 학습을 위한 자료를 제공해드립니다',
+    },
+
+    {
+      background: '/IMG/slide6.png',
+      title: '필요한 문제 개수와 원하는 문제 유형을 선택하세요!',
+      desc: '만들기 버튼으로 문제를 직접 만들어보세요!',
+    },
+  ];
+
+  const [slideIndex, setSlideIndex] = useState(0);
+
+  const handleSlideClicked = (index: number) => {
+    setSlideIndex(index);
+  };
+
+  return (
+    <div
+      className="fixed flex inset-0 items-center justify-center bg-black bg-opacity-50 backdrop-blur-md"
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ChoiTheCreator
body: 네 좋은생각인것같아요 ! 반영하겠습니다
created_at: 2025-03-10T12:32:53Z
author_association: COLLABORATOR
path: public/IMG/onBoardingBg.jpeg
user: login: ChoiTheCreator
body: 네 확인했습니다! 
created_at: 2025-03-10T12:33:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ChoiTheCreator
body: 평소에 함수형 컴포넌트 타입 지정하듯 하던대로 타입을 지정했는데, 사실 불필요한 렌더링이 일어날 가능성은 있어보입니다!

그러나 지금 단계에선 약간 사소하다고 판단하여 다음 task를 위해 잠깐 미루겠습니다!!
created_at: 2025-03-18T04:33:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+
+interface OnBoardingModalProps {
+  onClose: () => void;
+  //필요 props 추가할거 있으면 해야할듯 합니다.
+}
+
+const OnBoardingModal: React.FC<OnBoardingModalProps> = ({ onClose }) => {
+  //carousel IMG Array
+  const slides = [
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: ChoiTheCreator
body: 네 분리작업 완료했습니다.

정적인 파일 분리하는건 좋은 아이디어같아용
created_at: 2025-03-18T08:05:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+import OnBoardingCarousel from './OnBoardingCarousel';
+import { slides } from '@/app/constants/\bOnBoardingModalImageList';
path: src/app/components/molecules/OnBoardingModal.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove Unicode backspace character from import path**

There's an unexpected Unicode backspace character (\b) in your import path that could cause issues.

```diff
-import { slides } from '@/app/constants/\bOnBoardingModalImageList';
+import { slides } from '@/app/constants/OnBoardingModalImageList';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { slides } from '@/app/constants/OnBoardingModalImageList';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T08:08:42Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+export const slides = [
+  {
+    background: '/IMG/slide1.png',
+    title: '새 과목 폴더를 만들어보세요',
+    desc: '이미 만든 폴더는 내용을 수정하거나 삭제할 수 있어요!',
+  },
+  {
+    background: '/IMG/slide2.png',
+    title:
+      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+  },
+  {
+    background: '/IMG/slide3.png',
+    title:
+      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
+    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+  },
path: src/app/constants/OnBoardingModalImageList.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Duplicate content in slides 2 and 3.**

Slides at indices 1 and 2 (positions 2 and 3 in the carousel) have identical title and description text. This appears to be unintentional and could confuse users seeing the same content twice.


```diff
  {
    background: '/IMG/slide3.png',
    title:
-      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
-    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
+    title: '[Third slide title - different content needed]',
+    desc: '[Third slide description - different content needed]',
  },
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    background: '/IMG/slide2.png',
    title:
      '새 노트 만들기로 학습 콘텐츠를 생성하고, 노트를 리스트로 관리하세요!',
    desc: '더 이상 필요없는 노트는 삭제할 수 있어요.',
  },
  {
    background: '/IMG/slide3.png',
    title: '[Third slide title - different content needed]',
    desc: '[Third slide description - different content needed]',
  },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T08:08:42Z
author_association: NONE
diff_hunk: @@ -26,22 +30,48 @@ const HomePage = () => {
   const [selectedFolder, setSelectedFolder] = useState<Folder | null>(null);
   const [showModify, setShowModify] = useState<{ [key: string]: boolean }>({});
   const [showModal, setShowModal] = useState(false);
-  const [subject, setSubject] = useState("");
-  const [professor, setProfessor] = useState("");
+  const [subject, setSubject] = useState('');
+  const [professor, setProfessor] = useState('');
   const [isEditMode, setIsEditMode] = useState(false);
 
-  const handleCreateFolder = () => {
-    createFolder.mutate({
-      folderName: subject,
-      professorName: professor,
-    });
-    setShowModal(false);
-    setSubject("");
-    setProfessor("");
+
+  const handleCreateFolder = async () => {
+    try {
+      await createFolder.mutateAsync({
+        folderName: subject,
+        professorName: professor,
+      });
+      setShowModal(false);
+      setSubject('');
+      setProfessor('');
+    } catch (error) {
+      console.error('폴더 생성 실패:', error);
+    }
+
   };
 
   const handleUpdateFolder = () => {
-    if (!selectedFolder) return;
+    if (!selectedFolder) return;<<<<<<< feat/onboard-Modal
+    try {
+      await updateFolder.mutateAsync({
+        folderId: selectedFolder.folderId,
+        folderName: subject,
+        professorName: professor,
+      });
+      setShowModify((prev) => ({ ...prev, [selectedFolder.folderId]: false }));
+      setShowModal(false);
+    } catch (error) {
+      console.error('폴더 수정 실패:', error);
+    }
+  };
+
+  const handleDeleteFolder = async (folderId: number) => {
+    try {
+      await deleteFolder.mutateAsync(folderId);
+      setSelectedFolder(null);
+    } catch (error) {
+      console.error('폴더 삭제 실패:', error);
+    }
path: src/app/(routes)/home/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Duplicate function definition detected.**

There’s another `handleDeleteFolder` at lines 86-90. Please remove or consolidate to avoid confusion and potential errors.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T12:37:09Z
author_association: NONE
diff_hunk: @@ -26,22 +30,48 @@ const HomePage = () => {
   const [selectedFolder, setSelectedFolder] = useState<Folder | null>(null);
   const [showModify, setShowModify] = useState<{ [key: string]: boolean }>({});
   const [showModal, setShowModal] = useState(false);
-  const [subject, setSubject] = useState("");
-  const [professor, setProfessor] = useState("");
+  const [subject, setSubject] = useState('');
+  const [professor, setProfessor] = useState('');
   const [isEditMode, setIsEditMode] = useState(false);
 
-  const handleCreateFolder = () => {
-    createFolder.mutate({
-      folderName: subject,
-      professorName: professor,
-    });
-    setShowModal(false);
-    setSubject("");
-    setProfessor("");
+
+  const handleCreateFolder = async () => {
+    try {
+      await createFolder.mutateAsync({
+        folderName: subject,
+        professorName: professor,
+      });
+      setShowModal(false);
+      setSubject('');
+      setProfessor('');
+    } catch (error) {
+      console.error('폴더 생성 실패:', error);
+    }
+
   };
 
   const handleUpdateFolder = () => {
-    if (!selectedFolder) return;
+    if (!selectedFolder) return;<<<<<<< feat/onboard-Modal
+    try {
+      await updateFolder.mutateAsync({
+        folderId: selectedFolder.folderId,
+        folderName: subject,
+        professorName: professor,
+      });
+      setShowModify((prev) => ({ ...prev, [selectedFolder.folderId]: false }));
+      setShowModal(false);
+    } catch (error) {
+      console.error('폴더 수정 실패:', error);
+    }
+  };
path: src/app/(routes)/home/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Resolve merge conflict markers and async usage issue.**

1. Remove the `<<<<<<< feat/onboard-Modal` line (and related conflict markers) to avoid parse errors.  
2. Mark the function as `async` to properly use `await`.  



A possible fix:

```diff
-const handleUpdateFolder = () => {
-if (!selectedFolder) return;<<<<<<< feat/onboard-Modal
-try {
-  await updateFolder.mutateAsync({
-    folderId: selectedFolder.folderId,
-    folderName: subject,
-    professorName: professor,
-  });
-  ...
-} catch (error) {
-  console.error('폴더 수정 실패:', error);
-}
+const handleUpdateFolder = async () => {
+  if (!selectedFolder) return;
+  try {
+    await updateFolder.mutateAsync({
+      folderId: selectedFolder.folderId,
+      folderName: subject,
+      professorName: professor,
+    });
+    ...
+  } catch (error) {
+    console.error('폴더 수정 실패:', error);
+  }
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const handleUpdateFolder = async () => {
    if (!selectedFolder) return;
    try {
      await updateFolder.mutateAsync({
        folderId: selectedFolder.folderId,
        folderName: subject,
        professorName: professor,
      });
      setShowModify((prev) => ({ ...prev, [selectedFolder.folderId]: false }));
      setShowModal(false);
    } catch (error) {
      console.error('폴더 수정 실패:', error);
    }
  };
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 54-56: Expected a statement but instead found '<<<<<<< feat/onboard-Modal'.

Expected a statement here.


(parse)

---

[error] 56-56: `await` is only allowed within async functions and at the top levels of modules.



(parse)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T12:37:09Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-03-09T10:22:20Z
body: [vc]: #S7lGRSMiBYLdF73OoJfBm4babOi5rQUeMQ3YBBHTT0Y=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3ZWItdjMtMCIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ3ZWItdjMtMC1naXQtZmVhdC1vbmJvYXJkLW1vZGFsLWFpLXR1dG9yLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FpLXR1dG9yL3dlYi12My0wLzhTSlBXMmNGNEo0ZWJuSHNjVlZKYzFwRjJMb2MiLCJwcmV2aWV3VXJsIjoid2ViLXYzLTAtZ2l0LWZlYXQtb25ib2FyZC1tb2RhbC1haS10dXRvci52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoid2ViLXYzLTAtMmh0cyIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ3ZWItdjMtMC0yaHRzLWdpdC1mZWF0LW9uYm9hcmQtbW9kYWwtYWktdHV0b3IudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYWktdHV0b3Ivd2ViLXYzLTAtMmh0cy9GOTZiY2lLNjVMZk43YVg0VVBTTEVKcmNiN1BVIiwicHJldmlld1VybCI6IndlYi12My0wLTJodHMtZ2l0LWZlYXQtb25ib2FyZC1tb2RhbC1haS10dXRvci52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **web-v3-0** | ✅ Ready ([Inspect](https://vercel.com/ai-tutor/web-v3-0/8SJPW2cF4J4ebnHscVVJc1pF2Loc)) | [Visit Preview](https://web-v3-0-git-feat-onboard-modal-ai-tutor.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/web-v3-0-git-feat-onboard-modal-ai-tutor.vercel.app?via=pr-comment-feedback-link) | Mar 18, 2025 1:01pm |
| **web-v3-0-2hts** | ✅ Ready ([Inspect](https://vercel.com/ai-tutor/web-v3-0-2hts/F96bciK65LfN7aX4UPSLEJrcb7PU)) | [Visit Preview](https://web-v3-0-2hts-git-feat-onboard-modal-ai-tutor.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/web-v3-0-2hts-git-feat-onboard-modal-ai-tutor.vercel.app?via=pr-comment-feedback-link) | Mar 18, 2025 1:01pm |



author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-18T08:03:41Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces several new dependencies and implements an onboarding feature for first-time users. The changes add new React components for an onboarding modal and carousel, a custom hook for managing the onboarding state, and local storage utilities for user detection. The home page is updated to conditionally render the onboarding modal based on the user’s status. Additionally, coding style improvements are applied in string formatting across the codebase.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `package.json` | Added dependencies: `"react-slick": "^0.30.3"`, `"slick-carousel": "^1.8.1"` and dev dependency `"@types/react-slick": "^0.23.13"`. |
| `src/app/(routes)/home/page.tsx` | Updated import statements and string usage; integrated new hook `useOnboarding`; conditionally renders `OnBoardingModal` based on onboarding state; explicitly typed folder mapping. |
| `src/app/components/molecules/OnBoardingCarousel.tsx`,<br>`src/app/components/molecules/OnBoardingModal.tsx`,<br>`src/app/constants/OnBoardingModalImageList.ts` | Introduced new onboarding components: a carousel component using `react-slick`, a modal component to display onboarding slides, and a constant `slides` array for slide content. |
| `src/app/hooks/useOnboarding.ts`,<br>`src/app/utils/localstorage/index.ts` | Added a custom hook `useOnboarding` to manage first-time user state and new utility functions (`getIsFirstTimeUser` and `setIsFirstTimeUser`) to interact with localStorage. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as User
    participant HP as HomePage
    participant HO as useOnboarding
    participant LS as LocalStorage Utils
    participant OM as OnBoardingModal
    participant OC as OnBoardingCarousel

    U->>HP: Load Home Page
    HP->>HO: Initialize onboarding state
    HO->>LS: getIsFirstTimeUser()
    LS-->>HO: (true/false)
    alt First time user
        HO-->>HP: showOnboarding: true
        HP->>OM: Render OnBoardingModal
        OM->>OC: Initialize carousel with slides
        OC->>OM: onSlideChange callback
        U->>OM: Click close button
        OM->>HO: closeOnboarding()
        HO->>LS: setIsFirstTimeUser(false)
    else
        HO-->>HP: showOnboarding: false
    end
```

## Assessment against linked issues

| Objective | Addressed | Explanation |
|-----------|-----------|-------------|
| [#41] Implement onboarding modal with carousel integration | ✅ | |
| [#41] Implement first-time user detection and state persistence via localStorage | ✅ | |

## Poem
> Oh, I hopped through lines so neat,  
> Bringing onboarding right to your seat.  
> A carousel spins with cheerful delight,  
> Modal magic shining bright.  
> Code and carrots, all set to greet!  
> 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=AI-Tutor-2024/Web-v3.0&utm_content=45):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE