Merge branch 'feature/site-features-and-mockup'
This commit is contained in:
commit
8d6e6d7cd0
@ -1,6 +1,7 @@
|
|||||||
import {useEffect, useRef} from 'react';
|
import {useEffect, useRef} from 'react';
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
import {X} from 'lucide-react';
|
import {X} from 'lucide-react';
|
||||||
|
import {useScrollLock} from './use-scroll-lock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 바텀시트(모바일) · 가운데 카드(데스크톱) 모달 한 장.
|
* 바텀시트(모바일) · 가운데 카드(데스크톱) 모달 한 장.
|
||||||
@ -29,30 +30,15 @@ export function Modal({
|
|||||||
const onCloseRef = useRef(onClose);
|
const onCloseRef = useRef(onClose);
|
||||||
onCloseRef.current = onClose;
|
onCloseRef.current = onClose;
|
||||||
|
|
||||||
|
useScrollLock(open);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
const onKey = (event: KeyboardEvent) => {
|
const onKey = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') onCloseRef.current();
|
if (event.key === 'Escape') onCloseRef.current();
|
||||||
};
|
};
|
||||||
const scrollY = window.scrollY;
|
|
||||||
const body = document.body;
|
|
||||||
const prevPosition = body.style.position;
|
|
||||||
const prevTop = body.style.top;
|
|
||||||
const prevWidth = body.style.width;
|
|
||||||
const prevOverflow = body.style.overflow;
|
|
||||||
body.style.position = 'fixed';
|
|
||||||
body.style.top = `-${scrollY}px`;
|
|
||||||
body.style.width = '100%';
|
|
||||||
body.style.overflow = 'hidden';
|
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => {
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
body.style.position = prevPosition;
|
|
||||||
body.style.top = prevTop;
|
|
||||||
body.style.width = prevWidth;
|
|
||||||
body.style.overflow = prevOverflow;
|
|
||||||
window.scrollTo(0, scrollY);
|
|
||||||
window.removeEventListener('keydown', onKey);
|
|
||||||
};
|
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|||||||
@ -3,3 +3,4 @@ export {Carousel, CarouselSlide} from './Carousel';
|
|||||||
export {Card, Chip, Field, ActionLink} from './primitives';
|
export {Card, Chip, Field, ActionLink} from './primitives';
|
||||||
export {ShowMore} from './ShowMore';
|
export {ShowMore} from './ShowMore';
|
||||||
export {Modal} from './Modal';
|
export {Modal} from './Modal';
|
||||||
|
export {useScrollLock} from './use-scroll-lock';
|
||||||
|
|||||||
39
solution/site/src/lib/ui/use-scroll-lock.ts
Normal file
39
solution/site/src/lib/ui/use-scroll-lock.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 모달·오버레이가 열려 있는 동안 배경 스크롤을 잠근다 — **한 벌만 둔다.**
|
||||||
|
*
|
||||||
|
* ★ 왜 훅으로 뺐나 (2026-09-21, 사장님: "이거 목업 하나만 해결하면 끝이야? 훅으로 만들어놓던가")
|
||||||
|
* 같은 잠금·복귀 코드가 Modal · GallerySection · EventSection 세 곳에 따로 박혀 있었다.
|
||||||
|
* 모달 하나만 고치고 나니 나머지 둘은 그대로 남아 같은 스크롤 버그가 다시 났다 —
|
||||||
|
* 복사한 코드는 복사한 곳마다 따로 고쳐야 한다.
|
||||||
|
* ★ 복귀는 즉시 이동이어야 한다. `index.css` 의 전역 `html { scroll-behavior: smooth }` 때문에
|
||||||
|
* `window.scrollTo` 가 애니메이션으로 움직이며 "위로 튀었다 내려오는" 것처럼 보인다 —
|
||||||
|
* 이 한 번만 `scroll-behavior: auto` 로 강제한다.
|
||||||
|
*/
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
export function useScrollLock(active: boolean) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const body = document.body;
|
||||||
|
const prevPosition = body.style.position;
|
||||||
|
const prevTop = body.style.top;
|
||||||
|
const prevWidth = body.style.width;
|
||||||
|
const prevOverflow = body.style.overflow;
|
||||||
|
body.style.position = 'fixed';
|
||||||
|
body.style.top = `-${scrollY}px`;
|
||||||
|
body.style.width = '100%';
|
||||||
|
body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
body.style.position = prevPosition;
|
||||||
|
body.style.top = prevTop;
|
||||||
|
body.style.width = prevWidth;
|
||||||
|
body.style.overflow = prevOverflow;
|
||||||
|
const root = document.documentElement;
|
||||||
|
const prevScrollBehavior = root.style.scrollBehavior;
|
||||||
|
root.style.scrollBehavior = 'auto';
|
||||||
|
window.scrollTo(0, scrollY);
|
||||||
|
root.style.scrollBehavior = prevScrollBehavior;
|
||||||
|
};
|
||||||
|
}, [active]);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import {useCallback, useEffect, useState} from 'react';
|
|||||||
import {ChevronLeft, ChevronRight, X} from 'lucide-react';
|
import {ChevronLeft, ChevronRight, X} from 'lucide-react';
|
||||||
import {useSite} from '@site/lib/site-context';
|
import {useSite} from '@site/lib/site-context';
|
||||||
import {galleryImages} from '@site/lib/derive';
|
import {galleryImages} from '@site/lib/derive';
|
||||||
import {Carousel, CarouselSlide, Section} from '@site/lib/ui';
|
import {Carousel, CarouselSlide, Section, useScrollLock} from '@site/lib/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사진 갤러리.
|
* 사진 갤러리.
|
||||||
@ -29,6 +29,8 @@ export function GallerySection() {
|
|||||||
|
|
||||||
// 크게 보기 중에는 Esc 로 닫고 좌우 키로 넘긴다. 뒤 배경은 스크롤을 잠근다 —
|
// 크게 보기 중에는 Esc 로 닫고 좌우 키로 넘긴다. 뒤 배경은 스크롤을 잠근다 —
|
||||||
// 잠그지 않으면 모달 위에서 휠을 굴렸을 때 뒤 페이지가 밀린다.
|
// 잠그지 않으면 모달 위에서 휠을 굴렸을 때 뒤 페이지가 밀린다.
|
||||||
|
useScrollLock(openIndex !== null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openIndex === null) return;
|
if (openIndex === null) return;
|
||||||
const onKey = (event: KeyboardEvent) => {
|
const onKey = (event: KeyboardEvent) => {
|
||||||
@ -36,25 +38,8 @@ export function GallerySection() {
|
|||||||
if (event.key === 'ArrowLeft') step(-1);
|
if (event.key === 'ArrowLeft') step(-1);
|
||||||
if (event.key === 'ArrowRight') step(1);
|
if (event.key === 'ArrowRight') step(1);
|
||||||
};
|
};
|
||||||
const scrollY = window.scrollY;
|
|
||||||
const body = document.body;
|
|
||||||
const prevPosition = body.style.position;
|
|
||||||
const prevTop = body.style.top;
|
|
||||||
const prevWidth = body.style.width;
|
|
||||||
const prevOverflow = body.style.overflow;
|
|
||||||
body.style.position = 'fixed';
|
|
||||||
body.style.top = `-${scrollY}px`;
|
|
||||||
body.style.width = '100%';
|
|
||||||
body.style.overflow = 'hidden';
|
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => {
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
body.style.position = prevPosition;
|
|
||||||
body.style.top = prevTop;
|
|
||||||
body.style.width = prevWidth;
|
|
||||||
body.style.overflow = prevOverflow;
|
|
||||||
window.scrollTo(0, scrollY);
|
|
||||||
window.removeEventListener('keydown', onKey);
|
|
||||||
};
|
|
||||||
}, [openIndex, close, step]);
|
}, [openIndex, close, step]);
|
||||||
|
|
||||||
if (images.length === 0) return null;
|
if (images.length === 0) return null;
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
* 카드가 무엇을 흉내 내는지 한눈에 보여야 목록이 아니라 물건으로 읽힌다.
|
* 카드가 무엇을 흉내 내는지 한눈에 보여야 목록이 아니라 물건으로 읽힌다.
|
||||||
*/
|
*/
|
||||||
import {useCallback, useEffect, useState} from 'react';
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
import {ShowMore} from '@site/lib/ui';
|
import {ShowMore, useScrollLock} from '@site/lib/ui';
|
||||||
import {X} from 'lucide-react';
|
import {X} from 'lucide-react';
|
||||||
import type {EventItem} from '@o2o/shared';
|
import type {EventItem} from '@o2o/shared';
|
||||||
import {useSite} from '@site/lib/site-context';
|
import {useSite} from '@site/lib/site-context';
|
||||||
@ -90,30 +90,15 @@ export function EventSection() {
|
|||||||
const close = useCallback(() => setOpen(null), []);
|
const close = useCallback(() => setOpen(null), []);
|
||||||
|
|
||||||
// 창이 열려 있는 동안 Esc 로 닫고 뒤 배경 스크롤을 잠근다 — 안 잠그면 창 위에서 뒤가 밀린다.
|
// 창이 열려 있는 동안 Esc 로 닫고 뒤 배경 스크롤을 잠근다 — 안 잠그면 창 위에서 뒤가 밀린다.
|
||||||
|
useScrollLock(open !== null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open === null) return;
|
if (open === null) return;
|
||||||
const onKey = (event: KeyboardEvent) => {
|
const onKey = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') close();
|
if (event.key === 'Escape') close();
|
||||||
};
|
};
|
||||||
const scrollY = window.scrollY;
|
|
||||||
const body = document.body;
|
|
||||||
const prevPosition = body.style.position;
|
|
||||||
const prevTop = body.style.top;
|
|
||||||
const prevWidth = body.style.width;
|
|
||||||
const prevOverflow = body.style.overflow;
|
|
||||||
body.style.position = 'fixed';
|
|
||||||
body.style.top = `-${scrollY}px`;
|
|
||||||
body.style.width = '100%';
|
|
||||||
body.style.overflow = 'hidden';
|
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => {
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
body.style.position = prevPosition;
|
|
||||||
body.style.top = prevTop;
|
|
||||||
body.style.width = prevWidth;
|
|
||||||
body.style.overflow = prevOverflow;
|
|
||||||
window.scrollTo(0, scrollY);
|
|
||||||
window.removeEventListener('keydown', onKey);
|
|
||||||
};
|
|
||||||
}, [open, close]);
|
}, [open, close]);
|
||||||
|
|
||||||
if (parsed.items.length === 0) return null;
|
if (parsed.items.length === 0) return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user