From 30dbd448c50f64502a81e80732d040f3ed446a75 Mon Sep 17 00:00:00 2001 From: kinjaebeom Date: Fri, 28 Aug 2026 18:31:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Feat]=20=ED=99=88=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/SectionHeader.tsx | 36 +++ src/components/home/ActivitySummary.tsx | 31 ++ src/components/home/BonusBanner.tsx | 27 ++ src/components/home/CategoryGrid.tsx | 79 +++++ src/components/home/HomeHeader.tsx | 43 +++ src/components/home/HomeHero.tsx | 50 +++ src/components/home/MissionCard.tsx | 69 ++++ src/components/home/NearbySpotCard.tsx | 85 +++++ src/components/home/NearbySpotSection.tsx | 47 +++ .../home/RecommendedMissionSection.tsx | 46 +++ src/pages/HomePage.tsx | 305 +++++------------- 11 files changed, 585 insertions(+), 233 deletions(-) create mode 100644 src/components/common/SectionHeader.tsx create mode 100644 src/components/home/ActivitySummary.tsx create mode 100644 src/components/home/BonusBanner.tsx create mode 100644 src/components/home/CategoryGrid.tsx create mode 100644 src/components/home/HomeHeader.tsx create mode 100644 src/components/home/HomeHero.tsx create mode 100644 src/components/home/MissionCard.tsx create mode 100644 src/components/home/NearbySpotCard.tsx create mode 100644 src/components/home/NearbySpotSection.tsx create mode 100644 src/components/home/RecommendedMissionSection.tsx 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 ( +
+ Quespot 캐릭터 + +
+ + 소도시 미션에서 +
+ 추가 보너스 포인트! +
+ +

+ 인구감소지역 미션 완료 시 +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 캐릭터 + + 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와 함께 일상을 +
+ 여행으로 바꿔보세요 +

+ + +
+ + Quespot 캐릭터 +
+ ); +} \ 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} + + +
+ + + {mission.distance} + + + +{mission.points}P +
+
+
+ ); +} \ 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/pages/HomePage.tsx b/src/pages/HomePage.tsx index 4c03ef8..a109d45 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,257 +1,96 @@ import { useState } from "react"; -import { - Bell, - Building2, - Camera, - ChevronRight, - ClipboardCheck, - Compass, - Heart, - Leaf, - MapPin, - Palette, - ShoppingBag, - Star, - Sunrise, - Utensils, - Waves, - type LucideIcon, -} from "lucide-react"; -import { Button, SearchInput } from "../components/UI"; +import { useNavigate } from "react-router-dom"; + +import { SearchInput } from "@/components/UI"; +import HomeHeader from "@/components/home/HomeHeader"; +import HomeHero from "@/components/home/HomeHero"; +import ActivitySummary from "@/components/home/ActivitySummary"; +import CategoryGrid from "@/components/home/CategoryGrid"; +import RecommendedMissionSection from "@/components/home/RecommendedMissionSection"; +import NearbySpotSection from "@/components/home/NearbySpotSection"; +import BonusBanner from "@/components/home/BonusBanner"; + import { figmaAssets, homeCategories, nearbySpots, recommendedMissions, -} from "../data/quespot"; -import { useNavigate } from "react-router-dom"; +} from "@/data/quespot"; import { PATH } from "@/routes/paths"; -const categoryToneClasses = { - 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", -} as const; - -const missionToneClasses = { - cream: "bg-[linear-gradient(145deg,_#fff6cc_0%,_#fffef6_100%)]", - lavender: "bg-[linear-gradient(145deg,_#f6f0ff_0%,_#fffaff_100%)] 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%)]", -} as const; - - -const categoryIcons: Record = { - history: Building2, - nature: Leaf, - food: Utensils, - night: Sunrise, - photo: Camera, - activity: Waves, - shopping: ShoppingBag, - art: Palette, -}; - export default function HomePage() { + const navigate = useNavigate(); const [showAllMissions, setShowAllMissions] = useState(false); + const visibleMissions = showAllMissions ? recommendedMissions : recommendedMissions.slice(0, 2); - const navigate = useNavigate(); + return ( -
-
-
- - Quespot -
- -
+
+ { + // TODO: 알림함 페이지 연결 + }} + /> -
-
- 꿀법님 안녕하세요! -

- 미션으로 떠나는 -
- 특별한 여행 -

-

- Quespot과 함께 일상을 -
- 여행으로 바꿔보세요 -

- -
- Quespot 캐릭터 -
+ navigate(PATH.MISSIONS)} + /> - +
+ +
-
-
- 2개 - 완료 미션 -
-
- 350P - 누적 보상 -
-
- 2개 - 획득 배지 -
-
- 2개 - 스탬프 -
-
+
+ +
-
-

카테고리

-
- {homeCategories.map((category) => { - const Icon = categoryIcons[category.id] ?? Compass; +
+ +
- return ( - - ); - })} -
-
+
+ setShowAllMissions((value) => !value)} + onMissionClick={() => navigate(PATH.MISSION_DETAIL)} + /> +
-
-
-

추천 미션

- -
-
- {visibleMissions.map((mission) => ( -
navigate(PATH.MISSION_DETAIL)} - > -
- - {mission.visual} -
-
- {mission.category} - {mission.title} -
- - - {mission.distance} - - +{mission.points}P -
-
-
- ))} -
-
+
+ ({ + ...spot, + done: spot.done ?? false, + }))} + onMapClick={() => navigate(PATH.MAP)} + onSpotClick={(spot) => { + if (spot.done) { + navigate(PATH.MISSION_RECORD); + return; + } -
-
-

- 내 주변 스팟 -

- -
-
- {nearbySpots.map((spot) => ( -
- - - {spot.done ? ( - - ) : null} - {spot.badge ? {spot.badge} : null} - -
- {spot.name} -

- {spot.distance} - - {spot.status === "완료" ? ( - 완료 - ) : ( - `미션 ${spot.missionCount}개` - )} -

-
- - - {spot.rating} - - -
- ))} -
-
+ navigate(PATH.MISSION_DETAIL); + }} + /> +
-
- -
- 소도시 미션에서 추가 보너스 포인트! -

인구감소지역 미션 완료 시 +20% 보너스

-
-
+
+ +
); -} +} \ No newline at end of file From d8256b3a38890cb5e4b5413e68bfca00dc73aedb Mon Sep 17 00:00:00 2001 From: kinjaebeom Date: Fri, 28 Aug 2026 19:05:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20=ED=95=98=EB=8B=A8=20=EB=84=A4?= =?UTF-8?q?=EB=B8=8C=EB=B0=94=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/BottomNavigationLayout.tsx | 38 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) 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 ( -