웹 접근성 조치
Deploy Frontend / deploy (push) Successful in 9m6s

This commit is contained in:
2026-10-11 15:32:56 +09:00
parent 5ad64a596b
commit 48999f5699
19 changed files with 425 additions and 182 deletions
@@ -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>
);
};