diff --git a/docs/ui-patterns.md b/docs/ui-patterns.md index aeb62c6..873596b 100644 --- a/docs/ui-patterns.md +++ b/docs/ui-patterns.md @@ -146,7 +146,8 @@ UI 표현: ## 9. 모바일 대응 -- 면접 세션은 데스크탑 우선. 모바일 진입 시 "데스크탑 환경 권장" 배너 (dismissable) +- 면접 세션은 데스크탑 우선. 좁은 화면(<768px) 진입 시 1회성 안내 배너 — `SmallScreenNotice` + (dismiss 상태는 localStorage 영속, 마운트 시점 폭만 판정해 리사이즈마다 끼어들지 않음) - 외 페이지: 768px 이하에서 SideNav → Drawer로 전환, TopNav 햄버거 메뉴 --- diff --git a/frontend/src/features/interview/ui/live/InterviewStage.tsx b/frontend/src/features/interview/ui/live/InterviewStage.tsx index dba1717..043bfc3 100644 --- a/frontend/src/features/interview/ui/live/InterviewStage.tsx +++ b/frontend/src/features/interview/ui/live/InterviewStage.tsx @@ -8,6 +8,7 @@ import type { Session } from '@/domain/session' import type { ConnectionStatus, ThreadItem } from '../../model/useLiveInterview' import type { DeliveryMode } from '../../model/useDeliveryMode' import { ConnectionBanner } from './ConnectionBanner' +import { SmallScreenNotice } from './SmallScreenNotice' import { AnswerComposer } from './AnswerComposer' import { StageQuestion } from './StageQuestion' import { DeliveryModeToggle } from './DeliveryModeToggle' @@ -124,9 +125,13 @@ export function InterviewStage({ )}
-면접 화면은 데스크탑 우선으로 만들어졌다 — 웹캠 셀프뷰·질문 카드·컴포저가 한 화면에 + * 있어야 하고 마이크 권한도 필요하다. 좁은 화면에서 뭔가 어긋나 보일 때 이유를 모르면 + * 사용자는 제품이 고장난 줄 안다. 막지는 않고(진행은 가능하다) 기대치만 맞춘다. + */ +// 마운트 시점의 폭만 본다. 리사이즈에 반응하지 않는 건 의도다 — 한 번 안내하는 힌트이지 +// 창을 줄일 때마다 다시 끼어들 이유가 없다. 이펙트가 아니라 lazy 초기값으로 계산한다 +// (이펙트에서 setState 하면 렌더가 한 번 더 돈다 — react-hooks/set-state-in-effect). +function shouldShow(): boolean { + if (typeof window === 'undefined') return false + if (window.innerWidth >= MIN_COMFORTABLE_WIDTH) return false + try { + return window.localStorage.getItem(STORAGE_KEY) !== '1' + } catch { + // 프라이빗 모드 등 저장이 막힌 경우 — 안내는 보여준다. + return true + } +} + +export function SmallScreenNotice() { + const [visible, setVisible] = useState(shouldShow) + + if (!visible) return null + + const dismiss = () => { + setVisible(false) + try { + window.localStorage.setItem(STORAGE_KEY, '1') + } catch { + /* 무시 */ + } + } + + return ( +