From cc901ccfdabdd550efe4a12f5863f2f69b15a13a Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Sat, 11 Jul 2026 12:10:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(fe):=20push=20=EC=95=A1=ED=8B=B0=EB=B9=84?= =?UTF-8?q?=ED=8B=B0=20=EB=A1=9C=EB=94=A9=EC=9D=84=20shimmer=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=88=EB=A0=88=ED=86=A4=EC=9C=BC=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pending 동안 '불러오는 중…' 텍스트 하나만 그려서 라이트 테마에선 push 전환 내내 흰 화면이 슬라이드되어 보였다. 실제 레이아웃을 흉내 낸 스켈레톤을 깔아 로딩 중임이 보이게 한다. Co-Authored-By: Claude Fable 5 --- frontend/src/components/ui/skeleton.tsx | 16 ++++++ .../activities/event-detail-activity.tsx | 32 +++++++++-- .../activities/person-edit-activity.tsx | 36 ++++++++++++- .../activities/person/person-profile-view.tsx | 53 ++++++++++++++++++- .../person/person-timeline-view.tsx | 34 ++++++++++-- .../stackflow/activities/record-activity.tsx | 27 ++++++++-- frontend/src/styles.css | 13 +++++ 7 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/ui/skeleton.tsx diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..b53577f --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,16 @@ +import { cn } from '@/lib/utils' + +// push 전환 동안 pending 화면이 빈 배경(라이트 테마 = 흰 화면)으로 슬라이드되어 +// 보이지 않도록, 실제 레이아웃의 자리를 흉내 내는 shimmer 플레이스홀더 조각. +// 화면별 스켈레톤을 감싸는 쪽에서 role="status" aria-label="불러오는 중"을 달고, +// 조각 자체는 보조기기에 노출하지 않는다. +export function Skeleton({ className }: { className?: string }) { + return ( +
+
+
+ ) +} diff --git a/frontend/src/stackflow/activities/event-detail-activity.tsx b/frontend/src/stackflow/activities/event-detail-activity.tsx index e6d9abb..e746368 100644 --- a/frontend/src/stackflow/activities/event-detail-activity.tsx +++ b/frontend/src/stackflow/activities/event-detail-activity.tsx @@ -3,6 +3,7 @@ import { useFlow } from '@stackflow/react' import type { ActivityComponentType } from '@stackflow/react' import { ActivityShell } from '@/stackflow/components/activity-shell' import { MonogramAvatar } from '@/components/ui/monogram-avatar' +import { Skeleton } from '@/components/ui/skeleton' import { Badge } from '@/components/ui/badge' import { fetchEvent } from '@/lib/api/events' import { fetchPersons } from '@/lib/api/persons' @@ -47,9 +48,34 @@ export const EventDetailActivity: ActivityComponentType<'EventDetail'> = ({ if (eventQuery.isPending) { return ( -

- 불러오는 중… -

+
+ +

몽글 상세

+ +
+
+
+ + +
+ +
+ + +
+
+ + + +
+
) } diff --git a/frontend/src/stackflow/activities/person-edit-activity.tsx b/frontend/src/stackflow/activities/person-edit-activity.tsx index 469e9d5..f9febeb 100644 --- a/frontend/src/stackflow/activities/person-edit-activity.tsx +++ b/frontend/src/stackflow/activities/person-edit-activity.tsx @@ -6,6 +6,7 @@ import { ActivityShell } from '@/stackflow/components/activity-shell' import { FormPageHeader } from '@/components/layout/form-page-header' import { PersonForm, personToFormValues } from '@/components/person/person-form' import { ConfirmPopup } from '@/components/ui/confirm-popup' +import { Skeleton } from '@/components/ui/skeleton' import { fetchChips } from '@/lib/api/chips' import { deletePerson, fetchPerson, updatePerson } from '@/lib/api/persons' import { formatPersonName } from '@/lib/format' @@ -68,16 +69,47 @@ export const PersonEditActivity: ActivityComponentType<'PersonEdit'> = ({ )?.requestSubmit() } - if (!Number.isFinite(id) || personQuery.isPending) { + if (!Number.isFinite(id)) { return (

- {personQuery.isPending ? '불러오는 중…' : '잘못된 경로예요.'} + 잘못된 경로예요.

) } + if (personQuery.isPending) { + return ( + + pop()} + title="프로필 수정" + className="px-5" + /> +
+
+ + +
+
+ +
+ {[0, 1, 2].map((row) => ( +
+ + +
+ ))} +
+
+ ) + } + if (!person || personQuery.isError) { return ( diff --git a/frontend/src/stackflow/activities/person/person-profile-view.tsx b/frontend/src/stackflow/activities/person/person-profile-view.tsx index 2b72e12..315dd3a 100644 --- a/frontend/src/stackflow/activities/person/person-profile-view.tsx +++ b/frontend/src/stackflow/activities/person/person-profile-view.tsx @@ -10,6 +10,7 @@ import { ListGroupItem, ListGroupLabel, } from '@/components/ui/list-group' +import { Skeleton } from '@/components/ui/skeleton' import { coloredTagStyle, tagChipClass } from '@/components/ui/tag-chip' import { fetchPersonTimeline } from '@/lib/api/events' import { deletePerson, fetchPerson } from '@/lib/api/persons' @@ -59,14 +60,62 @@ export function PersonProfileView({ }, }) - if (!Number.isFinite(id) || personQuery.isPending) { + if (!Number.isFinite(id)) { return (

- {personQuery.isPending ? '불러오는 중…' : '잘못된 경로예요.'} + 잘못된 경로예요.

) } + if (personQuery.isPending) { + return ( +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+ + +
+ {[0, 1, 2, 3].map((cell) => ( +
+ + +
+ ))} +
+
+
+ + {[0, 1, 2].map((row) => ( + + + + ))} + +
+
+ ) + } + if (!person) { return (

diff --git a/frontend/src/stackflow/activities/person/person-timeline-view.tsx b/frontend/src/stackflow/activities/person/person-timeline-view.tsx index 30cba11..2880725 100644 --- a/frontend/src/stackflow/activities/person/person-timeline-view.tsx +++ b/frontend/src/stackflow/activities/person/person-timeline-view.tsx @@ -16,6 +16,7 @@ import { PersonPageHeader } from '@/components/person/person-page-header' import { MonogramAvatar } from '@/components/ui/monogram-avatar' import { Button } from '@/components/ui/button' import { ListGroup, ListGroupItem } from '@/components/ui/list-group' +import { Skeleton } from '@/components/ui/skeleton' import { fetchChips } from '@/lib/api/chips' import { fetchPersonTimeline } from '@/lib/api/events' import { fetchPerson } from '@/lib/api/persons' @@ -133,9 +134,36 @@ export function PersonTimelineView({ } > -

- 불러오는 중… -

+
+
+ + +
+ + + +
+ {[0, 1].map((stat) => ( +
+ + +
+ ))} +
+
+
+ + {[0, 1, 2].map((row) => ( + + + + + ))} + +
) } diff --git a/frontend/src/stackflow/activities/record-activity.tsx b/frontend/src/stackflow/activities/record-activity.tsx index 4e939fd..d482c2f 100644 --- a/frontend/src/stackflow/activities/record-activity.tsx +++ b/frontend/src/stackflow/activities/record-activity.tsx @@ -11,6 +11,7 @@ import { MonogramAvatar } from '@/components/ui/monogram-avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { tagChipBaseClass } from '@/components/ui/tag-chip' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' @@ -326,9 +327,29 @@ export const RecordActivity: ActivityComponentType<'Record'> = ({ params }) => { return ( {recordHeader} -

- 불러오는 중… -

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
) } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ccc9854..3a72438 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -115,6 +115,19 @@ --font-sans: var(--font-sans); } +@theme { + --animate-skeleton-shimmer: skeleton-shimmer 1.8s ease-in-out infinite; + + @keyframes skeleton-shimmer { + from { + transform: translateX(-100%); + } + to { + transform: translateX(100%); + } + } +} + @layer base { * { @apply border-border outline-ring/50;