-
Notifications
You must be signed in to change notification settings - Fork 1
✨Feat: 카카오맵 수정 및 AI 코스 추천/코스 세션 조회 api 연결 #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
6296b1a
339d762
ed53c8e
9c0a57a
c8f80fe
120cc93
902a002
41bb2fb
5de0d5b
ac7f16b
8acf7e9
7169487
1817266
9916376
1332fc2
24d75f3
07c8f49
4acf746
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,25 +2,45 @@ import Image from 'next/image'; | |||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/shared/lib'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { ControlBar } from '@/shared/components'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { BottomNav } from '@/shared/components/tab/BottomNav'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| purposes, | ||||||||||||||||||||||||||||||||||||||||||||
| stays, | ||||||||||||||||||||||||||||||||||||||||||||
| moves, | ||||||||||||||||||||||||||||||||||||||||||||
| } from '@/shared/constants/course/courseOptions'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useCourseSelection } from '@/shared/hooks/useCourseSelection'; | ||||||||||||||||||||||||||||||||||||||||||||
| import CourseSelectSection from '@/pages/map/components/CourseSelectSection'; | ||||||||||||||||||||||||||||||||||||||||||||
| import CourseInputSection from '@/pages/map/components/CourseInputSection'; | ||||||||||||||||||||||||||||||||||||||||||||
| import CourseSelectGroup from '@/shared/components/map/components/CourseSelectGroup'; | ||||||||||||||||||||||||||||||||||||||||||||
| import CourseInputSection from '@/shared/components/map/components/CourseInputSection'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from 'next/router'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useRecommendCourse } from '@/shared/api/course/queries/useRecommendCourse'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export default function CourseSettingPage() { | ||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||
| const { purpose, setPurpose, stay, setStay, move, setMove } = | ||||||||||||||||||||||||||||||||||||||||||||
| useCourseSelection(); | ||||||||||||||||||||||||||||||||||||||||||||
| const { mutate, isPending } = useRecommendCourse(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const { purpose, setPurpose, stay, setStay, move, setMove } = useCourseSelection(); | ||||||||||||||||||||||||||||||||||||||||||||
| const [mustVisitPlace, setMustVisitPlace] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const canProceed = Boolean(purpose && stay && move); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleNext = () => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (canProceed) router.push('/map/result'); | ||||||||||||||||||||||||||||||||||||||||||||
| if (!canProceed) return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| mutate( | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| travelPurpose: purpose!, | ||||||||||||||||||||||||||||||||||||||||||||
| stayDuration: stay!, | ||||||||||||||||||||||||||||||||||||||||||||
| transportation: move!, | ||||||||||||||||||||||||||||||||||||||||||||
| userLatitude: 37.4985, | ||||||||||||||||||||||||||||||||||||||||||||
| userLongitude: 126.7822, | ||||||||||||||||||||||||||||||||||||||||||||
| mustVisitPlace: mustVisitPlace || "", | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| onSuccess: (res) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (res.isSuccess) { | ||||||||||||||||||||||||||||||||||||||||||||
| router.push(`/map/result?sessionId=${res.result.sessionId}`); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| onError: (err) => { | ||||||||||||||||||||||||||||||||||||||||||||
| console.error('AI 코스 추천 실패:', err); | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 및 실패 응답 처리 부족 현재 에러 처리에 두 가지 문제가 있습니다:
다음과 같이 수정하세요: onSuccess: (res) => {
if (res.isSuccess) {
router.push(`/map/result?sessionId=${res.result.sessionId}`);
+ } else {
+ alert('코스 추천에 실패했습니다. 다시 시도해 주세요.');
}
},
onError: (err) => {
console.error('AI 코스 추천 실패:', err);
+ alert('네트워크 오류가 발생했습니다. 다시 시도해 주세요.');
},더 나은 UX를 위해 toast 라이브러리나 모달을 사용하는 것을 권장합니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -55,27 +75,21 @@ export default function CourseSettingPage() { | |||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| <div className='flex flex-col gap-[1.9rem] mb-[8rem]'> | ||||||||||||||||||||||||||||||||||||||||||||
| <CourseSelectSection | ||||||||||||||||||||||||||||||||||||||||||||
| title='여행 목적을 선택해 주세요' | ||||||||||||||||||||||||||||||||||||||||||||
| options={purposes} | ||||||||||||||||||||||||||||||||||||||||||||
| selected={purpose} | ||||||||||||||||||||||||||||||||||||||||||||
| onSelect={setPurpose} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| <CourseSelectSection | ||||||||||||||||||||||||||||||||||||||||||||
| title='체류 시간을 선택해 주세요' | ||||||||||||||||||||||||||||||||||||||||||||
| options={stays} | ||||||||||||||||||||||||||||||||||||||||||||
| selected={stay} | ||||||||||||||||||||||||||||||||||||||||||||
| onSelect={setStay} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| <CourseSelectSection | ||||||||||||||||||||||||||||||||||||||||||||
| title='이동 방식을 선택해 주세요' | ||||||||||||||||||||||||||||||||||||||||||||
| options={moves} | ||||||||||||||||||||||||||||||||||||||||||||
| selected={move} | ||||||||||||||||||||||||||||||||||||||||||||
| onSelect={setMove} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| <CourseInputSection onNext={handleNext} /> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <CourseSelectGroup | ||||||||||||||||||||||||||||||||||||||||||||
| purpose={purpose} | ||||||||||||||||||||||||||||||||||||||||||||
| stay={stay} | ||||||||||||||||||||||||||||||||||||||||||||
| move={move} | ||||||||||||||||||||||||||||||||||||||||||||
| setPurpose={setPurpose} | ||||||||||||||||||||||||||||||||||||||||||||
| setStay={setStay} | ||||||||||||||||||||||||||||||||||||||||||||
| setMove={setMove} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| <CourseInputSection | ||||||||||||||||||||||||||||||||||||||||||||
| value={mustVisitPlace} | ||||||||||||||||||||||||||||||||||||||||||||
| onChange={setMustVisitPlace} | ||||||||||||||||||||||||||||||||||||||||||||
| onNext={handleNext} | ||||||||||||||||||||||||||||||||||||||||||||
| isLoading={isPending} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| <BottomNav /> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { apiAuth } from "@/shared/api/instance"; | ||
| import type { CourseSessionResponse } from "@/shared/api/course/types/courseSession"; | ||
|
|
||
| export const getCourseSession = async (sessionId: string) => { | ||
| const { data } = await apiAuth.get<CourseSessionResponse>( | ||
| `/api/courses/session/${sessionId}`, | ||
| ); | ||
|
|
||
| if (!data.isSuccess) { | ||
| throw new Error(data.message || 'Failed to fetch course session'); | ||
| } | ||
|
|
||
| return data.result; | ||
| }; | ||
|
Comment on lines
+1
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 파일명 오타를 수정하세요. 파일명이 파일명 변경:
모든 import 경로도 업데이트해야 합니다. 🤖 Prompt for AI Agents
Comment on lines
+4
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이전 리뷰 피드백이 잘 반영되었습니다! API 응답 검증 로직( Minor:
if (!data.success) {
throw new Error(data.message || "Failed to fetch course session");
}
+
+ if (!data.data) {
+ throw new Error("Course session data is missing");
+ }
return data.data;Optional: 타입 안전성 개선 현재
type CourseSessionResponseSuccess = {
success: true;
code: string;
message: string;
data: {
memberId: number;
places: CoursePlace[];
createdAt: string;
travelPurpose: string;
stayDuration: string;
transportation: string;
mustVisitPlace?: string;
};
timestamp: string;
};
type CourseSessionResponseFailure = {
success: false;
code: string;
message: string;
data?: never;
timestamp: string;
};
export type CourseSessionResponse =
| CourseSessionResponseSuccess
| CourseSessionResponseFailure;이렇게 하면 TypeScript가 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { apiWithToken } from '@/shared/api/instance'; | ||
| import type { | ||
| RecommendCourseRequest, | ||
| RecommendCourseResponse, | ||
| } from '@/shared/api/course/types/recommendTypes'; | ||
|
|
||
| export const postRecommendCourse = async (body: RecommendCourseRequest) => { | ||
| const { data } = await apiWithToken.post<RecommendCourseResponse>( | ||
| '/api/courses/recommend', | ||
| body, | ||
| ); | ||
| return data; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { useQuery } from '@tanstack/react-query'; | ||
| import { getCourseSession } from '@/shared/api/course/api/getCourseSession'; | ||
| import type { CourseSessionResponse } from '@/shared/api/course/types/courseSession'; | ||
|
|
||
| export const useCourseSession = (sessionId: string) => { | ||
| return useQuery<CourseSessionResponse['result']>({ | ||
| queryKey: ['courseSession', sessionId], | ||
| queryFn: () => getCourseSession(sessionId), | ||
| enabled: Boolean(sessionId && sessionId.trim()), | ||
| staleTime: 1000 * 60 * 5, | ||
| refetchOnWindowFocus: true, | ||
| refetchOnReconnect: true, | ||
| refetchInterval: false, | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { useMutation } from '@tanstack/react-query'; | ||
| import { | ||
| postRecommendCourse, | ||
| } from '@/shared/api/course/api/postRecommendCourse'; | ||
| import type { | ||
| RecommendCourseRequest, | ||
| RecommendCourseResponse, | ||
| } from '@/shared/api/course/types/recommendTypes'; | ||
|
|
||
| export const useRecommendCourse = () => { | ||
| return useMutation<RecommendCourseResponse, Error, RecommendCourseRequest>({ | ||
| mutationFn: (body) => postRecommendCourse(body), | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| export interface CoursePlace { | ||
| placeId: number; | ||
| name: string; | ||
| address: string; | ||
| latitude: number; | ||
| longitude: number; | ||
| description: string; | ||
| placeImg: string; | ||
| } | ||
|
Comment on lines
+1
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainCoursePlace 인터페이스 중복 및 타입 불일치를 해결하세요.
서버가 null을 반환할 경우 런타임 에러가 발생할 수 있습니다. 두 인터페이스를 하나로 통합하고 올바른 nullability를 적용하세요. 다음 스크립트로 실제 API 응답을 확인하여 null 가능 여부를 검증하세요: 🏁 Script executed: #!/bin/bash
# Description: Check API response structure in related files
# Find API call implementations
rg -n -A 10 "getCourseSettion|getCourseSession" --type=ts --type=tsx
# Check for null handling in related hooks
rg -n "CoursePlace\[" --type=ts --type=tsxLength of output: 55 🏁 Script executed: #!/bin/bash
# Search for CoursePlace interface definitions across codebase
rg -n "interface CoursePlace" --type=ts
# Search for CoursePlace type definitions
rg -n "type CoursePlace" --type=ts
# Check recommendTypes.ts for CoursePlace definition
rg -n -A 10 "CoursePlace" src/shared/api/course/types/recommendTypes.ts
# Find files that import CoursePlace
rg -n "import.*CoursePlace" --type=ts
# Check the actual file mentioned in review
cat src/shared/api/course/types/courseSettion.tsLength of output: 1394 CoursePlace 인터페이스의 중복 정의 및 타입 불일치 해결 필요
현재 API가 null 값을 반환할 경우 타입 계약 위반으로 인한 런타임 에러가 발생할 수 있습니다. 올바른 nullability를 결정한 후 단일 정의로 통합하고, 모든 import를 해당 파일로 통일하세요. 🤖 Prompt for AI Agents |
||
|
|
||
| export interface CourseSessionResponse { | ||
| code: string; | ||
| message: string; | ||
| isSuccess: boolean; | ||
| result: { | ||
| memberId: number; | ||
| createdAt: string; | ||
| travelPurpose: string; | ||
| stayDuration: string; | ||
| transportation: string; | ||
| mustVisitPlace?: string; | ||
| places: CoursePlace[]; | ||
| }; | ||
| } | ||
|
Comment on lines
+1
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain파일명 오타를 수정하세요. 파일명이 다음 스크립트를 실행하여 이 파일을 import하는 모든 위치를 확인하세요: 🏁 Script executed: #!/bin/bash
# Description: Find all imports of the misspelled filename
# Search for imports of courseSettion
rg -n "courseSettion" --type=ts --type=tsxLength of output: 27 🏁 Script executed: #!/bin/bash
# Description: Search for misspelled filename and verify file existence
echo "=== Searching for all references to 'courseSettion' ==="
rg -n "courseSettion"
echo ""
echo "=== Searching for references to 'courseSession' ==="
rg -n "courseSession"
echo ""
echo "=== Finding TypeScript files with 'course' in the name ==="
fd -e ts -e tsx "course" --type fLength of output: 1463 🏁 Script executed: #!/bin/bash
# Description: Check the actual file at the mentioned path
echo "=== Checking if file exists at src/shared/api/course/types/courseSettion.ts ==="
if [ -f "src/shared/api/course/types/courseSettion.ts" ]; then
echo "File EXISTS at: src/shared/api/course/types/courseSettion.ts"
echo "=== First 10 lines of the file ==="
head -10 "src/shared/api/course/types/courseSettion.ts"
else
echo "File NOT FOUND at: src/shared/api/course/types/courseSettion.ts"
fi
echo ""
echo "=== Checking corrected filename ==="
if [ -f "src/shared/api/course/types/courseSession.ts" ]; then
echo "File EXISTS at: src/shared/api/course/types/courseSession.ts"
else
echo "File NOT FOUND at: src/shared/api/course/types/courseSession.ts"
fi
echo ""
echo "=== Listing all files in course/types directory ==="
ls -la "src/shared/api/course/types/" 2>/dev/null || echo "Directory not found"Length of output: 741 파일명 오타를 수정하세요: 현재 파일명이 수정이 필요한 파일들:
🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 좌표 사용
사용자 위치 대신 고정된 좌표
(37.4985, 126.7822)를 전송하고 있어, 모든 사용자에게 동일한 기준점에서 코스가 추천됩니다. 이는 사용자의 실제 위치 기반 추천이라는 기능 의도와 맞지 않습니다.다음 중 하나를 선택하세요:
방안 1 (권장): Geolocation API로 실제 사용자 위치 사용
방안 2: 하드코딩된 값이 의도된 것이라면 상수로 분리하고 주석 추가
🤖 Prompt for AI Agents