From 731416314d54a1eff98e7f758fb82a6483281ac2 Mon Sep 17 00:00:00 2001 From: yyj0917 Date: Tue, 23 Sep 2025 19:24:17 +0900 Subject: [PATCH 1/5] fix : mypage sheet style edit & weather, save api edit --- src/app/(pages)/course/[course_id]/page.tsx | 16 ++- .../(pages)/course/_components/course-map.tsx | 3 + .../_components/course-runway-point.tsx | 1 - .../course-surrond-info.tsx | 2 +- .../mypage/_components/place-pick-sheet.tsx | 35 ++---- .../save/_components/region-courses-list.tsx | 2 +- .../weather-current-section/index.tsx | 4 - .../weather/_components/weather-section.tsx | 15 --- .../_components/weather-weekly-card.tsx | 15 +-- .../weather/_hooks/running-condition.hook.ts | 111 +++++++++++++----- .../weather/_hooks/use-weather-data.ts | 3 - src/app/(pages)/weather/loading.tsx | 7 ++ src/components/navigation-bar.tsx | 13 +- src/interfaces/course/course.types.ts | 3 + src/interfaces/weather.types.ts | 2 + src/lib/api/courses/index.ts | 18 +++ src/lib/api/index.ts | 2 +- src/lib/api/save/index.ts | 11 +- 18 files changed, 166 insertions(+), 97 deletions(-) create mode 100644 src/app/(pages)/weather/loading.tsx diff --git a/src/app/(pages)/course/[course_id]/page.tsx b/src/app/(pages)/course/[course_id]/page.tsx index ab99224..f380e60 100644 --- a/src/app/(pages)/course/[course_id]/page.tsx +++ b/src/app/(pages)/course/[course_id]/page.tsx @@ -4,13 +4,16 @@ import { CourseDetail } from '../_components/course-detail'; import { CourseMap } from '../_components/course-map'; import { CourseDescription } from '../_components/course-description'; import { CourseRunwayPoint } from '../_components/course-runway-point'; -import { api } from '@/lib/api'; -import { getCourseAISummary, getCourseDetail } from '@/lib/api/courses'; +import { + getCourseAISummary, + getCourseDetail, + getUserCourseDetail, +} from '@/lib/api/courses'; import { Suspense } from 'react'; -import { Divide } from 'lucide-react'; import { LoadingSpinner } from '@/components/loading-spinner'; import MainPointLogo from '@/public/svg/logo/main-point-logo.svg'; import Link from 'next/link'; +import { cookies } from 'next/headers'; interface CoursePageProps { params: Promise<{ course_id: string }>; @@ -19,7 +22,11 @@ interface CoursePageProps { export default async function CoursePage({ params }: CoursePageProps) { const { course_id } = await params; - const courseDetail = await getCourseDetail(course_id).then(res => res.data); + const cookieStore = await cookies(); + const token = cookieStore.get('accessToken')?.value; + const courseDetail = token + ? await getUserCourseDetail(course_id).then(res => res.data) + : await getCourseDetail(course_id).then(res => res.data); if (!courseDetail) { return ( @@ -45,6 +52,7 @@ export default async function CoursePage({ params }: CoursePageProps) {
diff --git a/src/app/(pages)/course/_components/course-map.tsx b/src/app/(pages)/course/_components/course-map.tsx index 2938b95..a98a959 100644 --- a/src/app/(pages)/course/_components/course-map.tsx +++ b/src/app/(pages)/course/_components/course-map.tsx @@ -6,9 +6,11 @@ import GPXRouteMap from './gpx-parser-map'; export function CourseMap({ gpxUrl, crsKorNm, + isFavorite, }: { gpxUrl: string; crsKorNm: string; + isFavorite: boolean; }) { const [gpxContent, setGpxContent] = useState(''); const [isLoading, setIsLoading] = useState(true); @@ -16,6 +18,7 @@ export function CourseMap({ useEffect(() => { localStorage.setItem('crsKorNm', crsKorNm); + localStorage.setItem('isFavorite', isFavorite.toString()); const fetchGpxContent = async () => { try { setIsLoading(true); diff --git a/src/app/(pages)/course/_components/course-runway-point.tsx b/src/app/(pages)/course/_components/course-runway-point.tsx index b3da0d8..4cec5e1 100644 --- a/src/app/(pages)/course/_components/course-runway-point.tsx +++ b/src/app/(pages)/course/_components/course-runway-point.tsx @@ -1,5 +1,4 @@ import { getCourseAISummary } from '@/lib/api/courses'; -import MainPointLogo from '@/public/svg/logo/main-point-logo.svg'; export async function CourseRunwayPoint({ course_id }: { course_id: string }) { const courseAISummary = await getCourseAISummary(course_id).then( diff --git a/src/app/(pages)/course/_components/course-surrond-tour/course-surrond-info.tsx b/src/app/(pages)/course/_components/course-surrond-tour/course-surrond-info.tsx index 5f33c89..eb3dfb2 100644 --- a/src/app/(pages)/course/_components/course-surrond-tour/course-surrond-info.tsx +++ b/src/app/(pages)/course/_components/course-surrond-tour/course-surrond-info.tsx @@ -105,7 +105,7 @@ export function CourseSurrondInfo() { 주변 정보 보기 diff --git a/src/app/(pages)/mypage/_components/place-pick-sheet.tsx b/src/app/(pages)/mypage/_components/place-pick-sheet.tsx index 51d5106..859cc30 100644 --- a/src/app/(pages)/mypage/_components/place-pick-sheet.tsx +++ b/src/app/(pages)/mypage/_components/place-pick-sheet.tsx @@ -128,40 +128,29 @@ export function PlacePickSheet({ - - - { - setIsPickPlace( - defaultIsPickPlace - ? findPlaceByName(defaultIsPickPlace) - : null, - ); - }} - > - - - - + + + + 여행지 설정하기 + + + +
+

+ 여행, 어디로 떠나시나요? 완료하기 - - - -
-

- 여행, 어디로 떠나시나요?

{isPickPlace !== null ? ( diff --git a/src/app/(pages)/save/_components/region-courses-list.tsx b/src/app/(pages)/save/_components/region-courses-list.tsx index 360c75f..4acf2f2 100644 --- a/src/app/(pages)/save/_components/region-courses-list.tsx +++ b/src/app/(pages)/save/_components/region-courses-list.tsx @@ -13,7 +13,7 @@ function RegionSection({ region, courses }: RegionSectionProps) { return (

{region}

-
+
{courses.map(course => (
대기
-
매우 좋음
+
{data.airQuality}
{/* 러닝 지수 */} diff --git a/src/app/(pages)/weather/_hooks/running-condition.hook.ts b/src/app/(pages)/weather/_hooks/running-condition.hook.ts index f0ce278..2b7dbb1 100644 --- a/src/app/(pages)/weather/_hooks/running-condition.hook.ts +++ b/src/app/(pages)/weather/_hooks/running-condition.hook.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { FineDust, UvIndex } from '@/interfaces/weather.types'; +import { AirQuality, FineDust, UvIndex } from '@/interfaces/weather.types'; export type RunningIndex = '좋음' | '보통' | '나쁨'; @@ -18,9 +18,10 @@ export interface RunningCondition { export interface WeatherData { temperature: number; - windSpeed: number; // m/s - fineDust: FineDust; - uvIndex: UvIndex; + windSpeed?: number; // m/s + fineDust?: FineDust; + uvIndex?: UvIndex; + airQuality?: AirQuality; } /** @@ -28,22 +29,26 @@ export interface WeatherData { * @param weatherData 날씨 데이터 * @returns 러닝 지수 정보 */ -export function useRunningCondition(weatherData: WeatherData): RunningCondition { +export function useRunningCondition( + weatherData: WeatherData, +): RunningCondition { return useMemo(() => { - const { temperature, windSpeed, fineDust, uvIndex } = weatherData; + const { temperature, windSpeed, fineDust, uvIndex, airQuality } = + weatherData; // 각 요소별 점수 계산 const temperatureScore = calculateTemperatureScore(temperature); - const windSpeedScore = calculateWindSpeedScore(windSpeed); - const fineDustScore = calculateFineDustScore(fineDust); - const uvIndexScore = calculateUvIndexScore(uvIndex); - + const windSpeedScore = calculateWindSpeedScore(windSpeed ?? 0); + const fineDustScore = calculateFineDustScore(fineDust ?? '보통'); + const uvIndexScore = calculateUvIndexScore(uvIndex ?? '보통'); + const airQualityScore = calculateAirQualityScore(airQuality ?? '보통'); // 가중치 적용 (온도 40%, 풍속 20%, 미세먼지 25%, 자외선 15%) const totalScore = Math.round( temperatureScore.score * 0.4 + - windSpeedScore.score * 0.2 + - fineDustScore.score * 0.25 + - uvIndexScore.score * 0.15 + windSpeedScore.score * 0.2 + + fineDustScore.score * 0.25 + + uvIndexScore.score * 0.15 + + airQualityScore.score * 0.1, ); // 종합 지수 결정 @@ -57,11 +62,21 @@ export function useRunningCondition(weatherData: WeatherData): RunningCondition description = '모든 조건이 러닝하기에 최적입니다.'; } else if (totalScore >= 60) { index = '보통'; - tip = getModerateTip(temperatureScore, windSpeedScore, fineDustScore, uvIndexScore); + tip = getModerateTip( + temperatureScore, + windSpeedScore, + fineDustScore, + uvIndexScore, + ); description = '약간의 준비가 필요한 날씨입니다.'; } else { index = '나쁨'; - tip = getBadTip(temperatureScore, windSpeedScore, fineDustScore, uvIndexScore); + tip = getBadTip( + temperatureScore, + windSpeedScore, + fineDustScore, + uvIndexScore, + ); description = '러닝에 주의가 필요한 날씨입니다.'; } @@ -81,13 +96,16 @@ export function useRunningCondition(weatherData: WeatherData): RunningCondition } // 온도 점수 계산 -function calculateTemperatureScore(temperature: number): { score: number; status: string } { +function calculateTemperatureScore(temperature: number): { + score: number; + status: string; +} { if (temperature >= 10 && temperature <= 20) { return { score: 100, status: '최적' }; } else if (temperature >= 5 && temperature < 10) { - return { score: 80, status: '쌀쌀' }; + return { score: 70, status: '쌀쌀' }; } else if (temperature > 20 && temperature <= 25) { - return { score: 75, status: '따뜻' }; + return { score: 80, status: '따뜻' }; } else if (temperature > 25 && temperature <= 30) { return { score: 60, status: '더움' }; } else if (temperature > 30) { @@ -98,7 +116,10 @@ function calculateTemperatureScore(temperature: number): { score: number; status } // 풍속 점수 계산 -function calculateWindSpeedScore(windSpeed: number): { score: number; status: string } { +function calculateWindSpeedScore(windSpeed: number): { + score: number; + status: string; +} { if (windSpeed <= 2) { return { score: 100, status: '무풍' }; } else if (windSpeed <= 5) { @@ -113,7 +134,10 @@ function calculateWindSpeedScore(windSpeed: number): { score: number; status: st } // 미세먼지 점수 계산 -function calculateFineDustScore(fineDust: FineDust): { score: number; status: string } { +function calculateFineDustScore(fineDust: FineDust): { + score: number; + status: string; +} { switch (fineDust) { case '좋음': return { score: 100, status: '좋음' }; @@ -131,7 +155,10 @@ function calculateFineDustScore(fineDust: FineDust): { score: number; status: st } // 자외선 지수 점수 계산 -function calculateUvIndexScore(uvIndex: UvIndex): { score: number; status: string } { +function calculateUvIndexScore(uvIndex: UvIndex): { + score: number; + status: string; +} { switch (uvIndex) { case '낮음': return { score: 100, status: '낮음' }; @@ -150,33 +177,53 @@ function calculateUvIndexScore(uvIndex: UvIndex): { score: number; status: strin } } +// 공기 질 점수 계산 +function calculateAirQualityScore(airQuality: AirQuality): { + score: number; + status: string; +} { + switch (airQuality) { + case '좋음': + return { score: 100, status: '좋음' }; + case '보통': + return { score: 80, status: '보통' }; + case '나쁨': + return { score: 40, status: '나쁨' }; + case '매우나쁨': + return { score: 10, status: '매우나쁨' }; + case '점검중': + return { score: 50, status: '점검중' }; + default: + return { score: 50, status: '알 수 없음' }; + } +} // 보통 등급 팁 생성 function getModerateTip( temp: { score: number; status: string }, wind: { score: number; status: string }, dust: { score: number; status: string }, - uv: { score: number; status: string } + uv: { score: number; status: string }, ): string { const tips = []; - + if (temp.score < 70) { if (temp.status === '쌀쌀') tips.push('따뜻한 옷을 입으세요'); if (temp.status === '따뜻') tips.push('가벼운 옷을 입으세요'); if (temp.status === '더움') tips.push('충분한 수분을 섭취하세요'); } - + if (wind.score < 70) { tips.push('바람막이를 챙기세요'); } - + if (dust.score < 70) { tips.push('마스크를 착용하세요'); } - + if (uv.score < 70) { tips.push('자외선 차단제를 바르세요'); } - + return tips.length > 0 ? tips.join(', ') : '모자와 바람막이를 챙기세요!'; } @@ -185,25 +232,25 @@ function getBadTip( temp: { score: number; status: string }, wind: { score: number; status: string }, dust: { score: number; status: string }, - uv: { score: number; status: string } + uv: { score: number; status: string }, ): string { if (temp.score < 50) { if (temp.status === '매우 더움') return '실내 러닝을 권장합니다!'; if (temp.status === '추움') return '따뜻한 옷을 입고 러닝하세요!'; } - + if (dust.score < 50) { return '미세먼지가 심하니 실내 러닝을 권장합니다!'; } - + if (wind.score < 50) { return '강한 바람으로 러닝이 어려울 수 있습니다!'; } - + if (uv.score < 50) { return '자외선이 강하니 실내 러닝을 권장합니다!'; } - + return '러닝에 주의가 필요한 날씨입니다!'; } diff --git a/src/app/(pages)/weather/_hooks/use-weather-data.ts b/src/app/(pages)/weather/_hooks/use-weather-data.ts index 9f00d9e..3dbee46 100644 --- a/src/app/(pages)/weather/_hooks/use-weather-data.ts +++ b/src/app/(pages)/weather/_hooks/use-weather-data.ts @@ -8,9 +8,6 @@ import { } from '@/lib/api/weather'; export function useWeather(lat: number, lng: number, location: string) { - console.log('lat', lat); - console.log('lng', lng); - console.log('location', location); return useQuery({ queryKey: ['weather', lat, lng], queryFn: async () => { diff --git a/src/app/(pages)/weather/loading.tsx b/src/app/(pages)/weather/loading.tsx new file mode 100644 index 0000000..bd621a5 --- /dev/null +++ b/src/app/(pages)/weather/loading.tsx @@ -0,0 +1,7 @@ +import { LoadingSpinner } from '@/components/loading-spinner'; + +const WeatherLoading = () => { + return ; +}; + +export default WeatherLoading; diff --git a/src/components/navigation-bar.tsx b/src/components/navigation-bar.tsx index 9e7670a..d01b07d 100644 --- a/src/components/navigation-bar.tsx +++ b/src/components/navigation-bar.tsx @@ -18,7 +18,7 @@ import HeartInactive from '@/public/svg/course/heart-blank.svg'; import { CourseSurrondInfo } from '@/app/(pages)/course/_components/course-surrond-tour/course-surrond-info'; import React from 'react'; import { toast } from 'sonner'; -import { addCourseSave } from '@/lib/api/courses'; +import { addCourseSave, removeCourseSave } from '@/lib/api/courses'; interface NavItem { href: string; @@ -76,7 +76,18 @@ export default function NavigationBar() { const isCourseDetail = pathname.startsWith('/course'); const crsIdx = pathname.split('/course/')[1] ?? ''; + React.useEffect(() => { + const isFavorite = localStorage.getItem('isFavorite'); + setIsSaveActive(isFavorite === 'true'); + }, []); + const handleSaveClick = async () => { + if (isSaveActive) { + await removeCourseSave(crsIdx); + setIsSaveActive(false); + toast.success('찜 삭제'); + return; + } await addCourseSave(crsIdx); setIsSaveActive(!isSaveActive); toast.success('찜 완료'); diff --git a/src/interfaces/course/course.types.ts b/src/interfaces/course/course.types.ts index d7cd3f1..ae9cf8b 100644 --- a/src/interfaces/course/course.types.ts +++ b/src/interfaces/course/course.types.ts @@ -30,6 +30,9 @@ export interface Course { // 시간 정보 createdAt: string; // 등록일 (ISO 8601 형식) updatedAt: string; // 수정일 (ISO 8601 형식) + + // 찜 여부 + isFavorite?: boolean; // 찜 여부 } // API 응답 래퍼 타입 diff --git a/src/interfaces/weather.types.ts b/src/interfaces/weather.types.ts index bc5656c..4fbc630 100644 --- a/src/interfaces/weather.types.ts +++ b/src/interfaces/weather.types.ts @@ -8,6 +8,7 @@ export type Weather = | '흐림+소나기'; export type FineDust = '좋음' | '보통' | '나쁨' | '매우나쁨' | '점검중'; export type UvIndex = '낮음' | '보통' | '높음' | '매우높음' | '위험' | '점검중'; +export type AirQuality = '좋음' | '보통' | '나쁨' | '매우나쁨' | '점검중'; // 현재 날씨 데이터 타입 export interface WeatherNowData { @@ -27,6 +28,7 @@ export interface WeeklyWeatherData { weatherPm: Weather; // 오후 날씨 tempMin: number; // 최저 기온 tempMax: number; // 최고 기온 + airQuality: AirQuality; // 공기 질 지수 } // 주간 예보 API 응답 타입 diff --git a/src/lib/api/courses/index.ts b/src/lib/api/courses/index.ts index a9c78be..7b5b492 100644 --- a/src/lib/api/courses/index.ts +++ b/src/lib/api/courses/index.ts @@ -13,6 +13,16 @@ export function getCourseDetail( ): Promise> { return api.get(`/public/courses/${course_id}`); } +/** + * @description 코스 상세 조회 - 토큰o - 로그인 상태 + * @param course_id 코스 고유번호 + * @returns 코스 상세 정보 + */ +export function getUserCourseDetail( + course_id: string, +): Promise> { + return api.get(`/courses/${course_id}`); +} /** * @description 코스 분석 요약 조회 - 토큰x * @param course_id 코스 고유번호 @@ -57,3 +67,11 @@ export function getCourseTourInfo( export function addCourseSave(crsIdx: string): Promise> { return api.post(`/courses/${crsIdx}/favorite`); } +/** + * @description 코스 찜 삭제 + * @param crsIdx 코스 고유번호 + * @returns 코스 찜 삭제 정보 + */ +export function removeCourseSave(crsIdx: string): Promise> { + return api.delete(`/courses/${crsIdx}/favorite`); +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index b402757..32b2fe3 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -49,7 +49,7 @@ const getAuthToken = async (): Promise => { const { unstable_noStore } = require('next/cache'); unstable_noStore(); // 정적 생성 비활성화 const cookieStore = await cookies(); - const token = cookieStore.get('auth-token')?.value; + const token = cookieStore.get('accessToken')?.value; return token ?? null; } catch (error) { console.warn('서버에서 토큰 가져오기 실패:', error); diff --git a/src/lib/api/save/index.ts b/src/lib/api/save/index.ts index 2775cec..90df3d4 100644 --- a/src/lib/api/save/index.ts +++ b/src/lib/api/save/index.ts @@ -5,6 +5,13 @@ import { api } from '@/lib/api'; /** * 찜한 코스 목록 가져오는 API */ -export const getFavoriteCourses = async (): Promise> => { - return await api.get('/courses/favorite'); +export const getFavoriteCourses = async (): Promise< + ApiResponse +> => { + return await api.get('/courses/favorite', { + next: { + revalidate: 60, + tags: ['favorite'], + }, + }); }; From ed79867d6b64d0fd760ceac7dbc33b4a7bf7cef7 Mon Sep 17 00:00:00 2001 From: yyj0917 Date: Tue, 23 Sep 2025 19:35:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix=20:=20mypage=20sheet=20optimize=20ui=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/_components/place-pick-sheet.tsx | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/app/(pages)/mypage/_components/place-pick-sheet.tsx b/src/app/(pages)/mypage/_components/place-pick-sheet.tsx index 859cc30..bda20ad 100644 --- a/src/app/(pages)/mypage/_components/place-pick-sheet.tsx +++ b/src/app/(pages)/mypage/_components/place-pick-sheet.tsx @@ -57,23 +57,10 @@ export function PlacePickSheet({ refetch: () => void; defaultIsPickPlace: string | null; }) { - const [viewportHeight, setViewportHeight] = useState(0); - - useEffect(() => { - const updateViewportHeight = () => { - setViewportHeight(window.innerHeight); - }; - - updateViewportHeight(); - window.addEventListener('resize', updateViewportHeight); - - return () => window.removeEventListener('resize', updateViewportHeight); - }, []); const [isActive, setIsActive] = useState('강원'); const [isPickPlace, setIsPickPlace] = useState(() => { if (!defaultIsPickPlace) return null; - - const foundPlace = findPlaceByName(defaultIsPickPlace); + const foundPlace = findPlaceByName(defaultIsPickPlace.split(' ')[1]); return foundPlace ?? { name: defaultIsPickPlace, type: '시' }; }); const scrollRef = useRef(null); @@ -106,7 +93,7 @@ export function PlacePickSheet({ await setDestination(destinationName); refetch(); toast.success('여행지 설정이 완료되었습니다.'); - setIsPickPlace(null); + setIsPickPlace(isPickPlace); setIsActive('강원'); } catch (error) { toast.error('여행지 설정에 실패했습니다.'); @@ -168,7 +155,7 @@ export function PlacePickSheet({
-
+