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() { 설정 -
+ -
+