diff --git a/src/apis/auth/authApi.js b/src/apis/auth/authApi.js index b0fe00e..bd5e266 100644 --- a/src/apis/auth/authApi.js +++ b/src/apis/auth/authApi.js @@ -4,14 +4,12 @@ import axiosInstance from '@/apis/instance'; export const getKakaoLoginUrl = () => { const clientId = import.meta.env.VITE_KAKAO_CLIENT_ID; const redirectUri = import.meta.env.VITE_KAKAO_REDIRECT_URI; - + return `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`; }; // 카카오 액세스 토큰 받기 export const getKakaoAccessToken = async (code) => { - console.log('카카오 액세스 토큰 요청:', { code }); - const response = await fetch('https://kauth.kakao.com/oauth/token', { method: 'POST', headers: { @@ -32,27 +30,22 @@ export const getKakaoAccessToken = async (code) => { } const data = await response.json(); - console.log('카카오 액세스 토큰 발급 성공'); return data.access_token; }; -// 카카오 로그인 (액세스 토큰으로 로그인) +// 카카오 로그인 export const kakaoLogin = async (kakaoAccessToken, locationConsent = true) => { const requestData = { kakaoAccessToken: kakaoAccessToken, location_consent: locationConsent, - location_consent_version: "1.0" + location_consent_version: '1.0', }; - - console.log('카카오 로그인 요청 전체 데이터:', requestData); - console.log('액세스 토큰 길이:', kakaoAccessToken?.length); - console.log('액세스 토큰 타입:', typeof kakaoAccessToken); - console.log('location_consent 타입:', typeof locationConsent); - console.log('location_consent_version 타입:', typeof "1.0"); - + try { - const response = await axiosInstance.post('/api/auth/kakao/login', requestData); - console.log('카카오 로그인 성공:', response.data); + const response = await axiosInstance.post( + '/api/auth/kakao/login', + requestData + ); return response.data; } catch (error) { console.error('카카오 로그인 API 에러:', error); @@ -68,14 +61,12 @@ export const getGoogleLoginUrl = () => { const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID; const redirectUri = import.meta.env.VITE_GOOGLE_REDIRECT_URI; const scope = 'openid email profile'; - + return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}`; }; // 구글 액세스 토큰 받기 export const getGoogleAccessToken = async (code) => { - console.log('구글 액세스 토큰 요청:', { code }); - const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { @@ -97,22 +88,18 @@ export const getGoogleAccessToken = async (code) => { } const data = await response.json(); - console.log('구글 액세스 토큰 발급 성공'); return data.access_token; }; -// 구글 로그인 (액세스 토큰으로 로그인) -export const googleLogin = async (googleAccessToken, locationConsent = true) => { - console.log('구글 로그인 요청:', { - googleAccessToken: googleAccessToken?.substring(0, 10) + '...', - location_consent: locationConsent, - location_consent_version: "1.0" - }); - +// 구글 로그인 +export const googleLogin = async ( + googleAccessToken, + locationConsent = true +) => { const response = await axiosInstance.post('/api/auth/google/login', { googleAccessToken: googleAccessToken, location_consent: locationConsent, - location_consent_version: "1.0" + location_consent_version: '1.0', }); return response.data; }; @@ -126,7 +113,7 @@ export const logout = async () => { // 토큰 갱신 export const refreshToken = async (refreshToken) => { const response = await axiosInstance.post('/api/auth/refresh', { - refresh_token: refreshToken + refresh_token: refreshToken, }); return response.data; }; @@ -138,10 +125,13 @@ export const getUserInfo = async () => { }; // 위치정보 동의 업데이트 -export const updateLocationConsent = async (locationConsent, version = "1.0") => { +export const updateLocationConsent = async ( + locationConsent, + version = '1.0' +) => { const response = await axiosInstance.patch('/api/users/me/location-consent', { location_consent: locationConsent, - location_consent_version: version + location_consent_version: version, }); return response.data; }; @@ -156,4 +146,4 @@ export const deleteUser = async () => { export const getMyToilets = async () => { const response = await axiosInstance.get('/api/users/me/toilets'); return response.data; -}; \ No newline at end of file +}; diff --git a/src/apis/instance.js b/src/apis/instance.js index e765f53..fbd3907 100644 --- a/src/apis/instance.js +++ b/src/apis/instance.js @@ -20,26 +20,15 @@ const axiosInstance = axios.create({ headers: { 'Content-Type': 'application/json', }, - paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }), // 배열을 repeat 방식으로 직렬화 + paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }), }); -// 요청 인터셉터 (디버깅 로그 추가) +// 요청 인터셉터 axiosInstance.interceptors.request.use( (config) => { - console.log('axios 요청:', { - method: config.method?.toUpperCase(), - url: config.url, - baseURL: config.baseURL, - headers: config.headers, - params: config.params, - data: config.data, - }); - - // 인증이 필요한 요청에 토큰 추가 const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; - console.log('토큰 추가됨:', `Bearer ${token.substring(0, 10)}...`); } return config; }, @@ -52,10 +41,6 @@ axiosInstance.interceptors.request.use( // 응답 인터셉터 axiosInstance.interceptors.response.use( (response) => { - console.log('axios 응답 성공:', { - status: response.status, - data: response.data, - }); return response; }, async (error) => { @@ -67,12 +52,10 @@ axiosInstance.interceptors.response.use( const originalRequest = error.config; - // 401 에러 처리 (토큰 만료 등) if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { - // 리프레시 토큰 가져오기 const refreshToken = (() => { const nameEQ = 'refresh_token='; const ca = document.cookie.split(';'); @@ -96,19 +79,15 @@ axiosInstance.interceptors.response.use( if (refreshResponse.data.statusCode === 200) { const newAccessToken = refreshResponse.data.data.access_token; - - // 새 토큰을 쿠키에 저장 const expires = new Date(); - expires.setTime(expires.getTime() + 24 * 60 * 60 * 1000); // 1일 + expires.setTime(expires.getTime() + 24 * 60 * 60 * 1000); document.cookie = `access_token=${newAccessToken};expires=${expires.toUTCString()};path=/;SameSite=Strict;Secure`; - // 원래 요청에 새 토큰 설정 originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return axiosInstance(originalRequest); } } catch (refreshError) { console.error('토큰 갱신 실패:', refreshError); - // 리프레시 실패 시 로그아웃 처리 document.cookie = 'access_token=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;'; document.cookie = diff --git a/src/apis/register/registerApi.js b/src/apis/register/registerApi.js index 1b36d2f..8c97c39 100644 --- a/src/apis/register/registerApi.js +++ b/src/apis/register/registerApi.js @@ -14,20 +14,23 @@ export const createToilet = async (toiletData) => { * @param {FormData} imageData - 'files'를 키로 하는 이미지 파일 데이터 */ export const uploadToiletImages = async (imageData) => { - const response = await axiosInstance.post(`/api/toilets/images/upload`, imageData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + const response = await axiosInstance.post( + `/api/toilets/images/upload`, + imageData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); return response.data; }; /** * 화장실 이미지 단건 삭제 - * @param {number} imageId - 이미지 ID (파라미터명 수정) + * @param {number} imageId - 이미지 ID */ -export const deleteToiletImage = async (imageId) => { // ✅ image_id → imageId로 변경 - console.log('deleteToiletImage 호출됨, imageId:', imageId); - const response = await axiosInstance.delete(`/api/toilets/images/${imageId}`); // ✅ 변수명도 변경 +export const deleteToiletImage = async (imageId) => { + const response = await axiosInstance.delete(`/api/toilets/images/${imageId}`); return response.data; -}; \ No newline at end of file +}; diff --git a/src/components/mypage/ReviewCard.jsx b/src/components/mypage/ReviewCard.jsx index 3d00eef..d562ae0 100644 --- a/src/components/mypage/ReviewCard.jsx +++ b/src/components/mypage/ReviewCard.jsx @@ -9,12 +9,7 @@ const ReviewCard = ({ review, onDelete }) => { const renderStars = (rating) => { return Array.from({ length: 5 }, (_, index) => { const StarComponent = index < rating ? StarBlue : StarGray; - return ( - - ); + return ; }); }; @@ -23,7 +18,6 @@ const ReviewCard = ({ review, onDelete }) => { }; const handleDelete = () => { - console.log('리뷰 삭제:', review.id); if (onDelete) { onDelete(review.id); } @@ -31,16 +25,16 @@ const ReviewCard = ({ review, onDelete }) => { return (
- {/* 화장실 이름과 삭제 버튼 */} + {/* 화장실 이름 + 삭제 버튼 */}
- -
- {/* 별점과 작성자 정보 */} + {/* 프로필 + 별점 */}
@@ -62,15 +56,20 @@ const ReviewCard = ({ review, onDelete }) => {
- {/* 작성 날짜 */} + + {/* 작성 날짜 */}
{review.createdAt}
- {/* 이미지들 */} + + {/* 첨부 이미지 */} {review.images && review.images.length > 0 && (
{review.images.map((image, index) => ( -
- + {`리뷰 @@ -79,10 +78,10 @@ const ReviewCard = ({ review, onDelete }) => {
)} - {/* 리뷰 내용 */} + {/* 리뷰 본문 */}

{review.content}

- {/* 태그들 */} + {/* 태그 */}
{review.tags.map((tag, index) => (
@@ -94,4 +93,4 @@ const ReviewCard = ({ review, onDelete }) => { ); }; -export default ReviewCard; \ No newline at end of file +export default ReviewCard; diff --git a/src/components/toiletDetail/ReviewHeader.jsx b/src/components/toiletDetail/ReviewHeader.jsx index aa5bc07..93ece63 100644 --- a/src/components/toiletDetail/ReviewHeader.jsx +++ b/src/components/toiletDetail/ReviewHeader.jsx @@ -1,4 +1,3 @@ -// components/toiletDetail/ReviewHeader.jsx import { useState } from 'react'; import StarBlue from '@/assets/svg/toiletDetail/star-blue.svg?react'; import StarGray from '@/assets/svg/toiletDetail/star-gray.svg?react'; @@ -10,11 +9,10 @@ const ReviewHeader = ({ toilet, onSortChange, currentSort = 'latest' }) => { const sortOptions = [ { value: 'latest', label: '최신순' }, { value: 'rating_low', label: '별점낮은 순' }, - { value: 'rating_high', label: '별점 높은 순' } + { value: 'rating_high', label: '별점 높은 순' }, ]; const handleSortSelect = (sortValue) => { - console.log('Selected sort:', sortValue); setIsDropdownOpen(false); if (onSortChange) { onSortChange(sortValue); @@ -24,18 +22,15 @@ const ReviewHeader = ({ toilet, onSortChange, currentSort = 'latest' }) => { const renderStars = (rating) => { return Array.from({ length: 5 }, (_, index) => { const StarComponent = index < Math.floor(rating) ? StarBlue : StarGray; - return ( - - ); + return ; }); }; if (!toilet) return null; - const currentSortLabel = sortOptions.find(option => option.value === currentSort)?.label || '최신순'; + const currentSortLabel = + sortOptions.find((option) => option.value === currentSort)?.label || + '최신순'; return (
@@ -48,22 +43,29 @@ const ReviewHeader = ({ toilet, onSortChange, currentSort = 'latest' }) => {
{renderStars(toilet.rating.avg_rating)}
- {toilet.rating.avg_rating} + + {toilet.rating.avg_rating} +
- ({toilet.rating.total_reviews}건의 리뷰) + + ({toilet.rating.total_reviews}건의 리뷰) +
- + + {/* 정렬 드롭다운 */}
- + {isDropdownOpen && (
{sortOptions.map((option) => ( @@ -71,8 +73,8 @@ const ReviewHeader = ({ toilet, onSortChange, currentSort = 'latest' }) => { key={option.value} onClick={() => handleSortSelect(option.value)} className={`w-full px-4 py-3 text-left hover:bg-gray-0 transition-colors first:rounded-t-[10px] last:rounded-b-[10px] ${ - currentSort === option.value - ? 'text-main font-bold' + currentSort === option.value + ? 'text-main font-bold' : 'text-gray-7' }`} > @@ -86,4 +88,4 @@ const ReviewHeader = ({ toilet, onSortChange, currentSort = 'latest' }) => { ); }; -export default ReviewHeader; \ No newline at end of file +export default ReviewHeader; diff --git a/src/hooks/auth/useAuthApi.js b/src/hooks/auth/useAuthApi.js index dae59cd..b120a33 100644 --- a/src/hooks/auth/useAuthApi.js +++ b/src/hooks/auth/useAuthApi.js @@ -13,13 +13,14 @@ import useAuthStore from '@/stores/authStore'; // --- Helper function to set cookies --- const setCookie = (name, value, days) => { - let expires = ""; + let expires = ''; if (days) { const date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = "; expires=" + date.toUTCString(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = '; expires=' + date.toUTCString(); } - document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Strict; Secure"; + document.cookie = + name + '=' + (value || '') + expires + '; path=/; SameSite=Strict; Secure'; }; // --- Helper function to remove cookies --- @@ -27,41 +28,28 @@ const removeCookie = (name) => { document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; }; - // 카카오 로그인 mutation export const useKakaoLogin = () => { const { handleLoginSuccess } = useAuthStore(); return useMutation({ mutationFn: async ({ code, locationConsent = true }) => { - try { - console.log('1단계: 카카오 액세스 토큰 받기 시작'); - const kakaoAccessToken = await getKakaoAccessToken(code); - - console.log('2단계: 백엔드 로그인 시작'); - const result = await kakaoLogin(kakaoAccessToken, locationConsent); - - return result; - } catch (error) { - console.error('카카오 로그인 과정에서 에러 발생:', error); - throw error; - } + const kakaoAccessToken = await getKakaoAccessToken(code); + const result = await kakaoLogin(kakaoAccessToken, locationConsent); + return result; }, onSuccess: async (data) => { - console.log('로그인 API 응답:', data); if (data && data.data) { const { access_token, refresh_token } = data.data; - // ** MODIFIED: Store tokens in cookies ** - setCookie('access_token', access_token, 1); // 1 day expiry - setCookie('refresh_token', refresh_token, 7); // 7 day expiry + setCookie('access_token', access_token, 1); + setCookie('refresh_token', refresh_token, 7); } const success = await handleLoginSuccess(data); if (success) { window.location.href = '/'; } }, - onError: (error) => { - console.error('카카오 로그인 실패:', error); + onError: () => { alert('로그인에 실패했습니다. 다시 시도해주세요.'); window.location.href = '/login'; }, @@ -74,22 +62,13 @@ export const useGoogleLogin = () => { return useMutation({ mutationFn: async ({ code, locationConsent = true }) => { - try { - console.log('1단계: 구글 액세스 토큰 받기 시작'); - const googleAccessToken = await getGoogleAccessToken(code); - - console.log('2단계: 백엔드 로그인 시작'); - const result = await googleLogin(googleAccessToken, locationConsent); - return result; - } catch (error) { - console.error('구글 로그인 과정에서 에러 발생:', error); - throw error; - } + const googleAccessToken = await getGoogleAccessToken(code); + const result = await googleLogin(googleAccessToken, locationConsent); + return result; }, onSuccess: async (data) => { if (data && data.data) { const { access_token, refresh_token } = data.data; - // ** MODIFIED: Store tokens in cookies ** setCookie('access_token', access_token, 1); setCookie('refresh_token', refresh_token, 7); } @@ -98,8 +77,7 @@ export const useGoogleLogin = () => { window.location.href = '/'; } }, - onError: (error) => { - console.error('구글 로그인 실패:', error); + onError: () => { alert('로그인에 실패했습니다. 다시 시도해주세요.'); window.location.href = '/login'; }, @@ -113,15 +91,12 @@ export const useLogout = () => { return useMutation({ mutationFn: logout, onSuccess: async () => { - // ** MODIFIED: Remove tokens from cookies ** removeCookie('access_token'); removeCookie('refresh_token'); await storeLogout(); window.location.href = '/login'; }, - onError: (error) => { - console.error('로그아웃 실패:', error); - // Even if API fails, clear client-side auth + onError: () => { removeCookie('access_token'); removeCookie('refresh_token'); storeLogout(); @@ -149,12 +124,6 @@ export const useUpdateLocationConsent = () => { return useMutation({ mutationFn: ({ locationConsent, version }) => updateLocationConsent(locationConsent, version), - onSuccess: (data) => { - console.log('위치정보 동의 업데이트 성공:', data); - }, - onError: (error) => { - console.error('위치정보 동의 업데이트 실패:', error); - }, }); }; @@ -171,9 +140,8 @@ export const useDeleteUser = () => { alert('회원 탈퇴가 완료되었습니다.'); window.location.href = '/'; }, - onError: (error) => { - console.error('회원 탈퇴 실패:', error); + onError: () => { alert('회원 탈퇴 중 오류가 발생했습니다.'); }, }); -}; \ No newline at end of file +}; diff --git a/src/hooks/register/useRegisterApi.js b/src/hooks/register/useRegisterApi.js index 2a778a9..2fede27 100644 --- a/src/hooks/register/useRegisterApi.js +++ b/src/hooks/register/useRegisterApi.js @@ -1,5 +1,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { createToilet, uploadToiletImages, deleteToiletImage } from '@/apis/register/registerApi'; +import { + createToilet, + uploadToiletImages, + deleteToiletImage, +} from '@/apis/register/registerApi'; /** * 화장실 등록 mutation @@ -9,13 +13,12 @@ export const useCreateToilet = () => { return useMutation({ mutationFn: createToilet, - onSuccess: (data) => { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['mapMarkers'] }); queryClient.invalidateQueries({ queryKey: ['nearbyToilets'] }); - console.log('화장실 등록 성공:', data); }, - onError: (error) => { - console.error('화장실 등록 실패:', error); + onError: () => { + alert('화장실 등록에 실패했습니다.'); }, }); }; @@ -26,11 +29,7 @@ export const useCreateToilet = () => { export const useUploadToiletImages = () => { return useMutation({ mutationFn: (imageData) => uploadToiletImages(imageData), - onSuccess: (data) => { - console.log('화장실 이미지 업로드 성공:', data); - }, - onError: (error) => { - console.error('화장실 이미지 업로드 실패:', error); + onError: () => { alert('이미지 업로드에 실패했습니다. 파일 크기나 형식을 확인해주세요.'); }, }); @@ -41,14 +40,9 @@ export const useUploadToiletImages = () => { */ export const useDeleteToiletImage = () => { return useMutation({ - // ✨ 수정된 부분: 파라미터 이름을 imageId로 통일 mutationFn: (imageId) => deleteToiletImage(imageId), - onSuccess: (data) => { - console.log('화장실 이미지 삭제 성공:', data); - }, - onError: (error) => { - console.error('화장실 이미지 삭제 실패:', error); + onError: () => { alert('이미지 삭제에 실패했습니다.'); }, }); -}; \ No newline at end of file +}; diff --git a/src/hooks/review/useReviewApi.js b/src/hooks/review/useReviewApi.js index d43a67a..0457c56 100644 --- a/src/hooks/review/useReviewApi.js +++ b/src/hooks/review/useReviewApi.js @@ -1,6 +1,5 @@ /** * src/hooks/review/useReviewApi.js - * 리뷰 관련 API를 호출하는 react-query 훅을 관리합니다. */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import * as reviewApi from '@/apis/review/reviewApi'; @@ -12,29 +11,30 @@ export const useUploadReviewImages = () => { return useMutation({ mutationFn: reviewApi.uploadReviewImages, onError: (error) => { - console.error("Image upload failed:", error); - alert("이미지 업로드에 실패했습니다. 파일 크기(최대 10MB)나 형식을 확인해주세요."); - } + console.error('Image upload failed:', error); + alert( + '이미지 업로드에 실패했습니다. 파일 크기(최대 10MB)나 형식을 확인해주세요.' + ); + }, }); }; /** - * 리뷰 작성용 태그 목록을 조회하는 쿼리 훅 + * 리뷰 작성용 태그 목록 조회 */ export const useReviewTags = (options = {}) => useQuery({ queryKey: ['reviewTags'], queryFn: async () => { const response = await reviewApi.getReviewTags(); - return response.data.data; // 실제 태그 배열 반환 + return response.data.data; }, - staleTime: 5 * 60 * 1000, // 5분간 신선한 데이터로 간주 + staleTime: 5 * 60 * 1000, ...options, }); /** - * 리뷰를 생성하는 뮤테이션 훅 - * @param {number} toiletId - 리뷰를 작성할 화장실 ID + * 리뷰 생성 */ export const useCreateReview = (toiletId) => { const queryClient = useQueryClient(); @@ -45,24 +45,22 @@ export const useCreateReview = (toiletId) => { queryClient.invalidateQueries({ queryKey: ['toilet', toiletId] }); }, onError: (error) => { - console.error("Failed to create review:", error); - alert("리뷰 등록 중 오류가 발생했습니다."); - } + console.error('Failed to create review:', error); + alert('리뷰 등록 중 오류가 발생했습니다.'); + }, }); }; /** - * ✨ 추가된 부분: 리뷰 이미지 삭제를 위한 뮤테이션 훅 + * 리뷰 이미지 삭제 */ export const useDeleteReviewImage = () => { return useMutation({ mutationFn: (imageUrl) => reviewApi.deleteReviewImage(imageUrl), - onSuccess: () => { - console.log('리뷰 이미지 삭제 성공'); - }, + onSuccess: () => {}, onError: (error) => { - console.error("Failed to delete review image:", error); - alert("이미지 삭제 중 오류가 발생했습니다."); - } + console.error('Failed to delete review image:', error); + alert('이미지 삭제 중 오류가 발생했습니다.'); + }, }); -}; \ No newline at end of file +}; diff --git a/src/hooks/toilet/useToiletApi.js b/src/hooks/toilet/useToiletApi.js index e91f141..a9d7d4a 100644 --- a/src/hooks/toilet/useToiletApi.js +++ b/src/hooks/toilet/useToiletApi.js @@ -1,27 +1,27 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { - getToiletDetail, - getToiletReviews, - getToiletRating, - getToiletReviewCount, +import { + getToiletDetail, + getToiletReviews, + getToiletRating, + getToiletReviewCount, getToiletTopTags, getToiletRatingDistribution, createToilet, updateToilet, uploadToiletImages, deleteToiletImage, - deleteAllToiletImages + deleteAllToiletImages, } from '@/apis/toilet/toiletApi'; /** - * 화장실 상세 정보 조회 query + * 화장실 상세 정보 조회 */ export const useToiletDetail = (toiletId) => { return useQuery({ queryKey: ['toilet', toiletId], queryFn: () => getToiletDetail(toiletId), enabled: !!toiletId, - staleTime: 5 * 60 * 1000, // 5분 + staleTime: 5 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 상세 정보 조회 실패:', error); @@ -30,14 +30,14 @@ export const useToiletDetail = (toiletId) => { }; /** - * 화장실 리뷰 목록 조회 query + * 화장실 리뷰 목록 조회 */ export const useToiletReviews = (toiletId, params = {}) => { return useQuery({ queryKey: ['toiletReviews', toiletId, params], queryFn: () => getToiletReviews(toiletId, params), enabled: !!toiletId, - staleTime: 3 * 60 * 1000, // 3분 + staleTime: 3 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 리뷰 조회 실패:', error); @@ -46,14 +46,14 @@ export const useToiletReviews = (toiletId, params = {}) => { }; /** - * 화장실 평균 별점 조회 query + * 화장실 평균 별점 조회 */ export const useToiletRating = (toiletId) => { return useQuery({ queryKey: ['toiletRating', toiletId], queryFn: () => getToiletRating(toiletId), enabled: !!toiletId, - staleTime: 5 * 60 * 1000, // 5분 + staleTime: 5 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 평균 별점 조회 실패:', error); @@ -62,14 +62,14 @@ export const useToiletRating = (toiletId) => { }; /** - * 화장실 리뷰 개수 조회 query + * 화장실 리뷰 개수 조회 */ export const useToiletReviewCount = (toiletId) => { return useQuery({ queryKey: ['toiletReviewCount', toiletId], queryFn: () => getToiletReviewCount(toiletId), enabled: !!toiletId, - staleTime: 5 * 60 * 1000, // 5분 + staleTime: 5 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 리뷰 개수 조회 실패:', error); @@ -78,14 +78,14 @@ export const useToiletReviewCount = (toiletId) => { }; /** - * 화장실 인기 태그 TOP 3 조회 query + * 화장실 인기 태그 조회 */ export const useToiletTopTags = (toiletId) => { return useQuery({ queryKey: ['toiletTopTags', toiletId], queryFn: () => getToiletTopTags(toiletId), enabled: !!toiletId, - staleTime: 10 * 60 * 1000, // 10분 + staleTime: 10 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 인기 태그 조회 실패:', error); @@ -94,14 +94,14 @@ export const useToiletTopTags = (toiletId) => { }; /** - * 화장실 평점 분포 조회 query + * 화장실 평점 분포 조회 */ export const useToiletRatingDistribution = (toiletId) => { return useQuery({ queryKey: ['toiletRatingDistribution', toiletId], queryFn: () => getToiletRatingDistribution(toiletId), enabled: !!toiletId, - staleTime: 5 * 60 * 1000, // 5분 + staleTime: 5 * 60 * 1000, select: (data) => data.data, onError: (error) => { console.error('화장실 평점 분포 조회 실패:', error); @@ -117,11 +117,9 @@ export const useCreateToilet = () => { return useMutation({ mutationFn: createToilet, - onSuccess: (data) => { - // 지도 마커 목록 새로고침 + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['mapMarkers'] }); queryClient.invalidateQueries({ queryKey: ['nearbyToilets'] }); - console.log('화장실 등록 성공:', data); }, onError: (error) => { console.error('화장실 등록 실패:', error); @@ -136,12 +134,13 @@ export const useUpdateToilet = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ toiletId, toiletData }) => updateToilet(toiletId, toiletData), - onSuccess: (data, variables) => { - // 해당 화장실 정보 새로고침 - queryClient.invalidateQueries({ queryKey: ['toilet', variables.toiletId] }); + mutationFn: ({ toiletId, toiletData }) => + updateToilet(toiletId, toiletData), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: ['toilet', variables.toiletId], + }); queryClient.invalidateQueries({ queryKey: ['mapMarkers'] }); - console.log('화장실 정보 수정 성공:', data); }, onError: (error) => { console.error('화장실 정보 수정 실패:', error); @@ -156,11 +155,12 @@ export const useUploadToiletImages = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ toiletId, imageData }) => uploadToiletImages(toiletId, imageData), - onSuccess: (data, variables) => { - // 해당 화장실 정보 새로고침 - queryClient.invalidateQueries({ queryKey: ['toilet', variables.toiletId] }); - console.log('화장실 이미지 업로드 성공:', data); + mutationFn: ({ toiletId, imageData }) => + uploadToiletImages(toiletId, imageData), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: ['toilet', variables.toiletId], + }); }, onError: (error) => { console.error('화장실 이미지 업로드 실패:', error); @@ -176,13 +176,13 @@ export const useDeleteToiletImage = () => { return useMutation({ mutationFn: ({ toiletId, imageId }) => deleteToiletImage(toiletId, imageId), - onSuccess: (data, variables) => { - // 해당 화장실 정보 새로고침 - queryClient.invalidateQueries({ queryKey: ['toilet', variables.toiletId] }); - console.log('화장실 이미지 삭제 성공:', data); + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ + queryKey: ['toilet', variables.toiletId], + }); }, onError: (error) => { console.error('화장실 이미지 삭제 실패:', error); }, }); -}; \ No newline at end of file +}; diff --git a/src/pages/aiChatBot/AiChatbot.jsx b/src/pages/aiChatBot/AiChatbot.jsx index 82fe69d..8a89b63 100644 --- a/src/pages/aiChatBot/AiChatbot.jsx +++ b/src/pages/aiChatBot/AiChatbot.jsx @@ -20,7 +20,6 @@ export default function AiChatbot({ onClose }) { useEffect(() => { const fetchHistory = async () => { try { - console.log('[Chatbot] 대화 내역 조회 요청...'); const res = await getChatHistory(1, 20); if (res.data?.statusCode === 200) { @@ -30,11 +29,8 @@ export default function AiChatbot({ onClose }) { { id: c.id, sender: 'user', text: c.question }, { id: c.id, sender: 'bot', text: c.answer }, ]); - // 웰컴 메시지 유지 + history 이어붙이기 setMessages((prev) => [...prev, ...history]); } - } else { - console.warn('[Chatbot] 대화 내역 조회 실패:', res.data); } } catch (err) { console.error('[Chatbot] 대화 내역 조회 에러:', err); @@ -43,7 +39,7 @@ export default function AiChatbot({ onClose }) { fetchHistory(); }, []); - // 질문하기 (수정됨) + // 질문하기 const handleSend = async () => { if (!input.trim()) return; const userText = input.trim(); @@ -82,8 +78,6 @@ export default function AiChatbot({ onClose }) { const res = await deleteChat(chatId); if (res.data?.statusCode === 200) { setMessages((prev) => prev.filter((m) => m.id !== chatId)); - } else { - console.warn('[Chatbot] 삭제 실패:', res.data); } } catch (err) { console.error('[Chatbot] 삭제 에러:', err); @@ -149,7 +143,7 @@ export default function AiChatbot({ onClose }) {
)} - {/* 삭제 버튼 (API 응답 메시지일 때만) */} + {/* 삭제 버튼 */} {msg.id && (
handleImageRemove(id)} @@ -170,4 +194,4 @@ const ReviewToilet = () => { ); }; -export default ReviewToilet; \ No newline at end of file +export default ReviewToilet; diff --git a/src/pages/toiletDetail/ToiletDetailPage.jsx b/src/pages/toiletDetail/ToiletDetailPage.jsx index 2b281b5..97af2e7 100644 --- a/src/pages/toiletDetail/ToiletDetailPage.jsx +++ b/src/pages/toiletDetail/ToiletDetailPage.jsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { - useToiletDetail, - useToiletReviews, - useToiletRatingDistribution +import { + useToiletDetail, + useToiletReviews, + useToiletRatingDistribution, } from '@/hooks/toilet/useToiletApi'; import ToiletHeader from '@/components/toiletDetail/ToiletHeader'; import ToiletLocation from '@/components/toiletDetail/ToiletLocation'; @@ -12,59 +12,72 @@ import ReviewSection from '@/components/toiletDetail/ReviewSection'; const ToiletDetailPage = () => { const { id } = useParams(); - console.log('페이지에 전달된 toilet ID:', id); const [currentSort, setCurrentSort] = useState('latest'); const [currentPage, setCurrentPage] = useState(1); // API 데이터 조회 - const { data: toiletData, isLoading: toiletLoading, error: toiletError } = useToiletDetail(id); - const { data: reviewsData, isLoading: reviewsLoading } = useToiletReviews(id, { - page: currentPage, - size: 4, - sort: currentSort - }); - const { data: ratingDistributionData } = useToiletRatingDistribution(id); - - // 컴포넌트에 전달할 데이터 가공 - const currentToilet = toiletData ? { - ...toiletData, - rating: { - avg_rating: toiletData.rating?.avgRating || 0, - total_reviews: ratingDistributionData?.totalReviews || toiletData.rating?.totalReviews || 0 + const { + data: toiletData, + isLoading: toiletLoading, + error: toiletError, + } = useToiletDetail(id); + const { data: reviewsData, isLoading: reviewsLoading } = useToiletReviews( + id, + { + page: currentPage, + size: 4, + sort: currentSort, } - } : null; + ); + const { data: ratingDistributionData } = useToiletRatingDistribution(id); - const reviews = reviewsData?.reviews?.map(review => ({ - id: review.reviewId, - user: { name: review.userName }, - rating: review.rating, - content: review.content, - created_at: new Date(review.createdAt).toLocaleDateString(), - tags: review.tags?.map(tag => tag.tagName) || [], - images: review.imageUrls || [] - })) || []; - - const ratingDistribution = ratingDistributionData?.distribution?.map(item => { - const maxCount = Math.max(...(ratingDistributionData.distribution.map(d => d.count) || [0])); + // 가공 데이터 + const currentToilet = toiletData + ? { + ...toiletData, + rating: { + avg_rating: toiletData.rating?.avgRating || 0, + total_reviews: + ratingDistributionData?.totalReviews || + toiletData.rating?.totalReviews || + 0, + }, + } + : null; + + const reviews = + reviewsData?.reviews?.map((review) => ({ + id: review.reviewId, + user: { name: review.userName }, + rating: review.rating, + content: review.content, + created_at: new Date(review.createdAt).toLocaleDateString(), + tags: review.tags?.map((tag) => tag.tagName) || [], + images: review.imageUrls || [], + })) || []; + + const ratingDistribution = + ratingDistributionData?.distribution?.map((item) => { + const maxCount = Math.max( + ...(ratingDistributionData.distribution.map((d) => d.count) || [0]) + ); return { - ...item, - barWidth: maxCount > 0 ? Math.round((item.count / maxCount) * 217) : 0, + ...item, + barWidth: maxCount > 0 ? Math.round((item.count / maxCount) * 217) : 0, }; - }) || []; - - - const pagination = reviewsData ? { - page: reviewsData.currentPage, - size: 4, - total: reviewsData.totalElements, - total_pages: reviewsData.totalPages, - } : null; - - // 페이지 및 정렬 변경 핸들러 - const handlePageChange = (page) => { - setCurrentPage(page); - }; - + }) || []; + + const pagination = reviewsData + ? { + page: reviewsData.currentPage, + size: 4, + total: reviewsData.totalElements, + total_pages: reviewsData.totalPages, + } + : null; + + // 이벤트 핸들러 + const handlePageChange = (page) => setCurrentPage(page); const handleSortChange = (sortType) => { setCurrentSort(sortType); setCurrentPage(1); @@ -82,7 +95,9 @@ const ToiletDetailPage = () => { if (toiletError) { return (
-
오류가 발생했습니다: {toiletError.message}
+
+ 오류가 발생했습니다: {toiletError.message} +
); } @@ -90,7 +105,9 @@ const ToiletDetailPage = () => { if (!currentToilet) { return (
-
화장실 정보를 찾을 수 없습니다.
+
+ 화장실 정보를 찾을 수 없습니다. +
); } @@ -112,13 +129,13 @@ const ToiletDetailPage = () => {
- diff --git a/src/stores/toiletStore.js b/src/stores/toiletStore.js index 444cc47..0e2efa1 100644 --- a/src/stores/toiletStore.js +++ b/src/stores/toiletStore.js @@ -1,11 +1,15 @@ import { create } from 'zustand'; -import { mockToiletData, mockReviewsData, calculateRatingDistribution } from '@/mocks/toiletData'; +import { + mockToiletData, + mockReviewsData, + calculateRatingDistribution, +} from '@/mocks/toiletData'; const useToiletStore = create((set, get) => ({ currentToilet: null, reviews: [], allReviews: [], - baseAllReviews: [], // 원본 보관 + baseAllReviews: [], ratingDistribution: [], pagination: null, currentSort: 'latest', @@ -16,14 +20,16 @@ const useToiletStore = create((set, get) => ({ set({ isLoading: true, error: null }); try { await new Promise((resolve) => setTimeout(resolve, 500)); - const distribution = calculateRatingDistribution(mockReviewsData.allReviews); + const distribution = calculateRatingDistribution( + mockReviewsData.allReviews + ); set({ currentToilet: mockToiletData, baseAllReviews: mockReviewsData.allReviews, allReviews: mockReviewsData.allReviews .slice() - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), // ✅ 초기 최신순 = createdAt 기준 + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), ratingDistribution: distribution, isLoading: false, }); @@ -39,20 +45,20 @@ const useToiletStore = create((set, get) => ({ let sorted = baseAllReviews.slice(); switch (sortType) { case 'latest': - sorted.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); // ✅ 날짜 기준 최신순 + sorted.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); break; case 'rating_high': sorted.sort((a, b) => { const ratingDiff = (b.rating || 0) - (a.rating || 0); if (ratingDiff !== 0) return ratingDiff; - return new Date(b.createdAt) - new Date(a.createdAt); // 동점 시 최신순 + return new Date(b.createdAt) - new Date(a.createdAt); }); break; case 'rating_low': sorted.sort((a, b) => { const ratingDiff = (a.rating || 0) - (b.rating || 0); if (ratingDiff !== 0) return ratingDiff; - return new Date(b.createdAt) - new Date(a.createdAt); // 동점 시 최신순 + return new Date(b.createdAt) - new Date(a.createdAt); }); break; default: @@ -64,7 +70,6 @@ const useToiletStore = create((set, get) => ({ }, fetchReviews: async (toiletId, page = 1, size = 4) => { - console.log('fetchReviews called - toiletId:', toiletId, 'page:', page, 'size:', size); set({ isLoading: true, error: null }); try { await new Promise((resolve) => setTimeout(resolve, 300)); diff --git a/src/utils/locationUtils.js b/src/utils/locationUtils.js index de83a10..367e147 100644 --- a/src/utils/locationUtils.js +++ b/src/utils/locationUtils.js @@ -1,15 +1,12 @@ -/** - * 브라우저에서 사용자의 현재 위치를 가져오는 유틸리티 함수들 - */ - // 기본 위치 (홍대입구역) const DEFAULT_LOCATION = { latitude: 37.5563, - longitude: 126.9236 + longitude: 126.9236, }; /** - * 사용자의 현재 위치를 가져옵니다 + * 사용자의 현재 위치를 가져옴 + * Geolocation API 사용, 실패 시 기본 위치 반환. * @returns {Promise<{latitude: number, longitude: number}>} */ export const getCurrentLocation = () => { @@ -23,22 +20,21 @@ export const getCurrentLocation = () => { const options = { enableHighAccuracy: true, // 높은 정확도 요청 timeout: 10000, // 10초 타임아웃 - maximumAge: 300000 // 5분간 캐시된 위치 사용 + maximumAge: 300000, // 5분간 캐시된 위치 사용 }; navigator.geolocation.getCurrentPosition( (position) => { const location = { latitude: position.coords.latitude, - longitude: position.coords.longitude + longitude: position.coords.longitude, }; - console.log('현재 위치 조회 성공:', location); resolve(location); }, (error) => { console.warn('위치 조회 실패:', error.message); - - // 사용자가 위치 권한을 거부했거나 오류 발생 시 기본 위치 사용 + + // 오류 코드에 따른 처리 switch (error.code) { case error.PERMISSION_DENIED: console.warn('사용자가 위치 정보 제공을 거부했습니다.'); @@ -53,7 +49,7 @@ export const getCurrentLocation = () => { console.warn('알 수 없는 오류가 발생했습니다.'); break; } - + // 에러 발생 시에도 기본 위치 반환 resolve(DEFAULT_LOCATION); }, @@ -63,8 +59,8 @@ export const getCurrentLocation = () => { }; /** - * 위치 권한 상태를 확인합니다 - * @returns {Promise} 'granted', 'denied', 'prompt', 'unsupported' + * 위치 권한 상태를 확인 + * @returns {Promise} 'granted' | 'denied' | 'prompt' | 'unsupported' */ export const checkLocationPermission = async () => { if (!navigator.permissions) { @@ -73,7 +69,7 @@ export const checkLocationPermission = async () => { try { const result = await navigator.permissions.query({ name: 'geolocation' }); - return result.state; // 'granted', 'denied', 'prompt' + return result.state; } catch (error) { console.warn('위치 권한 확인 실패:', error); return 'unsupported'; @@ -81,20 +77,20 @@ export const checkLocationPermission = async () => { }; /** - * 위치 권한 요청 및 위치 조회 - * @param {Function} onSuccess - 위치 조회 성공 시 콜백 - * @param {Function} onError - 위치 조회 실패 시 콜백 + * 위치 권한 요청 및 현재 위치 조회 + * @param {Function} onSuccess - 위치 조회 성공 시 실행할 콜백 + * @param {Function} onError - 위치 조회 실패 시 실행할 콜백 */ export const requestLocationWithPermission = async (onSuccess, onError) => { try { const permission = await checkLocationPermission(); - + if (permission === 'denied') { const useDefault = confirm( '위치 정보 접근이 차단되어 있습니다. 기본 위치(홍대입구역)를 사용하시겠습니까?\n\n' + - '더 정확한 서비스를 위해 브라우저 설정에서 위치 권한을 허용해주세요.' + '더 정확한 서비스를 위해 브라우저 설정에서 위치 권한을 허용해주세요.' ); - + if (useDefault) { onSuccess(DEFAULT_LOCATION); } else { @@ -106,7 +102,6 @@ export const requestLocationWithPermission = async (onSuccess, onError) => { // 위치 조회 const location = await getCurrentLocation(); onSuccess(location); - } catch (error) { console.error('위치 조회 중 오류:', error); onError(error); @@ -114,7 +109,7 @@ export const requestLocationWithPermission = async (onSuccess, onError) => { }; /** - * 두 지점 간의 거리를 계산합니다 (Haversine formula) + * 두 지점 간의 거리를 계산 (Haversine formula) * @param {number} lat1 - 첫 번째 지점의 위도 * @param {number} lon1 - 첫 번째 지점의 경도 * @param {number} lat2 - 두 번째 지점의 위도 @@ -122,15 +117,17 @@ export const requestLocationWithPermission = async (onSuccess, onError) => { * @returns {number} 거리 (미터 단위) */ export const calculateDistance = (lat1, lon1, lat2, lon2) => { - const R = 6371000; // 지구의 반지름 (미터) - const dLat = (lat2 - lat1) * Math.PI / 180; - const dLon = (lon2 - lon1) * Math.PI / 180; - const a = - Math.sin(dLat/2) * Math.sin(dLat/2) + - Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * - Math.sin(dLon/2) * Math.sin(dLon/2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + const R = 6371000; // 지구 반지름 (미터) + const dLat = ((lat2 - lat1) * Math.PI) / 180; + const dLon = ((lon2 - lon1) * Math.PI) / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((lat1 * Math.PI) / 180) * + Math.cos((lat2 * Math.PI) / 180) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; - + return Math.round(distance); -}; \ No newline at end of file +};