diff --git a/src/components/common/SectionHeader.tsx b/src/components/common/SectionHeader.tsx
new file mode 100644
index 0000000..c891bf7
--- /dev/null
+++ b/src/components/common/SectionHeader.tsx
@@ -0,0 +1,36 @@
+import { ChevronRight } from "lucide-react";
+import type { ReactNode } from "react";
+
+type SectionHeaderProps = {
+ title: string;
+ icon?: ReactNode;
+ actionLabel?: string;
+ onActionClick?: () => void;
+};
+
+export default function SectionHeader({
+ title,
+ icon,
+ actionLabel,
+ onActionClick,
+}: SectionHeaderProps) {
+ return (
+
+
+ {icon}
+ {title}
+
+
+ {actionLabel ? (
+
+ ) : null}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/ActivitySummary.tsx b/src/components/home/ActivitySummary.tsx
new file mode 100644
index 0000000..b43cf0e
--- /dev/null
+++ b/src/components/home/ActivitySummary.tsx
@@ -0,0 +1,31 @@
+type ActivitySummaryItem = {
+ value: string;
+ label: string;
+};
+
+type ActivitySummaryProps = {
+ items: ActivitySummaryItem[];
+};
+
+export default function ActivitySummary({ items }: ActivitySummaryProps) {
+ return (
+
+ {items.map((item) => (
+
+
+ {item.value}
+
+
+ {item.label}
+
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/BonusBanner.tsx b/src/components/home/BonusBanner.tsx
new file mode 100644
index 0000000..8844864
--- /dev/null
+++ b/src/components/home/BonusBanner.tsx
@@ -0,0 +1,27 @@
+type BonusBannerProps = {
+ mascotSrc: string;
+};
+
+export default function BonusBanner({ mascotSrc }: BonusBannerProps) {
+ return (
+
+
+
+
+
+ 소도시 미션에서
+
+ 추가 보너스 포인트!
+
+
+
+ 인구감소지역 미션 완료 시 +20% 보너스
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/CategoryGrid.tsx b/src/components/home/CategoryGrid.tsx
new file mode 100644
index 0000000..3a619ea
--- /dev/null
+++ b/src/components/home/CategoryGrid.tsx
@@ -0,0 +1,79 @@
+import {
+ Building2,
+ Camera,
+ Compass,
+ Leaf,
+ Palette,
+ ShoppingBag,
+ Sunrise,
+ Utensils,
+ Waves,
+ type LucideIcon,
+} from "lucide-react";
+import SectionHeader from "@/components/common/SectionHeader";
+
+const categoryToneClasses: Record = {
+ violet: "bg-[#eee1ff] text-[#8b5cf6]",
+ green: "bg-[#e1faef] text-green-600",
+ amber: "bg-[#fff1c9] text-amber-600",
+ blue: "bg-[#dcecff] text-[#2577ff]",
+ pink: "bg-[#ffe1f0] text-pink-500",
+ cyan: "bg-[#cff9fb] text-cyan-600",
+ rose: "bg-[#ffdede] text-red-500",
+ purple: "bg-[#f0e3ff] text-purple-600",
+};
+
+const categoryIcons: Record = {
+ history: Building2,
+ nature: Leaf,
+ food: Utensils,
+ night: Sunrise,
+ photo: Camera,
+ activity: Waves,
+ shopping: ShoppingBag,
+ art: Palette,
+};
+
+type Category = {
+ id: string;
+ label: string;
+ tone: string;
+};
+
+type CategoryGridProps = {
+ categories: Category[];
+};
+
+export default function CategoryGrid({ categories }: CategoryGridProps) {
+ return (
+
+
+
+
+ {categories.map((category) => {
+ const Icon = categoryIcons[category.id] ?? Compass;
+ const toneClass =
+ categoryToneClasses[category.tone] ?? categoryToneClasses.blue;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/HomeHeader.tsx b/src/components/home/HomeHeader.tsx
new file mode 100644
index 0000000..5e11fa8
--- /dev/null
+++ b/src/components/home/HomeHeader.tsx
@@ -0,0 +1,43 @@
+import { Bell } from "lucide-react";
+
+type HomeHeaderProps = {
+ mascotSrc: string;
+ notificationCount?: number;
+ onBellClick?: () => void;
+};
+
+export default function HomeHeader({
+ mascotSrc,
+ notificationCount = 0,
+ onBellClick,
+}: HomeHeaderProps) {
+ return (
+
+
+

+
+ Quespot
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/HomeHero.tsx b/src/components/home/HomeHero.tsx
new file mode 100644
index 0000000..c8b2666
--- /dev/null
+++ b/src/components/home/HomeHero.tsx
@@ -0,0 +1,50 @@
+import { ChevronRight } from "lucide-react";
+import { Button } from "@/components/UI";
+
+type HomeHeroProps = {
+ nickname: string;
+ mascotSrc: string;
+ onExploreClick: () => void;
+};
+
+export default function HomeHero({
+ nickname,
+ mascotSrc,
+ onExploreClick,
+}: HomeHeroProps) {
+ return (
+
+
+
+ {nickname}님 안녕하세요!
+
+
+
+ 미션으로 떠나는
+
+ 특별한 여행
+
+
+
+ Questy와 함께 일상을
+
+ 여행으로 바꿔보세요
+
+
+
}
+ onClick={onExploreClick}
+ >
+ 미션 탐색
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/MissionCard.tsx b/src/components/home/MissionCard.tsx
new file mode 100644
index 0000000..7d2305e
--- /dev/null
+++ b/src/components/home/MissionCard.tsx
@@ -0,0 +1,69 @@
+import { Heart, MapPin } from "lucide-react";
+
+const missionToneClasses: Record = {
+ cream: "bg-[linear-gradient(145deg,#fff6cc_0%,#fffef6_100%)]",
+ lavender: "bg-[linear-gradient(145deg,#f1e8ff_0%,#fffaff_100%)]",
+ mint: "bg-[linear-gradient(145deg,#ddfaee_0%,#fafffd_100%)]",
+ peach: "bg-[linear-gradient(145deg,#ffe6d7_0%,#fff9f5_100%)]",
+ blue: "bg-[linear-gradient(145deg,#dff1ff_0%,#f7fcff_100%)]",
+};
+
+type Mission = {
+ id: string | number;
+ title: string;
+ category: string;
+ distance: string;
+ points: number;
+ visual: string;
+ tone: string;
+};
+
+type MissionCardProps = {
+ mission: Mission;
+ onClick: () => void;
+};
+
+export default function MissionCard({ mission, onClick }: MissionCardProps) {
+ const toneClass = missionToneClasses[mission.tone] ?? missionToneClasses.blue;
+
+ return (
+
+
+
+
+
+ {mission.visual}
+
+
+
+
+
+ {mission.category}
+
+
+
+ {mission.title}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/NearbySpotCard.tsx b/src/components/home/NearbySpotCard.tsx
new file mode 100644
index 0000000..e6b9495
--- /dev/null
+++ b/src/components/home/NearbySpotCard.tsx
@@ -0,0 +1,85 @@
+import { ChevronRight, ClipboardCheck, MapPin, Star } from "lucide-react";
+
+type NearbySpot = {
+ id: string | number;
+ name: string;
+ distance: string;
+ done?: boolean;
+ status?: string;
+ missionCount?: number;
+ rating?: number;
+ badge?: number;
+};
+
+type NearbySpotCardProps = {
+ spot: NearbySpot;
+ onClick: () => void;
+};
+
+export default function NearbySpotCard({ spot, onClick }: NearbySpotCardProps) {
+ const isDone = Boolean(spot.done);
+
+ return (
+
+
+
+
+ {isDone ? (
+
+ ) : null}
+
+ {spot.badge ? (
+
+ {spot.badge}
+
+ ) : null}
+
+
+
+
+ {spot.name}
+
+
+
+ {spot.distance}
+
+
+
+ {isDone || spot.status === "완료" ? (
+ 완료
+ ) : (
+ `미션 ${spot.missionCount ?? 0}개`
+ )}
+
+
+
+ {spot.rating !== undefined ? (
+
+
+ {spot.rating}
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/NearbySpotSection.tsx b/src/components/home/NearbySpotSection.tsx
new file mode 100644
index 0000000..741245c
--- /dev/null
+++ b/src/components/home/NearbySpotSection.tsx
@@ -0,0 +1,47 @@
+import { MapPin } from "lucide-react";
+import SectionHeader from "@/components/common/SectionHeader";
+import NearbySpotCard from "@/components/home/NearbySpotCard";
+
+type NearbySpot = {
+ id: string | number;
+ name: string;
+ distance: string;
+ done?: boolean;
+ status?: string;
+ missionCount?: number;
+ rating?: number;
+ badge?: number;
+};
+
+type NearbySpotSectionProps = {
+ spots: NearbySpot[];
+ onMapClick: () => void;
+ onSpotClick: (spot: NearbySpot) => void;
+};
+
+export default function NearbySpotSection({
+ spots,
+ onMapClick,
+ onSpotClick,
+}: NearbySpotSectionProps) {
+ return (
+
+ }
+ actionLabel="지도보기"
+ onActionClick={onMapClick}
+ />
+
+
+ {spots.map((spot) => (
+ onSpotClick(spot)}
+ />
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/RecommendedMissionSection.tsx b/src/components/home/RecommendedMissionSection.tsx
new file mode 100644
index 0000000..8e0a3c9
--- /dev/null
+++ b/src/components/home/RecommendedMissionSection.tsx
@@ -0,0 +1,46 @@
+import SectionHeader from "@/components/common/SectionHeader";
+import MissionCard from "@/components/home/MissionCard";
+
+type Mission = {
+ id: string | number;
+ title: string;
+ category: string;
+ distance: string;
+ points: number;
+ visual: string;
+ tone: string;
+};
+
+type RecommendedMissionSectionProps = {
+ missions: Mission[];
+ showAll: boolean;
+ onToggleShowAll: () => void;
+ onMissionClick: (mission: Mission) => void;
+};
+
+export default function RecommendedMissionSection({
+ missions,
+ showAll,
+ onToggleShowAll,
+ onMissionClick,
+}: RecommendedMissionSectionProps) {
+ return (
+
+
+
+
+ {missions.map((mission) => (
+ onMissionClick(mission)}
+ />
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/layouts/BottomNavigationLayout.tsx b/src/layouts/BottomNavigationLayout.tsx
index 7e71aae..49f6432 100644
--- a/src/layouts/BottomNavigationLayout.tsx
+++ b/src/layouts/BottomNavigationLayout.tsx
@@ -38,7 +38,7 @@ const NAV_ITEMS = [
export default function BottomNavigationLayout() {
return (
<>
-
+
@@ -46,29 +46,41 @@ export default function BottomNavigationLayout() {
>
);
}
+
const BottomNavigation = () => {
return (
-