709 lines
23 KiB
TypeScript
709 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { getSnRowNumber } from "@/shared/lib/row-number";
|
|
import LoadingOverlay from "@/components/ui/loading-bar";
|
|
import { fileDownload } from "@/components/lib/utils";
|
|
import { getStartYearArray } from "@/components/ui/year-range-select-box";
|
|
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
|
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
|
import useStore from "@/entities/reference-data/model/reference-data-store";
|
|
import {
|
|
Button,
|
|
Input,
|
|
Label,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@common/components";
|
|
import {
|
|
STANDARD_ROW_NUMBER_HEADER,
|
|
StandardDataTable,
|
|
StandardTableSortHeader,
|
|
type UiTableSort,
|
|
} from "@common/business_components";
|
|
import {
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
type ColumnDef,
|
|
type PaginationState,
|
|
} from "@tanstack/react-table";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import {
|
|
downloadExmnBusinessReport,
|
|
getExmnBusinesses,
|
|
} from "../api/exmn-business-api";
|
|
import type {
|
|
ExmnBusiness,
|
|
ExmnBusinessSearchParams,
|
|
} from "../model/exmn-business-types";
|
|
import { ExmnStatusDetailPanel } from "./exmn-status-detail-panel";
|
|
import { EXMN_TABLE_HEADER_CLASS } from "./exmn-standard-data-table";
|
|
|
|
const BUSINESS_LIST_PAGE_SIZE = 10;
|
|
|
|
const BUSINESS_TABLE_HEAD = [
|
|
{ id: "rowNumber", title: STANDARD_ROW_NUMBER_HEADER, sortKey: "exmnBizId" },
|
|
{ id: "bizCyclText", title: "사업차수", sortKey: "bizCycl" },
|
|
{ id: "bizYr", title: "사업년도", sortKey: "bizYr" },
|
|
{ id: "ctpvNm", title: "시도", sortKey: "ctpvNm" },
|
|
{ id: "sggNm", title: "시군구", sortKey: "sggNm" },
|
|
{ id: "bizNm", title: "사업명", sortKey: "bizNm" },
|
|
{ id: "trtdstCntText", title: "처리구역·분구(개소)", sortKey: "trtdstCnt" },
|
|
{
|
|
id: "prcisExmnPrlgText",
|
|
title: "정밀조사 연장(km)",
|
|
sortKey: "prcisExmnPrlg",
|
|
},
|
|
// 첨부서비스에서 현재 페이지 파일명을 보강하므로 전체 결과 파일명 정렬은 지원하지 않는다.
|
|
{ id: "reportYnText", title: "보고서", sortKey: null },
|
|
];
|
|
|
|
interface BusinessFilters {
|
|
bizCycl: string;
|
|
bizYr: string;
|
|
ctpvCd: string;
|
|
sggCd: string;
|
|
ctpvNm: string;
|
|
sggNm: string;
|
|
bizNm: string;
|
|
}
|
|
|
|
const EMPTY_FILTERS: BusinessFilters = {
|
|
bizCycl: "",
|
|
bizYr: "0",
|
|
ctpvCd: "0",
|
|
sggCd: "0",
|
|
ctpvNm: "",
|
|
sggNm: "",
|
|
bizNm: "",
|
|
};
|
|
|
|
type Notice = { type: "success" | "error" | "info"; text: string } | null;
|
|
|
|
/** 사업정보 조회·등록과 선택 사업의 조사결과 등록 진입을 제공하는 SFR-010-004 메인 화면이다. */
|
|
export function ExmnRegistrationPage() {
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
/** React 개발 모드의 effect 재실행에서도 최초 목록 조회를 한 번만 수행한다. */
|
|
const initialBusinessesLoadedRef = useRef(false);
|
|
// 입력 중 조건과 실제 서버 조회 조건을 분리해 조회·초기화 동작을 명확히 유지한다.
|
|
const [draftFilters, setDraftFilters] =
|
|
useState<BusinessFilters>(EMPTY_FILTERS);
|
|
const [appliedFilters, setAppliedFilters] =
|
|
useState<BusinessFilters>(EMPTY_FILTERS);
|
|
const [businesses, setBusinesses] = useState<ExmnBusiness[]>([]);
|
|
const [totalElements, setTotalElements] = useState(0);
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
pageIndex: 0,
|
|
pageSize: BUSINESS_LIST_PAGE_SIZE,
|
|
});
|
|
// 표시명 컬럼도 같은 이름 필드를 사용하는 서버 단일 정렬 상태다.
|
|
const [sort, setSort] = useState<UiTableSort>();
|
|
// 선택 사업 ID는 서버 재조회 뒤에도 같은 행을 복원할 수 있도록 객체와 별도로 유지한다.
|
|
const [selectedBusinessId, setSelectedBusinessId] = useState<string | null>(
|
|
null,
|
|
);
|
|
// 목록 행을 선택한 뒤 스크롤과 키보드 포커스를 함께 이동할 상세현황 영역이다.
|
|
const detailPanelRef = useRef<HTMLDivElement>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [notice, setNotice] = useState<Notice>(null);
|
|
|
|
/** 팝업과 동일한 행정구역 공통코드를 검색 Select에 준비한다. */
|
|
useEffect(() => {
|
|
if (ctpv.length === 0) void fetchCtpv();
|
|
if (sgg.length === 0) void fetchSgg();
|
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
|
|
|
const availableSgg = useMemo(() => {
|
|
if (draftFilters.ctpvCd === "0") return sgg;
|
|
return sgg.filter((option) =>
|
|
String(option.val).startsWith(draftFilters.ctpvCd.slice(0, 2)),
|
|
);
|
|
}, [draftFilters.ctpvCd, sgg]);
|
|
|
|
const selectedBusiness = useMemo(
|
|
() =>
|
|
businesses.find(
|
|
(business) => business.exmnBizId === selectedBusinessId,
|
|
) ?? null,
|
|
[businesses, selectedBusinessId],
|
|
);
|
|
|
|
/** 목록 행 선택 후 렌더링된 상세현황을 화면 상단으로 이동하고 포커스를 부여한다. */
|
|
const handleBusinessRowClick = (business: ExmnBusiness) => {
|
|
setSelectedBusinessId(business.exmnBizId);
|
|
window.requestAnimationFrame(() => {
|
|
const detailPanel = detailPanelRef.current;
|
|
if (!detailPanel) return;
|
|
detailPanel.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
detailPanel.focus({ preventScroll: true });
|
|
});
|
|
};
|
|
|
|
/** 사업 목록의 숫자·행정구역·사업보고서 파일명을 표시 전용 값으로 변환한다. */
|
|
const businessRows = useMemo(
|
|
() =>
|
|
businesses.map((business, index) => ({
|
|
...business,
|
|
rowNumber: getSnRowNumber({
|
|
total: totalElements,
|
|
rowIndex: index,
|
|
pageIndex: pagination.pageIndex,
|
|
pageSize: pagination.pageSize,
|
|
}),
|
|
bizCyclText: business.bizCycl == null ? "-" : `${business.bizCycl}차`,
|
|
trtdstCntText: Number(business.trtdstCnt ?? 0).toLocaleString("ko-KR"),
|
|
prcisExmnPrlgText: Number(business.prcisExmnPrlg ?? 0).toLocaleString(
|
|
"ko-KR",
|
|
{ maximumFractionDigits: 3 },
|
|
),
|
|
reportYnText:
|
|
business.bizRptpFileName?.trim() ||
|
|
(business.reportYn === "Y" || business.bizRptpAtchFileId
|
|
? "있음"
|
|
: "없음"),
|
|
})),
|
|
[businesses, pagination.pageIndex, pagination.pageSize, totalElements],
|
|
);
|
|
|
|
/** 화면 조건과 현재 정렬을 0-base 서버 Page 계약으로 변환한다. */
|
|
const buildSearchParams = (
|
|
filters: BusinessFilters,
|
|
pageIndex: number,
|
|
nextSort?: UiTableSort,
|
|
): ExmnBusinessSearchParams => ({
|
|
bizCycl: filters.bizCycl || undefined,
|
|
bizYr: filters.bizYr !== "0" ? filters.bizYr : undefined,
|
|
ctpvNm: filters.ctpvNm || undefined,
|
|
sggNm: filters.sggNm || undefined,
|
|
bizNm: filters.bizNm || undefined,
|
|
page: pageIndex,
|
|
size: BUSINESS_LIST_PAGE_SIZE,
|
|
sort: nextSort ? `${nextSort.key},${nextSort.direction}` : undefined,
|
|
});
|
|
|
|
/** 지정 조회조건·정렬·페이지로 사업 목록을 읽고 현재 페이지 선택을 복원한다. */
|
|
const fetchBusinesses = async (
|
|
filters: BusinessFilters,
|
|
pageIndex = 0,
|
|
nextSort: UiTableSort | undefined = sort,
|
|
) => {
|
|
setIsLoading(true);
|
|
setNotice(null);
|
|
try {
|
|
const result = await getExmnBusinesses(
|
|
buildSearchParams(filters, pageIndex, nextSort),
|
|
);
|
|
const content = result.content;
|
|
setBusinesses(content);
|
|
setTotalElements(result.totalElements);
|
|
setSelectedBusinessId((current) =>
|
|
content.some((item) => item.exmnBizId === current) ? current : null,
|
|
);
|
|
setPagination((current) => ({
|
|
...current,
|
|
pageIndex: result.number,
|
|
}));
|
|
return true;
|
|
} catch (error) {
|
|
setBusinesses([]);
|
|
setTotalElements(0);
|
|
setSelectedBusinessId(null);
|
|
setNotice({
|
|
type: "error",
|
|
text:
|
|
error instanceof Error
|
|
? error.message
|
|
: "조사사업 목록 조회 중 오류가 발생했습니다.",
|
|
});
|
|
return false;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 화면 최초 진입 시 빈 조회조건으로 실제 조사사업 목록을 조회한다. */
|
|
useEffect(() => {
|
|
if (initialBusinessesLoadedRef.current) return;
|
|
initialBusinessesLoadedRef.current = true;
|
|
void fetchBusinesses(EMPTY_FILTERS, 0, undefined);
|
|
// 최초 진입 시점의 빈 조회조건으로 한 번만 실행한다.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
/** 입력 중 조건을 확정하고 첫 페이지를 조회한다. */
|
|
const handleSearch = async () => {
|
|
setAppliedFilters(draftFilters);
|
|
await fetchBusinesses(draftFilters, 0, sort);
|
|
};
|
|
|
|
/** CS 초기화 버튼으로 모든 검색조건과 페이지를 최초 조회 기준으로 복원한다. */
|
|
const handleReset = async () => {
|
|
setDraftFilters(EMPTY_FILTERS);
|
|
setAppliedFilters(EMPTY_FILTERS);
|
|
setSelectedBusinessId(null);
|
|
setSort(undefined);
|
|
await fetchBusinesses(EMPTY_FILTERS, 0, undefined);
|
|
};
|
|
|
|
/** 목록의 보고서 링크에서 첨부서비스 원본 PDF를 내려받는다. */
|
|
const handleReportDownload = async (business: ExmnBusiness) => {
|
|
const attachmentId = business.bizRptpAtchFileId?.trim();
|
|
if (!attachmentId) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "다운로드할 사업보고서 첨부파일 정보가 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
setIsLoading(true);
|
|
setNotice(null);
|
|
try {
|
|
const reportBlob = await downloadExmnBusinessReport(attachmentId);
|
|
fileDownload(
|
|
reportBlob,
|
|
business.bizRptpFileName?.trim() || `${business.bizNm}_사업보고서.pdf`,
|
|
);
|
|
} catch (error) {
|
|
setNotice({
|
|
type: "error",
|
|
text:
|
|
error instanceof Error
|
|
? error.message
|
|
: "사업보고서 다운로드 중 오류가 발생했습니다.",
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 공통 WindowPopup 체계로 사업정보 등록 화면을 연다. */
|
|
const openBusinessRegister = () => {
|
|
setNotice(null);
|
|
const popup = openWindowPopup("exmnBusinessRegister", {
|
|
sourcePath: window.location.pathname,
|
|
});
|
|
if (!popup) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "조사사업 등록 팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.",
|
|
});
|
|
}
|
|
};
|
|
|
|
/** 선택 사업을 기존 등록 팝업의 수정 모드로 열어 기본정보·처리분구·양식을 다시 관리한다. */
|
|
const openBusinessUpdate = () => {
|
|
if (!selectedBusiness) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "사업 목록에서 수정할 사업을 먼저 선택해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
setNotice(null);
|
|
const popup = openWindowPopup("exmnBusinessRegister", {
|
|
exmnBizId: selectedBusiness.exmnBizId,
|
|
sourcePath: window.location.pathname,
|
|
});
|
|
if (!popup) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "사업수정 팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.",
|
|
});
|
|
}
|
|
};
|
|
|
|
/** 선택 사업 컨텍스트를 전달해 공통 WindowPopup 결과등록 화면을 연다. */
|
|
const openResultRegister = () => {
|
|
if (!selectedBusiness) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "사업 목록에서 대상 사업을 먼저 선택해주세요.",
|
|
});
|
|
return;
|
|
}
|
|
setNotice(null);
|
|
const popup = openWindowPopup("exmnResultRegister", {
|
|
exmnBizId: selectedBusiness.exmnBizId,
|
|
bizYr: selectedBusiness.bizYr,
|
|
bizCycl: selectedBusiness.bizCycl ?? 0,
|
|
bizNm: selectedBusiness.bizNm,
|
|
ctpvNm: selectedBusiness.ctpvNm,
|
|
sggNm: selectedBusiness.sggNm,
|
|
trtdstCnt: selectedBusiness.trtdstCnt,
|
|
prcisExmnPrlg: selectedBusiness.prcisExmnPrlg,
|
|
tpco: selectedBusiness.tpco,
|
|
reportYn: selectedBusiness.reportYn ?? "N",
|
|
sourcePath: window.location.pathname,
|
|
});
|
|
if (!popup) {
|
|
setNotice({
|
|
type: "error",
|
|
text: "결과등록 팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.",
|
|
});
|
|
}
|
|
};
|
|
|
|
/** WindowPopup의 등록·저장 완료 메시지를 받아 부모 목록과 안내 상태를 동기화한다. */
|
|
useEffect(() => {
|
|
const handlePopupMessage = (event: MessageEvent) => {
|
|
if (event.origin !== window.location.origin) return;
|
|
if (
|
|
event.data?.type === WINDOW_POPUP_MESSAGE_TYPE.EXMN_BUSINESS_CREATED
|
|
) {
|
|
setSelectedBusinessId(null);
|
|
void fetchBusinesses(appliedFilters, 0, sort).then((refreshed) => {
|
|
if (refreshed)
|
|
setNotice({ type: "success", text: "조사사업이 등록되었습니다." });
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (
|
|
event.data?.type === WINDOW_POPUP_MESSAGE_TYPE.EXMN_BUSINESS_UPDATED
|
|
) {
|
|
void fetchBusinesses(
|
|
appliedFilters,
|
|
pagination.pageIndex,
|
|
sort,
|
|
).then((refreshed) => {
|
|
if (refreshed)
|
|
setNotice({ type: "success", text: "조사사업이 수정되었습니다." });
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (
|
|
event.data?.type === WINDOW_POPUP_MESSAGE_TYPE.EXMN_RESULT_COMMITTED
|
|
) {
|
|
void fetchBusinesses(
|
|
appliedFilters,
|
|
pagination.pageIndex,
|
|
sort,
|
|
).then((refreshed) => {
|
|
if (refreshed)
|
|
setNotice({
|
|
type: "success",
|
|
text: "조사결과 저장을 완료했습니다.",
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
window.addEventListener("message", handlePopupMessage);
|
|
return () => window.removeEventListener("message", handlePopupMessage);
|
|
}, [appliedFilters, pagination.pageIndex, sort]);
|
|
|
|
/** 정렬 변경 시 첫 페이지와 선택 사업을 초기화하고 서버 전체 결과를 다시 조회한다. */
|
|
const handleBusinessSortChange = (nextSort?: UiTableSort) => {
|
|
setSort(nextSort);
|
|
setSelectedBusinessId(null);
|
|
void fetchBusinesses(appliedFilters, 0, nextSort);
|
|
};
|
|
|
|
/** 보고서 다운로드와 행 선택을 유지하며 조사사업 목록을 공통 DataTable 열로 변환한다. */
|
|
const columns = useMemo<ColumnDef<(typeof businessRows)[number]>[]>(
|
|
() =>
|
|
BUSINESS_TABLE_HEAD.map((head) => ({
|
|
id: head.id,
|
|
accessorFn: (row) => row[head.id as keyof typeof row],
|
|
header: () =>
|
|
head.sortKey === null ? (
|
|
head.title
|
|
) : (
|
|
<StandardTableSortHeader
|
|
label={head.title}
|
|
sortKey={head.sortKey}
|
|
sort={sort}
|
|
onSortChange={handleBusinessSortChange}
|
|
/>
|
|
),
|
|
cell: ({ row, getValue }) => {
|
|
const business = row.original;
|
|
const isReportCell = head.id === "reportYnText";
|
|
const reportAvailable =
|
|
isReportCell && Boolean(business.bizRptpAtchFileId?.trim());
|
|
const value = String(getValue() ?? "");
|
|
|
|
if (reportAvailable) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="hover:text-primary/80 block w-full truncate font-semibold text-primary underline underline-offset-4"
|
|
aria-label={`${business.reportYnText} 다운로드`}
|
|
title={`${business.reportYnText} 다운로드`}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
void handleReportDownload(business);
|
|
}}
|
|
>
|
|
{value}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span
|
|
className="block min-w-0 whitespace-normal break-words"
|
|
title={value}
|
|
>
|
|
{value}
|
|
</span>
|
|
);
|
|
},
|
|
meta: {
|
|
width:
|
|
head.id === "bizNm"
|
|
? 240
|
|
: head.id === "reportYnText"
|
|
? 170
|
|
: head.id === "trtdstCntText" || head.id === "prcisExmnPrlgText"
|
|
? 125
|
|
: head.id === "rowNumber"
|
|
? 60
|
|
: 90,
|
|
align: "center" as const,
|
|
},
|
|
})),
|
|
[appliedFilters, sort],
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data: businessRows,
|
|
columns,
|
|
state: {
|
|
pagination,
|
|
rowSelection: selectedBusinessId ? { [selectedBusinessId]: true } : {},
|
|
},
|
|
getRowId: (row) => row.exmnBizId,
|
|
rowCount: totalElements,
|
|
manualPagination: true,
|
|
onPaginationChange: (updater) => {
|
|
const next =
|
|
typeof updater === "function" ? updater(pagination) : updater;
|
|
if (next.pageIndex === pagination.pageIndex) return;
|
|
setSelectedBusinessId(null);
|
|
void fetchBusinesses(appliedFilters, next.pageIndex, sort);
|
|
},
|
|
getCoreRowModel: getCoreRowModel(),
|
|
});
|
|
|
|
return (
|
|
<div className="sn-exmn-registration-page flex min-h-0 flex-col">
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
<div className="card card-border !mt-6">
|
|
<form
|
|
className="flex flex-wrap items-center gap-2 p-2"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void handleSearch();
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Label className="mr-2 whitespace-nowrap text-label">
|
|
사업차수
|
|
</Label>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
className="w-[8rem]"
|
|
value={draftFilters.bizCycl}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
bizCycl: event.target.value,
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Label className="mr-2 whitespace-nowrap text-label">
|
|
사업년도
|
|
</Label>
|
|
<Select
|
|
value={draftFilters.bizYr}
|
|
onValueChange={(value) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
bizYr: String(value),
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[11rem]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{getStartYearArray({ placeholder: "전체" }).map((option) => (
|
|
<SelectItem
|
|
key={String(option.val)}
|
|
value={String(option.val)}
|
|
>
|
|
{option.text}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Label className="mr-2 whitespace-nowrap text-label">시도</Label>
|
|
<Select
|
|
value={draftFilters.ctpvCd}
|
|
onValueChange={(value) => {
|
|
const ctpvCd = String(value);
|
|
const selected = ctpv.find(
|
|
(option) => String(option.val) === ctpvCd,
|
|
);
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
ctpvCd,
|
|
ctpvNm: ctpvCd === "0" ? "" : (selected?.text ?? ""),
|
|
sggCd: "0",
|
|
sggNm: "",
|
|
}));
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[14rem]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{[{ val: "0", text: "전체" }, ...ctpv].map((option) => (
|
|
<SelectItem
|
|
key={String(option.val)}
|
|
value={String(option.val)}
|
|
>
|
|
{option.text}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Label className="mr-2 whitespace-nowrap text-label">시군구</Label>
|
|
<Select
|
|
value={draftFilters.sggCd}
|
|
onValueChange={(value) => {
|
|
const sggCd = String(value);
|
|
const selected = availableSgg.find(
|
|
(option) => String(option.val) === sggCd,
|
|
);
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
sggCd,
|
|
sggNm: sggCd === "0" ? "" : (selected?.text ?? ""),
|
|
}));
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[14rem]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{[{ val: "0", text: "전체" }, ...availableSgg].map((option) => (
|
|
<SelectItem
|
|
key={String(option.val)}
|
|
value={String(option.val)}
|
|
>
|
|
{option.text}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Label className="mr-2 whitespace-nowrap text-label">사업명</Label>
|
|
<Input
|
|
className="w-[24rem]"
|
|
value={draftFilters.bizNm}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
bizNm: event.target.value,
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<Button type="submit" className="w-[7.4rem]" disabled={isLoading}>
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
className="w-[8.8rem]"
|
|
disabled={isLoading}
|
|
onClick={() => void handleReset()}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
|
|
{notice && (
|
|
<div
|
|
role="alert"
|
|
className={`mx-[1.2rem] mt-4 rounded border px-4 py-3 text-sm ${
|
|
notice.type === "error"
|
|
? "border-red-200 bg-red-50 text-red-700"
|
|
: notice.type === "info"
|
|
? "border-primary bg-primary-transparent text-primary"
|
|
: "border-emerald-200 bg-emerald-50 text-emerald-700"
|
|
}`}
|
|
>
|
|
{notice.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 min-w-0">
|
|
<StandardDataTable
|
|
table={table}
|
|
title="조사사업 목록"
|
|
className={EXMN_TABLE_HEADER_CLASS}
|
|
scrollMode="horizontal"
|
|
minTableWidth="110rem"
|
|
onRowClick={handleBusinessRowClick}
|
|
actions={
|
|
<>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
disabled={isLoading}
|
|
onClick={openBusinessRegister}
|
|
>
|
|
조사사업등록
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
disabled={isLoading}
|
|
onClick={openResultRegister}
|
|
>
|
|
결과등록
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
disabled={isLoading || !selectedBusiness}
|
|
onClick={openBusinessUpdate}
|
|
>
|
|
수정
|
|
</Button>
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
<div
|
|
ref={detailPanelRef}
|
|
tabIndex={-1}
|
|
aria-label="조사 상세현황"
|
|
className="scroll-mt-[1.2rem] focus:outline-none"
|
|
>
|
|
<ExmnStatusDetailPanel business={selectedBusiness} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|