Skip to content

Commit

Permalink
Merge pull request #63 from wafflestudio/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
SeohyunLilyChoi authored Jan 25, 2025
2 parents 15d9e7d + 85776e7 commit 7cefa38
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 63 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Redirect from '@/routes/Redirect';

import RegisterPage from './components/home/Topbar/Menu/RegisterPage';
import MyReservationDetails from './components/profile/MyReservationDetails';
import ReviewForm from './components/profile/ReviewForm';
import MyReservations from './routes/MyReservations';
import Profile from './routes/Profile';
import ProfileEdit from './routes/ProfileEdit';
Expand All @@ -29,6 +30,7 @@ export const App = () => {
<Route path="/profile/edit" element={<ProfileEdit />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/MyReservations" element={<MyReservations />} />
<Route path="/reviews/:reservationId" element={<ReviewForm />} />
<Route
path="/reservations/:reservationId"
element={<MyReservationDetails />}
Expand Down
3 changes: 3 additions & 0 deletions src/components/profile/MyReservationItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ const MyReservationItems = () => {
{pastMyReservationItems.map((reservation) => (
<div
key={reservation.reservationId}
onClick={() =>
void navigate(`/reviews/${reservation.reservationId}`)
}
className="p-4 bg-white rounded-lg shadow-md"
>
<img
Expand Down
2 changes: 1 addition & 1 deletion src/components/profile/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const ProfileEditForm = () => {
</div>
<button
type="submit"
className="w-full py-2 bg-blue-500 text-white font-semibold rounded hover:bg-blue-600"
className="w-full py-2 bg-airbnb text-white font-semibold rounded hover:bg-airbnb-hover"
>
수정 완료
</button>
Expand Down
119 changes: 119 additions & 0 deletions src/components/profile/ReviewForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import StarIcon from '@mui/icons-material/Star';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import axios from 'axios';
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

const ReviewForm = () => {
const { reservationId } = useParams<{ reservationId: string }>();
const navigate = useNavigate();
const [content, setContent] = useState<string>('');
const [rating, setRating] = useState<number>(0);
const [hoverRating, setHoverRating] = useState<number>(0);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (reservationId === '') {
setError('예약 ID가 유효하지 않습니다.');
return;
}

if (rating < 1 || rating > 5) {
setError('평점을 선택해주세요.');
return;
}

try {
const token = localStorage.getItem('token');
if (token === null || (typeof token === 'string' && token === '')) {
setError('로그인되지 않았습니다.');
return;
}

await axios.post(
'/api/v1/reviews',
{
reservationId: Number(reservationId),
content,
rating,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);

alert('리뷰가 성공적으로 등록되었습니다.');
setTimeout(() => void navigate('/profile'));
} catch (err) {
console.error(err);
setError('리뷰 작성 중 오류가 발생했습니다.');
}
};

return (
<div className="flex justify-center items-center min-h-screen bg-gray-100">
<form
onSubmit={void handleSubmit}
className="p-6 bg-white rounded-lg shadow-md w-96"
>
<h1 className="text-2xl font-bold mb-4">리뷰 작성</h1>
{error !== null && <p className="text-red-500 mb-4">{error}</p>}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">평점</label>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<div
key={star}
onMouseEnter={() => {
if (star > rating) setHoverRating(star);
}}
onMouseLeave={() => {
if (star > rating) setHoverRating(0);
}}
onMouseDown={() => {
setRating(star);
}}
onMouseUp={() => {
setRating(star);
}}
className="cursor-pointer"
>
{star <= rating ? (
<StarIcon fontSize="large" className="text-airbnb" />
) : star <= hoverRating ? (
<StarIcon fontSize="large" className="text-[#ffa8b8]" />
) : (
<StarBorderIcon fontSize="large" className="text-gray-300" />
)}
</div>
))}
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">리뷰 내용</label>
<textarea
value={content}
onChange={(e) => {
setContent(e.target.value);
}}
className="w-full p-2 border border-gray-300 rounded"
rows={4}
required
/>
</div>
<button
type="submit"
className="w-full py-2 bg-airbnb text-white font-semibold rounded hover:bg-airbnb-hover"
>
리뷰 등록하기
</button>
</form>
</div>
);
};

export default ReviewForm;
171 changes: 109 additions & 62 deletions src/components/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import axios, { AxiosError } from 'axios';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import Reservation from '../roomdetail/Reservation';

type ProfileInfo = {
userId: 0;
nickname: 'string';
Expand All @@ -16,9 +18,29 @@ type ProfileInfo = {
imageUrl: 'string';
};

type Reservation = {
reservationId: number;
place: string;
startDate: string;
endDate: string;
imageUrl: string;
};

type Review = {
content: string;
rating: number;
place: string;
startDate: string;
endDate: string;
};

const UserProfile = () => {
const navigate = useNavigate();
const [profile, setProfile] = useState<ProfileInfo | null>(null);
const [upcomingReservations, setUpcomingReservations] = useState<
Reservation[]
>([]);
const [reviews, setReviews] = useState<Review[]>([]);
const [error, setError] = useState<string>('');
const [imageError, setImageError] = useState(false);

Expand All @@ -35,13 +57,35 @@ const UserProfile = () => {

const fetchProfile = async () => {
try {
const response = await axios.get<ProfileInfo>('/api/v1/profile', {
headers: {
Authorization: `Bearer ${token}`,
const profileResponse = await axios.get<ProfileInfo>(
'/api/v1/profile',
{
headers: {
Authorization: `Bearer ${token}`,
},
},
validateStatus: (status) => status < 400,
);
const profileData = profileResponse.data;
setProfile(profileData);

const reservationsResponse = await axios.get<{
content: Reservation[];
}>(`/api/v1/reservations/user/${profileData.userId}`, {
headers: { Authorization: `Bearer ${token}` },
});

const now = new Date();
const upcoming = reservationsResponse.data.content.filter(
(reservation) => new Date(reservation.startDate) >= now,
);
setUpcomingReservations(upcoming);

const reviewsResponse = await axios.get<{
content: Review[];
}>(`/api/v1/reviews/user/${profileData.userId}`, {
headers: { Authorization: `Bearer ${token}` },
});
setProfile(response.data);
setReviews(reviewsResponse.data.content);
} catch (err) {
if (err instanceof AxiosError) {
console.error('프로필 데이터를 가져오는 중 오류 발생:', err.message);
Expand Down Expand Up @@ -97,15 +141,19 @@ const UserProfile = () => {
<div className="w-24">
<p className="text-[0.625rem]">다가오는 여행</p>
<div className="flex items-end">
<p className="mr-0.5 text-[1.375rem] font-semibold">1</p>
<p className="mr-0.5 text-[1.375rem] font-semibold">
{upcomingReservations.length}
</p>
<p className="py-[4.3px] text-[0.625rem] font-semibold"></p>
</div>
</div>
<hr className="w-full my-3 border-t border-gray-300" />
<div className="w-24">
<p className="text-[0.625rem]">내가 작성한 후기</p>
<div className="flex items-end">
<p className="mr-0.5 text-[1.375rem] font-semibold">5</p>
<p className="mr-0.5 text-[1.375rem] font-semibold">
{reviews.length}
</p>
<p className="py-[4.3px] text-[0.625rem] font-semibold"></p>
</div>
</div>
Expand Down Expand Up @@ -136,17 +184,34 @@ const UserProfile = () => {
</button>
<p className="text-s">{profile.bio}</p>
<hr className="w-full mt-10 mb-8 border-t border-gray-300" />

{/* 예약 */}
<p className="text-xl">다가오는 여행</p>
<div className="flex w-full scrollbar-hidden overflow-x-auto gap-2">
<div className="mt-8 p-6 w-80 content-between min-h-[224px] bg-white rounded-2xl border border-gray-300">
<div className="flex w-68 h-28 bg-gray-300 hover:bg-gray-400 items-center justify-center">
<ImageIcon className="w-14 h-14 text-white" />
</div>
<p className="mt-2 text-lg">강릉시</p>
<p className="text-base text-gray-500">
2025년 2월 20일 - 2025년 2월 21일
</p>
</div>
<div className="flex w-full mt-8 scrollbar-hidden overflow-x-auto gap-2">
{upcomingReservations.length > 0 ? (
upcomingReservations.map((reservation) => (
<div
key={reservation.reservationId}
className="p-4 bg-white content-between rounded-2xl min-w-80 min-h-[224px] border border-gray-300 cursor-pointer hover:shadow-lg"
onClick={(e) => {
e.preventDefault();
void navigate(`/reservations/${reservation.reservationId}`);
}}
>
<img
src={reservation.imageUrl}
alt={reservation.place}
className="w-full h-32 object-cover rounded-md mb-2"
/>
<h3 className="text-lg font-semibold">{reservation.place}</h3>
<p className="text-sm text-gray-500">
{reservation.startDate} ~ {reservation.endDate}
</p>
</div>
))
) : (
<p>다가오는 여행이 없습니다.</p>
)}
</div>
<button
onClick={(e) => {
Expand All @@ -158,53 +223,35 @@ const UserProfile = () => {
지난 여행 보기
</button>
<hr className="w-full my-8 border-t border-gray-300" />

{/* 리뷰 */}
<p className="text-xl">내가 작성한 후기</p>
<div className="flex w-full scrollbar-hidden overflow-x-auto gap-2">
<div className="mt-8 p-6 min-w-72 min-h-[224px] bg-white rounded-2xl border border-gray-300">
<p className="h-2/3 text-base text-ellipsis">
&quot;너무 친절하시고, 숙소도 깔끔해서 잘 쉬었습니다! 간단한
한식 조식 주시는데 엄청 맛있었어요. 역시 전주..&quot;
</p>
<div className="flex items-center">
<div className="flex mr-4 w-14 h-14 bg-gray-300 hover:bg-gray-400 items-center justify-center">
<ImageIcon className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-base text-black">전주시</p>
<p className="text-sm text-gray-500">2024년 10월</p>
</div>
</div>
</div>
<div className="mt-8 p-6 min-w-72 min-h-[224px] bg-white rounded-2xl border border-gray-300">
<p className="h-2/3 text-base text-ellipsis">
&quot;숙소 근처에 유명한 시장이 있어서 좋았어요 방도 엄청
깨끗해요!&quot;
</p>
<div className="flex items-center">
<div className="flex mr-4 w-14 h-14 bg-gray-300 hover:bg-gray-400 items-center justify-center">
<ImageIcon className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-base text-black">속초시</p>
<p className="text-sm text-gray-500">2024년 7월</p>
<div className="flex w-full scrollbar-hidden overflow-x-auto gap-2 mt-8">
{reviews.length > 0 ? (
reviews.map((review, index) => (
<div
key={index}
className="p-6 min-w-80 min-h-[224px] bg-white rounded-2xl border border-gray-300"
>
<p className="h-2/3 text-base text-ellipsis">
&quot;{review.content}&quot;
</p>
<div className="flex items-center">
<div className="flex mr-4 w-14 h-14 bg-gray-300 hover:bg-gray-400 items-center justify-center">
<ImageIcon className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-base text-black">{review.place}</p>
<p className="text-sm text-gray-500">
{review.startDate} - {review.endDate}
</p>
</div>
</div>
</div>
</div>
</div>
<div className="mt-8 p-6 min-w-72 min-h-[224px] bg-white rounded-2xl border border-gray-300">
<p className="h-2/3 text-base text-ellipsis">
&quot;안쪽 골목에 있어서 처음에 찾을 때 조금 헤맸는데, 큰 길이
아니라서 조용히 쉴 수 있었습니다. 다음에 또 갈게요!&quot;
</p>
<div className="flex items-center">
<div className="flex mr-4 w-14 h-14 bg-gray-300 hover:bg-gray-400 items-center justify-center">
<ImageIcon className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-base text-black">마포구</p>
<p className="text-sm text-gray-500">2023년 12월</p>
</div>
</div>
</div>
))
) : (
<p>작성한 후기가 없습니다.</p>
)}
</div>
<button className="mt-6 p-[10px] text-md underline rounded-lg bg-white hover:bg-gray-100">
후기 모두 보기
Expand Down

0 comments on commit 7cefa38

Please sign in to comment.