하루 하나씩 작성하는 TIL #45
작성된 댓글을 마이페이지에 로드하고, api및 라우터를 설정하였다.
import React, { useState, useEffect, useContext } from 'react';
import SavedShopsList from './SavedShopsList.jsx';
import ReviewsList from './ReviewsList.jsx';
import { getUserInfo } from '../../api/auth';
import { AuthContext } from '../../contexts/AuthContext';
const MyPage = () => {
const { userInfo } = useContext(AuthContext);
const [nickname, setNickname] = useState('');
const [userId, setUserId] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchNickname = async () => {
try {
if (userInfo) {
const userId = userInfo.id;
const user = await getUserInfo(userId);
if (user) {
setNickname(user.nickname);
setUserId(userId);
} else {
console.error('유저 정보를 불러오는데 실패했습니다.');
}
}
} catch (error) {
console.error('닉네임을 불러오는데에 실패했습니다.', error.message);
} finally {
setLoading(false);
}
};
fetchNickname();
}, [userInfo]);
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="w-full min-h-screen flex flex-col items-center">
<div className="container bg-[#E5E1EF] p-4 rounded-lg shadow mt-16 mb-20 w-3/4">
<h1 className="text-3xl font-bold mb-10">💜{nickname || 'Loading...'}님의 페이지💜</h1>
<div className="my-4">
<h2 className="text-xl font-semibold mb-4">
💜{nickname || 'Loading...'}💜님의 찜 목록
</h2>
<div className="p-4 bg-gray-100 rounded-lg shadow min-h-40 mb-8">
<SavedShopsList userId={userId} />
</div>
</div>
<div className="my-4">
<h2 className="text-xl font-semibold mb-4">
💜{nickname || 'Loading...'}💜님이 작성한 리뷰
</h2>
<div className="p-4 bg-gray-100 rounded-lg shadow min-h-40 mb-4">
<ReviewsList userId={userId} />
</div>
</div>
</div>
</div>
);
};
export default MyPage;
import React, { createContext, useState, useEffect } from 'react';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
const storedUserInfo = localStorage.getItem('userInfo');
if (storedUserInfo) {
setIsAuthenticated(true);
setUserInfo(JSON.parse(storedUserInfo));
}
}, []);
const login = (userInfo) => {
localStorage.setItem('userInfo', JSON.stringify(userInfo));
setIsAuthenticated(true);
setUserInfo(userInfo);
};
const logout = () => {
localStorage.removeItem('userInfo');
setIsAuthenticated(false);
setUserInfo(null);
};
return (
<AuthContext.Provider value={{ isAuthenticated, userInfo, login, logout }}>
{children}
</AuthContext.Provider>
);
};
import React, { useContext, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../contexts/AuthContext';
import Swal from 'sweetalert2';
const PrivateRoute = ({ children }) => {
const { isAuthenticated } = useContext(AuthContext);
const [showAlert, setShowAlert] = useState(false);
useEffect(() => {
if (!isAuthenticated) {
Swal.fire('로그인되지 않은 사용자입니다!', '로그인부터 해주세요', 'warning').then(() => {
setShowAlert(true);
});
}
}, [isAuthenticated]);
if (showAlert) {
return <Navigate to="/login" />;
}
return isAuthenticated ? children : null;
};
export default PrivateRoute;
import React, { useEffect, useState, useContext } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { PiHeart, PiHeartFill } from 'react-icons/pi';
import { useLocation, useParams } from 'react-router-dom';
import Swal from 'sweetalert2';
import { addLike, deleteLike, isLikedShop } from '../../api/like';
import { addReview, deleteReview, getShopReviewsByShopId, modifyReview } from '../../api/review';
import mainIcon from '../../assets/mainIcon.png';
import { AuthContext } from '../../contexts/AuthContext';
const DetailPage = () => {
const [isLiked, setIsLiked] = useState(false);
const [userId, setUserId] = useState('');
const [newReview, setNewReview] = useState('');
const { shopId } = useParams();
const location = useLocation();
const queryClient = useQueryClient();
const { isAuthenticated, userInfo } = useContext(AuthContext);
const { place_name: shop_name, road_address_name } = location.state;
useEffect(() => {
if (userInfo) {
setUserId(userInfo.id);
}
}, [userInfo]);
useEffect(() => {
(async () => {
if (userId && shopId) {
const likedStatus = await isLikedShop({ userId, shopId });
setIsLiked(likedStatus);
}
})();
}, [userId, shopId]);
const addLikeMutation = useMutation({
mutationFn: (data) => addLike(data),
onSuccess: () => setIsLiked(true)
});
const deleteLikeMutation = useMutation({
mutationFn: (data) => deleteLike(data),
onSuccess: () => setIsLiked(false)
});
const { data: reviews } = useQuery({
queryKey: ['reviews', shopId],
queryFn: () => getShopReviewsByShopId(shopId),
enabled: !!shopId
});
const addReviewMutation = useMutation({
mutationFn: (data) => addReview(data),
onSuccess: () => {
queryClient.invalidateQueries(['reviews', shopId]);
setNewReview('');
}
});
const updateReviewMutation = useMutation({
mutationFn: (data) => modifyReview(data),
onSuccess: () => queryClient.invalidateQueries(['reviews', shopId])
});
const deleteReviewMutation = useMutation({
mutationFn: (data) => deleteReview(data),
onSuccess: () => queryClient.invalidateQueries(['reviews', shopId])
});
const handleLike = async () => {
if (!isAuthenticated) {
Swal.fire('Error', '해당 기능은 로그인 후 이용 가능합니다.', 'error');
return;
}
if (isLiked) await deleteLikeMutation.mutateAsync({ userId, shopId });
else await addLikeMutation.mutateAsync({ userId, shopId, shop_name });
};
const handleAddReview = async () => {
if (!newReview.trim()) {
Swal.fire('Error', '입력 후 등록해주세요.', 'error');
return;
}
await addReviewMutation.mutateAsync({ userId, shopId, content: newReview });
};
const handleUpdateReview = async (reviewId, newContent) => {
await updateReviewMutation.mutateAsync({ reviewId, content: newContent });
};
const handleDeleteReview = async (reviewId) => {
await deleteReviewMutation.mutateAsync(reviewId);
};
return (
<div className="flex min-h-screen justify-center">
<main className="w-full max-w-[1320px] p-4">
<div className="mt-[20px] flex flex-col gap-6 rounded-lg bg-hover p-[48px_36px_30px_36px] shadow-lg md:flex-row">
<img src={mainIcon} alt="Store" className="h-64 w-full rounded-lg bg-white object-cover shadow-md md:w-1/2" />
<div className="relative flex w-full flex-col rounded-lg bg-white p-6 shadow-md md:w-1/2">
<h1 className="text-3xl font-bold">{shop_name}</h1>
<p className="mt-2 text-gray-700">{road_address_name}</p>
<button onClick={handleLike} className="absolute bottom-4 right-4 text-3xl text-point">
{isLiked ? <PiHeartFill /> : <PiHeart />}
</button>
</div>
</div>
<div className="mt-6 rounded-lg bg-hover p-[48px_36px_30px_36px] shadow-md">
<section className="rounded-lg bg-white p-4 shadow-md">
<div className="flex items-center space-x-[22px]">
<textarea
placeholder="리뷰를 남겨주세요"
className="h-[70px] w-full resize-none rounded-lg border p-2 focus:outline-active"
value={newReview}
onChange={(e) => setNewReview(e.target.value)}
/>
<button onClick={handleAddReview} className="h-[35px] w-[76px] rounded-lg bg-point text-white">
등록
</button>
</div>
</section>
<section className="mt-6 space-y-[30px]">
{reviews?.map((review) => (
<div key={review.id} className="rounded-lg bg-white p-4 shadow-md">
<div className="flex items-center justify-between">
<div>
<p className="font-bold">{review.user_name}</p>
<p>{review.content}</p>
</div>
{userId === review.user_id && (
<div className="flex space-x-[22px]">
<button
onClick={() => handleUpdateReview(review.id, prompt('리뷰를 수정하세요:', review.content))}
className="h-[35px] w-[76px] rounded-lg bg-point text-white"
>
수정
</button>
<button
onClick={() => handleDeleteReview(review.id)}
className="h-[35px] w-[76px] rounded-lg bg-main text-white"
>
삭제
</button>
</div>
)}
</div>
</div>
))}
</section>
</div>
</main>
</div>
);
};
export default DetailPage;
import supabase from '../supabase/supabaseClient';
import Swal from 'sweetalert2';
export const isLikedShop = async ({ userId, shopId }) => {
try {
const { data, error } = await supabase
.from('likes')
.select('*')
.eq('user_id', userId)
.eq('kakao_shop_id', shopId);
if (error) throw error;
return data.length ? true : false;
} catch {
Swal.fire('Error', '좋아요를 가져오는 중 오류가 발생하였습니다', 'error');
}
};
export const addLike = async ({ userId, shopId, shop_name }) => {
try {
const { error } = await supabase.from('likes').insert({
user_id: userId,
kakao_shop_id: shopId,
shop_name
});
if (error) throw error;
Swal.fire('Success', '좋아요를 눌렀습니다!', 'success');
} catch (err) {
Swal.fire('Error', '좋아요 과정 중 오류가 발생하였습니다', 'error');
}
};
export const deleteLike = async ({ userId, shopId }) => {
try {
const { error } = await supabase.from('likes').delete().eq('user_id', userId).eq('kakao_shop_id', shopId);
if (error) throw error;
Swal.fire('Success', '좋아요를 취소하였습니다', 'success');
} catch (err) {
Swal.fire('Error', '좋아요를 취소중 오류가 발생하였습니다', 'error');
}
};
export const getUserLikedShops = async (userId) => {
try {
const { data, error } = await supabase.from('likes').select('shop_name').eq('user_id', userId);
if (error) throw error;
return data;
} catch (err) {
Swal.fire('Error', '찜한 상점을 불러오는 중 오류가 발생하였습니다', 'error');
}
};
import supabase from '../supabase/supabaseClient';
import Swal from 'sweetalert2';
// 리뷰 추가 함수
export const addReview = async ({ userId, shopId, content }) => {
try {
const { error } = await supabase.from('reviews').insert({
user_id: userId,
shop_id: shopId,
content
});
if (error) throw error;
Swal.fire('Success', '리뷰가 등록되었습니다', 'success');
} catch (err) {
Swal.fire('Error', '리뷰 등록 과정 중 오류가 발생했습니다', 'error');
}
};
// 리뷰 삭제 함수
export const deleteReview = async (reviewId) => {
try {
const result = await Swal.fire({
title: '정말 삭제하시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '삭제',
cancelButtonText: '취소'
});
if (result.isConfirmed) {
const { error } = await supabase.from('reviews').delete().eq('id', reviewId);
if (error) throw error;
Swal.fire('Success', '리뷰를 삭제하였습니다', 'success');
}
} catch (err) {
Swal.fire('Error', '리뷰 삭제 중 오류가 발생하였습니다', 'error');
}
};
// 리뷰 수정 함수
export const modifyReview = async ({ reviewId, content }) => {
try {
const { error } = await supabase
.from('reviews')
.update({
content
})
.eq('id', reviewId);
if (error) throw error;
Swal.fire('Success', '리뷰를 수정하였습니다', 'success');
} catch (err) {
Swal.fire('Error', '리뷰 수정 중 오류가 발생하였습니다', 'error');
}
};
// 특정 가게의 리뷰 가져오기 함수
export const getShopReviewsByShopId = async (shopId) => {
try {
const { data, error } = await supabase.from('reviews').select('*').eq('shop_id', shopId);
if (error) throw error;
return data;
} catch (err) {
Swal.fire('Error', '리뷰를 가져오는 과정 중 오류가 발생했습니다', 'error');
}
};
// 특정 유저의 리뷰 가져오기 함수
export const getUserReviewsByUserId = async (userId) => {
try {
const { data, error } = await supabase.from('reviews').select('*').eq('user_id', userId);
if (error) throw error;
return data;
} catch (err) {
Swal.fire('Error', '리뷰를 가져오는 과정 중 오류가 발생했습니다', 'error');
}
};
import React, { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';
const Comment = ({ comment, onEdit, onDelete }) => {
const { isAuthenticated, userInfo } = useContext(AuthContext);
const isAuthor = userInfo && userInfo.id === comment.userId;
return (
<div className="comment">
<p>{comment.text}</p>
{isAuthenticated && isAuthor && (
<div>
<button onClick={() => onEdit(comment.id)}>Edit</button>
<button onClick={() => onDelete(comment.id)}>Delete</button>
</div>
)}
</div>
);
};
export default Comment;
import React, { useContext, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../contexts/AuthContext';
import Swal from 'sweetalert2';
const PrivateRoute = ({ children }) => {
const { isAuthenticated } = useContext(AuthContext);
const [showAlert, setShowAlert] = useState(false);
useEffect(() => {
if (!isAuthenticated) {
Swal.fire('로그인되지 않은 사용자입니다!', '로그인부터 해주세요', 'warning').then(() => {
setShowAlert(true);
});
}
}, [isAuthenticated]);
if (showAlert) {
return <Navigate to="/login" />;
}
return isAuthenticated ? children : null;
};
export default PrivateRoute;
import React, { useState, useEffect } from 'react';
import { getUserReviewsByUserId } from '../../api/review';
const ReviewsList = ({ userId }) => {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const fetchReviews = async () => {
try {
const data = await getUserReviewsByUserId(userId);
setReviews(data);
} catch (error) {
console.error('리뷰를 불러오는 데 실패했습니다:', error.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (userId) {
fetchReviews();
}
}, [userId]);
if (loading) {
return <div>Loading...</div>;
}
return (
<ul className="list-none">
{reviews.map((review) => (
<li key={review.id} className="p-2 bg-white rounded shadow mb-2">
<p>{review.content}</p>
<small>{new Date(review.created_at).toLocaleDateString()}</small>
</li>
))}
</ul>
);
};
export default ReviewsList;
import React, { useState, useEffect } from 'react';
import { getUserLikedShops } from '../../api/like'; // API 함수 import
const SavedShopsList = ({ userId }) => {
const [shops, setShops] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSavedShops = async () => {
try {
// 기존의 supabase 클라이언트 직접 호출을 API 함수 호출로 변경
const data = await getUserLikedShops(userId); // API 함수 호출
setShops(data.map(item => item.shop_name)); // 데이터 처리
} catch (error) {
console.error('찜한 상점을 불러오는 데 실패했습니다:', error.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchSavedShops();
}
}, [userId]);
if (loading) {
return <div>Loading...</div>;
}
return (
<ul className="list-none">
{shops.map((shopName, index) => (
<li key={index} className="p-2 bg-white rounded shadow mb-2">
{shopName}
</li>
))}
</ul>
);
};
export default SavedShopsList;
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
const queryClient = new QueryClient();
function QueryProvider({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
);
}
export default QueryProvider;
validateForm.jsx
import Swal from 'sweetalert2';
export const validateForm = ({ email, password, nickname = null }) => {
if (nickname !== null && nickname.trim() === '') {
Swal.fire({
title: '오류',
text: '닉네임을 입력하세요.',
icon: 'error',
confirmButtonText: '확인'
});
return false;
}
if (!email || !password) {
Swal.fire({
title: '오류',
text: '이메일과 비밀번호를 입력하세요.',
icon: 'error',
confirmButtonText: '확인'
});
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
Swal.fire({
title: '오류',
text: '올바른 이메일 형식이 아닙니다.',
icon: 'error',
confirmButtonText: '확인'
});
return false;
}
if (password.length < 6) {
Swal.fire({
title: '오류',
text: '비밀번호는 6자리 이상이어야 합니다.',
icon: 'error',
confirmButtonText: '확인'
});
return false;
}
return true;
};
완성 화면
로그인된 정보가 없는 상태에서 마이페이지 접근 시 로그인 페이지로 네비게이트