@@ -1,5 +1,7 @@
|
||||
import { useRef, type MouseEvent } from "react";
|
||||
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import type { UseEmblaCarouselType } from "embla-carousel-react";
|
||||
import { Pause, Play } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { getSiteInfo, getBannerList, getImageUrlByAtflCd } from "../../api";
|
||||
@@ -80,6 +82,31 @@ export function Footer() {
|
||||
bannerList?.filter((banner) => banner.bnrTypeCd === "BNRFTER") ?? [];
|
||||
|
||||
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
|
||||
const [carouselApi, setCarouselApi] = useState<UseEmblaCarouselType[1]>();
|
||||
const [isAutoplaying, setIsAutoplaying] = useState(true);
|
||||
|
||||
// 사용자 조작·초점 진입 등으로 자동 넘김이 멈추거나 재개될 때 버튼 상태를 동기화한다
|
||||
useEffect(() => {
|
||||
if (!carouselApi) return;
|
||||
|
||||
const syncAutoplayState = () => setIsAutoplaying(plugin.current.isPlaying());
|
||||
syncAutoplayState();
|
||||
carouselApi.on("autoplay:play", syncAutoplayState);
|
||||
carouselApi.on("autoplay:stop", syncAutoplayState);
|
||||
|
||||
return () => {
|
||||
carouselApi.off("autoplay:play", syncAutoplayState);
|
||||
carouselApi.off("autoplay:stop", syncAutoplayState);
|
||||
};
|
||||
}, [carouselApi]);
|
||||
|
||||
const handleAutoplayToggle = () => {
|
||||
if (plugin.current.isPlaying()) {
|
||||
plugin.current.stop();
|
||||
} else {
|
||||
plugin.current.play();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopupLinkClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -89,9 +116,10 @@ export function Footer() {
|
||||
return (
|
||||
<footer className="flex w-full shrink-0 flex-col items-start border-t border-solid border-border bg-background px-3 pb-1 pt-4 text-foreground sm:flex-row sm:px-6">
|
||||
<div className="mb-4 w-full sm:mb-0 sm:w-[55%]">
|
||||
<h6 className="mb-2 text-lg font-bold sm:mb-4 sm:text-xl md:text-2xl">
|
||||
{/* 제목 수준 건너뜀(h3 → h6) 방지: 사이트명은 섹션 제목이 아니므로 p 사용 */}
|
||||
<p className="mb-2 text-lg font-bold sm:mb-4 sm:text-xl md:text-2xl">
|
||||
{siteInfo?.siteNm ?? ""}
|
||||
</h6>
|
||||
</p>
|
||||
<p className="my-1 text-sm opacity-60 sm:text-base md:text-xl">
|
||||
{siteInfo?.siteAddr ?? ""}
|
||||
</p>
|
||||
@@ -132,10 +160,26 @@ export function Footer() {
|
||||
>지원센타</a>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="mt-4 flex justify-end sm:mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAutoplayToggle}
|
||||
aria-label={isAutoplaying ? "배너 자동 넘김 멈춤" : "배너 자동 넘김 시작"}
|
||||
className="flex items-center gap-1 rounded-sm border border-border bg-background px-2 py-1 text-sm text-accent-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{isAutoplaying ? (
|
||||
<Pause className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<Play className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
<span>{isAutoplaying ? "멈춤" : "시작"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<Carousel
|
||||
setApi={setCarouselApi}
|
||||
plugins={[plugin.current]}
|
||||
opts={{ loop: true }}
|
||||
className="relative my-4 h-20 sm:my-8 sm:h-24"
|
||||
className="relative mb-4 mt-2 h-20 sm:mb-8 sm:h-24"
|
||||
>
|
||||
<CarouselContent>
|
||||
{mainBanners && mainBanners.length > 0 ? (
|
||||
|
||||
@@ -292,7 +292,11 @@ export function Header({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="h-[3rem] w-[15rem]" />
|
||||
// h1 안에는 div(Skeleton)를 둘 수 없어 인라인 요소로 로딩 자리표시
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="block h-[3rem] w-[15rem] animate-pulse rounded-md bg-[rgba(0,0,0,0.1)]"
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="text-[1.2rem] font-semibold text-foreground"
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../dialog";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMenuDetail } from "../../api";
|
||||
import { ENDPOINT } from "../../config";
|
||||
@@ -24,72 +30,78 @@ const PageTitle = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title">{title}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{menuPaths.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="diveicon di-home-fill text-foreground" />
|
||||
{menuPaths?.map((path, i) => (
|
||||
<React.Fragment key={`${i}-${path}`}>
|
||||
{i !== 0 && (
|
||||
<i className="diveicon di-arrow-forward text-foreground" />
|
||||
)}
|
||||
<span className="text-xl text-foreground">{path}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsHelpDialogOpen(true)}
|
||||
className="flex cursor-pointer items-center gap-3 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<img
|
||||
src="/images/help.png"
|
||||
alt="도움말"
|
||||
className="h-[20px] w-[20px]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title">{title}</h2>
|
||||
</div>
|
||||
|
||||
{/* 도움말 다이얼로그 */}
|
||||
<Dialog open={isHelpDialogOpen} onOpenChange={setIsHelpDialogOpen}>
|
||||
<DialogContent className="flex max-h-[90vh] flex-col sm:max-w-[85rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>도움말</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto py-4">
|
||||
{menuDetail?.menuHlpCn ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: menuDetail.menuHlpCn
|
||||
.replace(/<body[^>]*>/gi, "<div>")
|
||||
.replace(/<\/body>/gi, "</div>"),
|
||||
}}
|
||||
style={{
|
||||
fontSize: "inherit",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-lg text-muted-foreground">
|
||||
현재 페이지에 대한 도움말이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{menuPaths.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="diveicon di-home-fill text-foreground" />
|
||||
{menuPaths?.map((path, i) => (
|
||||
<React.Fragment key={`${i}-${path}`}>
|
||||
{i !== 0 && (
|
||||
<i className="diveicon di-arrow-forward text-foreground" />
|
||||
)}
|
||||
<span className="text-xl text-foreground">{path}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 도움말 다이얼로그: DialogTrigger 로 연결해야 닫을 때 키보드 초점이 도움말 버튼으로 복귀한다 */}
|
||||
<Dialog open={isHelpDialogOpen} onOpenChange={setIsHelpDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-3 rounded-sm transition-opacity hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<img
|
||||
src="/images/help.png"
|
||||
alt="도움말"
|
||||
className="h-[20px] w-[20px]"
|
||||
/>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="flex max-h-[90vh] flex-col sm:max-w-[85rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>도움말</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* 스크롤 영역이 초점을 받을 수 있어야 키보드(Tab/방향키)로 내용 이동·스크롤이 가능하다 */}
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label="도움말 내용"
|
||||
className="min-h-0 flex-1 overflow-y-auto rounded-sm py-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
||||
>
|
||||
{menuDetail?.menuHlpCn ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: menuDetail.menuHlpCn
|
||||
.replace(/<body[^>]*>/gi, "<div>")
|
||||
.replace(/<\/body>/gi, "</div>"),
|
||||
}}
|
||||
style={{
|
||||
fontSize: "inherit",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-lg text-muted-foreground">
|
||||
현재 페이지에 대한 도움말이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user