@@ -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">></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">></span>}
|
||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
||||
<Badge color={getBadgeColor(badge.state)}>
|
||||
{badge.label}({getBadgeStateText(badge.state)})
|
||||
</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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">></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">></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">
|
||||
|
||||
Reference in New Issue
Block a user