From 41c6bde82624104995555129c0f7578c5908d987 Mon Sep 17 00:00:00 2001 From: chunjaemin Date: Sun, 26 Apr 2026 00:24:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT/#95]=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(protected)/student/my-reviews.tsx | 5 + src/pages/student/my-reviews/index.ts | 1 + .../student/my-reviews/model/mockReviews.ts | 35 +++++ src/pages/student/my-reviews/model/types.ts | 1 + .../student/my-reviews/ui/MyReviewsPage.tsx | 123 ++++++++++++++++++ .../student/profile/ui/StudentProfilePage.tsx | 6 +- src/shared/assets/icons/index.ts | 1 + src/shared/assets/icons/sort-arrow-down.svg | 3 + 8 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/app/(protected)/student/my-reviews.tsx create mode 100644 src/pages/student/my-reviews/index.ts create mode 100644 src/pages/student/my-reviews/model/mockReviews.ts create mode 100644 src/pages/student/my-reviews/model/types.ts create mode 100644 src/pages/student/my-reviews/ui/MyReviewsPage.tsx create mode 100644 src/shared/assets/icons/sort-arrow-down.svg 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..391046d --- /dev/null +++ b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx @@ -0,0 +1,123 @@ +import { router } from "expo-router"; +import { useCallback, useMemo, useState } from "react"; +import { FlatList, Pressable, Text, View } from "react-native"; +import { ReviewCard } from "@/entities/review"; +import { BackArrowIcon } from "@/shared/assets/icons"; +import { colorTokens } from "@/shared/styles/tokens"; +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"; + +type SortType = "latest" | "oldest"; + +const SORT_ITEMS: { label: string; value: SortType }[] = [ + { label: "최신순", value: "latest" }, + { label: "오래된순", value: "oldest" }, +]; + +const listContentStyle = { + gap: 20, + paddingHorizontal: 24, + paddingBottom: 20, +} as const; + +export function MyReviewsPage() { + const [sort, setSort] = useState("latest"); + const [isSortSheetVisible, setSortSheetVisible] = useState(false); + + const sortedReviews = useMemo(() => { + return [...mockReviews].sort((a, b) => { + if (sort === "latest") + return b.createdAt.getTime() - a.createdAt.getTime(); + return a.createdAt.getTime() - b.createdAt.getTime(); + }); + }, [sort]); + + const renderItem = useCallback( + ({ item }: { item: Review }) => ( + {} }} + /> + ), + [], + ); + + return ( + <> + + {/* 헤더 */} + + router.back()} hitSlop={8}> + + + + + 내가 작성한 리뷰 + + + + + + {/* 서브헤더 */} + + + 작성한 리뷰가{" "} + {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 @@ + + + From c83f3fe9acd4494c21eae85693c9efeaa8900533 Mon Sep 17 00:00:00 2001 From: chunjaemin Date: Sun, 26 Apr 2026 01:28:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[STYLE/#95]=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=20=EB=A6=AC=EB=B7=B0=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../student/my-reviews/model/mockReviews.ts | 62 +++++++++---------- .../student/my-reviews/ui/MyReviewsPage.tsx | 30 +++------ src/shared/ui/app-top-bar/AppTopBar.tsx | 14 ++++- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/pages/student/my-reviews/model/mockReviews.ts b/src/pages/student/my-reviews/model/mockReviews.ts index 5af3e8a..e2a9910 100644 --- a/src/pages/student/my-reviews/model/mockReviews.ts +++ b/src/pages/student/my-reviews/model/mockReviews.ts @@ -1,35 +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"), - }, + // { + // 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/ui/MyReviewsPage.tsx b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx index 391046d..ecb4da2 100644 --- a/src/pages/student/my-reviews/ui/MyReviewsPage.tsx +++ b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx @@ -1,9 +1,9 @@ -import { router } from "expo-router"; import { useCallback, useMemo, useState } from "react"; import { FlatList, Pressable, Text, View } from "react-native"; import { ReviewCard } from "@/entities/review"; -import { BackArrowIcon } from "@/shared/assets/icons"; +import { SortArrowDownIcon } from "@/shared/assets/icons"; 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"; @@ -52,25 +52,10 @@ export function MyReviewsPage() { contentContainerClassName="flex-1" withBottomInset > - {/* 헤더 */} - - router.back()} hitSlop={8}> - - - - - 내가 작성한 리뷰 - - - - + {/* 서브헤더 */} - + 작성한 리뷰가{" "} {sortedReviews.length}건 @@ -84,12 +69,17 @@ export function MyReviewsPage() { {SORT_ITEMS.find((item) => item.value === sort)?.label} + {/* 리뷰 목록 또는 빈 상태 */} {sortedReviews.length === 0 ? ( - + 아직 작성된 리뷰가 없어요! 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 && } ); } From a5a2ef670cfc1c3c39c1b76fa779796914471785 Mon Sep 17 00:00:00 2001 From: chunjaemin Date: Sun, 26 Apr 2026 01:44:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[REFACTOR/#95]=20sorted=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=ED=95=A8=EC=88=98=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../student/my-reviews/model/mockReviews.ts | 62 +++++++++---------- .../student/my-reviews/ui/MyReviewsPage.tsx | 24 ++++--- src/shared/lib/hooks/useSortedByDate.ts | 17 +++++ 3 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 src/shared/lib/hooks/useSortedByDate.ts diff --git a/src/pages/student/my-reviews/model/mockReviews.ts b/src/pages/student/my-reviews/model/mockReviews.ts index e2a9910..5af3e8a 100644 --- a/src/pages/student/my-reviews/model/mockReviews.ts +++ b/src/pages/student/my-reviews/model/mockReviews.ts @@ -1,35 +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"), - // }, + { + 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/ui/MyReviewsPage.tsx b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx index ecb4da2..eea74b9 100644 --- a/src/pages/student/my-reviews/ui/MyReviewsPage.tsx +++ b/src/pages/student/my-reviews/ui/MyReviewsPage.tsx @@ -1,7 +1,11 @@ -import { useCallback, useMemo, useState } from "react"; +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"; @@ -10,9 +14,7 @@ import { PageLayout } from "@/shared/ui/layout"; import { mockReviews } from "../model/mockReviews"; import type { Review } from "../model/types"; -type SortType = "latest" | "oldest"; - -const SORT_ITEMS: { label: string; value: SortType }[] = [ +const SORT_ITEMS: { label: string; value: SortOrder }[] = [ { label: "최신순", value: "latest" }, { label: "오래된순", value: "oldest" }, ]; @@ -24,16 +26,12 @@ const listContentStyle = { } as const; export function MyReviewsPage() { - const [sort, setSort] = useState("latest"); const [isSortSheetVisible, setSortSheetVisible] = useState(false); - - const sortedReviews = useMemo(() => { - return [...mockReviews].sort((a, b) => { - if (sort === "latest") - return b.createdAt.getTime() - a.createdAt.getTime(); - return a.createdAt.getTime() - b.createdAt.getTime(); - }); - }, [sort]); + const { + sort, + setSort, + sortedItems: sortedReviews, + } = useSortedByDate(mockReviews); const renderItem = useCallback( ({ item }: { item: Review }) => ( 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 }; +}