diff --git a/src/app/(protected)/student/my-reviews.tsx b/src/app/(protected)/student/my-reviews.tsx new file mode 100644 index 0000000..4750f04 --- /dev/null +++ b/src/app/(protected)/student/my-reviews.tsx @@ -0,0 +1,5 @@ +import { MyReviewsPage } from "@/pages/student/my-reviews"; + +export default function MyReviewsScreen() { + return ; +} diff --git a/src/pages/student/my-reviews/index.ts b/src/pages/student/my-reviews/index.ts new file mode 100644 index 0000000..6c56d0d --- /dev/null +++ b/src/pages/student/my-reviews/index.ts @@ -0,0 +1 @@ +export { MyReviewsPage } from "./ui/MyReviewsPage"; diff --git a/src/pages/student/my-reviews/model/mockReviews.ts b/src/pages/student/my-reviews/model/mockReviews.ts new file mode 100644 index 0000000..5af3e8a --- /dev/null +++ b/src/pages/student/my-reviews/model/mockReviews.ts @@ -0,0 +1,35 @@ +import type { Review } from "./types"; + +export const mockReviews: Review[] = [ + { + id: "1", + department: "IT대학", + studentStatus: "재학생", + rating: 5, + content: + "제휴 혜택 덕분에 부담 없이 자주 방문하게 됐어요. 음식도 맛있고 직원분들도 친절해서 매번 만족스러워요.", + images: [ + require("@/shared/assets/images/icon.png"), + require("@/shared/assets/images/icon.png"), + "skeleton", + ], + createdAt: new Date("2025-03-15T18:36:00"), + }, + { + id: "2", + department: "경영대학", + studentStatus: "휴학생", + rating: 3, + content: "가격 대비 괜찮은 편이에요. 혜택 적용이 간편해서 좋았습니다.", + createdAt: new Date("2025-03-10T12:20:00"), + }, + { + id: "3", + department: "사회과학대학", + studentStatus: "재학생", + rating: 4, + content: + "학생 할인이 적용돼서 자주 이용하고 있어요. 앞으로도 계속 제휴 유지해줬으면 좋겠어요!", + createdAt: new Date("2025-03-05T09:00:00"), + }, +]; diff --git a/src/pages/student/my-reviews/model/types.ts b/src/pages/student/my-reviews/model/types.ts new file mode 100644 index 0000000..b119bc2 --- /dev/null +++ b/src/pages/student/my-reviews/model/types.ts @@ -0,0 +1 @@ +export type { Review, ReviewImage } from "@/entities/review"; diff --git a/src/pages/student/my-reviews/ui/MyReviewsPage.tsx b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx new file mode 100644 index 0000000..eea74b9 --- /dev/null +++ b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx @@ -0,0 +1,111 @@ +import { useCallback, useState } from "react"; +import { FlatList, Pressable, Text, View } from "react-native"; +import { ReviewCard } from "@/entities/review"; +import { SortArrowDownIcon } from "@/shared/assets/icons"; +import { + type SortOrder, + useSortedByDate, +} from "@/shared/lib/hooks/useSortedByDate"; +import { colorTokens } from "@/shared/styles/tokens"; +import { AppTopBar } from "@/shared/ui/app-top-bar"; +import { DarkSelectBottomSheet } from "@/shared/ui/bottom-sheet"; +import { SmallButton } from "@/shared/ui/buttons/ActionButton"; +import { PageLayout } from "@/shared/ui/layout"; +import { mockReviews } from "../model/mockReviews"; +import type { Review } from "../model/types"; + +const SORT_ITEMS: { label: string; value: SortOrder }[] = [ + { label: "최신순", value: "latest" }, + { label: "오래된순", value: "oldest" }, +]; + +const listContentStyle = { + gap: 20, + paddingHorizontal: 24, + paddingBottom: 20, +} as const; + +export function MyReviewsPage() { + const [isSortSheetVisible, setSortSheetVisible] = useState(false); + const { + sort, + setSort, + sortedItems: sortedReviews, + } = useSortedByDate(mockReviews); + + const renderItem = useCallback( + ({ item }: { item: Review }) => ( + {} }} + /> + ), + [], + ); + + return ( + <> + + + + {/* 서브헤더 */} + + + 작성한 리뷰가{" "} + {sortedReviews.length}건 + 있어요 + + setSortSheetVisible(true)} + hitSlop={8} + className="flex-row items-center gap-[5px]" + > + + {SORT_ITEMS.find((item) => item.value === sort)?.label} + + + + + + {/* 리뷰 목록 또는 빈 상태 */} + {sortedReviews.length === 0 ? ( + + + + 아직 작성된 리뷰가 없어요! + + + 제휴 리뷰를 작성하면 혜택을 받을 수 있어요. + + + {}}>리뷰 작성하기 + + ) : ( + + style={{ flex: 1 }} + data={sortedReviews} + keyExtractor={(item) => item.id} + renderItem={renderItem} + contentContainerStyle={listContentStyle} + /> + )} + + setSortSheetVisible(false)} + /> + + ); +} diff --git a/src/pages/student/profile/ui/StudentProfilePage.tsx b/src/pages/student/profile/ui/StudentProfilePage.tsx index 858f7f3..af50d8d 100644 --- a/src/pages/student/profile/ui/StudentProfilePage.tsx +++ b/src/pages/student/profile/ui/StudentProfilePage.tsx @@ -12,7 +12,11 @@ export function StudentProfilePage() { const router = useRouter(); const myAccountItems: AccountMenuItemProps[] = [ - { label: "내가 작성한 리뷰", iconName: "writing" }, + { + label: "내가 작성한 리뷰", + iconName: "writing", + onPress: () => router.push("../my-reviews"), + }, { label: "로그아웃", iconName: "exitRight" }, ]; diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts index 5bc1e74..4f1a44a 100644 --- a/src/shared/assets/icons/index.ts +++ b/src/shared/assets/icons/index.ts @@ -18,6 +18,7 @@ export { default as LoginNoIcon } from "./login-no-icon.svg"; export { default as Logo } from "./logo.svg"; export { default as QRIcon } from "./qr-icon.svg"; export { default as SearchIcon } from "./search-icon.svg"; +export { default as SortArrowDownIcon } from "./sort-arrow-down.svg"; export { default as SpeechBubbleIcon } from "./speech-bubble-icon.svg"; export { default as StampActive } from "./stamp-active.svg"; export { default as StampInactive } from "./stamp-inactive.svg"; diff --git a/src/shared/assets/icons/sort-arrow-down.svg b/src/shared/assets/icons/sort-arrow-down.svg new file mode 100644 index 0000000..21c720b --- /dev/null +++ b/src/shared/assets/icons/sort-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/lib/hooks/useSortedByDate.ts b/src/shared/lib/hooks/useSortedByDate.ts new file mode 100644 index 0000000..5de999d --- /dev/null +++ b/src/shared/lib/hooks/useSortedByDate.ts @@ -0,0 +1,17 @@ +import { useMemo, useState } from "react"; + +export type SortOrder = "latest" | "oldest"; + +export function useSortedByDate(items: T[]) { + const [sort, setSort] = useState("latest"); + + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => + sort === "latest" + ? b.createdAt.getTime() - a.createdAt.getTime() + : a.createdAt.getTime() - b.createdAt.getTime(), + ); + }, [items, sort]); + + return { sort, setSort, sortedItems }; +} diff --git a/src/shared/ui/app-top-bar/AppTopBar.tsx b/src/shared/ui/app-top-bar/AppTopBar.tsx index 37c1147..78de18c 100644 --- a/src/shared/ui/app-top-bar/AppTopBar.tsx +++ b/src/shared/ui/app-top-bar/AppTopBar.tsx @@ -6,9 +6,15 @@ import { colorTokens } from "@/shared/styles/tokens"; interface AppTopBarProps { title: string; onBack?: () => void; + titleAlign?: "left" | "center"; } -export function AppTopBar({ title, onBack }: AppTopBarProps) { +export function AppTopBar({ + title, + onBack, + titleAlign = "center", +}: AppTopBarProps) { + const isLeft = titleAlign === "left"; return ( router.back())}> @@ -18,10 +24,12 @@ export function AppTopBar({ title, onBack }: AppTopBarProps) { color={colorTokens.contentPrimary} /> - + {title} - + {!isLeft && } ); }