From ad0c77eedadb08582e2804cdbde5e935876f15e5 Mon Sep 17 00:00:00 2001 From: Haeun Date: Wed, 11 Feb 2026 22:30:03 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=A1=9C=EB=94=A9=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9UI=20=EC=A0=81=EC=9A=A9(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/book/components/BookInfo.tsx | 8 ++- .../components/GatheringBookshelfSection.tsx | 6 ++- .../components/GatheringMeetingSection.tsx | 6 ++- .../topics/components/ProposedTopicList.tsx | 4 +- .../topics/components/TopicHeader.tsx | 13 ++++- .../topics/components/TopicSkeleton.tsx | 13 +++++ src/features/topics/components/index.ts | 1 + src/pages/Gatherings/GatheringDetailPage.tsx | 7 +-- src/pages/Gatherings/GatheringListPage.tsx | 8 +-- src/pages/Gatherings/GatheringSettingPage.tsx | 7 +-- src/pages/Meetings/MeetingDetailPage.tsx | 51 +++++++++---------- src/pages/PreOpinions/PreOpinionListPage.tsx | 3 +- src/routes/PrivateRoute.tsx | 4 +- src/shared/ui/Spinner.tsx | 42 +++++++++++++++ src/shared/ui/index.ts | 1 + 15 files changed, 122 insertions(+), 52 deletions(-) create mode 100644 src/features/topics/components/TopicSkeleton.tsx create mode 100644 src/shared/ui/Spinner.tsx diff --git a/src/features/book/components/BookInfo.tsx b/src/features/book/components/BookInfo.tsx index a22dc1d..589d99a 100644 --- a/src/features/book/components/BookInfo.tsx +++ b/src/features/book/components/BookInfo.tsx @@ -1,4 +1,5 @@ import { Division } from '@/shared/components/Division' +import { Spinner } from '@/shared/ui' import { Switch } from '@/shared/ui/Switch' import { useBookDetail } from '../hooks' @@ -14,7 +15,12 @@ const BookInfo = ({ bookId, isRecording, onToggleRecording }: BookInfoProps) => const { data, isLoading, isError } = useBookDetail(bookId) // if (isLoading) return - if (isLoading) return
로딩중...
+ if (isLoading) + return ( +
+ +
+ ) if (isError || !data) return
책 정보를 불러올 수 없습니다.
return ( diff --git a/src/features/gatherings/components/GatheringBookshelfSection.tsx b/src/features/gatherings/components/GatheringBookshelfSection.tsx index 19b3b34..430e7c9 100644 --- a/src/features/gatherings/components/GatheringBookshelfSection.tsx +++ b/src/features/gatherings/components/GatheringBookshelfSection.tsx @@ -1,6 +1,8 @@ import { ChevronLeft, ChevronRight } from 'lucide-react' import { useRef } from 'react' +import { Spinner } from '@/shared/ui' + import { useGatheringBooks } from '../hooks/useGatheringBooks' import EmptyState from './EmptyState' import GatheringBookCard from './GatheringBookCard' @@ -44,7 +46,9 @@ export default function GatheringBookshelfSection({ gatheringId }: GatheringBook return (

모임 책장

-
로딩 중...
+
+ +
) } diff --git a/src/features/gatherings/components/GatheringMeetingSection.tsx b/src/features/gatherings/components/GatheringMeetingSection.tsx index bd1a19a..2f92ba7 100644 --- a/src/features/gatherings/components/GatheringMeetingSection.tsx +++ b/src/features/gatherings/components/GatheringMeetingSection.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useUserProfile } from '@/features/user' import { PAGE_SIZES, ROUTES } from '@/shared/constants' -import { Button, Pagination, Tabs, TabsList, TabsTrigger } from '@/shared/ui' +import { Button, Pagination, Spinner, Tabs, TabsList, TabsTrigger } from '@/shared/ui' import type { GatheringUserRole, MeetingFilter } from '../gatherings.types' import { useGatheringMeetings, useMeetingTabCounts } from '../hooks' @@ -133,7 +133,9 @@ export default function GatheringMeetingSection({ {/* 약속 목록 */} {isLoading ? ( -
로딩 중...
+
+ +
) : totalCount === 0 ? ( ) : ( diff --git a/src/features/topics/components/ProposedTopicList.tsx b/src/features/topics/components/ProposedTopicList.tsx index ae7112f..08c1135 100644 --- a/src/features/topics/components/ProposedTopicList.tsx +++ b/src/features/topics/components/ProposedTopicList.tsx @@ -10,7 +10,6 @@ type ProposedTopicListProps = { hasNextPage: boolean isFetchingNextPage: boolean onLoadMore: () => void - pageSize?: number gatheringId: number meetingId: number } @@ -20,7 +19,6 @@ export default function ProposedTopicList({ hasNextPage, isFetchingNextPage, onLoadMore, - pageSize = 5, gatheringId, meetingId, }: ProposedTopicListProps) { @@ -58,7 +56,7 @@ export default function ProposedTopicList({ )} {/* 무한 스크롤 로딩 상태 */} - {isFetchingNextPage && } + {isFetchingNextPage && } {/* 무한 스크롤 트리거 */} {hasNextPage && !isFetchingNextPage &&
} diff --git a/src/features/topics/components/TopicHeader.tsx b/src/features/topics/components/TopicHeader.tsx index 5f43a9d..36a593e 100644 --- a/src/features/topics/components/TopicHeader.tsx +++ b/src/features/topics/components/TopicHeader.tsx @@ -1,6 +1,8 @@ import { format } from 'date-fns' import { Check } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { ROUTES } from '@/shared/constants' import { Button } from '@/shared/ui' type ProposedHeaderProps = { @@ -10,6 +12,8 @@ type ProposedHeaderProps = { confirmedTopicDate: string | null proposedTopicsCount: number onOpenChange: (open: boolean) => void + gatheringId: number + meetingId: number } type ConfirmedHeaderProps = { @@ -22,6 +26,7 @@ type ConfirmedHeaderProps = { type TopicHeaderProps = ProposedHeaderProps | ConfirmedHeaderProps export default function TopicHeader(props: TopicHeaderProps) { + const navigate = useNavigate() return ( <> {/* 제안탭 */} @@ -58,7 +63,13 @@ export default function TopicHeader(props: TopicHeaderProps) { )} - {props.actions.canSuggest && } + {props.actions.canSuggest && ( + + )}
)} diff --git a/src/features/topics/components/TopicSkeleton.tsx b/src/features/topics/components/TopicSkeleton.tsx new file mode 100644 index 0000000..24f88c7 --- /dev/null +++ b/src/features/topics/components/TopicSkeleton.tsx @@ -0,0 +1,13 @@ +import TopicListSkeleton from '@/features/topics/components/TopicListSkeleton' + +export default function TopicSkeleton() { + return ( +
+
+

+

+
+ +
+ ) +} diff --git a/src/features/topics/components/index.ts b/src/features/topics/components/index.ts index e77446c..8d810b3 100644 --- a/src/features/topics/components/index.ts +++ b/src/features/topics/components/index.ts @@ -7,3 +7,4 @@ export { default as ProposedTopicList } from './ProposedTopicList' export { default as TopicCard } from './TopicCard' export { default as TopicHeader } from './TopicHeader' export { default as TopicListSkeleton } from './TopicListSkeleton' +export { default as TopicSkeleton } from './TopicSkeleton' diff --git a/src/pages/Gatherings/GatheringDetailPage.tsx b/src/pages/Gatherings/GatheringDetailPage.tsx index 36b0371..cefa750 100644 --- a/src/pages/Gatherings/GatheringDetailPage.tsx +++ b/src/pages/Gatherings/GatheringDetailPage.tsx @@ -12,6 +12,7 @@ import { } from '@/features/gatherings' import { ROUTES } from '@/shared/constants' import { useScrollCollapse } from '@/shared/hooks' +import { Spinner } from '@/shared/ui' import { useGlobalModalStore } from '@/store/globalModalStore' export default function GatheringDetailPage() { @@ -89,11 +90,7 @@ export default function GatheringDetailPage() { // 로딩 상태 if (isLoading || !gathering) { - return ( -
-

로딩 중...

-
- ) + return } return ( diff --git a/src/pages/Gatherings/GatheringListPage.tsx b/src/pages/Gatherings/GatheringListPage.tsx index 326003a..36ea31f 100644 --- a/src/pages/Gatherings/GatheringListPage.tsx +++ b/src/pages/Gatherings/GatheringListPage.tsx @@ -11,7 +11,7 @@ import { } from '@/features/gatherings' import { ROUTES } from '@/shared/constants' import { useInfiniteScroll } from '@/shared/hooks' -import { Button, Tabs, TabsList, TabsTrigger } from '@/shared/ui' +import { Button, Spinner, Tabs, TabsList, TabsTrigger } from '@/shared/ui' import { useGlobalModalStore } from '@/store' type TabValue = 'all' | 'favorites' @@ -106,7 +106,7 @@ export default function GatheringListPage() { <> {isLoading ? (
-

로딩 중...

+
) : gatherings.length === 0 ? ( @@ -128,7 +128,7 @@ export default function GatheringListPage() { )} {isFetchingNextPage && (
-

로딩 중...

+
)} @@ -138,7 +138,7 @@ export default function GatheringListPage() { <> {isFavoritesLoading ? (
-

로딩 중...

+
) : favorites.length === 0 ? ( diff --git a/src/pages/Gatherings/GatheringSettingPage.tsx b/src/pages/Gatherings/GatheringSettingPage.tsx index 20aea74..346f334 100644 --- a/src/pages/Gatherings/GatheringSettingPage.tsx +++ b/src/pages/Gatherings/GatheringSettingPage.tsx @@ -20,6 +20,7 @@ import { Button, Container, Input, + Spinner, Tabs, TabsContent, TabsList, @@ -179,11 +180,7 @@ export default function GatheringSettingPage() { } if (isLoading) { - return ( -
-

로딩 중...

-
- ) + return } if (!gathering || gathering.currentUserRole !== 'LEADER') return null diff --git a/src/pages/Meetings/MeetingDetailPage.tsx b/src/pages/Meetings/MeetingDetailPage.tsx index cbef6d5..aec2a2d 100644 --- a/src/pages/Meetings/MeetingDetailPage.tsx +++ b/src/pages/Meetings/MeetingDetailPage.tsx @@ -18,10 +18,11 @@ import { ConfirmTopicModal, ProposedTopicList, TopicHeader, + TopicSkeleton, useConfirmedTopics, useProposedTopics, } from '@/features/topics' -import { Tabs, TabsContent, TabsList, TabsTrigger, TextButton } from '@/shared/ui' +import { Spinner, Tabs, TabsContent, TabsList, TabsTrigger, TextButton } from '@/shared/ui' export default function MeetingDetailPage() { const { gatheringId, meetingId } = useParams<{ gatheringId: string; meetingId: string }>() @@ -29,7 +30,11 @@ export default function MeetingDetailPage() { const [activeTab, setActiveTab] = useState('PROPOSED') const [isConfirmTopicOpen, setIsConfirmTopicOpen] = useState(false) - const { data: meeting, isLoading, error } = useMeetingDetail(Number(meetingId)) + const { + data: meeting, + isLoading: meetingLoading, + error: meetingError, + } = useMeetingDetail(Number(meetingId)) // 제안된 주제 조회 (무한 스크롤) const { @@ -65,15 +70,11 @@ export default function MeetingDetailPage() { if (confirmedError) { alert(`확정된 주제 조회 실패: ${confirmedError.userMessage}`) } - }, [proposedError, confirmedError]) - - if (error) { - return ( -
-

{error.userMessage}

-
- ) - } + if (meetingError) { + alert(`약속 조회 실패: ${meetingError.userMessage}`) + } + // navigate(ROUTES.GATHERING_DETAIL(gatheringId), { replace: true }) + }, [proposedError, confirmedError, meetingError]) return ( <> @@ -88,9 +89,9 @@ export default function MeetingDetailPage() {
{/* 약속 로딩 적용 */}
- {isLoading ? ( + {meetingLoading ? (
-

로딩 중...

+
) : meeting ? ( <> @@ -138,11 +139,9 @@ export default function MeetingDetailPage() { - {isProposedLoading ? ( -
-

로딩 중...

-
- ) : proposedTopicsInfiniteData ? ( + {isProposedLoading || !proposedTopicsInfiniteData ? ( + + ) : (
- ) : null} + )}
- {isConfirmedLoading ? ( -
-

로딩 중...

-
- ) : confirmedTopicsInfiniteData ? ( + {isConfirmedLoading || !confirmedTopicsInfiniteData ? ( + + ) : (
- ) : null} + )}
diff --git a/src/pages/PreOpinions/PreOpinionListPage.tsx b/src/pages/PreOpinions/PreOpinionListPage.tsx index 9095786..05c9be7 100644 --- a/src/pages/PreOpinions/PreOpinionListPage.tsx +++ b/src/pages/PreOpinions/PreOpinionListPage.tsx @@ -7,6 +7,7 @@ import { usePreOpinionAnswers, } from '@/features/preOpinions' import SubPageHeader from '@/shared/components/SubPageHeader' +import { Spinner } from '@/shared/ui' import { useGlobalModalStore } from '@/store' export default function PreOpinionListPage() { @@ -52,7 +53,7 @@ export default function PreOpinionListPage() { const selectedMember = data?.members.find((m) => m.memberInfo.userId === activeMemberId) - if (isLoading) return
로딩중...
+ if (isLoading) return return ( <> diff --git a/src/routes/PrivateRoute.tsx b/src/routes/PrivateRoute.tsx index 00fee9b..a51227a 100644 --- a/src/routes/PrivateRoute.tsx +++ b/src/routes/PrivateRoute.tsx @@ -3,7 +3,7 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom' import { ApiError } from '@/api' import { useAuth } from '@/features/auth' import { ROUTES } from '@/shared/constants' -import { ErrorFallback } from '@/shared/ui' +import { ErrorFallback, Spinner } from '@/shared/ui' export function PrivateRoute() { const { data: user, isLoading, isError, error, refetch } = useAuth() @@ -14,7 +14,7 @@ export function PrivateRoute() { if (isLoading) { return (
-

Loading...

+
) } diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx new file mode 100644 index 0000000..16981ea --- /dev/null +++ b/src/shared/ui/Spinner.tsx @@ -0,0 +1,42 @@ +type SpinnerProps = { + height?: 'full' | 'fit' +} + +const SpinnerSvg = () => ( + + + + + + + + + + + +) + +export function Spinner({ height = 'fit' }: SpinnerProps) { + if (height === 'full') { + return ( +
+ +
+ ) + } + + return +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 7d90eea..24db004 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -17,6 +17,7 @@ export * from './Pagination' export * from './Popover' export * from './SearchField' export * from './Select' +export * from './Spinner' export * from './StarRatingFilter' export * from './Switch' export * from './Tabs' From 9495855460bccbc937fddb98801e0f66ab48d704 Mon Sep 17 00:00:00 2001 From: Haeun Date: Thu, 12 Feb 2026 02:06:21 +0900 Subject: [PATCH 2/2] =?UTF-8?q?style:=20clipPath=20=EC=A0=9C=EA=B1=B0=20(#?= =?UTF-8?q?74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Spinner.tsx | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx index 16981ea..e9a7a2a 100644 --- a/src/shared/ui/Spinner.tsx +++ b/src/shared/ui/Spinner.tsx @@ -11,21 +11,14 @@ const SpinnerSvg = () => ( fill="none" className="animate-spin" > - - - - - - - - - + + )