Claude Code · claude-opus-5-5 · React (TSX)
라이트박스가 열려 있는 동안 document.body 의 overflow 를 잠시 hidden 으로 바꿉니다.
결과 코드펼치기
import { useCallback, useEffect, useRef, useState } from "react";
const GROUPS: { title: string; photos: { src: string; name: string }[] }[] = [
{
title: "로비 · 라운지",
photos: [
{ src: "https://nnuer.site/media/FYtz8JVI2ZgB", name: "로비" },
{ src: "https://nnuer.site/media/ACZ8f8usUHbc", name: "창가 라운지" },
{ src: "https://nnuer.site/media/oKFbMVX4tS5I", name: "테라스 카페" },
],
},
{
title: "스튜디오",
photos: [
{ src: "https://nnuer.site/media/betlMhmdcBcN", name: "스튜디오 A 작업대" },
{ src: "https://nnuer.site/media/mFeUmbAeXyFY", name: "스튜디오 A 리뷰석" },
{ src: "https://nnuer.site/media/lSBnvazHctQ3", name: "스튜디오 B 작업대" },
{ src: "https://nnuer.site/media/vSAVTa0EygQF", name: "스튜디오 B 리뷰석" },
{ src: "https://nnuer.site/media/RA2andSsj9Mo", name: "편집 코너" },
{ src: "https://nnuer.site/media/6ayCkPRaDLTD", name: "보정 데스크" },
],
},
{
title: "장비실",
photos: [
{ src: "https://nnuer.site/media/PniLADeFOiqT", name: "대여 장비 진열" },
{ src: "https://nnuer.site/media/tKB7ObZUD2H1", name: "액세서리 보관함" },
{ src: "https://nnuer.site/media/l1fT68SfxHEQ", name: "필름 카메라 코너" },
],
},
{
title: "개인 작업실",
photos: [
{ src: "https://nnuer.site/media/FQ0OkmAl2ny6", name: "작업실 1" },
{ src: "https://nnuer.site/media/7t5KQTMBq53J", name: "작업실 2" },
{ src: "https://nnuer.site/media/SXQxsQ8yvCVI", name: "작업실 3" },
],
},
];
const ALL = GROUPS.flatMap((g) => g.photos);
const FONT = '"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif';
export default function FacilityGallery() {
const [index, setIndex] = useState<number | null>(null);
const opener = useRef<HTMLButtonElement | null>(null);
const closeBtn = useRef<HTMLButtonElement>(null);
const close = useCallback(() => {
setIndex(null);
opener.current?.focus();
}, []);
const move = useCallback((step: number) => {
setIndex((i) => (i === null ? i : (i + step + ALL.length) % ALL.length));
}, []);
useEffect(() => {
if (index === null) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
if (e.key === "ArrowLeft") move(-1);
if (e.key === "ArrowRight") move(1);
};
document.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prev;
document.removeEventListener("keydown", onKey);
};
}, [index, close, move]);
useEffect(() => {
if (index !== null) closeBtn.current?.focus();
}, [index !== null]); // eslint-disable-line react-hooks/exhaustive-deps
let flat = -1;
return (
<section className="w-full bg-white" style={{ fontFamily: FONT }}>
<div className="mx-auto w-full max-w-[1200px] px-4 py-12 sm:px-6 lg:py-16">
{GROUPS.map((g, gi) => (
<section key={g.title} className={gi > 0 ? "mt-[72px]" : ""}>
<h2 className="mb-5 text-2xl font-extrabold text-[#172031]">{g.title}</h2>
<ul className="grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-3">
{g.photos.map((p) => {
flat += 1;
const i = flat;
return (
<li key={p.src}>
<button
type="button"
onClick={(e) => {
opener.current = e.currentTarget;
setIndex(i);
}}
className="group block w-full text-left"
>
<span className="block aspect-[4/3] overflow-hidden rounded-2xl bg-[#f4f6f9]">
<img src={p.src} alt="" loading="lazy" className="h-full w-full object-cover transition-transform duration-[400ms] group-hover:scale-105" />
</span>
<span className="mt-3 block text-sm font-bold text-[#172031] sm:text-base">{p.name}</span>
</button>
</li>
);
})}
</ul>
</section>
))}
</div>
{index !== null && (
<div
role="dialog"
aria-modal="true"
aria-label="공간 사진 크게 보기"
onClick={(e) => {
if (e.target === e.currentTarget) close();
}}
className="fixed inset-0 z-[300] flex items-center justify-center bg-[rgba(12,17,27,0.94)] px-3 py-16 sm:px-20 sm:py-14"
>
<figure className="m-0 w-full max-w-[1200px]">
<img src={ALL[index].src} alt={ALL[index].name} className="mx-auto max-h-[calc(100vh-160px)] rounded-[10px]" />
<figcaption className="mt-3.5 text-center font-semibold text-white">
{ALL[index].name}
<span className="ml-2.5 tabular-nums text-white/50">
{index + 1} / {ALL.length}
</span>
</figcaption>
</figure>
<button ref={closeBtn} type="button" onClick={close} className="absolute right-5 top-5 h-[52px] rounded-full bg-white/10 px-5 font-bold text-white hover:bg-[#F5C842] hover:text-[#1B263B]">
닫기
</button>
<button
type="button"
onClick={() => move(-1)}
aria-label="이전 사진"
className="absolute bottom-4 left-4 flex h-[52px] w-[52px] items-center justify-center rounded-full bg-white/10 text-white hover:bg-[#F5C842] hover:text-[#1B263B] sm:bottom-auto sm:top-1/2 sm:-translate-y-1/2"
>
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<button
type="button"
onClick={() => move(1)}
aria-label="다음 사진"
className="absolute bottom-4 right-4 flex h-[52px] w-[52px] items-center justify-center rounded-full bg-white/10 text-white hover:bg-[#F5C842] hover:text-[#1B263B] sm:bottom-auto sm:top-1/2 sm:-translate-y-1/2"
>
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</div>
)}
</section>
);
}
적용 사이트 0
이 블록이 실제로 쓰인 사이트입니다.
아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.
재현 후기
v1 · 0개아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.