123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState, useRef } from "react";
|
|
|
|
interface ProjectPageViewControlsProps {
|
|
slugs: string[];
|
|
trackId: string;
|
|
}
|
|
|
|
export default function ProjectPageViewControls({
|
|
slugs,
|
|
trackId,
|
|
}: ProjectPageViewControlsProps) {
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
const [canScrollRight, setCanScrollRight] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const track = document.getElementById(trackId);
|
|
if (!track) return;
|
|
|
|
const checkScroll = () => {
|
|
const { scrollLeft, scrollWidth, clientWidth } = track;
|
|
setCanScrollLeft(scrollLeft > 5);
|
|
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 5);
|
|
};
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
const id = entry.target.id;
|
|
const index = slugs.findIndex((s) => `project-${s}` === id);
|
|
if (index !== -1) {
|
|
setActiveIndex(index);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{ root: track, threshold: 0.6 }
|
|
);
|
|
|
|
slugs.forEach((slug) => {
|
|
const el = document.getElementById(`project-${slug}`);
|
|
if (el) observer.observe(el);
|
|
});
|
|
|
|
track.addEventListener("scroll", checkScroll, { passive: true });
|
|
checkScroll();
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
track.removeEventListener("scroll", checkScroll);
|
|
};
|
|
}, [slugs, trackId]);
|
|
|
|
const scroll = (direction: "left" | "right") => {
|
|
const track = document.getElementById(trackId);
|
|
if (!track) return;
|
|
const cardWidth = track.firstElementChild?.clientWidth || 300;
|
|
const delta = direction === "left" ? -(cardWidth + 24) : cardWidth + 24;
|
|
|
|
const prefersReducedMotion = window.matchMedia(
|
|
"(prefers-reduced-motion: reduce)"
|
|
).matches;
|
|
|
|
track.scrollBy({
|
|
left: delta,
|
|
behavior: prefersReducedMotion ? "auto" : "smooth",
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-between mt-6 pt-4 border-t border-line desktop:hidden">
|
|
{/* Indicator dots using semantic anchor links */}
|
|
<div className="flex items-center gap-2" role="tablist" aria-label="프로젝트 위치">
|
|
{slugs.map((slug, idx) => (
|
|
<a
|
|
key={slug}
|
|
href={`#project-${slug}`}
|
|
role="tab"
|
|
aria-selected={activeIndex === idx}
|
|
aria-label={`${slugs.length}개 중 ${idx + 1}번째 프로젝트로 이동`}
|
|
className={`h-2.5 rounded-full transition-all duration-300 ${
|
|
activeIndex === idx
|
|
? "w-8 bg-vermillion"
|
|
: "w-2.5 bg-line hover:bg-ink-mute"
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => scroll("left")}
|
|
disabled={!canScrollLeft}
|
|
aria-label="이전 프로젝트"
|
|
aria-controls={trackId}
|
|
className="inline-flex items-center justify-center w-9 h-9 border border-line bg-paper text-ink transition hover:border-vermillion hover:text-vermillion disabled:opacity-30 disabled:pointer-events-none rounded"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => scroll("right")}
|
|
disabled={!canScrollRight}
|
|
aria-label="다음 프로젝트"
|
|
aria-controls={trackId}
|
|
className="inline-flex items-center justify-center w-9 h-9 border border-line bg-paper text-ink transition hover:border-vermillion hover:text-vermillion disabled:opacity-30 disabled:pointer-events-none rounded"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|