feat(sn): CS 기준 UI 통일 및 지도 팝업 표준화
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import moment from "moment";
|
||||
|
||||
import { SelectBox } from "@common/business_components/ui";
|
||||
import {
|
||||
StandardFormField,
|
||||
StandardFormSection,
|
||||
} from "@common/business_components";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components/ui";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { formatDate } from "../../common/util";
|
||||
import {
|
||||
@@ -37,6 +42,7 @@ import type {
|
||||
} from "../model/obslt-ppl-types";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
type FormKey = keyof ObsltPplBusinessSaveRequest;
|
||||
@@ -47,6 +53,8 @@ const BIZ_SE_OPTIONS = [
|
||||
{ text: "정밀조사", val: "정밀조사" },
|
||||
{ text: "정비사업", val: "정비사업" },
|
||||
];
|
||||
const EMPTY_SELECT_VALUE = "__none__";
|
||||
type FieldSpan = 1 | 2 | 3;
|
||||
|
||||
/** 등록·수정 표에서 필드 순서, 라벨, 입력 종류와 필수 여부를 한 곳에서 관리한다. */
|
||||
const FIELDS: Array<{
|
||||
@@ -57,7 +65,7 @@ const FIELDS: Array<{
|
||||
placeholder?: string;
|
||||
}> = [
|
||||
{ key: "bizSe", label: "사업구분", required: true },
|
||||
{ key: "prcisExmnCycl", label: "정밀조사차수" },
|
||||
{ key: "prcisExmnCycl", label: "정밀조사 차수" },
|
||||
{ key: "bizCd", label: "사업코드", required: true },
|
||||
{ key: "envoNm", label: "환경청" },
|
||||
{ key: "bizNm", label: "사업명", required: true },
|
||||
@@ -70,11 +78,11 @@ const FIELDS: Array<{
|
||||
{ key: "evdncDcmntSbmsnStts", label: "증빙서류 제출여부" },
|
||||
{ key: "bgngYmd", label: "착수(착공) 예정일자", placeholder: "YYYY-MM-DD" },
|
||||
{ key: "cmcnYmd", label: "준공(예정)일자", placeholder: "YYYY-MM-DD" },
|
||||
{ key: "bizPrplDlyRsnCn", label: "사업추진 부진사유" },
|
||||
{ key: "bizPrplDlyRsnCn", label: "사업추진 부진사유", type: "textarea" },
|
||||
{ key: "bizPicNm", label: "담당자 이름" },
|
||||
{ key: "bizPicCnpl", label: "담당자 연락처" },
|
||||
{ key: "rmrkCn", label: "비고", },
|
||||
{ key: "pbcpOpnnCn", label: "공단의견", },
|
||||
{ key: "rmrkCn", label: "비고", type: "textarea" },
|
||||
{ key: "pbcpOpnnCn", label: "공단 의견", type: "textarea" },
|
||||
];
|
||||
|
||||
const NUMBER_KEYS = new Set<FormKey>([
|
||||
@@ -144,6 +152,8 @@ const toRequest = (form: FormState): ObsltPplBusinessSaveRequest =>
|
||||
export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const isModify = Boolean(id);
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [form, setForm] = useState<FormState>(createEmptyForm);
|
||||
@@ -157,15 +167,8 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
const listQueryString = searchParams.toString();
|
||||
const listHref = `/oper/obsltPplBiz${listQueryString ? `?${listQueryString}` : ""}`;
|
||||
|
||||
const ctpvOptions = useMemo(
|
||||
() => [{ text: "전체", val: "0", hidden: true }, ...ctpv],
|
||||
[ctpv],
|
||||
);
|
||||
const representativeSggOptions = useMemo(
|
||||
() => [
|
||||
{ text: "전체", val: "0", hidden: true },
|
||||
...buildObsltPplRepresentativeSggOptions(sgg, ctpvCd),
|
||||
],
|
||||
() => buildObsltPplRepresentativeSggOptions(sgg, ctpvCd),
|
||||
[ctpvCd, sgg],
|
||||
);
|
||||
|
||||
@@ -245,8 +248,9 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
try {
|
||||
if (id) await updateObsltPplBusiness(id, toRequest(form));
|
||||
else await createObsltPplBusiness(toRequest(form));
|
||||
window.alert(isModify ? "수정되었습니다." : "등록되었습니다.");
|
||||
router.push(listHref);
|
||||
alert(isModify ? "수정되었습니다." : "등록되었습니다.", () =>
|
||||
router.push(listHref),
|
||||
);
|
||||
} catch (error) {
|
||||
setNotice(
|
||||
error instanceof Error ? error.message : "저장하지 못했습니다.",
|
||||
@@ -258,12 +262,11 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
|
||||
/** 사용자 확인 후 현재 사업을 논리삭제하고 목록으로 이동한다. */
|
||||
const handleDelete = async () => {
|
||||
if (!id || !window.confirm("삭제하시겠습니까?")) return;
|
||||
if (!id || !(await confirm("삭제하시겠습니까?"))) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await deleteObsltPplBusiness(id);
|
||||
window.alert("삭제되었습니다.");
|
||||
router.push(listHref);
|
||||
alert("삭제되었습니다.", () => router.push(listHref));
|
||||
} catch (error) {
|
||||
setNotice(
|
||||
error instanceof Error ? error.message : "삭제하지 못했습니다.",
|
||||
@@ -273,153 +276,258 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (key: FormKey, span: FieldSpan = 1) => {
|
||||
const field = FIELDS.find((candidate) => candidate.key === key);
|
||||
if (!field) return null;
|
||||
|
||||
const fieldId = `obslt-ppl-${field.key}`;
|
||||
let control: ReactNode;
|
||||
|
||||
if (field.key === "bizSe") {
|
||||
control = (
|
||||
<Select
|
||||
value={form.bizSe}
|
||||
onValueChange={(value) => {
|
||||
clearFieldError("bizSe");
|
||||
setForm((current) => ({ ...current, bizSe: String(value) }));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={fieldId} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BIZ_SE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.val} value={option.val}>
|
||||
{option.text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else if (field.key === "envoNm") {
|
||||
control = (
|
||||
<Select
|
||||
value={form.envoNm || EMPTY_SELECT_VALUE}
|
||||
onValueChange={(value) => {
|
||||
clearFieldError("envoNm");
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
envoNm: value === EMPTY_SELECT_VALUE ? "" : String(value),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={fieldId} className="w-full">
|
||||
<SelectValue placeholder="환경청 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={EMPTY_SELECT_VALUE}>선택</SelectItem>
|
||||
{OBSLT_PPL_ENV_OFFICES.map((office) => (
|
||||
<SelectItem key={office.name} value={office.name}>
|
||||
{office.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else if (field.type === "textarea") {
|
||||
control = (
|
||||
<Textarea
|
||||
id={fieldId}
|
||||
className="min-h-[7.2rem] resize-y !text-[1.3rem]"
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(event) => {
|
||||
clearFieldError(field.key);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
control = (
|
||||
<Input
|
||||
id={fieldId}
|
||||
className="w-full !text-[1.3rem]"
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
step={field.type === "number" ? "any" : undefined}
|
||||
maxLength={DATE_KEYS.has(field.key) ? 10 : undefined}
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(event) => {
|
||||
clearFieldError(field.key);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: DATE_KEYS.has(field.key)
|
||||
? formatDateInput(event.target.value)
|
||||
: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardFormField
|
||||
key={field.key}
|
||||
id={fieldId}
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
error={fieldErrors[field.key]}
|
||||
span={span}
|
||||
>
|
||||
{control}
|
||||
</StandardFormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<LoadingOverlay isLoading={loading} />
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
노후하수관로 사업 {isModify ? "수정" : "등록"}
|
||||
노후 하수관로 정비사업 {isModify ? "상세/수정" : "등록"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="18%" />
|
||||
<col width="82%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도,시군구 선택</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<SelectBox
|
||||
label="시도"
|
||||
className=""
|
||||
labelClass="w-16 whitespace-nowrap text-[1.3rem] text-gray-600"
|
||||
selectClass="h-11 w-[180px] rounded-lg bg-white"
|
||||
selectArray={ctpvOptions}
|
||||
value={ctpvCd}
|
||||
onSelectValue={(value) => {
|
||||
const nextCtpvCd = String(value);
|
||||
clearFieldError("region");
|
||||
setCtpvCd(nextCtpvCd);
|
||||
setSggCd("0");
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
ctpvNm:
|
||||
ctpv.find(
|
||||
(option) => String(option.val) === nextCtpvCd,
|
||||
)?.text ?? "",
|
||||
sigunNm: "",
|
||||
sggCd: "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<SelectBox
|
||||
label="시군구"
|
||||
className=""
|
||||
labelClass="w-20 whitespace-nowrap text-[1.3rem] text-gray-600"
|
||||
selectClass="h-11 w-[220px] rounded-lg bg-white"
|
||||
selectArray={representativeSggOptions}
|
||||
value={sggCd}
|
||||
onSelectValue={(value) => {
|
||||
const nextSggCd = String(value);
|
||||
const selectedOption = representativeSggOptions.find(
|
||||
(option) => String(option.val) === nextSggCd,
|
||||
);
|
||||
clearFieldError("region");
|
||||
setSggCd(nextSggCd);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
sigunNm: selectedOption?.text ?? "",
|
||||
sggCd: nextSggCd === "0" ? "" : nextSggCd,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.region && (
|
||||
<p className="mt-1 text-[1.2rem] text-red-600" role="alert">
|
||||
{fieldErrors.region}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{FIELDS.map((field) => (
|
||||
<TableRow key={field.key}>
|
||||
<TableHead data-required={field.required || undefined}>
|
||||
{field.label}
|
||||
</TableHead>
|
||||
<TableCell className="border">
|
||||
{field.key === "bizSe" ? (
|
||||
<SelectBox
|
||||
className=""
|
||||
selectClass="h-11 rounded-lg bg-white"
|
||||
selectArray={BIZ_SE_OPTIONS}
|
||||
value={form.bizSe}
|
||||
onSelectValue={(value) => {
|
||||
clearFieldError("bizSe");
|
||||
setForm({ ...form, bizSe: String(value) });
|
||||
}}
|
||||
/>
|
||||
) : field.type === "textarea" ? (
|
||||
<textarea
|
||||
className="min-h-24 w-full rounded border bg-white p-3 text-[1.3rem]"
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => {
|
||||
clearFieldError(field.key);
|
||||
setForm({ ...form, [field.key]: e.target.value });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="!text-[1.3rem]"
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
step={field.type === "number" ? "any" : undefined}
|
||||
maxLength={DATE_KEYS.has(field.key) ? 10 : undefined}
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => {
|
||||
clearFieldError(field.key);
|
||||
setForm({
|
||||
...form,
|
||||
[field.key]: DATE_KEYS.has(field.key)
|
||||
? formatDateInput(e.target.value)
|
||||
: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{fieldErrors[field.key] && (
|
||||
<p className="mt-1 text-[1.2rem] text-red-600" role="alert">
|
||||
{fieldErrors[field.key]}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="card space-y-4">
|
||||
{notice && (
|
||||
<p className="mt-3 text-sm text-red-600" role="alert">
|
||||
<p
|
||||
className="rounded-sm border border-red-200 bg-red-50 px-4 py-3 text-[1.3rem] text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
{notice}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button size="sm" onClick={() => router.push(listHref)}>
|
||||
목록
|
||||
</Button>
|
||||
|
||||
<StandardFormSection title="기본 정보">
|
||||
{renderField("bizSe")}
|
||||
{renderField("prcisExmnCycl")}
|
||||
{renderField("bizCd")}
|
||||
{renderField("bizNm", 2)}
|
||||
{renderField("prplPrst")}
|
||||
</StandardFormSection>
|
||||
|
||||
<StandardFormSection title="지역 및 일정">
|
||||
<StandardFormField
|
||||
id="obslt-ppl-ctpv"
|
||||
label="시도"
|
||||
required
|
||||
error={fieldErrors.region}
|
||||
>
|
||||
<Select
|
||||
value={ctpvCd}
|
||||
onValueChange={(value) => {
|
||||
const nextCtpvCd = String(value);
|
||||
clearFieldError("region");
|
||||
setCtpvCd(nextCtpvCd);
|
||||
setSggCd("0");
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
ctpvNm:
|
||||
ctpv.find((option) => String(option.val) === nextCtpvCd)
|
||||
?.text ?? "",
|
||||
sigunNm: "",
|
||||
sggCd: "",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="obslt-ppl-ctpv" className="w-full">
|
||||
<SelectValue placeholder="시도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0" disabled>
|
||||
선택
|
||||
</SelectItem>
|
||||
{ctpv.map((option) => (
|
||||
<SelectItem
|
||||
key={String(option.val)}
|
||||
value={String(option.val)}
|
||||
>
|
||||
{option.text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</StandardFormField>
|
||||
|
||||
<StandardFormField id="obslt-ppl-sgg" label="시군구">
|
||||
<Select
|
||||
value={sggCd}
|
||||
onValueChange={(value) => {
|
||||
const nextSggCd = String(value);
|
||||
const selectedOption = representativeSggOptions.find(
|
||||
(option) => String(option.val) === nextSggCd,
|
||||
);
|
||||
clearFieldError("region");
|
||||
setSggCd(nextSggCd);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
sigunNm: selectedOption?.text ?? "",
|
||||
sggCd: nextSggCd === "0" ? "" : nextSggCd,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="obslt-ppl-sgg" className="w-full">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">선택</SelectItem>
|
||||
{representativeSggOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={String(option.val)}
|
||||
value={String(option.val)}
|
||||
>
|
||||
{option.text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</StandardFormField>
|
||||
|
||||
{renderField("envoNm")}
|
||||
{renderField("bgngYr")}
|
||||
{renderField("cmcnYr")}
|
||||
{renderField("evdncDcmntSbmsnStts")}
|
||||
{renderField("bgngYmd")}
|
||||
{renderField("cmcnYmd", 2)}
|
||||
</StandardFormSection>
|
||||
|
||||
<StandardFormSection title="사업량">
|
||||
{renderField("planPrlg")}
|
||||
{renderField("frhfyrExpcPrlg")}
|
||||
{renderField("rmndPrlg")}
|
||||
</StandardFormSection>
|
||||
|
||||
<StandardFormSection title="담당자 및 의견">
|
||||
{renderField("bizPicNm")}
|
||||
{renderField("bizPicCnpl", 2)}
|
||||
{renderField("bizPrplDlyRsnCn", 3)}
|
||||
{renderField("rmrkCn", 3)}
|
||||
{renderField("pbcpOpnnCn", 3)}
|
||||
</StandardFormSection>
|
||||
|
||||
<div className="flex justify-center gap-4 pb-8 pt-6">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-sm bg-blue-600 px-4 py-2 !text-white hover:bg-blue-700"
|
||||
type="button"
|
||||
size="default"
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="default"
|
||||
color="white"
|
||||
onClick={() => router.push(listHref)}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
{isModify && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-sm bg-red-600 px-4 py-2 !text-white hover:bg-red-700"
|
||||
type="button"
|
||||
size="default"
|
||||
color="red"
|
||||
onClick={() => void handleDelete()}
|
||||
>
|
||||
삭제
|
||||
|
||||
@@ -7,14 +7,25 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import {
|
||||
SearchDiv,
|
||||
SearchFormLeft,
|
||||
SearchFormRight,
|
||||
SelectBox,
|
||||
UiTable,
|
||||
} from "@common/business_components/ui";
|
||||
import ListDiv from "@common/business_components/ui/list_div";
|
||||
import { Button, Input, Label } from "@common/components/ui";
|
||||
StandardDataTable,
|
||||
getStandardRowNumber,
|
||||
} from "@common/business_components";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
getAllObsltPplBusinesses,
|
||||
@@ -38,8 +49,6 @@ import { openWindowPopup } from "../../windowPopup/lib/openWindowPopup";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../../windowPopup/model/types";
|
||||
|
||||
const DEFAULT_LIST_SIZE = 10;
|
||||
const ACTION_BUTTON_CLASS =
|
||||
"rounded-sm bg-blue-600 px-4 py-2 text-sm font-medium !text-white hover:bg-blue-700 disabled:!text-white";
|
||||
const BIZ_SE_OPTIONS = [
|
||||
{ text: "전체", val: "0" },
|
||||
{ text: "정밀조사", val: "정밀조사" },
|
||||
@@ -50,6 +59,12 @@ const ALL_REGION_OPTION = { text: "전체", val: "0" };
|
||||
type ObsltPplTableRow = ObsltPplBusiness & {
|
||||
rowNumber: number;
|
||||
};
|
||||
type ObsltPplTableColumn = {
|
||||
id: keyof ObsltPplTableRow;
|
||||
title: string;
|
||||
width: number;
|
||||
align?: "left" | "center" | "right";
|
||||
};
|
||||
type ObsltPplSearch = {
|
||||
bizSe: string;
|
||||
ctpvNm: string;
|
||||
@@ -74,31 +89,42 @@ const createListQueryString = (
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
const TABLE_HEADLIST = [
|
||||
{ id: "rowNumber", title: "NO", widthPercent: 4 },
|
||||
{ id: "bizSe", title: "사업구분", widthPercent: 6 },
|
||||
{ id: "prcisExmnCycl", title: "정밀조사차수", widthPercent: 6 },
|
||||
{ id: "bizCd", title: "사업코드", widthPercent: 7 },
|
||||
//{ id: "sggCd", title: "시군구코드", widthPercent: 6 },
|
||||
{ id: "ctpvNm", title: "시도", widthPercent: 6 },
|
||||
{ id: "sigunNm", title: "시군", widthPercent: 6 },
|
||||
{ id: "envoNm", title: "환경청", widthPercent: 8 },
|
||||
{ id: "bizNm", title: "사업명", widthPercent: 16 },
|
||||
{ id: "prplPrst", title: "추진현황", widthPercent: 6 },
|
||||
{ id: "bgngYr", title: "착수년도", widthPercent: 5 },
|
||||
{ id: "cmcnYr", title: "준공(예정)년도", widthPercent: 6 },
|
||||
{ id: "planPrlg", title: "계획연장(km)", widthPercent: 6 },
|
||||
{ id: "frhfyrExpcPrlg", title: "상반기(예상)연장(km)", widthPercent: 8 },
|
||||
{ id: "rmndPrlg", title: "잔여연장(km)", widthPercent: 6 },
|
||||
{ id: "evdncDcmntSbmsnStts", title: "증빙서류\n제출여부", widthPercent: 6 },
|
||||
{ id: "bgngYmd", title: "착수일자", widthPercent: 7 },
|
||||
{ id: "cmcnYmd", title: "준공(예정)일자", widthPercent: 7 },
|
||||
{ id: "bizPrplDlyRsnCn", title: "사업추진 부진사유", widthPercent: 26 },
|
||||
{ id: "bizPicNm", title: "담당자 이름", widthPercent: 6 },
|
||||
{ id: "bizPicCnpl", title: "담당자 연락처", widthPercent: 8 },
|
||||
{ id: "rmrkCn", title: "비고", widthPercent: 10 },
|
||||
{ id: "pbcpOpnnCn", title: "공단\n의견", widthPercent: 2 },
|
||||
/** 다열 목록은 화면별 CSS 대신 공통 스크롤 표에 전달할 실제 컬럼 폭만 정의한다. */
|
||||
const TABLE_COLUMNS: ObsltPplTableColumn[] = [
|
||||
{ id: "rowNumber", title: "순번", width: 60 },
|
||||
{ id: "bizSe", title: "사업구분", width: 85 },
|
||||
{ id: "prcisExmnCycl", title: "정밀조사\n차수", width: 85 },
|
||||
{ id: "bizCd", title: "사업코드", width: 105 },
|
||||
{ id: "ctpvNm", title: "시도", width: 90 },
|
||||
{ id: "sigunNm", title: "시군구", width: 90 },
|
||||
{ id: "envoNm", title: "환경청", width: 120 },
|
||||
{ id: "bizNm", title: "사업명", width: 240, align: "left" },
|
||||
{ id: "prplPrst", title: "추진현황", width: 85 },
|
||||
{ id: "bgngYr", title: "착수년도", width: 75 },
|
||||
{ id: "cmcnYr", title: "준공(예정)\n년도", width: 90 },
|
||||
{ id: "planPrlg", title: "계획연장\n(km)", width: 95, align: "right" },
|
||||
{
|
||||
id: "frhfyrExpcPrlg",
|
||||
title: "상반기(예상)\n연장(km)",
|
||||
width: 120,
|
||||
align: "right",
|
||||
},
|
||||
{ id: "rmndPrlg", title: "잔여연장\n(km)", width: 95, align: "right" },
|
||||
{ id: "evdncDcmntSbmsnStts", title: "증빙서류\n제출여부", width: 90 },
|
||||
{ id: "bgngYmd", title: "착수일자", width: 105 },
|
||||
{ id: "cmcnYmd", title: "준공(예정)\n일자", width: 110 },
|
||||
{
|
||||
id: "bizPrplDlyRsnCn",
|
||||
title: "사업추진 부진사유",
|
||||
width: 300,
|
||||
align: "left",
|
||||
},
|
||||
{ id: "bizPicNm", title: "담당자 이름", width: 90 },
|
||||
{ id: "bizPicCnpl", title: "담당자 연락처", width: 120 },
|
||||
{ id: "rmrkCn", title: "비고", width: 160, align: "left" },
|
||||
{ id: "pbcpOpnnCn", title: "공단\n의견", width: 90, align: "left" },
|
||||
];
|
||||
const TABLE_MIN_WIDTH = "280rem";
|
||||
|
||||
const EMPTY_PAGE: ObsltPplPage = {
|
||||
content: [],
|
||||
@@ -108,8 +134,6 @@ const EMPTY_PAGE: ObsltPplPage = {
|
||||
size: DEFAULT_LIST_SIZE,
|
||||
};
|
||||
|
||||
const DATE_FIELD_SET = new Set<keyof ObsltPplBusiness>(["bgngYmd", "cmcnYmd"]);
|
||||
|
||||
/** 목록 표시값을 공통 포맷으로 정리한다. */
|
||||
const formatBusinessForDisplay = (row: ObsltPplBusiness): ObsltPplBusiness => ({
|
||||
...row,
|
||||
@@ -118,6 +142,13 @@ const formatBusinessForDisplay = (row: ObsltPplBusiness): ObsltPplBusiness => ({
|
||||
cmcnYmd: formatDate(row.cmcnYmd) || null,
|
||||
});
|
||||
|
||||
/** 공통 DataTable 셀에 표시할 문자열을 null과 숫자까지 일관되게 변환한다. */
|
||||
const formatTableValue = (value: unknown) => {
|
||||
if (typeof value === "number") return value.toLocaleString("ko-KR");
|
||||
if (value === null || value === undefined) return "";
|
||||
return String(value);
|
||||
};
|
||||
|
||||
/** 노후하수관로 사업 목록, 검색, xlsx 업로드·다운로드 진입점을 제공한다. */
|
||||
export function ObsltPplListPage() {
|
||||
const router = useRouter();
|
||||
@@ -140,7 +171,6 @@ export function ObsltPplListPage() {
|
||||
const [rows, setRows] = useState<ObsltPplTableRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [resetPageNo, setResetPageNo] = useState(false);
|
||||
// 선택박스의 코드와 API 검색에 사용하는 행정구역명을 함께 관리한다.
|
||||
const [ctpvCd, setCtpvCd] = useState(() => searchParams.get("ctpvCd") ?? "0");
|
||||
const [sggCd, setSggCd] = useState(() => searchParams.get("sggCd") ?? "0");
|
||||
@@ -159,6 +189,67 @@ export function ObsltPplListPage() {
|
||||
return buildObsltPplRepresentativeSggOptions(sgg, ctpvCd);
|
||||
}, [ctpvCd, sgg]);
|
||||
|
||||
/** 공통 DataTable이 표시할 22개 업무 컬럼과 폭·정렬 규칙을 구성한다. */
|
||||
const columns = useMemo<ColumnDef<ObsltPplTableRow>[]>(
|
||||
() =>
|
||||
TABLE_COLUMNS.map((column) => ({
|
||||
id: column.id,
|
||||
accessorFn: (row) => row[column.id],
|
||||
header: () => (
|
||||
<span className="whitespace-pre-line">{column.title}</span>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const displayValue = formatTableValue(getValue());
|
||||
return (
|
||||
<span
|
||||
className="block whitespace-normal break-words"
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
width: column.width,
|
||||
align: column.align ?? "center",
|
||||
},
|
||||
})),
|
||||
[],
|
||||
);
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: page - 1,
|
||||
pageSize: DEFAULT_LIST_SIZE,
|
||||
};
|
||||
|
||||
/** 서버 페이지 번호와 URL 복귀 조건을 공통 DataTable 페이지 상태에 연결한다. */
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
state: { pagination },
|
||||
rowCount: data.totalElements,
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) => String(row.obsltSwagePplBizSn),
|
||||
onPaginationChange: (updater) => {
|
||||
const next =
|
||||
typeof updater === "function" ? updater(pagination) : updater;
|
||||
const nextPage = next.pageIndex + 1;
|
||||
if (nextPage === page) return;
|
||||
|
||||
setPage(nextPage);
|
||||
const queryString = createListQueryString(
|
||||
nextPage,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.replace(
|
||||
`/oper/obsltPplBiz${queryString ? `?${queryString}` : ""}`,
|
||||
{ scroll: false },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/** 공통 레이아웃에서 아직 지역 데이터를 불러오지 못한 경우 공통 store 조회를 보완한다. */
|
||||
useEffect(() => {
|
||||
if (ctpv.length === 0) void fetchCtpv();
|
||||
@@ -184,9 +275,12 @@ export function ObsltPplListPage() {
|
||||
setRows(
|
||||
result.content.map((row, index) => ({
|
||||
...formatBusinessForDisplay(row),
|
||||
rowNumber:
|
||||
result.totalElements -
|
||||
((targetPage - 1) * DEFAULT_LIST_SIZE + index),
|
||||
rowNumber: getStandardRowNumber({
|
||||
total: result.totalElements,
|
||||
rowIndex: index,
|
||||
pageIndex: targetPage - 1,
|
||||
pageSize: DEFAULT_LIST_SIZE,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -199,7 +293,6 @@ export function ObsltPplListPage() {
|
||||
setRows([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setResetPageNo(false);
|
||||
}
|
||||
},
|
||||
[appliedSearch, page],
|
||||
@@ -221,8 +314,7 @@ export function ObsltPplListPage() {
|
||||
`최종확정예산사업 ${inserted.toLocaleString()}건을 등록했습니다.${skipped ? ` 이미 등록된 ${skipped.toLocaleString()}건은 제외했습니다.` : ""}`,
|
||||
);
|
||||
setPage(1);
|
||||
setResetPageNo(true);
|
||||
void load(1, appliedSearch);
|
||||
if (page === 1) void load(1, appliedSearch);
|
||||
};
|
||||
window.addEventListener("message", receivePopupMessage);
|
||||
return () => window.removeEventListener("message", receivePopupMessage);
|
||||
@@ -256,9 +348,8 @@ export function ObsltPplListPage() {
|
||||
setNotice(
|
||||
`총 ${result.importedCount.toLocaleString()}건을 저장했습니다. (정밀조사 ${result.precisionSurveyCount.toLocaleString()}건, 정비사업 ${result.maintenanceCount.toLocaleString()}건)`,
|
||||
);
|
||||
setResetPageNo(true);
|
||||
setPage(1);
|
||||
await load(1, appliedSearch);
|
||||
if (page === 1) await load(1, appliedSearch);
|
||||
} catch (error) {
|
||||
setNotice(
|
||||
error instanceof Error ? error.message : "업로드하지 못했습니다.",
|
||||
@@ -289,236 +380,245 @@ export function ObsltPplListPage() {
|
||||
};
|
||||
|
||||
/** 입력 중 조건을 실제 조회조건으로 확정하고 첫 페이지를 다시 조회한다. */
|
||||
const handleSearch = async () => {
|
||||
const handleSearch = () => {
|
||||
const nextSearch = { ...search };
|
||||
setNotice("");
|
||||
setAppliedSearch(nextSearch);
|
||||
setAppliedCtpvCd(ctpvCd);
|
||||
setAppliedSggCd(sggCd);
|
||||
setResetPageNo(true);
|
||||
const queryString = createListQueryString(1, nextSearch, ctpvCd, sggCd);
|
||||
router.replace(`/oper/obsltPplBiz${queryString ? `?${queryString}` : ""}`, {
|
||||
scroll: false,
|
||||
});
|
||||
if (page === 1) {
|
||||
await load(1, nextSearch);
|
||||
return;
|
||||
}
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
/** CS 목록 화면과 같은 초기화 동작으로 기본 사업구분과 전체 지역을 다시 조회한다. */
|
||||
const handleReset = () => {
|
||||
const nextSearch: ObsltPplSearch = {
|
||||
bizSe: "정밀조사",
|
||||
ctpvNm: "",
|
||||
sigunNm: "",
|
||||
bizNm: "",
|
||||
};
|
||||
setSearch(nextSearch);
|
||||
setAppliedSearch(nextSearch);
|
||||
setCtpvCd("0");
|
||||
setSggCd("0");
|
||||
setAppliedCtpvCd("0");
|
||||
setAppliedSggCd("0");
|
||||
setNotice("");
|
||||
setPage(1);
|
||||
router.replace("/oper/obsltPplBiz?bizSe=정밀조사", { scroll: false });
|
||||
};
|
||||
|
||||
/** 적용된 목록 조건을 보존한 채 신규 등록 화면으로 이동한다. */
|
||||
const handleRegister = () => {
|
||||
const queryString = createListQueryString(
|
||||
page,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.push(
|
||||
`/oper/obsltPplBiz/register${queryString ? `?${queryString}` : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
/** 선택한 목록 행의 수정·상세 화면을 현재 목록 복귀 조건과 함께 연다. */
|
||||
const handleOpenDetail = (row: ObsltPplTableRow) => {
|
||||
const queryString = createListQueryString(
|
||||
page,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.push(
|
||||
`/oper/obsltPplBiz/register/${row.obsltSwagePplBizSn}${queryString ? `?${queryString}` : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay isLoading={loading} />
|
||||
<SearchDiv>
|
||||
<SearchFormLeft>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">
|
||||
<SelectBox
|
||||
label="사업구분"
|
||||
className=""
|
||||
labelClass="w-20 whitespace-nowrap text-xl text-gray-600"
|
||||
selectClass="w-[120px] rounded-lg bg-white"
|
||||
selectArray={BIZ_SE_OPTIONS}
|
||||
value={search.bizSe}
|
||||
onSelectValue={(value) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
bizSe: String(value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SelectBox
|
||||
label="시도"
|
||||
className=""
|
||||
labelClass="w-16 whitespace-nowrap text-xl text-gray-600"
|
||||
selectClass="w-[140px] rounded-lg bg-white"
|
||||
selectArray={[ALL_REGION_OPTION, ...ctpv]}
|
||||
value={ctpvCd}
|
||||
onSelectValue={(value) => {
|
||||
const nextCtpvCd = String(value);
|
||||
setCtpvCd(nextCtpvCd);
|
||||
setSggCd("0");
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
ctpvNm:
|
||||
ctpv.find((option) => String(option.val) === nextCtpvCd)
|
||||
?.text ?? "",
|
||||
sigunNm: "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SelectBox
|
||||
label="시군구"
|
||||
className=""
|
||||
labelClass="w-20 whitespace-nowrap text-xl text-gray-600"
|
||||
selectClass="w-[140px] rounded-lg bg-white"
|
||||
selectArray={[ALL_REGION_OPTION, ...availableSgg]}
|
||||
value={sggCd}
|
||||
onSelectValue={(value) => {
|
||||
const nextSggCd = String(value);
|
||||
setSggCd(nextSggCd);
|
||||
const selectedOption = availableSgg.find(
|
||||
(option) => String(option.val) === nextSggCd,
|
||||
);
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
sigunNm: selectedOption?.text ?? "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Label className="w-20 text-xl text-gray-600">사업명</Label>
|
||||
<Input
|
||||
className="w-[220px] !text-2xl"
|
||||
value={search.bizNm}
|
||||
onChange={(event) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
bizNm: event.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchFormLeft>
|
||||
<SearchFormRight className="ml-auto flex shrink-0 items-center justify-end px-4">
|
||||
<div className="card card-border !mt-6">
|
||||
<form
|
||||
className="flex flex-wrap items-center gap-2 p-2"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
handleSearch();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className={ACTION_BUTTON_CLASS}
|
||||
size="sm"
|
||||
onClick={() => void handleSearch()}
|
||||
<Label className="mr-2 whitespace-nowrap text-label">
|
||||
사업구분
|
||||
</Label>
|
||||
<Select
|
||||
value={search.bizSe}
|
||||
onValueChange={(value) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
bizSe: String(value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
className={ACTION_BUTTON_CLASS}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const queryString = createListQueryString(
|
||||
page,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.push(
|
||||
`/oper/obsltPplBiz/register${queryString ? `?${queryString}` : ""}`,
|
||||
);
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BIZ_SE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.val} value={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={ctpvCd}
|
||||
onValueChange={(value) => {
|
||||
const nextCtpvCd = String(value);
|
||||
setCtpvCd(nextCtpvCd);
|
||||
setSggCd("0");
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
ctpvNm:
|
||||
ctpv.find((option) => String(option.val) === nextCtpvCd)
|
||||
?.text ?? "",
|
||||
sigunNm: "",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
<Button
|
||||
className={ACTION_BUTTON_CLASS}
|
||||
size="sm"
|
||||
onClick={handleOpenCfmtnBgt}
|
||||
>
|
||||
최종확정예산사업
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
className="hidden"
|
||||
onChange={(event) => void handleUpload(event.target.files?.[0])}
|
||||
/>
|
||||
<Button
|
||||
className={ACTION_BUTTON_CLASS}
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
업로드
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-sm bg-green-600 px-4 py-2 text-sm font-medium !text-white hover:bg-green-700"
|
||||
size="sm"
|
||||
onClick={() => void handleDownload()}
|
||||
>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[ALL_REGION_OPTION, ...ctpv].map((option) => (
|
||||
<SelectItem
|
||||
key={String(option.val)}
|
||||
value={String(option.val)}
|
||||
>
|
||||
{option.text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</SearchFormRight>
|
||||
</SearchDiv>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mr-2 whitespace-nowrap text-label">시군구</Label>
|
||||
<Select
|
||||
value={sggCd}
|
||||
onValueChange={(value) => {
|
||||
const nextSggCd = String(value);
|
||||
setSggCd(nextSggCd);
|
||||
const selectedOption = availableSgg.find(
|
||||
(option) => String(option.val) === nextSggCd,
|
||||
);
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
sigunNm: selectedOption?.text ?? "",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[ALL_REGION_OPTION, ...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
|
||||
htmlFor="obslt-ppl-biz-name"
|
||||
className="mr-2 whitespace-nowrap text-label"
|
||||
>
|
||||
사업명
|
||||
</Label>
|
||||
<Input
|
||||
id="obslt-ppl-biz-name"
|
||||
className="w-[22rem] rounded-sm !text-[1.3rem]"
|
||||
value={search.bizNm}
|
||||
onChange={(event) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
bizNm: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-[7.4rem]">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="w-[8.8rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{notice && (
|
||||
<div
|
||||
role="status"
|
||||
className="mx-[1.2rem] mt-4 rounded border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-700"
|
||||
className="mx-[1.2rem] mt-4 rounded-sm border border-primary bg-primary-transparent px-4 py-3 text-[1.3rem] text-primary"
|
||||
>
|
||||
{notice}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListDiv className="overflow-x-auto">
|
||||
<div className="w-[130%]">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-medium">
|
||||
노후하수관로 정밀조사 및 정비사업 현황{" "}
|
||||
<span className="text-base text-gray-500">
|
||||
({data.totalElements.toLocaleString("ko-KR")}건)
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<UiTable<ObsltPplTableRow>
|
||||
headName=""
|
||||
headlist={TABLE_HEADLIST}
|
||||
tableData={rows}
|
||||
pageSize={DEFAULT_LIST_SIZE}
|
||||
total={data.totalElements}
|
||||
curPage={page}
|
||||
widthFixed
|
||||
onPageChange={(nextPage) => {
|
||||
setPage(nextPage);
|
||||
const queryString = createListQueryString(
|
||||
nextPage,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.replace(
|
||||
`/oper/obsltPplBiz${queryString ? `?${queryString}` : ""}`,
|
||||
{ scroll: false },
|
||||
);
|
||||
}}
|
||||
resetPageNo={resetPageNo}
|
||||
cellClick={(index) => {
|
||||
if (typeof index !== "number" || !rows[index]) return;
|
||||
const queryString = createListQueryString(
|
||||
page,
|
||||
appliedSearch,
|
||||
appliedCtpvCd,
|
||||
appliedSggCd,
|
||||
);
|
||||
router.push(
|
||||
`/oper/obsltPplBiz/register/${rows[index].obsltSwagePplBizSn}${queryString ? `?${queryString}` : ""}`,
|
||||
);
|
||||
}}
|
||||
renderCell={(row, headId) => {
|
||||
if (!DATE_FIELD_SET.has(headId as keyof ObsltPplBusiness))
|
||||
return undefined;
|
||||
return formatDate(
|
||||
row[headId as keyof ObsltPplTableRow] as
|
||||
| string
|
||||
| null
|
||||
| undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<div className="mt-4 w-full min-w-0 max-w-full overflow-hidden">
|
||||
<StandardDataTable
|
||||
table={table}
|
||||
title="노후 하수관로 정비사업 목록"
|
||||
scrollMode="horizontal"
|
||||
minTableWidth={TABLE_MIN_WIDTH}
|
||||
className="table-fixed"
|
||||
onRowClick={handleOpenDetail}
|
||||
actions={
|
||||
<>
|
||||
<Button type="button" onClick={() => void handleDownload()}>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
<Button type="button" onClick={handleRegister}>
|
||||
등록
|
||||
</Button>
|
||||
<Button type="button" onClick={handleOpenCfmtnBgt}>
|
||||
최종확정예산사업
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
className="hidden"
|
||||
onChange={(event) => void handleUpload(event.target.files?.[0])}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
업로드
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user