diff --git a/index.html b/index.html
index 68e10dc..6b4e8af 100644
--- a/index.html
+++ b/index.html
@@ -5,20 +5,9 @@
-
-
+
+
+
diff --git a/package.json b/package.json
index 9a14df8..bd66785 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@tanstack/react-query": "^5.59.20",
"@tanstack/react-query-devtools": "^5.60.5",
@@ -22,6 +23,7 @@
"clsx": "^2.1.1",
"echarts": "^5.5.1",
"echarts-wordcloud": "^2.1.0",
+ "embla-carousel-react": "^8.5.1",
"lucide-react": "^0.456.0",
"next-themes": "^0.4.3",
"postcss": "^8.4.47",
diff --git a/public/ogImage.png b/public/ogImage.png
new file mode 100644
index 0000000..e1ed79c
Binary files /dev/null and b/public/ogImage.png differ
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/images/onboard1.png b/src/assets/images/onboard1.png
new file mode 100644
index 0000000..0d7f282
Binary files /dev/null and b/src/assets/images/onboard1.png differ
diff --git a/src/assets/images/onboard2.png b/src/assets/images/onboard2.png
new file mode 100644
index 0000000..d9ccbdc
Binary files /dev/null and b/src/assets/images/onboard2.png differ
diff --git a/src/assets/images/onboard3.png b/src/assets/images/onboard3.png
new file mode 100644
index 0000000..70f3175
Binary files /dev/null and b/src/assets/images/onboard3.png differ
diff --git a/src/assets/images/onboard4.png b/src/assets/images/onboard4.png
new file mode 100644
index 0000000..1647bbd
Binary files /dev/null and b/src/assets/images/onboard4.png differ
diff --git a/src/components/pages/main/home/Calendar.tsx b/src/components/pages/main/home/Calendar.tsx
index f77a554..1037627 100644
--- a/src/components/pages/main/home/Calendar.tsx
+++ b/src/components/pages/main/home/Calendar.tsx
@@ -55,7 +55,7 @@ const Calendar = ({
useEffect(() => {
const calendar = generateCalendar(currentDate.getFullYear(), currentDate.getMonth());
setCalendarDays(calendar);
- }, [calendarData, currentDate]);
+ }, [calendarData]);
const generateCalendar = (year: number, month: number) => {
const firstDayOfMonth = new Date(year, month, 1);
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..e9b0171
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,240 @@
+import * as React from "react";
+import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return;
+ }
+
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext]
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return;
+ }
+
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+});
+Carousel.displayName = "Carousel";
+
+const CarouselContent = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+ }
+);
+CarouselContent.displayName = "CarouselContent";
+
+const CarouselItem = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+ }
+);
+CarouselItem.displayName = "CarouselItem";
+
+const CarouselPrevious = React.forwardRef>(
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+ );
+ }
+);
+CarouselPrevious.displayName = "CarouselPrevious";
+
+const CarouselNext = React.forwardRef>(
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+ );
+ }
+);
+CarouselNext.displayName = "CarouselNext";
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+};
diff --git a/src/hooks/useInView.ts b/src/hooks/useInView.ts
index edd9434..9bbbe40 100644
--- a/src/hooks/useInView.ts
+++ b/src/hooks/useInView.ts
@@ -17,8 +17,8 @@ const useInView = (threshold: number, onViewEscape?: () => vo
if (entry.isIntersecting) {
setIsInView(true);
} else {
- setIsInView(false);
if (onViewEscape) onViewEscape();
+ else setIsInView(false);
}
});
},
diff --git a/src/pages/main/Home.tsx b/src/pages/main/Home.tsx
index 1ba1148..c83bce4 100644
--- a/src/pages/main/Home.tsx
+++ b/src/pages/main/Home.tsx
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import Calendar from "@/components/pages/main/home/Calendar";
import useCalendarData from "@/hooks/query/useCalendarData";
+import Onboarding from "../user/Onboarding";
const STORAGE_KEY = "currentDate";
/**
* 메인 화면
@@ -17,6 +18,16 @@ const Home = () => {
return storedDate ? new Date(storedDate) : new Date();
});
const { calendarData, isActiveToday } = useCalendarData(currentDate);
+ const [showOnboarding, setShowOnboarding] = useState(false);
+
+ useEffect(() => {
+ const hasVisited = localStorage.getItem("hasVisited");
+
+ if (!hasVisited) {
+ setShowOnboarding(true);
+ localStorage.setItem("hasVisited", "true");
+ }
+ }, []);
const navigate = useNavigate();
const onClickCreateDiary = () => {
@@ -79,6 +90,7 @@ const Home = () => {
bgColor="dark"
/>
+ {showOnboarding && setShowOnboarding(false)} />}
);
};
diff --git a/src/pages/user/Onboarding.tsx b/src/pages/user/Onboarding.tsx
new file mode 100644
index 0000000..067a5b7
--- /dev/null
+++ b/src/pages/user/Onboarding.tsx
@@ -0,0 +1,129 @@
+import Button from "@/components/common/Button/Button";
+import ScrollLayout from "@/components/Layout/ScrollLayout";
+import { Carousel, CarouselApi, CarouselContent, CarouselItem } from "@/components/ui/carousel";
+import { useEffect, useState } from "react";
+import img1 from "@/assets/images/onboard1.png";
+import img2 from "@/assets/images/onboard2.png";
+import img3 from "@/assets/images/onboard3.png";
+import img4 from "@/assets/images/onboard4.png";
+import useInView from "@/hooks/useInView";
+import { FADEINANIMATION } from "@/styles/animations";
+
+interface OnboardingProps {
+ onClose: () => void;
+}
+
+const Onboarding = ({ onClose }: OnboardingProps) => {
+ const [api, setApi] = useState();
+ const { elementRef: ref1, isInView: inView1 } = useInView(0.7, () => {});
+ const { elementRef: ref2, isInView: inView2 } = useInView(0.7, () => {});
+ const { elementRef: ref3, isInView: inView3 } = useInView(0.7, () => {});
+ const { elementRef: ref4, isInView: inView4 } = useInView(0.7, () => {});
+ const [current, setCurrent] = useState(0);
+
+ const handleSelect = () => {
+ if (!api) return;
+ const selectedIndex = api.selectedScrollSnap();
+ setCurrent(selectedIndex);
+ };
+
+ useEffect(() => {
+ if (!api) return;
+
+ api.on("select", handleSelect);
+
+ return () => {
+ api.off("select", handleSelect);
+ };
+ }, [api, handleSelect]);
+
+ return (
+
+
+ {Array.from({ length: 4 }, (_, index) => (
+
+ ))}
+
+
+
+
+
+
날짜를 클릭해
+
+ 일기를 작성하거나 확인하세요
+
+
+
+
+
+
+
+
+ 일기를 작성하면 AI가 감정을
+
+
+ 분석하고 관련 이미지를 생성합니다
+
+
+
+
+
+
+
+
+ 다른 사람들의 일기를 통해
+
+
+ 새로운 이야기를 만나보세요
+
+
+
+
+
+
+
+
+ 내 일기의 감정과 키워드를 분석한
+
+
+ 통계를 확인하세요
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Onboarding;
diff --git a/yarn.lock b/yarn.lock
index 764c832..b610446 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -741,7 +741,7 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
-"@radix-ui/react-slot@1.1.0":
+"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==
@@ -2675,6 +2675,24 @@ electron-to-chromium@^1.5.4:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz#5203ce5d6054857d84ba84d3681cbe59132ade78"
integrity sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==
+embla-carousel-react@^8.5.1:
+ version "8.5.1"
+ resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.5.1.tgz#e06ff28cb53698d453ffad89423c23d725e9b010"
+ integrity sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==
+ dependencies:
+ embla-carousel "8.5.1"
+ embla-carousel-reactive-utils "8.5.1"
+
+embla-carousel-reactive-utils@8.5.1:
+ version "8.5.1"
+ resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.1.tgz#3059ab2f72f04988a96694f700a772a72bb75ffb"
+ integrity sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==
+
+embla-carousel@8.5.1:
+ version "8.5.1"
+ resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.5.1.tgz#8d83217e831666f6df573b0d3727ff0ae9208002"
+ integrity sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"