Compare commits

...
2 Commits
Author SHA1 Message Date
jcchoi a8eb8f9ab1 로고 alt 작업
Deploy Frontend / deploy (push) Successful in 7m43s
2026-10-11 16:16:14 +09:00
jcchoi 6471f4365a 접근성 2026-10-11 16:14:08 +09:00
8 changed files with 76 additions and 53 deletions
@@ -115,13 +115,13 @@ function AcntgCodeTabPanel() {
</colgroup>
<TableHeader>
<TableRow>
<TableHead>코드</TableHead>
<TableHead>코드명</TableHead>
<TableHead>설명</TableHead>
<TableHead>사용 여부</TableHead>
<TableHead>정렬순서</TableHead>
<TableHead>구분</TableHead>
<TableHead>관리</TableHead>
<TableHead scope="col">코드</TableHead>
<TableHead scope="col">코드명</TableHead>
<TableHead scope="col">설명</TableHead>
<TableHead scope="col">사용 여부</TableHead>
<TableHead scope="col">정렬순서</TableHead>
<TableHead scope="col">구분</TableHead>
<TableHead scope="col">관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -40,9 +40,10 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
value={row.acntgSeCd}
onChange={(e) => onChange({ acntgSeCd: e.target.value })}
placeholder="코드"
title="코드"
maxLength={10}
disabled={!row.isNew}
className="flex-1"
className="flex-1 disabled:opacity-100"
/>
</div>
</TableCell>
@@ -51,6 +52,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
value={row.acntgSeCdNm ?? ""}
onChange={(e) => onChange({ acntgSeCdNm: e.target.value })}
placeholder="코드명"
title="코드명"
maxLength={100}
/>
</TableCell>
@@ -59,6 +61,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
value={row.expln ?? ""}
onChange={(e) => onChange({ expln: e.target.value })}
placeholder="설명"
title="설명"
maxLength={1000}
/>
</TableCell>
@@ -66,6 +69,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
<RadioGroup
value={row.useYn === "N" ? "N" : "Y"}
onValueChange={(v) => onChange({ useYn: v })}
aria-label="사용 여부"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
@@ -82,6 +86,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
<Input
type="number"
min={0}
title="정렬순서"
value={row.sortSeq ?? ""}
onChange={(e) =>
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
@@ -115,15 +115,15 @@ function CnstpCodeTabPanel() {
</colgroup>
<TableHeader>
<TableRow>
<TableHead>코드</TableHead>
<TableHead>코드명</TableHead>
<TableHead>설명</TableHead>
<TableHead>숫자 여부</TableHead>
<TableHead>소숫점 단위</TableHead>
<TableHead>사용 여부</TableHead>
<TableHead>정렬순서</TableHead>
<TableHead>구분</TableHead>
<TableHead>관리</TableHead>
<TableHead scope="col">코드</TableHead>
<TableHead scope="col">코드명</TableHead>
<TableHead scope="col">설명</TableHead>
<TableHead scope="col">숫자 여부</TableHead>
<TableHead scope="col">소숫점 단위</TableHead>
<TableHead scope="col">사용 여부</TableHead>
<TableHead scope="col">정렬순서</TableHead>
<TableHead scope="col">구분</TableHead>
<TableHead scope="col">관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -40,9 +40,10 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
value={row.cnstpSeCd}
onChange={(e) => onChange({ cnstpSeCd: e.target.value })}
placeholder="코드"
title="코드"
maxLength={10}
disabled={!row.isNew}
className="flex-1"
className="flex-1 disabled:opacity-100"
/>
</div>
</TableCell>
@@ -51,6 +52,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
value={row.cnstpSeCdNm ?? ""}
onChange={(e) => onChange({ cnstpSeCdNm: e.target.value })}
placeholder="코드명"
title="코드명"
maxLength={100}
/>
</TableCell>
@@ -59,6 +61,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
value={row.expln ?? ""}
onChange={(e) => onChange({ expln: e.target.value })}
placeholder="설명"
title="설명"
maxLength={1000}
/>
</TableCell>
@@ -66,15 +69,16 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
<RadioGroup
value={row.numYn === "N" ? "N" : "Y"}
onValueChange={(v) => onChange({ numYn: v })}
aria-label="숫자 여부"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="Y" id={`${row.rowKey}-num-y`} />
<Label htmlFor={`${row.rowKey}-use-y`}>예</Label>
<Label htmlFor={`${row.rowKey}-num-y`}>예</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="N" id={`${row.rowKey}-num-n`} />
<Label htmlFor={`${row.rowKey}-use-n`}>아니오</Label>
<Label htmlFor={`${row.rowKey}-num-n`}>아니오</Label>
</div>
</RadioGroup>
</TableCell>
@@ -82,6 +86,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
<Input
type="number"
min={0}
title="소숫점 단위"
value={row.dcptUnitCnt ?? ""}
onChange={(e) =>
onChange({ dcptUnitCnt: e.target.value ? Number(e.target.value) : undefined })
@@ -93,6 +98,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
<RadioGroup
value={row.useYn === "N" ? "N" : "Y"}
onValueChange={(v) => onChange({ useYn: v })}
aria-label="사용 여부"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
@@ -109,6 +115,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
<Input
type="number"
min={0}
title="정렬순서"
value={row.sortSeq ?? ""}
onChange={(e) =>
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
@@ -115,13 +115,13 @@ function FnrsSharCodeTabPanel() {
</colgroup>
<TableHeader>
<TableRow>
<TableHead>코드</TableHead>
<TableHead>코드명</TableHead>
<TableHead>설명</TableHead>
<TableHead>사용 여부</TableHead>
<TableHead>정렬순서</TableHead>
<TableHead>구분</TableHead>
<TableHead>관리</TableHead>
<TableHead scope="col">코드</TableHead>
<TableHead scope="col">코드명</TableHead>
<TableHead scope="col">설명</TableHead>
<TableHead scope="col">사용 여부</TableHead>
<TableHead scope="col">정렬순서</TableHead>
<TableHead scope="col">구분</TableHead>
<TableHead scope="col">관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -40,9 +40,10 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
value={row.fnrsSharSeCd}
onChange={(e) => onChange({ fnrsSharSeCd: e.target.value })}
placeholder="코드"
title="코드"
maxLength={10}
disabled={!row.isNew}
className="flex-1"
className="flex-1 disabled:opacity-100"
/>
</div>
</TableCell>
@@ -51,6 +52,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
value={row.fnrsSharSeCdNm ?? ""}
onChange={(e) => onChange({ fnrsSharSeCdNm: e.target.value })}
placeholder="코드명"
title="코드명"
maxLength={100}
/>
</TableCell>
@@ -59,6 +61,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
value={row.expln ?? ""}
onChange={(e) => onChange({ expln: e.target.value })}
placeholder="설명"
title="설명"
maxLength={1000}
/>
</TableCell>
@@ -66,6 +69,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
<RadioGroup
value={row.useYn === "N" ? "N" : "Y"}
onValueChange={(v) => onChange({ useYn: v })}
aria-label="사용 여부"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
@@ -82,6 +86,7 @@ function FnrsSharSeRowComponent({ row, depth, onChange, onAddChild, onDelete }:
<Input
type="number"
min={0}
title="정렬순서"
value={row.sortSeq ?? ""}
onChange={(e) =>
onChange({ sortSeq: e.target.value ? Number(e.target.value) : undefined })
@@ -81,22 +81,27 @@ export function Footer() {
const mainBanners =
bannerList?.filter((banner) => banner.bnrTypeCd === "BNRFTER") ?? [];
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
// 기본은 정지 상태, 사용자가 시작 버튼을 눌러야 자동 넘김
const plugin = useRef(
Autoplay({ delay: 2000, stopOnInteraction: true, playOnInit: false }),
);
const [carouselApi, setCarouselApi] = useState<UseEmblaCarouselType[1]>();
const [isAutoplaying, setIsAutoplaying] = useState(true);
const [isAutoplaying, setIsAutoplaying] = useState(false);
// 사용자 조작·초점 진입 등으로 자동 넘김이 멈추거나 재개될 때 버튼 상태를 동기화한다
// (autoplay:play/stop 이벤트는 플러그인 내부 상태 변경 "전"에 발생하므로 isPlaying() 대신 이벤트 종류로 판단)
useEffect(() => {
if (!carouselApi) return;
const syncAutoplayState = () => setIsAutoplaying(plugin.current.isPlaying());
syncAutoplayState();
carouselApi.on("autoplay:play", syncAutoplayState);
carouselApi.on("autoplay:stop", syncAutoplayState);
const handleAutoplayPlay = () => setIsAutoplaying(true);
const handleAutoplayStop = () => setIsAutoplaying(false);
setIsAutoplaying(plugin.current.isPlaying());
carouselApi.on("autoplay:play", handleAutoplayPlay);
carouselApi.on("autoplay:stop", handleAutoplayStop);
return () => {
carouselApi.off("autoplay:play", syncAutoplayState);
carouselApi.off("autoplay:stop", syncAutoplayState);
carouselApi.off("autoplay:play", handleAutoplayPlay);
carouselApi.off("autoplay:stop", handleAutoplayStop);
};
}, [carouselApi]);
@@ -160,26 +165,11 @@ 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 mb-4 mt-2 h-20 sm:mb-8 sm:h-24"
className="relative my-4 h-20 sm:my-8 sm:h-24"
>
<CarouselContent>
{mainBanners && mainBanners.length > 0 ? (
@@ -206,6 +196,22 @@ export function Footer() {
</CarouselItem>
)}
</CarouselContent>
{/* 자동 넘김 정지/시작: 좌측 화살표 앞에 배치 (탭 순서도 화살표보다 먼저) */}
<button
type="button"
onClick={handleAutoplayToggle}
title={isAutoplaying ? "배너 자동 넘김 정지" : "배너 자동 넘김 시작"}
className="absolute -left-10 top-1/2 z-10 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-input !bg-background 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="h-4 w-4" aria-hidden="true" />
) : (
<Play className="h-4 w-4" aria-hidden="true" />
)}
<span className="sr-only">
{isAutoplaying ? "배너 자동 넘김 정지" : "배너 자동 넘김 시작"}
</span>
</button>
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-background text-accent-foreground" />
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-background text-accent-foreground" />
</Carousel>
@@ -284,7 +284,7 @@ export function Header({
ref={logoRef}
src={getLogoImage()}
className="h-[3rem]"
alt={`${siteInfo?.siteNm}`}
alt={`유역하수도 통합 정보 플랫폼 HASUON - ${siteInfo?.siteNm}`}
onLoad={() => {
if (logoRef.current) {
setLogoWidth(logoRef.current.offsetWidth);