Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/api/feedback/feedback.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import api from '@/api/api';

interface FeedbackRequest {
feedbackContent: string;
}

export const postFeedback = async (body: FeedbackRequest) => {
const res = await api.post('/feedback', body);
return res.data;
};
14 changes: 14 additions & 0 deletions src/api/review/myBookmark.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import api from '@/api/api';
import type { PaginatedBookmarkResponse } from '@/types/review';

interface PageRequest {
page: number;
size: number;
}

export const getMyBookmarks = async (
params: PageRequest
): Promise<PaginatedBookmarkResponse> => {
const res = await api.get('/profile/bookmark', { params });
return res.data.data;
};
22 changes: 22 additions & 0 deletions src/api/review/myReview.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import api from '@/api/api';
import type { ReviewItem } from '@/types/review';

interface PageRequest {
page: number;
size: number;
}

interface PaginatedReviewResponse {
content: ReviewItem[];
page: number;
size: number;
totalPages: number;
totalElements: number;
}

export const getMyReviews = async (
params: PageRequest
): Promise<PaginatedReviewResponse> => {
const res = await api.get('/profile/reviews', { params });
return res.data.data;
};
83 changes: 55 additions & 28 deletions src/pages/my/MyBookmark.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
import { useEffect, useState } from 'react';
import { ReviewCard, Header } from '@/components';
import { mockMyReviews } from '@/__mocks/mockReviews';
import { getMyBookmarks } from '@/api/review/myBookmark.api';
import type { BookmarkReviewItem } from '@/types/review';

const MyBookmarkPage = () => {
const [bookmarks, setBookmarks] = useState<BookmarkReviewItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchBookmarks = async () => {
try {
const { content } = await getMyBookmarks({ page: 1, size: 10 });
setBookmarks(content);
} catch (err: unknown) {
console.error(err);
if (err instanceof Error) {
setError(err.message);
} else {
setError('북마크를 불러오지 못했습니다.');
}
} finally {
setLoading(false);
}
};

fetchBookmarks();
}, []);

return (
<div>
<div className="w-full px-4">
<Header
leftSection="BACK"
rightSection="KEBAB"
onKebabClick={() => console.log('케밥버튼 클릭')}
className="bg-gray-900"
>
북마크
</Header>
<div className="w-full px-4">
<Header
leftSection="BACK"
onKebabClick={() => console.log('케밥버튼 클릭')}
className="bg-gray-900"
>
북마크
</Header>

{/* 리뷰 목록 */}
<main className="flex flex-col gap-y-3 py-4 pt-[68px]">
{mockMyReviews.map((review) => (
<ReviewCard
key={review.id}
imageUrl={review.imageUrl}
tags={review.tags}
title={review.title}
description={review.description}
likeCount={review.likeCount}
onClick={() => {
console.log(`Review ${review.id} clicked`);
}}
/>
))}
</main>
</div>
<main className="flex flex-col gap-y-3 py-4 pt-[68px]">
{loading && <p className="text-white">로딩 중...</p>}
{error && <p className="text-red-500">{error}</p>}
{!loading && !error && bookmarks.length === 0 && (
<p className="text-gray-400">북마크한 후기가 없습니다.</p>
)}
{bookmarks.map((review) => (
<ReviewCard
key={review.reviewId}
imageUrl={review.thumbnailUrl}
tags={(review.hashtags ?? []).map((tag) => tag.hashTagName)}
title={review.title}
description={review.content}
likeCount={review.heartCount}
onClick={() => {
console.log(`Bookmark ${review.reviewId} clicked`);
}}
/>
))}
</main>
</div>
);
};
Expand Down
32 changes: 20 additions & 12 deletions src/pages/my/MyFeedback.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
import { useNavigate } from 'react-router-dom';
import { useToastStore } from '@/store';
import { postFeedback } from '@/api//feedback/feedback.api';
import { useState } from 'react';
import { Button, Header } from '@/components';

export default function MyFeedbackPage() {
const [feedbackText, setFeedbackText] = useState('');
const MAX_LENGTH = 1000;

const isButtonDisabled = feedbackText.length === 0;

const navigate = useNavigate();
const { show } = useToastStore();

const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (e.target.value.length > MAX_LENGTH) {
setFeedbackText(e.target.value.slice(0, MAX_LENGTH));
} else {
setFeedbackText(e.target.value);
const value = e.target.value;
setFeedbackText(value.length > MAX_LENGTH ? value.slice(0, MAX_LENGTH) : value);
};

const handleSubmit = async () => {
try {
await postFeedback({ feedbackContent: feedbackText });
show('SEEAT 팀에게 의견을 보냈어요!');
navigate('/my');
} catch (err) {
console.error('피드백 전송 실패:', err);
show('의견 보내기에 실패했어요.');
}
};

return (
<div className="flex h-screen flex-col text-white">
<div className="mx-auto flex w-full max-w-md flex-grow flex-col px-4">
{/* 헤더 */}
<Header leftSection="BACK" rightSection="KEBAB" className="bg-gray-900">
<Header leftSection="BACK" className="bg-gray-900">
의견 보내기
</Header>

<main className="flex flex-grow flex-col px-2 pt-[88px] pb-4">
<div className="flex w-full flex-col gap-2">
<label className="text-body-2 text-gray-300">SEEAT에게 하고 싶은 말을 보내주세요</label>

{/* textarea와 글자수 카운터를 감싸는 컨테이너 */}
<div className="relative w-full">
<textarea
value={feedbackText}
onChange={handleTextChange}
placeholder="피드백을 남겨주시면 SEEAT이 더 좋은 서비스를 제공할 수 있어요!"
className="placeholder:text-body-2 h-[116px] w-full resize-none rounded-lg border border-gray-800 bg-gray-900 p-3 pr-14 outline-none placeholder:text-gray-500"
/>
{/* 글자 수 카운터 */}
<div className="text-caption-3 absolute right-3 bottom-3 text-gray-500">
{feedbackText.length}/{MAX_LENGTH}
</div>
</div>
</div>
<div className="flex-grow" />

{/* 하단 버튼 영역 */}
<div className="mt-4">
<Button
variant="primary"
Expand All @@ -52,7 +60,7 @@ export default function MyFeedbackPage() {
className="w-full"
fontType="title-3"
disabled={isButtonDisabled}
onClick={() => alert('피드백이 전송되었습니다!')}
onClick={handleSubmit}
>
보내기
</Button>
Expand Down
75 changes: 52 additions & 23 deletions src/pages/my/MyReview.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,59 @@
import { ReviewCard, Header } from '@/components';
import { mockMyReviews } from '@/__mocks/mockReviews';
import { useEffect, useState } from 'react';
import { Header, ReviewCard } from '@/components';
import { getMyReviews } from '@/api/review/myReview.api';
import type { ReviewItem } from '@/types/review';

export default function MyReviewPage() {
const [reviews, setReviews] = useState<ReviewItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchReviews = async () => {
try {
const { content } = await getMyReviews({ page: 1, size: 10 });
setReviews(content);
} catch (err: unknown) {
console.error(err);
if (err instanceof Error) {
setError(err.message);
} else {
setError('후기를 불러오지 못했습니다.');
}
} finally {
setLoading(false);
}
};

fetchReviews();
}, []);

return (
<div>
<div className="w-full px-4">
<Header leftSection="BACK" rightSection="KEBAB" className="bg-gray-900">
나의 후기
</Header>
<div className="w-full px-4">
<Header leftSection="BACK" className="bg-gray-900">
나의 후기
</Header>

<main className="flex flex-col gap-y-3 py-4 pt-[88px]">
{mockMyReviews.map((review) => (
<ReviewCard
key={review.id}
imageUrl={review.imageUrl}
tags={review.tags}
title={review.title}
description={review.description}
likeCount={review.likeCount}
onClick={() => {
console.log(`Review ${review.id} clicked`);
}}
/>
))}
</main>
</div>
<main className="flex flex-col gap-y-3 py-4 pt-[60px]">
{loading && <p className="text-white">로딩 중...</p>}
{error && <p className="text-red-500">{error}</p>}
{!loading && !error && reviews.length === 0 && (
<p className="text-gray-400">작성한 후기가 없습니다.</p>
)}
{reviews.map((review) => (
<ReviewCard
key={review.reviewId}
imageUrl={review.thumbnailUrl}
tags={(review.hashtags ?? []).map((tag) => tag.hashTagName)}
title={review.title}
description={review.content}
likeCount={review.heartCount}
onClick={() => {
console.log(`Review ${review.reviewId} clicked`);
}}
/>
))}
</main>
</div>
);
}
18 changes: 18 additions & 0 deletions src/types/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,21 @@ export interface SeatReviewBlock {
averageRating: number;
reviews: ReviewItem[];
}

export interface BookmarkReviewItem {
reviewId: number;
thumbnailUrl: string;
hashtags: { hashTagId: number; hashTagName: string }[];
title: string;
content: string;
heartCount: number;
createdAt: string;
rating: number;
}

export interface PaginatedBookmarkResponse {
content: BookmarkReviewItem[];
hasNext: boolean;
page: number;
size: number;
}