From 9d3554f328d68a3e585e6ed4af6bb183ab95af9c Mon Sep 17 00:00:00 2001 From: kinjaebeom Date: Sat, 29 Aug 2026 20:27:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[Feat]=20=EB=AF=B8=EC=85=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 57 +++ src/assets/icons/Questy.svg | 9 + src/pages/mission/MissionDetailPage.tsx | 505 ++++++++++++++++++------ src/pages/mission/MissionPage.tsx | 367 ++++++++++++++++- 5 files changed, 825 insertions(+), 114 deletions(-) create mode 100644 src/assets/icons/Questy.svg diff --git a/package.json b/package.json index b173fb3..40ae7f3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "@vis.gl/react-google-maps": "^1.9.0", "lucide-react": "^0.468.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b23bb6..81e64dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tailwindcss/vite': specifier: ^4.3.3 version: 4.3.3(vite@6.4.3(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vis.gl/react-google-maps': + specifier: ^1.9.0 + version: 1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) lucide-react: specifier: ^0.468.0 version: 0.468.0(react@19.2.7) @@ -287,6 +290,9 @@ packages: cpu: [x64] os: [win32] + '@googlemaps/js-api-loader@2.1.1': + resolution: {integrity: sha512-yUpAwksbHrlZIWD49JmveNSfBG4oAK0AwMknfSaPMnP5N7UT8oFRVCqwjGb1XQovi//7KLbPQKZpbofiLGzpDw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -308,6 +314,7 @@ packages: engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -355,66 +362,79 @@ packages: resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.4': resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.4': resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.4': resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.4': resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.4': resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.4': resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.4': resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.4': resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.4': resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.4': resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.4': resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.4': resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.62.4': resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} @@ -552,24 +572,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.3': resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.3': resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} @@ -619,6 +643,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/google.maps@3.66.0': + resolution: {integrity: sha512-HOvl5nXOBazgI9rgG79mWm4sNuA08ig+AMvdCQ/rEAzXJQFfKj/LqjaP+fHLM5JFa68+VKiN15NWFBos8/Kr9w==} + '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} @@ -630,6 +657,12 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@vis.gl/react-google-maps@1.9.0': + resolution: {integrity: sha512-ELL/zo8mRMPGDQZwHRClf2tE1/PMYYKjg+C/dUn8rmlQ0e+s7rkcNmCgaeBAV48GJzEcrBsr+vjDv4AvzJrlUA==} + peerDependencies: + react: '>=16.8.0 || ^19.0 || ^19.0.0-rc' + react-dom: '>=16.8.0 || ^19.0 || ^19.0.0-rc' + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -721,6 +754,10 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + fast-equals@6.0.2: + resolution: {integrity: sha512-sAjhj9ZhOxYCGiNMnZLaucOqf5ZeFnHNoKoAZiD9thhJ0N8RP85qJK759/97C/3L7NzzmGVB5uiX9AUpySZmUQ==} + engines: {node: '>=6.0.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -808,24 +845,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1225,6 +1266,10 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@googlemaps/js-api-loader@2.1.1': + dependencies: + '@types/google.maps': 3.66.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1493,6 +1538,8 @@ snapshots: '@types/estree@1.0.9': {} + '@types/google.maps@3.66.0': {} + '@types/node@24.13.2': dependencies: undici-types: 7.18.2 @@ -1506,6 +1553,14 @@ snapshots: dependencies: csstype: 3.2.3 + '@vis.gl/react-google-maps@1.9.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@googlemaps/js-api-loader': 2.1.1 + '@types/google.maps': 3.66.0 + fast-equals: 6.0.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 @@ -1608,6 +1663,8 @@ snapshots: estree-walker@2.0.2: {} + fast-equals@6.0.2: {} + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 diff --git a/src/assets/icons/Questy.svg b/src/assets/icons/Questy.svg new file mode 100644 index 0000000..2e64470 --- /dev/null +++ b/src/assets/icons/Questy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pages/mission/MissionDetailPage.tsx b/src/pages/mission/MissionDetailPage.tsx index 19f5688..94a70f7 100644 --- a/src/pages/mission/MissionDetailPage.tsx +++ b/src/pages/mission/MissionDetailPage.tsx @@ -1,149 +1,428 @@ +import type { ReactNode } from "react"; +import { useMemo } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; import { - Award, - CalendarClock, - Camera, - CheckCircle2, - ChevronRight, + ArrowLeft, + Building2, + Check, Clock3, + Crosshair, Heart, + Landmark, + Leaf, + Lock, MapPin, - Navigation, - Route, - Share2, - ShieldCheck, Sparkles, - Star, + Sunrise, + Utensils, + Zap, + type LucideIcon, } from "lucide-react"; -import { SubHeader } from "../../components/DeviceFrame"; -import { Button, LocationBadge } from "../../components/UI"; -import { recommendedMissions } from "../../data/quespot"; -import { useNavigate } from "react-router-dom"; + +import HomeHeader from "@/components/home/HomeHeader"; import { PATH } from "@/routes/paths"; +import QuestySvg from "@/assets/icons/Questy.svg"; + +type MissionCategory = "history" | "nature" | "food" | "night"; +type MissionStatus = "completed" | "available" | "locked"; + +type MissionIconKey = + | "building" + | "landmark" + | "leaf" + | "food" + | "night" + | "lock"; + +type MissionFromList = { + id: number; + title: string; + location: string; + address: string; + distance: string; + category: MissionCategory; + categoryLabel: string; + iconKey: MissionIconKey; + status: MissionStatus; + points?: number; + duration?: string; +}; + +type MissionDetailState = { + mission?: MissionFromList; +}; + +type DetailMission = { + id: number; + title: string; + place: string; + address: string; + distance: string; + points: number; + duration: string; + category: MissionCategory; + categoryLabel: string; + status: MissionStatus; + iconKey: MissionIconKey; + description: string; +}; + +const iconMap: Record = { + building: Building2, + landmark: Landmark, + leaf: Leaf, + food: Utensils, + night: Sunrise, + lock: Lock, +}; + +const DEFAULT_MISSION: DetailMission = { + id: 1, + title: "경복궁 정문 인증샷", + place: "경복궁", + address: "서울 종로구", + distance: "1.2km", + points: 150, + duration: "약 30분", + category: "history", + categoryLabel: "역사", + status: "completed", + iconKey: "building", + description: + "경복궁 정문 주변을 방문하고 GPS 인증과 사진 촬영을 통해 미션을 완료해보세요.", +}; -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 CATEGORY_STYLE: Record< + MissionCategory, + { + hero: string; + heroIcon: string; + chipText: string; + iconBg: string; + } +> = { + history: { + hero: "bg-[#B794FF]", + heroIcon: "text-[#8B5CF6]", + chipText: "text-[#8B5CF6]", + iconBg: "bg-[#EEE1FF]", + }, + nature: { + hero: "bg-[#7EE4BB]", + heroIcon: "text-[#10B981]", + chipText: "text-[#10B981]", + iconBg: "bg-[#DFFBEE]", + }, + food: { + hero: "bg-[#FFD778]", + heroIcon: "text-[#E58A14]", + chipText: "text-[#E58A14]", + iconBg: "bg-[#FFF0C9]", + }, + night: { + hero: "bg-[#92C7FF]", + heroIcon: "text-[#3B82F6]", + chipText: "text-[#3B82F6]", + iconBg: "bg-[#DCE8FF]", + }, +}; +const missionSteps = [ + "장소로 이동", + "GPS 도착 인증", + "사진 촬영 & 제출", + "AI 자동 검증", + "보상 수령", +]; export default function MissionDetailPage() { const navigate = useNavigate(); - const mission = recommendedMissions[1]; + const location = useLocation(); + const state = location.state as MissionDetailState | null; + + const mission = useMemo(() => { + if (!state?.mission) return DEFAULT_MISSION; + + return { + id: state.mission.id, + title: state.mission.title, + place: state.mission.location, + address: state.mission.address, + distance: state.mission.distance, + points: state.mission.points ?? 150, + duration: state.mission.duration ?? "약 30분", + category: state.mission.category, + categoryLabel: state.mission.categoryLabel, + status: state.mission.status, + iconKey: state.mission.iconKey, + description: `${state.mission.location}에서 진행하는 지역 탐험 미션입니다. 장소에 도착해 인증을 완료하고 보상 포인트를 받아보세요.`, + }; + }, [state]); + + const Icon = iconMap[mission.iconKey] ?? Building2; + const style = CATEGORY_STYLE[mission.category]; + + const isCompleted = mission.status === "completed"; + return ( - <> - navigate(-1)} - action={ -
- - -
- } +
+ { + // TODO: 알림함 연결 + }} /> -
-
-
- {mission.category} -

{mission.title}

-

{mission.place}

-
- {mission.visual} -
+
+
-
-
- - 완료 보상 - +{mission.points}P -
-
- - 예상 시간 - {mission.duration} -
-
- - 난이도 - {mission.difficulty} -
-
+
-
-
- -

미션 소개

-
-

{mission.description}

-
+ -
-
- -

장소 정보

-
- {mission.place} -

{mission.address}

-
- - - + + + + +
+
+ + + {mission.categoryLabel} - {mission.distance} + + {isCompleted ? ( + + + 완료 + + ) : null}
-
-
-
- -

인증 가이드

-
-

{mission.guide}

-
+

+ {mission.title} +

+ +

+ + {mission.place} + · + {mission.address} +

+
+
+ +
+
+ } + iconClassName="bg-[#FFECEF] text-[#FF2D45]" + value={mission.distance} + label="거리" + /> + + } + iconClassName="bg-[#EAF5FF] text-[#5BB5F8]" + value={`+${mission.points}P`} + label="보상 포인트" + /> + + } + iconClassName="bg-[#FFF6D9] text-[#F59E0B]" + value={mission.duration} + label="예상 시간" + /> +
+ +
-
-
- -

진행 순서

+
+
+ + + +

+ 수행 방법 +

-
    - {mission.steps.map((step, index) => ( -
  1. - {index + 1} -

    {step}

    - {index === mission.steps.length - 1 ? ( - - ) : null} + +
      + {missionSteps.map((step, index) => ( +
    1. + + + + + + {step} + + + + {index + 1} +
    2. ))}
-
-
- - 4.8 - 참여자 만족도 +
+ } + iconClassName="bg-[#EAF5FF] text-[#5BB5F8]" + title="주변 로컬" + description="맛집 · 카페 추천" + /> + + } + iconClassName="bg-[#FFF6D9] text-[#F59E0B]" + title="보너스 지역" + description="추가 포인트 안내" + highlighted + /> +
+ +
+ + + + +
+ + 미션 완료! + +

+ +{mission.points}P 보상이 지급됐어요 +

-

“짧은 시간 안에 지역 분위기를 느낄 수 있어서 좋아요.”

+ +
- + 미션 다시 인증하기 +
- +
+ ); +} + +type InfoCardProps = { + icon: ReactNode; + iconClassName: string; + value: string; + label: string; +}; + +function InfoCard({ icon, iconClassName, value, label }: InfoCardProps) { + return ( +
+ + {icon} + + + + {value} + + + + {label} + +
); } + +type SmallFeatureCardProps = { + icon: ReactNode; + iconClassName: string; + title: string; + description: string; + highlighted?: boolean; +}; + +function SmallFeatureCard({ + icon, + iconClassName, + title, + description, + highlighted = false, +}: SmallFeatureCardProps) { + return ( +
+ + {icon} + + +
+ + {title} + + +

+ {description} +

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/mission/MissionPage.tsx b/src/pages/mission/MissionPage.tsx index 311a9d4..ff3c559 100644 --- a/src/pages/mission/MissionPage.tsx +++ b/src/pages/mission/MissionPage.tsx @@ -1,3 +1,368 @@ +import { useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Building2, + ChevronRight, + Check, + Landmark, + Leaf, + Lock, + MapPin, + Search, + Sunrise, + Utensils, + type LucideIcon, +} from "lucide-react"; + +import HomeHeader from "@/components/home/HomeHeader"; +import { PATH } from "@/routes/paths"; +import QuestySvg from "@/assets/icons/Questy.svg"; + +type MissionCategory = "all" | "history" | "nature" | "food" | "night"; +type MissionStatus = "completed" | "available" | "locked"; + +export type MissionItem = { + id: number; + title: string; + location: string; + address: string; + distance: string; + category: Exclude; + categoryLabel: string; + iconKey: "building" | "landmark" | "leaf" | "food" | "night" | "lock"; + status: MissionStatus; + points?: number; + duration?: string; +}; + +const CATEGORY_TABS: { + id: MissionCategory; + label: string; + icon?: LucideIcon; +}[] = [ + { id: "all", label: "전체" }, + { id: "history", label: "역사", icon: Building2 }, + { id: "nature", label: "자연", icon: Leaf }, + { id: "food", label: "음식", icon: Utensils }, + { id: "night", label: "야경", icon: Sunrise }, +]; + +const iconMap: Record = { + building: Building2, + landmark: Landmark, + leaf: Leaf, + food: Utensils, + night: Sunrise, + lock: Lock, +}; + +const MISSIONS: MissionItem[] = [ + { + id: 1, + title: "경복궁 정문 인증샷", + location: "경복궁", + address: "서울 종로구", + distance: "1.2km", + category: "history", + categoryLabel: "역사", + iconKey: "building", + status: "completed", + points: 150, + duration: "약 30분", + }, + { + id: 2, + title: "북촌 한옥 골목 탐험", + location: "북촌 한옥마을", + address: "서울 종로구", + distance: "2.1km", + category: "history", + categoryLabel: "문화", + iconKey: "landmark", + status: "completed", + points: 150, + duration: "약 30분", + }, + { + id: 3, + title: "인사동 전통찻집 방문", + location: "인사동", + address: "서울 종로구", + distance: "2.5km", + category: "food", + categoryLabel: "음식", + iconKey: "food", + status: "available", + points: 120, + duration: "약 25분", + }, + { + id: 4, + title: "남산타워 야경 포착", + location: "남산서울타워", + address: "서울 용산구", + distance: "4.8km", + category: "night", + categoryLabel: "야경", + iconKey: "night", + status: "available", + points: 300, + duration: "약 40분", + }, + { + id: 5, + title: "해운대 일출 인증", + location: "해운대 해수욕장", + address: "부산 해운대구", + distance: "325km", + category: "nature", + categoryLabel: "자연", + iconKey: "lock", + status: "locked", + points: 200, + duration: "약 30분", + }, +]; + +const CATEGORY_STYLE: Record< + Exclude, + { + chip: string; + iconPanel: string; + iconText: string; + } +> = { + history: { + chip: "bg-[#EEE1FF] text-[#8B5CF6]", + iconPanel: "bg-[#EDE9FE]", + iconText: "text-[#8B5CF6]", + }, + nature: { + chip: "bg-[#DFFBEE] text-[#65D3AE]", + iconPanel: "bg-[#E7F5F0]", + iconText: "text-[#B9C0CC]", + }, + food: { + chip: "bg-[#FFF0C9] text-[#E58A14]", + iconPanel: "bg-[#FFF9DF]", + iconText: "text-[#E58A14]", + }, + night: { + chip: "bg-[#DCE8FF] text-[#4A7DFF]", + iconPanel: "bg-[#DCE8F5]", + iconText: "text-[#4A7DFF]", + }, +}; + export default function MissionPage() { - return
미션페이지입니다.
; + const navigate = useNavigate(); + const [selectedCategory, setSelectedCategory] = + useState("all"); + + const filteredMissions = useMemo(() => { + if (selectedCategory === "all") return MISSIONS; + + return MISSIONS.filter((mission) => mission.category === selectedCategory); + }, [selectedCategory]); + + return ( +
+ { + // TODO: 알림함 연결 + }} + /> + +
+

+ 미션 탐색 +

+ + + +
+ {CATEGORY_TABS.map((tab) => { + const isActive = selectedCategory === tab.id; + const Icon = tab.icon; + + return ( + + ); + })} +
+
+ +
+ {filteredMissions.map((mission) => ( + { + if (mission.status === "locked") return; + + navigate(PATH.MISSION_DETAIL, { + state: { + mission, + }, + }); + }} + /> + ))} +
+
+ ); } + +type MissionListCardProps = { + mission: MissionItem; + onClick: () => void; +}; + +function MissionListCard({ mission, onClick }: MissionListCardProps) { + const Icon = iconMap[mission.iconKey]; + const isCompleted = mission.status === "completed"; + const isLocked = mission.status === "locked"; + const style = CATEGORY_STYLE[mission.category]; + + return ( +
+
+ + + {isCompleted ? ( + + + + ) : null} +
+ +
+ + + {mission.categoryLabel} + + +
+

+ {mission.title} +

+ +

+ + {mission.location} + · + {mission.distance} +

+ +
+ {isCompleted ? ( + + + 완료 + + ) : isLocked ? ( + + + 잠김 + + ) : ( + + +{mission.points}P + + )} +
+
+
+ + +
+ ); + + function navigateToDetailFallback() { + onClick(); + } +} + +type CategoryMiniIconProps = { + category: Exclude; +}; + +function CategoryMiniIcon({ category }: CategoryMiniIconProps) { + const iconMap: Record, LucideIcon> = { + history: Building2, + nature: Leaf, + food: Utensils, + night: Sunrise, + }; + + const Icon = iconMap[category]; + + return ; +} \ No newline at end of file From b9d414a8981e2bc0cf60d7b1cdd42a4abb81a404 Mon Sep 17 00:00:00 2001 From: kinjaebeom Date: Tue, 1 Sep 2026 22:47:49 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[Fix]=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=ED=8D=BC=EB=B8=94?= =?UTF-8?q?=EB=A6=AC=EC=8B=B1=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icons/QuestyMain.svg | 9 + src/assets/icons/star.svg | 3 + src/index.css | 8 + src/layouts/BottomNavigationLayout.tsx | 33 +- src/layouts/QuespotPageLayout.tsx | 45 +++ src/pages/HomePage.tsx | 118 +++---- src/pages/mission/BonusRegionPage.tsx | 151 +++++++++ src/pages/mission/LocalRecommendPage.tsx | 242 ++++++++++++++ src/pages/mission/MissionDetailPage.tsx | 393 +++++++++++++---------- src/pages/mission/MissionPage.tsx | 28 +- src/routes/paths.ts | 2 + src/routes/router.tsx | 22 +- 12 files changed, 786 insertions(+), 268 deletions(-) create mode 100644 src/assets/icons/QuestyMain.svg create mode 100644 src/assets/icons/star.svg create mode 100644 src/layouts/QuespotPageLayout.tsx create mode 100644 src/pages/mission/BonusRegionPage.tsx create mode 100644 src/pages/mission/LocalRecommendPage.tsx diff --git a/src/assets/icons/QuestyMain.svg b/src/assets/icons/QuestyMain.svg new file mode 100644 index 0000000..d2267be --- /dev/null +++ b/src/assets/icons/QuestyMain.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/star.svg b/src/assets/icons/star.svg new file mode 100644 index 0000000..e1f9af2 --- /dev/null +++ b/src/assets/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/index.css b/src/index.css index 395f3c8..c9d008b 100644 --- a/src/index.css +++ b/src/index.css @@ -3,3 +3,11 @@ @theme { --color-primary: #5bb5f8; } +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.no-scrollbar::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/src/layouts/BottomNavigationLayout.tsx b/src/layouts/BottomNavigationLayout.tsx index 49f6432..de03ede 100644 --- a/src/layouts/BottomNavigationLayout.tsx +++ b/src/layouts/BottomNavigationLayout.tsx @@ -38,7 +38,7 @@ const NAV_ITEMS = [ export default function BottomNavigationLayout() { return ( <> -
+
@@ -49,38 +49,29 @@ export default function BottomNavigationLayout() { const BottomNavigation = () => { return ( -