From f161a3660d3cfa7e68f43cd4dc91c9a6edc105f7 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 14:42:37 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD=20UI=20=EA=B2=B9?= =?UTF-8?q?=EC=B9=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/BonusBanner.tsx | 14 +- .../profile/ProfileChoiceSelect.tsx | 6 +- src/components/profile/ProfileDatePicker.tsx | 8 +- src/pages/Auth/ProfileSetupPage.tsx | 2 +- src/pages/My/MyPage.tsx | 134 ++++++++++++++++++ 5 files changed, 150 insertions(+), 14 deletions(-) diff --git a/src/components/home/BonusBanner.tsx b/src/components/home/BonusBanner.tsx index 8844864..4534517 100644 --- a/src/components/home/BonusBanner.tsx +++ b/src/components/home/BonusBanner.tsx @@ -5,11 +5,13 @@ type BonusBannerProps = { export default function BonusBanner({ mascotSrc }: BonusBannerProps) { return (
- Quespot 캐릭터 + + Quespot 캐릭터 +
@@ -24,4 +26,4 @@ export default function BonusBanner({ mascotSrc }: BonusBannerProps) {
); -} \ No newline at end of file +} diff --git a/src/components/profile/ProfileChoiceSelect.tsx b/src/components/profile/ProfileChoiceSelect.tsx index 1638080..c012bb8 100644 --- a/src/components/profile/ProfileChoiceSelect.tsx +++ b/src/components/profile/ProfileChoiceSelect.tsx @@ -102,17 +102,17 @@ export default function ProfileChoiceSelect({ ) : null; return ( -
+
{label} {required ? * : (선택)} {sheet} diff --git a/src/components/profile/ProfileDatePicker.tsx b/src/components/profile/ProfileDatePicker.tsx index 372b67e..1f96312 100644 --- a/src/components/profile/ProfileDatePicker.tsx +++ b/src/components/profile/ProfileDatePicker.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from "lucide-react"; +import { CalendarDays, ChevronDown, ChevronLeft, ChevronRight, X } from "lucide-react"; import { createPortal } from "react-dom"; interface ProfileDatePickerProps { @@ -106,7 +106,7 @@ export default function ProfileDatePicker({ value, onChange, isOpen, onOpenChang const dateValue = toDateValue(visibleYear, visibleMonth, day); const disabled = new Date(visibleYear, visibleMonth, day) > maxDate; const selected = value === dateValue; - return ; + return ; })}
@@ -119,9 +119,9 @@ export default function ProfileDatePicker({ value, onChange, isOpen, onOpenChang ) : null; return ( -
+
생년월일 * - + ) : ( + + )} +
+
+ + {isNotificationSettingsLoading + ? "알림 설정을 확인하고 있어요." + : isNotificationSettingsError + ? "알림 설정을 불러오지 못했어요." + : isNotificationUpdating + ? "알림 설정을 변경하고 있어요." + : notificationSettings?.pushEnabled + ? "현재 알림을 받고 있어요." + : "현재 알림이 꺼져 있어요."} + + {!isNotificationSettingsLoading && + !isNotificationSettingsError && + !isNotificationUpdating ? ( + + {notificationSettings?.pushEnabled ? "켜짐" : "꺼짐"} + + ) : null} +
+ +
From 454b178f05a18cb3e3d395ed3d413322be4cd12d Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 14:43:10 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=ED=8A=B8=20=ED=88=AC=EB=AA=85=EB=B0=B0=EA=B2=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/HomePage.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 8d1969f..216862c 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { SearchInput } from "@/components/UI"; @@ -26,7 +27,6 @@ import { getEquippedQuestyAsset } from "@/utils/questyAsset"; import { PATH } from "@/routes/paths"; import type { MissionCategory } from "@/types/mission"; -import QuestySvg from "@/assets/icons/Questy.svg"; import { Header } from "@/components/common/Header"; import DefaultQuestySvg from "@/assets/questy.svg"; @@ -41,6 +41,7 @@ const HOME_CATEGORY_API_VALUES: Record = { export default function HomePage() { const navigate = useNavigate(); + const [searchKeyword, setSearchKeyword] = useState(""); const { profileQuery, questyQuery } = useHomeProfile(); const { toggleMissionLike, pendingMissionId } = useToggleMissionLike(); const { coordinates, status: locationStatus } = useCurrentCoordinates(); @@ -74,7 +75,18 @@ export default function HomePage() { />
- + { + const trimmedKeyword = keyword.trim(); + if (!trimmedKeyword) return; + navigate( + `${PATH.MISSIONS}?keyword=${encodeURIComponent(trimmedKeyword)}`, + ); + }} + placeholder="미션 · 장소 · 지역을 검색해보세요" + value={searchKeyword} + />
@@ -145,7 +157,7 @@ export default function HomePage() {
- +
From 8c6dca6d8524b23a318c5348f15fdcf337f9df39 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 14:43:28 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[Feat]=20=ED=99=88=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/UI.tsx | 42 +++++++++++++++++++++++++++---- src/pages/mission/MissionPage.tsx | 7 +++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/components/UI.tsx b/src/components/UI.tsx index 67c0bc4..1609e10 100644 --- a/src/components/UI.tsx +++ b/src/components/UI.tsx @@ -73,12 +73,44 @@ export function WireImage({ ); } -export function SearchInput({ placeholder }: { placeholder: string }) { +type SearchInputProps = { + placeholder: string; + value: string; + onChange: (value: string) => void; + onSearch: (value: string) => void; +}; + +export function SearchInput({ + placeholder, + value, + onChange, + onSearch, +}: SearchInputProps) { return ( - +
{ + event.preventDefault(); + onSearch(value); + }} + role="search" + > + + onChange(event.target.value)} + placeholder={placeholder} + type="search" + value={value} + /> +
); } diff --git a/src/pages/mission/MissionPage.tsx b/src/pages/mission/MissionPage.tsx index a478a94..a90ca14 100644 --- a/src/pages/mission/MissionPage.tsx +++ b/src/pages/mission/MissionPage.tsx @@ -115,8 +115,13 @@ export default function MissionPage() { const [searchParams, setSearchParams] = useSearchParams(); const selectedCategory = getMissionCategory(searchParams.get("category")); + const searchKeyword = searchParams.get("keyword") ?? ""; - const [keyword, setKeyword] = useState(""); + const [keyword, setKeyword] = useState(searchKeyword); + + useEffect(() => { + setKeyword(searchKeyword); + }, [searchKeyword]); const { currentLocation, locationStatus } = useCurrentLocation(); From 29787eb07af155309f7a18f293c9160b6c09e044 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 14:47:29 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[Feat]=20=EB=B3=B4=EC=83=81=ED=83=AD=20?= =?UTF-8?q?=EB=B2=A1=ED=84=B0=EC=95=84=EC=9D=B4=EC=BD=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/reward/RewardTabs.tsx | 26 +++++++++++++++++++++----- src/data/reward.ts | 6 +----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/reward/RewardTabs.tsx b/src/components/reward/RewardTabs.tsx index 0c5556f..f34d25b 100644 --- a/src/components/reward/RewardTabs.tsx +++ b/src/components/reward/RewardTabs.tsx @@ -1,5 +1,17 @@ import type { RewardTab } from "@/data/reward"; import { rewardTabs } from "@/data/reward"; +import { + ClipboardList, + Medal, + Stamp, + type LucideIcon, +} from "lucide-react"; + +const rewardTabIconMap: Record = { + badges: Medal, + stamps: Stamp, + history: ClipboardList, +}; type RewardTabsProps = { selectedTab: RewardTab; @@ -14,6 +26,7 @@ export default function RewardTabs({
{rewardTabs.map((tab) => { const isActive = selectedTab === tab.id; + const Icon = rewardTabIconMap[tab.id]; return (
); -} \ No newline at end of file +} diff --git a/src/data/reward.ts b/src/data/reward.ts index 53986dc..d8f1918 100644 --- a/src/data/reward.ts +++ b/src/data/reward.ts @@ -3,7 +3,6 @@ export type RewardTab = "badges" | "stamps" | "history"; export type RewardTabItem = { id: RewardTab; label: string; - emoji: string; }; export type BadgeItem = { @@ -45,17 +44,14 @@ export const rewardTabs: RewardTabItem[] = [ { id: "badges", label: "배지", - emoji: "🏅", }, { id: "stamps", label: "스탬프", - emoji: "✉️", }, { id: "history", label: "내역", - emoji: "📋", }, ]; @@ -303,4 +299,4 @@ export const rewardCoupons: CouponItem[] = [ status: "used", usable: false, }, -]; \ No newline at end of file +]; From 21e1de47a00e6cb4d67c9cf64b2a748d4eb58a1b Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 14:50:00 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[Feat]=20=EC=B6=94=EC=B2=9C=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=B3=84=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/MissionCard.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/home/MissionCard.tsx b/src/components/home/MissionCard.tsx index 86bb911..8e442bf 100644 --- a/src/components/home/MissionCard.tsx +++ b/src/components/home/MissionCard.tsx @@ -1,4 +1,5 @@ import { Heart, Loader2, MapPin } from "lucide-react"; +import { categoryToneClasses } from "@/components/home/CategoryGrid"; const missionToneClasses: Record = { cream: "bg-[linear-gradient(145deg,#fff6cc_0%,#fffef6_100%)]", @@ -8,6 +9,15 @@ const missionToneClasses: Record = { blue: "bg-[linear-gradient(145deg,#dff1ff_0%,#f7fcff_100%)]", }; +const missionCategoryToneClasses: Record = { + 역사: categoryToneClasses.violet, + 문화: categoryToneClasses.pink, + 자연: categoryToneClasses.green, + 음식: categoryToneClasses.amber, + 야경: categoryToneClasses.blue, + 기타: categoryToneClasses.cyan, +}; + export type RecommendedMissionCardItem = { id: string | number; title: string; @@ -34,6 +44,8 @@ export default function MissionCard({ isLikePending = false, }: MissionCardProps) { const toneClass = missionToneClasses[mission.tone] ?? missionToneClasses.blue; + const categoryToneClass = + missionCategoryToneClasses[mission.category] ?? categoryToneClasses.blue; return (
- + {mission.category} From ac95362a9b032ca8bd56f9d8c1bb5d192ebeb4c5 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 15:06:10 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[Feat]=20=ED=99=88=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/customize/combinations.ts | 39 +++++++++++++++-- src/components/home/HomeHero.tsx | 26 +++++++++++- src/index.css | 63 +++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/assets/customize/combinations.ts b/src/assets/customize/combinations.ts index 8d198f3..ad8ec12 100644 --- a/src/assets/customize/combinations.ts +++ b/src/assets/customize/combinations.ts @@ -15,12 +15,43 @@ const itemOrder = [ export function getQuestyCombinationAsset(itemIds: readonly string[]) { const equipped = new Set(itemIds); - const name = itemOrder.filter((itemId) => equipped.has(itemId)).join("__") || "base"; + const orderedItemIds = itemOrder.filter((itemId) => equipped.has(itemId)); + const name = orderedItemIds.join("__") || "base"; const asset = combinationAssets[`./combinations/${name}.png`]; - if (!asset) { - throw new Error(`Missing Questy combination asset: ${name}`); + if (asset) return asset; + + // 서버 상태와 로컬 상태가 충돌해 존재하지 않는 조합이 만들어져도 + // 화면 전체를 중단하지 않고, 가장 많은 아이템을 유지하는 조합을 찾는다. + const candidates = Array.from( + { length: 2 ** orderedItemIds.length }, + (_, mask) => mask, + ).sort( + (left, right) => + countBits(right) - countBits(left) || right - left, + ); + + for (const mask of candidates) { + const fallbackName = + orderedItemIds + .filter((_, index) => (mask & (1 << index)) !== 0) + .join("__") || "base"; + const fallbackAsset = + combinationAssets[`./combinations/${fallbackName}.png`]; + + if (fallbackAsset) return fallbackAsset; + } + + return combinationAssets["./combinations/base.png"]; +} + +function countBits(value: number) { + let count = 0; + + while (value > 0) { + count += value & 1; + value >>= 1; } - return asset; + return count; } diff --git a/src/components/home/HomeHero.tsx b/src/components/home/HomeHero.tsx index 2241ba9..39a8480 100644 --- a/src/components/home/HomeHero.tsx +++ b/src/components/home/HomeHero.tsx @@ -1,4 +1,4 @@ -import { ChevronRight } from "lucide-react"; +import { ChevronRight, Cloud, Plane } from "lucide-react"; type HomeHeroProps = { nickname: string; @@ -15,6 +15,30 @@ export default function HomeHero({
+
안녕 {nickname}님! diff --git a/src/index.css b/src/index.css index 12977e1..f7b938d 100644 --- a/src/index.css +++ b/src/index.css @@ -126,15 +126,76 @@ animation: questy-scene-breathe 10s ease-in-out infinite; } +@keyframes home-hero-plane-flight { + 0% { + opacity: 0; + transform: translate(-20px, 76px) rotate(-26deg) scale(0.84); + } + + 10% { + opacity: 1; + } + + 28% { + opacity: 1; + transform: translate(48px, 28px) rotate(-18deg) scale(0.96); + } + + 48% { + opacity: 1; + transform: translate(98px, 54px) rotate(12deg) scale(1); + } + + 68% { + opacity: 1; + transform: translate(151px, 24px) rotate(-20deg) scale(0.96); + } + + 88%, + 100% { + opacity: 0; + transform: translate(220px, -4px) rotate(-14deg) scale(0.84); + } +} + +.animate-home-hero-plane { + animation: home-hero-plane-flight 7s cubic-bezier(0.42, 0, 0.35, 1) infinite; +} + +@keyframes home-hero-route-pulse { + 0%, + 100% { + opacity: 0.35; + stroke-dashoffset: 0; + } + + 50% { + opacity: 0.85; + stroke-dashoffset: -24; + } +} + +.animate-home-hero-route { + animation: home-hero-route-pulse 3.4s linear infinite; +} + @media (prefers-reduced-motion: reduce) { + .animate-questy-float, .animate-questy-cloud, .animate-questy-cloud-delayed, .animate-questy-sparkle, .animate-questy-sparkle-delayed, .animate-questy-shadow, - .animate-questy-scene { + .animate-questy-scene, + .animate-home-hero-plane, + .animate-home-hero-route { animation: none; } + + .animate-home-hero-plane { + opacity: 0.75; + transform: translate(142px, 26px) rotate(-30deg) scale(0.9); + } } @keyframes questy-tumble { From b0801a9d31f589e3afca470721e50d91f1f80a14 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 15:09:18 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[Fix]=20=EC=A7=80=EB=8F=84=20=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/map/MapPage.tsx | 74 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/src/pages/map/MapPage.tsx b/src/pages/map/MapPage.tsx index 37b85ca..b2ad046 100644 --- a/src/pages/map/MapPage.tsx +++ b/src/pages/map/MapPage.tsx @@ -1,4 +1,11 @@ -import { useEffect, useMemo, useState } from "react"; +import { + useEffect, + useMemo, + useRef, + useState, + type PointerEvent as ReactPointerEvent, + type WheelEvent as ReactWheelEvent, +} from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { MapPin, @@ -47,6 +54,13 @@ export default function MapPage() { const [selectedSpotId, setSelectedSpotId] = useState(""); const [isPlaceListOpen, setIsPlaceListOpen] = useState(false); const [isMissionSheetOpen, setIsMissionSheetOpen] = useState(false); + const spotScrollDragRef = useRef<{ + pointerId: number; + startX: number; + scrollLeft: number; + moved: boolean; + } | null>(null); + const suppressSpotClickRef = useRef(false); const { currentLocation, locationStatus } = useCurrentLocation(); @@ -117,6 +131,45 @@ export default function MapPage() { }); }; + const handleSpotScrollPointerDown = ( + event: ReactPointerEvent, + ) => { + spotScrollDragRef.current = { + pointerId: event.pointerId, + startX: event.clientX, + scrollLeft: event.currentTarget.scrollLeft, + moved: false, + }; + event.currentTarget.setPointerCapture(event.pointerId); + }; + + const handleSpotScrollPointerMove = ( + event: ReactPointerEvent, + ) => { + const drag = spotScrollDragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + + const distance = event.clientX - drag.startX; + if (Math.abs(distance) > 4) drag.moved = true; + event.currentTarget.scrollLeft = drag.scrollLeft - distance; + }; + + const handleSpotScrollPointerEnd = ( + event: ReactPointerEvent, + ) => { + const drag = spotScrollDragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + + suppressSpotClickRef.current = drag.moved; + spotScrollDragRef.current = null; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + window.setTimeout(() => { + suppressSpotClickRef.current = false; + }, 0); + }; + console.log(data); return ( @@ -237,7 +290,17 @@ export default function MapPage() {
-
+
) => { + event.currentTarget.scrollLeft += event.deltaX || event.deltaY; + }} + > {isNearLoading && (
데이터를 불러오는 중 입니다... @@ -253,7 +316,10 @@ export default function MapPage() { key={spot.districtCode} spot={spot} selected={selectedSpot?.districtCode === spot.districtCode} - onClick={() => handleSelectSpotCard(spot.districtCode)} + onClick={() => { + if (suppressSpotClickRef.current) return; + handleSelectSpotCard(spot.districtCode); + }} /> ))}
@@ -546,7 +612,7 @@ function SpotSummaryCard({ spot, selected, onClick }: SpotSummaryCardProps) { type="button" onClick={onClick} className={[ - "flex min-w-[82px] flex-col items-center rounded-[16px] border-2 px-[12px] py-[12px] transition active:scale-[0.98]", + "flex min-w-[82px] shrink-0 flex-col items-center rounded-[16px] border-2 px-[12px] py-[12px] transition active:scale-[0.98]", selected ? "border-[#5BB5F8] bg-primary text-white" : isCompleted From bb8b66de71197104a0dfb34481a7e75e3872f806 Mon Sep 17 00:00:00 2001 From: Hyobee02 Date: Wed, 16 Sep 2026 15:12:09 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[Fix]=20=ED=97=A4=EB=8D=94=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=8A=B8=20=ED=81=AC=EA=B8=B0=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/components/common/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index eeb2820..055a300 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -15,10 +15,10 @@ export function Header({ children }: HeaderProps) { return (