웹 접근성 조치
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
+13 -2
View File
@@ -16,9 +16,20 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef<typeof Select>
useYn?: string;
placeholder?: string;
className?: string;
/** 레이블(label htmlFor) 연결용 트리거 id */
id?: string;
title?: string;
}
function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
function CodeSelect({
upCd,
useYn,
placeholder = "선택",
className,
id,
title,
...props
}: CodeSelectProps) {
const listQuery = useCodeList(useYn ? undefined : upCd);
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
@@ -27,7 +38,7 @@ function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }
return (
<Select {...props} disabled={props.disabled || isLoading}>
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectTrigger id={id} title={title} className={cn("w-[12rem]", className)}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
@@ -84,9 +84,9 @@ function CklstArtclFormDialog({
/>
</div>
<div className="space-y-2">
<Label>사용여부</Label>
<Label htmlFor="cklstArtclUseYn">사용여부</Label>
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
<SelectTrigger className="w-full">
<SelectTrigger id="cklstArtclUseYn" className="w-full">
<SelectValue placeholder="사용여부" />
</SelectTrigger>
<SelectContent>
@@ -100,8 +100,9 @@ function CklstMngFormDialog({
/>
</div>
<div className="space-y-2">
<Label>문서구분</Label>
<Label htmlFor="cklstDocSeComCd">문서구분</Label>
<CodeSelect
id="cklstDocSeComCd"
upCd="ASDOCSE"
value={docSeComCd}
onValueChange={(v) => setValue("docSeComCd", v)}
@@ -110,8 +111,9 @@ function CklstMngFormDialog({
/>
</div>
<div className="space-y-2">
<Label>업무구분</Label>
<Label htmlFor="cklstTaskSeComCd">업무구분</Label>
<CodeSelect
id="cklstTaskSeComCd"
upCd={docSeComCd || "ASDOCSE"}
value={watch("taskSeComCd")}
onValueChange={(v) => setValue("taskSeComCd", v)}
@@ -121,9 +123,9 @@ function CklstMngFormDialog({
/>
</div>
<div className="space-y-2">
<Label>사용여부</Label>
<Label htmlFor="cklstUseYn">사용여부</Label>
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
<SelectTrigger className="w-full">
<SelectTrigger id="cklstUseYn" className="w-full">
<SelectValue placeholder="사용여부" />
</SelectTrigger>
<SelectContent>
@@ -44,65 +44,78 @@ function CklstPreviewDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[64rem]">
<DialogContent className="flex max-h-[90vh] max-w-[64rem] flex-col">
<DialogHeader>
<DialogTitle>체크리스트 미리보기 — {cklstNm}</DialogTitle>
</DialogHeader>
{displayRows.length === 0 ? (
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
) : (
<Table caption={`체크리스트 미리보기 ${cklstNm} 목록으로 점검항목, 주요 검토내용, 검토 여부 정보 제공`}>
<colgroup>
<col width="25%" />
<col width="50%" />
<col width="25%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead align="left">점검항목</TableHead>
<TableHead align="left">주요 검토내용</TableHead>
<TableHead>검토 여부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{displayRows.map(({ row, depth }) => {
const branchPrefix = formatTreeBranchPrefix(depth);
const showRadios = row.bchckUseYn !== "N";
// 항목이 많아 창 높이를 넘으면 표 영역만 스크롤 (초점을 받아 키보드로도 스크롤 가능)
<div
tabIndex={0}
role="region"
aria-label="체크리스트 미리보기 목록"
className="min-h-0 flex-1 overflow-y-auto rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
>
<Table
caption={`체크리스트 미리보기 ${cklstNm} 목록으로 점검항목, 주요 검토내용, 검토 여부 정보 제공`}
>
<colgroup>
<col width="25%" />
<col width="50%" />
<col width="25%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead align="left">점검항목</TableHead>
<TableHead align="left">주요 검토내용</TableHead>
<TableHead>검토 여부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{displayRows.map(({ row, depth }) => {
const branchPrefix = formatTreeBranchPrefix(depth);
const showRadios = row.bchckUseYn !== "N";
if (row.useYn === "N") return null;
if (row.useYn === "N") return null;
return (
<TableRow key={row.rowKey}>
<TableCell align="left">
<span className="inline-flex items-center gap-1">
{branchPrefix ? (
<span className="font-mono text-muted">{branchPrefix}</span>
) : null}
<span>{getArtclLabel(row.cklstArtclSn)}</span>
</span>
</TableCell>
<TableCell>{row.rvwCn ?? "—"}</TableCell>
<TableCell align="center">
{showRadios ? (
<RadioGroup className="flex items-center justify-center">
<div className="flex items-center gap-1">
<RadioGroupItem value="Y" id={`${row.rowKey}-preview-y`} />
<Label htmlFor={`${row.rowKey}-preview-y`}>예</Label>
</div>
<div className="flex items-center gap-1">
<RadioGroupItem value="N" id={`${row.rowKey}-preview-n`} />
<Label htmlFor={`${row.rowKey}-preview-n`}>아니오</Label>
</div>
</RadioGroup>
) : (
"—"
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
return (
<TableRow key={row.rowKey}>
<TableCell align="left">
<span className="inline-flex items-center gap-1">
{branchPrefix ? (
<span className="font-mono text-muted">{branchPrefix}</span>
) : null}
<span>{getArtclLabel(row.cklstArtclSn)}</span>
</span>
</TableCell>
<TableCell>{row.rvwCn ?? "—"}</TableCell>
<TableCell align="center">
{showRadios ? (
<RadioGroup
aria-label="검토 여부"
className="flex items-center justify-center"
>
<div className="flex items-center gap-1">
<RadioGroupItem value="Y" id={`${row.rowKey}-preview-y`} />
<Label htmlFor={`${row.rowKey}-preview-y`}>예</Label>
</div>
<div className="flex items-center gap-1">
<RadioGroupItem value="N" id={`${row.rowKey}-preview-n`} />
<Label htmlFor={`${row.rowKey}-preview-n`}>아니오</Label>
</div>
</RadioGroup>
) : (
"—"
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</DialogContent>
</Dialog>
@@ -289,13 +289,13 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
</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>
@@ -55,7 +55,11 @@ function CklstRvwRowComponent({
onValueChange={(v) => onChange({ cklstArtclSn: Number(v), isDirty: true })}
disabled={!row.isNew}
>
<SelectTrigger className="w-full flex-1">
{/* 비활성(기존 항목) 상태에서도 텍스트 명도 대비 4.5:1 이상 유지 */}
<SelectTrigger
title="점검항목 선택"
className="w-full flex-1 disabled:opacity-100"
>
<SelectValue placeholder="점검항목 선택" />
</SelectTrigger>
<SelectContent>
@@ -73,12 +77,14 @@ function CklstRvwRowComponent({
value={row.rvwCn ?? ""}
onChange={(e) => onChange({ rvwCn: e.target.value, isDirty: true })}
placeholder="주요 검토내용"
title="주요 검토내용"
/>
</TableCell>
<TableCell align="center">
<RadioGroup
value={row.useYn ?? "Y"}
onValueChange={(v) => onChange({ useYn: v as "Y" | "N", isDirty: true })}
aria-label="사용 여부"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
@@ -95,6 +101,7 @@ function CklstRvwRowComponent({
<RadioGroup
value={row.bchckUseYn ?? "Y"}
onValueChange={(v) => onChange({ bchckUseYn: v as "Y" | "N", isDirty: true })}
aria-label="체크 사용"
className="flex justify-center gap-4"
>
<div className="flex items-center gap-2">
@@ -111,6 +118,7 @@ function CklstRvwRowComponent({
<Input
type="number"
min={0}
title="정렬 순서"
value={row.sortSeq ?? ""}
onChange={(e) =>
onChange({
@@ -79,6 +79,17 @@ function getBadgeColor(state: BadgeState) {
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -95,7 +106,9 @@ function ProgressStepBadge({ prgrsSttsComCd }: ProgressStepBadgeProps) {
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
@@ -120,6 +120,17 @@ function getBadgeColor(state: BadgeState) {
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -137,7 +148,9 @@ function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadg
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
+14 -1
View File
@@ -121,6 +121,17 @@ function getBadgeColor(state: BadgeState) {
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -138,7 +149,9 @@ function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadg
{badges.map((badge, index) => (
<Fragment key={badge.label}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
@@ -1,6 +1,7 @@
"use client";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { AxiosError } from "axios";
import { useAlertStore } from "@/shared/lib";
import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw";
@@ -10,6 +11,14 @@ import { Badge, Button } from "@common/components";
import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config";
/** 서버 오류 응답의 message 를 우선 표시하고, 없으면 기본 문구를 사용한다 */
function getErrorMessage(error: unknown, fallback: string) {
if (error instanceof AxiosError) {
return error.response?.data?.message || fallback;
}
return fallback;
}
interface RvwMnpwBlockProps {
block: RvwMnpwBlockConfig;
items: RvwMnpwMngResponseDto[];
@@ -33,8 +42,7 @@ function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) {
});
alert("배치되었습니다.");
} catch (error) {
const message = error instanceof Error ? error.message : "배치에 실패했습니다.";
alert(message);
alert(getErrorMessage(error, "배치에 실패했습니다."));
}
};
@@ -48,8 +56,7 @@ function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) {
userUuid: item.userUuid,
});
} catch (error) {
const message = error instanceof Error ? error.message : "제거에 실패했습니다.";
alert(message);
alert(getErrorMessage(error, "제거에 실패했습니다."));
}
};
@@ -119,6 +119,17 @@ function getBadgeColor(state: BadgeState) {
}
}
function getBadgeStateText(state: BadgeState) {
switch (state) {
case "completed":
return "완료";
case "current":
return "진행중";
case "pending":
return "대기";
}
}
function formatOptional(value: string | undefined) {
return value?.trim() ? value : "-";
}
@@ -136,7 +147,9 @@ function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadg
{badges.map((badge, index) => (
<Fragment key={`${badge.label}-${index}`}>
{index > 0 && <span className="mx-2">&gt;</span>}
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
<Badge color={getBadgeColor(badge.state)}>
{badge.label}({getBadgeStateText(badge.state)})
</Badge>
</Fragment>
))}
</div>
@@ -120,6 +120,7 @@ function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey, decimals }: Tmplnt
interface TmplntFacilityMatrixProps {
swtrmtFcltCd: string;
facilityName?: string;
sido: string;
gugun: string;
facilityIndex: number;
@@ -129,6 +130,7 @@ interface TmplntFacilityMatrixProps {
function TmplntFacilityMatrix({
swtrmtFcltCd,
facilityName,
sido,
gugun,
facilityIndex,
@@ -147,7 +149,10 @@ function TmplntFacilityMatrix({
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
return (
<Table caption="처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공" variant="secondary">
<Table
caption={`${facilityName ? `${facilityName} ` : ""}처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공`}
variant="secondary"
>
<colgroup>
<col width="12%" />
<col width="9.75%" />
@@ -317,7 +322,10 @@ function TmplntFacilityAttachments({
return (
<div className="mt-6 space-y-2">
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
<Table caption="처리구역 필수 첨부파일 목록으로 파일명, 파일 확장자, 파일첨부 정보 제공" variant="secondary">
<Table
caption={`${facilityName ? `${facilityName} ` : ""}처리구역 필수 첨부파일 목록으로 파일명, 파일 확장자, 파일첨부 정보 제공`}
variant="secondary"
>
<colgroup>
<col width="40%" />
<col width="15%" />
@@ -481,6 +489,8 @@ function TmplntFormSection() {
<TabsTrigger
key={field.id}
value={field.swtrmtFcltCd}
id={`tmplnt-form-tab-${field.swtrmtFcltCd}`}
aria-controls={`tmplnt-form-panel-${field.swtrmtFcltCd}`}
className="px-6 py-3 text-2xl"
>
{facilityName}
@@ -497,10 +507,14 @@ function TmplntFormSection() {
<TabsContent
key={field.id}
value={field.swtrmtFcltCd}
id={`tmplnt-form-panel-${field.swtrmtFcltCd}`}
aria-labelledby={`tmplnt-form-tab-${field.swtrmtFcltCd}`}
className="mt-0 border-0 p-0 shadow-none"
>
<h3 className="sr-only">{facilityName} 하수도정비기본계획</h3>
<TmplntFacilityMatrix
swtrmtFcltCd={field.swtrmtFcltCd}
facilityName={facilityName}
sido={sido ?? ""}
gugun={gugun ?? ""}
facilityIndex={facilityIndex}
@@ -77,6 +77,7 @@ function getDtlValue(
interface TmplntFacilityMatrixProps {
item: SewerBscPlanTmplntResponseDto;
facilityName?: string;
sido?: string;
gugun?: string;
crtrYr?: string;
@@ -85,6 +86,7 @@ interface TmplntFacilityMatrixProps {
function TmplntFacilityMatrix({
item,
facilityName,
sido,
gugun,
crtrYr,
@@ -102,7 +104,10 @@ function TmplntFacilityMatrix({
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
return (
<Table caption="처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공" variant="secondary">
<Table
caption={`${facilityName ? `${facilityName} ` : ""}처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공`}
variant="secondary"
>
<colgroup>
<col width="12%" />
<col width="9.75%" />
@@ -217,6 +222,8 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
<TabsTrigger
key={item.swtrmtFcltCd}
value={item.swtrmtFcltCd ?? ""}
id={`tmplnt-view-tab-${item.swtrmtFcltCd}`}
aria-controls={`tmplnt-view-panel-${item.swtrmtFcltCd}`}
className="!px-6 !py-3 text-xl"
>
{facilityName}
@@ -228,10 +235,17 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
{tmplntList.map((item) => {
const facilityName = resolveFacilityName(item, facilityOptions);
return (
<TabsContent key={item.swtrmtFcltCd} value={item.swtrmtFcltCd ?? ""}>
<TabsContent
key={item.swtrmtFcltCd}
value={item.swtrmtFcltCd ?? ""}
id={`tmplnt-view-panel-${item.swtrmtFcltCd}`}
aria-labelledby={`tmplnt-view-tab-${item.swtrmtFcltCd}`}
>
<h3 className="sr-only">{facilityName} 하수도정비기본계획</h3>
<div className="overflow-x-auto">
<TmplntFacilityMatrix
item={item}
facilityName={facilityName}
sido={det.sido}
gugun={det.gugun}
crtrYr={dtl?.crtrYr}
@@ -242,7 +256,10 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
{(item.atchFileGroupId || item.atchFileInfo?.orgnlFileNm) && (
<div className="mt-6 space-y-2">
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
<Table caption="처리구역 필수 첨부파일 목록으로 파일명, 파일첨부 정보 제공" variant="secondary">
<Table
caption={`${facilityName ? `${facilityName} ` : ""}처리구역 필수 첨부파일 목록으로 파일명, 파일첨부 정보 제공`}
variant="secondary"
>
<colgroup>
<col width="35%" />
<col width="65%" />
@@ -127,7 +127,9 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
<TableHeader>
{canSidoGugunSelectable && (
<TableRow>
<TableHead scope="row" data-required>시도</TableHead>
<TableHead scope="row" data-required>
<label htmlFor="sewer-bsc-plan-apply-sido">시도</label>
</TableHead>
<TableCell>
<Select
value={sidoComCd || undefined}
@@ -136,7 +138,12 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
setSggComCd("");
}}
>
<SelectTrigger className="w-full">
<SelectTrigger
id="sewer-bsc-plan-apply-sido"
title="시도 선택"
aria-required="true"
className="w-full"
>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@@ -148,14 +155,16 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
</SelectContent>
</Select>
</TableCell>
<TableHead scope="row">시군구</TableHead>
<TableHead scope="row">
<label htmlFor="sewer-bsc-plan-apply-sgg">시군구</label>
</TableHead>
<TableCell>
<Select
value={sggComCd || undefined}
onValueChange={setSggComCd}
disabled={!sidoComCd || isUserGugunPending}
>
<SelectTrigger className="w-full">
<SelectTrigger id="sewer-bsc-plan-apply-sgg" title="시군구 선택" className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@@ -170,12 +179,17 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
</TableRow>
)}
<TableRow>
<TableHead scope="row" data-required>계획구분</TableHead>
<TableHead id="sewer-bsc-plan-apply-plan-se-label" scope="row" data-required>
계획구분
</TableHead>
<TableCell colSpan={3}>
<FieldSet className="px-2">
{/* 라디오 그룹이 어떤 항목의 선택인지 알 수 있도록 th 와 연결 */}
<RadioGroup
value={sewerBscPlanSeComCd || undefined}
onValueChange={setSewerBscPlanSeComCd}
aria-labelledby="sewer-bsc-plan-apply-plan-se-label"
aria-required="true"
>
<div className="flex flex-wrap gap-4">
{planSeOptions?.map((option) => (
@@ -199,10 +213,17 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
</TableCell>
</TableRow>
<TableRow>
<TableHead scope="row" data-required>업무구분</TableHead>
<TableHead id="sewer-bsc-plan-apply-task-se-label" scope="row" data-required>
업무구분
</TableHead>
<TableCell colSpan={3}>
<FieldSet className="px-2">
<RadioGroup value={taskSeComCd || undefined} onValueChange={setTaskSeComCd}>
<RadioGroup
value={taskSeComCd || undefined}
onValueChange={setTaskSeComCd}
aria-labelledby="sewer-bsc-plan-apply-task-se-label"
aria-required="true"
>
<div className="flex flex-wrap gap-4">
{taskSeOptions?.map((option) => (
<FieldLabel
@@ -13,6 +13,9 @@ import {
import { type SelectedOgnzUser, toSelectedOgnzUser } from "../model/selected-ognz-user";
const FOCUS_RING_CLASS_NAME =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring";
interface OgnzUserTreeNodeProps {
node: OgnzUserTreeResponseDto;
depth?: number;
@@ -56,7 +59,10 @@ function OgnzUserTreeNode({
<CollapsibleTrigger asChild>
<button
type="button"
className="group flex w-full items-center gap-2 rounded-sm p-2 text-left hover:bg-accent"
className={cn(
"group flex w-full items-center gap-2 rounded-sm p-2 text-left hover:bg-accent",
FOCUS_RING_CLASS_NAME,
)}
>
<ChevronRight
className={cn(
@@ -73,31 +79,51 @@ function OgnzUserTreeNode({
{node.users.map((user) => {
const isSelected = selectedUserId === user.userUnqId;
const selectedUser = toSelectedOgnzUser(user, node);
const rowClassName = cn(
"group flex w-full cursor-pointer items-center gap-2 rounded-sm p-2 text-left hover:bg-accent",
isSelected && "!border !border-primary !bg-primary-transparent",
);
const userLabel = (
<>
<span className="text-[1.4rem] text-label">{user.userNm}</span>
<span className="text-xl text-muted-foreground">({user.jbpsSeNm})</span>
</>
);
// 선택 버튼이 없으면 행 자체를 버튼으로 렌더링해 키보드(Tab/Enter/Space)로 선택 가능하게 한다.
if (!showSelectButton) {
return (
<button
key={user.userUnqId}
type="button"
style={{ paddingInlineStart: indent + 24 }}
className={cn(rowClassName, FOCUS_RING_CLASS_NAME)}
aria-pressed={isSelected}
onClick={() => onSelect(selectedUser)}
>
{userLabel}
</button>
);
}
return (
<div
key={user.userUnqId}
style={{ paddingInlineStart: indent + 24 }}
className={cn(
"group flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
isSelected && "!border !border-primary !bg-primary-transparent",
)}
className={rowClassName}
onClick={() => onSelect(selectedUser)}
>
<span className="text-[1.4rem] text-label">{user.userNm}</span>
<span className="text-xl text-muted-foreground">({user.jbpsSeNm})</span>
{showSelectButton ? (
<Button
type="button"
size="sm"
onClick={(e) => {
e.stopPropagation();
onSelect(selectedUser);
}}
>
선택
</Button>
) : null}
{userLabel}
<Button
type="button"
size="sm"
onClick={(e) => {
e.stopPropagation();
onSelect(selectedUser);
}}
>
선택
</Button>
</div>
);
})}
@@ -115,7 +115,9 @@ function PbadmsTaskApplyDialog({
<TableHeader>
{canSidoGugunSelectable && (
<TableRow>
<TableHead scope="row" data-required>시도</TableHead>
<TableHead scope="row" data-required>
<label htmlFor="pbadms-task-apply-sido">시도</label>
</TableHead>
<TableCell>
<Select
value={sidoComCd || undefined}
@@ -124,7 +126,12 @@ function PbadmsTaskApplyDialog({
setSggComCd("");
}}
>
<SelectTrigger className="w-full">
<SelectTrigger
id="pbadms-task-apply-sido"
title="시도 선택"
aria-required="true"
className="w-full"
>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@@ -136,14 +143,16 @@ function PbadmsTaskApplyDialog({
</SelectContent>
</Select>
</TableCell>
<TableHead scope="row">시군구</TableHead>
<TableHead scope="row">
<label htmlFor="pbadms-task-apply-sgg">시군구</label>
</TableHead>
<TableCell>
<Select
value={sggComCd || undefined}
onValueChange={setSggComCd}
disabled={!sidoComCd || isUserGugunPending}
>
<SelectTrigger className="w-full">
<SelectTrigger id="pbadms-task-apply-sgg" title="시군구 선택" className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@@ -158,9 +167,12 @@ function PbadmsTaskApplyDialog({
</TableRow>
)}
<TableRow>
<TableHead scope="row" data-required>업무구분</TableHead>
<TableHead id="pbadms-task-apply-task-label" scope="row" data-required>
업무구분
</TableHead>
<TableCell colSpan={3}>
<FieldSet className="px-2">
{/* 체크박스 그룹이 어떤 항목의 선택인지 알 수 있도록 th 와 연결 */}
<FieldSet className="px-2" aria-labelledby="pbadms-task-apply-task-label">
<FieldGroup data-slot="checkbox-group" className="flex-row flex-wrap">
{taskOptions?.map((option) => (
<Field key={option.lwpoCd} orientation="horizontal" className="w-auto">