From 1e38563a48574ae1b9ef39e6b17890c54229033e Mon Sep 17 00:00:00 2001 From: choihooo Date: Wed, 11 Mar 2026 13:27:23 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix(a11y):=20Skip-to-Content=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=88=AC=EB=AA=85=20=EC=B2=98=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - opacity: 0 + pointer-events: none으로 기본 상태에서 안 보이게 처리 - 포커스 시에만 opacity: 1로 표시되도록 개선 - left: -9999px 방식 제거 (스크린 리더 호환성 개선) Co-Authored-By: Claude Sonnet 4.6 --- packages/web/src/app/globals.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 8ae903f..0e2aba4 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -315,7 +315,7 @@ /* ── Skip to Content Link (Accessibility) ── */ .skip-to-content { position: absolute; - left: -9999px; + left: 0; top: 0; z-index: 9999; padding: 1rem 2rem; @@ -325,10 +325,13 @@ border-radius: 0 0 0.5rem 0; border: 1px solid hsl(var(--border)); border-top: none; + opacity: 0; + pointer-events: none; } .skip-to-content:focus { - left: 0; + opacity: 1; + pointer-events: auto; outline: 2px solid hsl(var(--ring)); outline-offset: -2px; } From 6f075394f6ae62dc2c2f15c2feb5d6ba62b0cf8d Mon Sep 17 00:00:00 2001 From: choihooo Date: Wed, 11 Mar 2026 15:46:53 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?=EA=B3=BC=20=EB=8B=B9=EA=B2=A8=EC=84=9C=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/src/components/layout/main-layout.tsx | 13 ++++++--- .../src/components/layout/notice-banner.tsx | 29 +++++++++++++++++-- .../src/components/layout/pull-to-refresh.tsx | 2 +- .../web/src/components/layout/sidebar.tsx | 9 +++--- packages/web/src/hooks/use-pull-to-refresh.ts | 17 +++++++++-- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/web/src/components/layout/main-layout.tsx b/packages/web/src/components/layout/main-layout.tsx index 93d367f..e145be7 100644 --- a/packages/web/src/components/layout/main-layout.tsx +++ b/packages/web/src/components/layout/main-layout.tsx @@ -38,12 +38,17 @@ function MainContent({ id="main-content" tabIndex={-1} className={cn( - 'flex-1 min-w-0 transition-[margin-left] duration-200', + 'flex-1 min-w-0 min-h-0 transition-[margin-left] duration-200', showSidebar && (collapsed ? 'md:ml-16' : 'md:ml-60'), 'pb-20 md:pb-0' // bottom nav spacing on mobile )} > -
{children}
+
+ {children} +
); } @@ -81,14 +86,14 @@ export function MainLayout({ }; return ( -
+
본문으로 바로가기
{!isAdmin && } -
+
{showSidebar && ( void; +} + +export function NoticeBanner({ onHeightChange }: NoticeBannerProps) { const [notice, setNotice] = useState(null); const [bannerState, setBannerState] = useState('open'); const [loaded, setLoaded] = useState(false); + const bannerRef = useRef(null); useEffect(() => { fetch('/api/notice-banner') @@ -56,6 +61,25 @@ export function NoticeBanner() { .catch(() => setLoaded(true)); }, []); + useEffect(() => { + if (!loaded || !notice || bannerState === 'closed') { + onHeightChange?.(0); + return; + } + + const element = bannerRef.current; + if (!element) return; + + const updateHeight = () => onHeightChange?.(element.offsetHeight); + + updateHeight(); + + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + + return () => observer.disconnect(); + }, [bannerState, loaded, notice, onHeightChange]); + if (!loaded || !notice || bannerState === 'closed') return null; const handleToggle = () => { @@ -78,6 +102,7 @@ export function NoticeBanner() { return (
{/* Content wrapper that translates down */} -
+
{children}
diff --git a/packages/web/src/components/layout/sidebar.tsx b/packages/web/src/components/layout/sidebar.tsx index 14906f1..02d23a2 100644 --- a/packages/web/src/components/layout/sidebar.tsx +++ b/packages/web/src/components/layout/sidebar.tsx @@ -129,9 +129,6 @@ function SidebarContent({ }: SidebarContentProps) { return (
- {/* Spacer to match header height */} -
- {/* ── Primary navigation ───────────────────────────────────────── */}