diff --git a/src/components/HeaderTableInfo/HeaderTableInfo.tsx b/src/components/HeaderTableInfo/HeaderTableInfo.tsx index 7401405a..bb3a8f26 100644 --- a/src/components/HeaderTableInfo/HeaderTableInfo.tsx +++ b/src/components/HeaderTableInfo/HeaderTableInfo.tsx @@ -17,8 +17,8 @@ export default function HeaderTableInfo(props: HeaderTitleProps) { )} {!isLoading && ( -
-

{name}

+
+

{name}

)} diff --git a/src/components/HeaderTitle/HeaderTitle.tsx b/src/components/HeaderTitle/HeaderTitle.tsx index db0d1df0..a8992b18 100644 --- a/src/components/HeaderTitle/HeaderTitle.tsx +++ b/src/components/HeaderTitle/HeaderTitle.tsx @@ -16,7 +16,7 @@ export default function HeaderTitle(props: HeaderTitleProps) {
)} {!isLoading && ( -

+

{title}

)} diff --git a/src/components/TimerProgressBar/TimerProgressBar.tsx b/src/components/TimerProgressBar/TimerProgressBar.tsx index fb720bb5..899c4564 100644 --- a/src/components/TimerProgressBar/TimerProgressBar.tsx +++ b/src/components/TimerProgressBar/TimerProgressBar.tsx @@ -48,7 +48,7 @@ export default function TimerProgressBar({ return (
-
+
+
{children}
); } -StickyTriSectionHeader.Left = function Left(props: PropsWithChildren) { - const { children } = props; +interface SectionProps extends PropsWithChildren { + className?: string; +} + +StickyTriSectionHeader.Left = function Left(props: SectionProps) { + const { children, className } = props; return ( -
+
{children}
); }; -StickyTriSectionHeader.Center = function Center(props: PropsWithChildren) { - const { children } = props; +StickyTriSectionHeader.Center = function Center(props: SectionProps) { + const { children, className } = props; return ( -
+
{children}
); @@ -72,7 +87,7 @@ StickyTriSectionHeader.Right = function Right(props: PropsWithChildren) { return ( <> -
+
{isGuestFlow() && (
{t('비회원 모드')} diff --git a/src/layout/components/main/ContentContainer.tsx b/src/layout/components/main/ContentContainer.tsx index 6b252ed8..2aab98e8 100644 --- a/src/layout/components/main/ContentContainer.tsx +++ b/src/layout/components/main/ContentContainer.tsx @@ -11,7 +11,7 @@ export default function ContentContainer({ return (
{children} diff --git a/src/layout/defaultLayout/DefaultLayout.tsx b/src/layout/defaultLayout/DefaultLayout.tsx index f9e7d8d1..b62be81f 100644 --- a/src/layout/defaultLayout/DefaultLayout.tsx +++ b/src/layout/defaultLayout/DefaultLayout.tsx @@ -6,7 +6,11 @@ import ContentContainer from '../components/main/ContentContainer'; function DefaultLayout(props: PropsWithChildren) { const { children } = props; - return
{children}
; + return ( +
+ {children} +
+ ); } DefaultLayout.Header = StickyTriSectionHeader; diff --git a/src/page/AudienceSharePage/AudienceSharePage.test.tsx b/src/page/AudienceSharePage/AudienceSharePage.test.tsx index 3198e25d..e20245a8 100644 --- a/src/page/AudienceSharePage/AudienceSharePage.test.tsx +++ b/src/page/AudienceSharePage/AudienceSharePage.test.tsx @@ -223,7 +223,9 @@ describe('AudienceSharePage', () => { await screen.findByText('토론 시작을 대기 중입니다.'), ).toBeInTheDocument(); expect(screen.getByText('공유 토론 테이블')).toBeInTheDocument(); - expect(screen.getByText('공유 토론 주제')).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: '공유 토론 주제' }), + ).toBeInTheDocument(); expect( screen.getByRole('button', { name: '언어 선택' }), ).toBeInTheDocument(); @@ -259,7 +261,25 @@ describe('AudienceSharePage', () => { renderPage('/live/123'); expect(await screen.findByText('테이블 이름 없음')).toBeInTheDocument(); - expect(screen.getByText('주제 없음')).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: '주제 없음' }), + ).toBeInTheDocument(); + }); + + it('모바일에서는 헤더 대신 본문 상단에 주제를 표시한다', async () => { + mockUseAudienceShareState.mockReturnValue({ + status: 'waiting', + error: null, + }); + renderPage('/live/123'); + + const mobileAgenda = await screen.findByTestId('mobile-agenda'); + + expect(mobileAgenda).toHaveTextContent('공유 토론 주제'); + expect(mobileAgenda).toHaveClass('md:hidden'); + expect( + screen.getByRole('heading', { name: '공유 토론 주제' }).parentElement, + ).toHaveClass('hidden', 'md:flex'); }); it('displaying 상태 (NORMAL)에서는 AudienceNormalTimer에 올바른 props가 전달된다', async () => { diff --git a/src/page/AudienceSharePage/AudienceSharePage.tsx b/src/page/AudienceSharePage/AudienceSharePage.tsx index 9bea16b7..98df4cb8 100644 --- a/src/page/AudienceSharePage/AudienceSharePage.tsx +++ b/src/page/AudienceSharePage/AudienceSharePage.tsx @@ -45,7 +45,7 @@ function ErrorContent({ message, onReload }: ErrorContentProps) { data-testid="audience-share-error-icon" aria-hidden="true" /> -

+

{message}

@@ -144,7 +144,7 @@ export default function AudienceSharePage() { case 'NORMAL_TIMER': return ( -
+
+
- {isHeaderVisible && debateTableQuery.data ? ( + {shouldShowHeader ? ( - + - - + {/* 세로 모드 모바일에서는 헤더 공간이 좁아 주제를 본문 상단에 표시 */} + + ) : null}
- {renderContent()} + {shouldShowHeader ? ( +

+ {agendaLabel} +

+ ) : null} +
{renderContent()}
diff --git a/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx b/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx index d9de53dd..416fd653 100644 --- a/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx +++ b/src/page/AudienceSharePage/components/AudienceNormalTimer.test.tsx @@ -23,12 +23,12 @@ describe('AudienceNormalTimer', () => { const timerValue = screen.getByTestId('timer-value'); expect(debateIcon).toHaveAttribute('aria-hidden', 'true'); - expect(debateIcon).toHaveClass('h-[34px]'); - expect(participantRow).toHaveClass('mt-[24px]', 'text-[34px]'); + expect(debateIcon).toHaveClass('h-[20px]', 'md:h-[34px]'); + expect(participantRow).toHaveClass('md:mt-[24px]', 'md:text-[34px]'); expect(screen.getByText('찬성 팀')).toBeInTheDocument(); expect(screen.getByText('김토론 토론자')).toBeInTheDocument(); expect(timerValue).toHaveClass( - 'mt-[64px]', + 'md:mt-[64px]', 'grid-cols-[2ch_1ch_2ch]', 'gap-x-[0.33ch]', ); @@ -40,7 +40,7 @@ describe('AudienceNormalTimer', () => { 'aria-valuenow', '50', ); - expect(screen.getByRole('progressbar')).toHaveClass('mt-[108px]'); + expect(screen.getByRole('progressbar')).toHaveClass('md:mt-[108px]'); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); diff --git a/src/page/AudienceSharePage/components/AudienceNormalTimer.tsx b/src/page/AudienceSharePage/components/AudienceNormalTimer.tsx index 0e2033e6..5192d4b6 100644 --- a/src/page/AudienceSharePage/components/AudienceNormalTimer.tsx +++ b/src/page/AudienceSharePage/components/AudienceNormalTimer.tsx @@ -50,18 +50,18 @@ export default function AudienceNormalTimer({ const isOvertime = remainingTime < 0; return ( -
-

+
+

{speechTypeLabel}

{stance !== 'NEUTRAL' && (
); diff --git a/src/page/AudienceSharePage/components/AudienceTimeBasedTimer.tsx b/src/page/AudienceSharePage/components/AudienceTimeBasedTimer.tsx index 61e37595..f9d66271 100644 --- a/src/page/AudienceSharePage/components/AudienceTimeBasedTimer.tsx +++ b/src/page/AudienceSharePage/components/AudienceTimeBasedTimer.tsx @@ -28,7 +28,7 @@ export default function AudienceTimeBasedTimer({ }: AudienceTimeBasedTimerProps) { return (
{ ); expect(screen.getByText('전체 시간')).toHaveClass( - 'h-[48px]', - 'w-[144px]', + 'md:h-[48px]', + 'md:w-[144px]', 'bg-default-black', - 'text-[24px]', + 'md:text-[24px]', ); expect(screen.getByText('현재 시간')).toHaveClass( - 'h-[64px]', - 'w-[200px]', + 'md:h-[64px]', + 'md:w-[200px]', 'bg-camp-red', - 'text-[32px]', - ); + 'md:text-[32px]', + ); + // 모바일(768px 미만, 가로 모드)에서는 배지를 화면에서만 숨기고 스크린 리더에는 남긴다 + ['전체 시간', '현재 시간'].forEach((label) => { + expect(screen.getByText(label)).toHaveClass( + 'sr-only', + 'md:not-sr-only', + 'short:sr-only', + ); + }); expect(screen.getByTestId('cons-total-timer')).toHaveAttribute( 'aria-label', '04 : 00', diff --git a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx index b0ccc3d3..be1e69f5 100644 --- a/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx +++ b/src/page/AudienceSharePage/components/AudienceTimeBasedTimerDisplay.tsx @@ -35,7 +35,7 @@ function TimerValue({ return ( @@ -90,7 +90,7 @@ export default function AudienceTimeBasedTimerDisplay({

@@ -103,20 +103,21 @@ export default function AudienceTimeBasedTimerDisplay({ seconds={totalRemainingTime} testId={`${teamId}-total-timer`} isDisabled={!isCurrentTeam} - className="mt-[64px] text-[70px] xl:text-[110px]" + className="mt-[24px] text-[length:min(11vw,64px)] md:mt-[64px] md:text-[70px] xl:text-[110px] short:mt-[8px] short:text-[length:min(22vh,96px)]" /> ) : ( <> + {/* 모바일(세로·가로)에서는 배지를 화면에서 숨기고 글자 크기 차이로 전체/현재 시간을 구분 */} @@ -126,11 +127,11 @@ export default function AudienceTimeBasedTimerDisplay({ seconds={totalRemainingTime} testId={`${teamId}-total-timer`} isDisabled={!isCurrentTeam} - className="mt-[12px] text-[108px]" + className="mt-[16px] text-[length:min(7.7vw,45px)] md:mt-[12px] md:text-[48px] lg:text-[56px] xl:text-[72px] short:mt-[8px] short:text-[length:min(14vh,45px)]" /> )} diff --git a/src/page/AudienceSharePage/stories/AudienceShareSimulator.stories.tsx b/src/page/AudienceSharePage/stories/AudienceShareSimulator.stories.tsx index 8ed8df56..ee1898fa 100644 --- a/src/page/AudienceSharePage/stories/AudienceShareSimulator.stories.tsx +++ b/src/page/AudienceSharePage/stories/AudienceShareSimulator.stories.tsx @@ -216,8 +216,9 @@ function AudienceShareSimulator() { }; return ( -
-
+ // 모바일에서는 청중 화면을 실제 크기로 먼저 보여주고, 컨트롤 패널은 아래로 스크롤해 사용 +
+
} /> } /> diff --git a/tailwind.config.js b/tailwind.config.js index 2c477a6e..175c8500 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -32,7 +32,8 @@ export default { }, }, backgroundImage: { - brandBackground: 'radial-gradient(80% 80% at 50% 50%, #fecd4c21 0%, #ffffff42 100%)', + brandBackground: + 'radial-gradient(80% 80% at 50% 50%, #fecd4c21 0%, #ffffff42 100%)', }, animation: { rotate: 'rotate 5s linear infinite', @@ -103,6 +104,8 @@ export default { md: '768px', lg: '1280px', xl: { raw: '(min-width: 1600px) and (min-height: 1024px)' }, + // 가로 모드 휴대폰처럼 세로 공간이 좁은 화면 + short: { raw: '(max-height: 500px)' }, }, }, };