From 0311d86f728a62bbbc5ecfb04f17515b9f63f0f4 Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Sun, 19 Jul 2026 16:50:10 +0900 Subject: [PATCH] =?UTF-8?q?refactor(fe):=20=EB=B3=B8=EB=AC=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88?= =?UTF-8?q?=EB=A5=BC=20ScrollBody=EB=A1=9C=20=EC=8A=B9=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 여러 화면에 복붙되던 본문 스크롤 클래스 묶음을 ScrollBody 컴포넌트로 모았다. iOS 관성 스크롤과 스크롤바 자리 예약이라는 암묵지를 한 곳에 담고, pad 프로퍼티로 하단 여백만 화면별로 고른다. 홈 설정, 태그 설정, 몽글 상세 화면은 기존에 이 묶음의 일부가 빠져 있어 스크롤바가 나타날 때 본문 폭이 밀렸는데, ScrollBody로 옮기면서 스크롤바 자리 예약과 iOS 관성 스크롤을 함께 얻는다. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../timeline/timeline-scroll-shell.tsx | 8 +- .../src/components/ui/scroll-body.stories.tsx | 73 +++++++++++++++++++ frontend/src/components/ui/scroll-body.tsx | 31 ++++++++ .../activities/event-detail-activity.tsx | 5 +- .../activities/person-edit-activity.tsx | 5 +- .../activities/person/person-profile-view.tsx | 5 +- .../settings/home-settings-activity.tsx | 5 +- .../activities/settings/tag-settings-page.tsx | 5 +- frontend/src/stackflow/tabs/people-tab.tsx | 5 +- frontend/src/stackflow/tabs/settings-tab.tsx | 5 +- 10 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/ui/scroll-body.stories.tsx create mode 100644 frontend/src/components/ui/scroll-body.tsx diff --git a/frontend/src/components/timeline/timeline-scroll-shell.tsx b/frontend/src/components/timeline/timeline-scroll-shell.tsx index 292460a..27dec1c 100644 --- a/frontend/src/components/timeline/timeline-scroll-shell.tsx +++ b/frontend/src/components/timeline/timeline-scroll-shell.tsx @@ -1,4 +1,5 @@ import type { ReactNode, RefObject } from 'react' +import { ScrollBody } from '@/components/ui/scroll-body' // 고정 헤더 + 내부 스크롤 본문. 바깥 셸(TabShell/ActivityShell의 fixed 레이아웃)이 // 세로 flex 컨테이너라는 전제로 동작한다. @@ -17,12 +18,9 @@ export function TimelineScrollShell({ {header !== undefined ? (
{header}
) : null} -
+ {children} -
+ ) } diff --git a/frontend/src/components/ui/scroll-body.stories.tsx b/frontend/src/components/ui/scroll-body.stories.tsx new file mode 100644 index 0000000..dadd151 --- /dev/null +++ b/frontend/src/components/ui/scroll-body.stories.tsx @@ -0,0 +1,73 @@ +import { ScrollBody } from '@/components/ui/scroll-body' +import type { Meta, StoryObj } from '@storybook/react-vite' + +const meta = { + title: 'UI/ScrollBody', + component: ScrollBody, + tags: ['autodocs'], + argTypes: { + pad: { + control: 'select', + options: ['tabbar', 'screen', 'none'], + }, + }, + // flex-1 본문이라, 높이가 정해진 세로 flex 컨테이너 안에서만 스크롤이 드러난다. + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +const LINES = Array.from( + { length: 24 }, + (_, i) => `${i + 1}번째 줄 · 스크롤을 확인하기 위한 긴 본문 내용이에요.`, +) + +function Filler() { + return ( +
+ {LINES.map((line) => ( +

+ {line} +

+ ))} +
+ ) +} + +// 탭바가 겹치는 탭 화면. 하단 pb-24로 마지막 항목이 탭바에 가리지 않게 한다. +export const Tabbar: Story = { + args: { pad: 'tabbar' }, + render: (args) => ( + + + + ), +} + +// 일반 전체 화면. 하단 pb-8 여백만 둔다. +export const Screen: Story = { + args: { pad: 'screen' }, + render: (args) => ( + + + + ), +} + +// 하단 여백 없이 콘텐츠 끝까지. 하단에 별도 고정 바가 붙는 화면에 쓴다. +export const None: Story = { + args: { pad: 'none' }, + render: (args) => ( + + + + ), +} diff --git a/frontend/src/components/ui/scroll-body.tsx b/frontend/src/components/ui/scroll-body.tsx new file mode 100644 index 0000000..e8a9bf7 --- /dev/null +++ b/frontend/src/components/ui/scroll-body.tsx @@ -0,0 +1,31 @@ +import type { ComponentProps } from 'react' +import { cn } from '@/lib/utils' + +export type ScrollBodyPad = 'tabbar' | 'screen' | 'none' + +// pad별 하단 여백: 탭바가 겹치는 탭 화면(tabbar)·일반 화면 하단 여백(screen)·여백 없음(none). +const PAD_CLASS: Record = { + tabbar: 'pb-24', + screen: 'pb-8', + none: '', +} + +// 세로 flex 셸(TabShell/ActivityShell fixed 레이아웃) 안에서 본문만 세로로 스크롤시키는 컨테이너. +// iOS 관성 스크롤과 스크롤바 자리(gutter) 예약을 함께 켜, 스크롤바가 나타날 때 본문 폭이 밀리지 않게 한다. +// 화면별 간격(space-y-* 등)은 컴포넌트에 굳히지 않고 className으로 넘긴다. +export function ScrollBody({ + pad = 'tabbar', + className, + ...props +}: ComponentProps<'div'> & { pad?: ScrollBodyPad }) { + return ( +
+ ) +} diff --git a/frontend/src/stackflow/activities/event-detail-activity.tsx b/frontend/src/stackflow/activities/event-detail-activity.tsx index c6969fe..11f2a76 100644 --- a/frontend/src/stackflow/activities/event-detail-activity.tsx +++ b/frontend/src/stackflow/activities/event-detail-activity.tsx @@ -5,6 +5,7 @@ import type { ActivityComponentType } from '@stackflow/react' import { eventQuery, personQuery } from '@/apis/queries' import { ActivityShell } from '@/stackflow/components/activity-shell' import { MonogramAvatar } from '@/components/ui/monogram-avatar' +import { ScrollBody } from '@/components/ui/scroll-body' import { Badge } from '@/components/ui/badge' import { StatusMessage } from '@/components/ui/status-message' import { EventPhotoGallery } from '@/components/events/event-photo-gallery' @@ -77,7 +78,7 @@ export const EventDetailActivity: ActivityComponentType<'EventDetail'> = ({ -
+

= ({ ) : null} -

+
) } diff --git a/frontend/src/stackflow/activities/person-edit-activity.tsx b/frontend/src/stackflow/activities/person-edit-activity.tsx index b4357a0..91d1630 100644 --- a/frontend/src/stackflow/activities/person-edit-activity.tsx +++ b/frontend/src/stackflow/activities/person-edit-activity.tsx @@ -8,6 +8,7 @@ import { PersonEditForm } from '@/components/person/person-edit-form' import { personToFormValues } from '@/components/person/person-form' import { usePersonDelete } from '@/components/person/use-person-delete' import { Button } from '@/components/ui/button' +import { ScrollBody } from '@/components/ui/scroll-body' import { StatusMessage } from '@/components/ui/status-message' import { personMutation } from '@/apis/mutations' import { chipQuery, homeQuery, personQuery } from '@/apis/queries' @@ -87,7 +88,7 @@ export const PersonEditActivity: ActivityComponentType<'PersonEdit'> = ({ className="px-5" /> -
+
= ({

) : null}
-
+
-
+
@@ -168,7 +169,7 @@ export function PeopleTab() { onToggleFavorite={(id) => favoriteMutation.mutate(id)} /> )} -
+ ) } diff --git a/frontend/src/stackflow/tabs/settings-tab.tsx b/frontend/src/stackflow/tabs/settings-tab.tsx index 6ef7fbd..c7b84ef 100644 --- a/frontend/src/stackflow/tabs/settings-tab.tsx +++ b/frontend/src/stackflow/tabs/settings-tab.tsx @@ -8,6 +8,7 @@ import { useTheme } from '@/components/theme-provider' import { Button } from '@/components/ui/button' import { ConfirmPopup } from '@/components/ui/confirm-popup' import { PageTitle } from '@/components/ui/page-title' +import { ScrollBody } from '@/components/ui/scroll-body' import { ListGroup } from '@/components/ui/list-group' import { ListGroupItem } from '@/components/ui/list-group-item' import { ListGroupLabel } from '@/components/ui/list-group-label' @@ -46,7 +47,7 @@ export function SettingsTab() { 설정 -
+ -
+