Merge branch 'iipbs/as/dev'
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { DesignListPage } from "@/pages/design-cnstn";
|
||||
|
||||
export default DesignListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<DesignListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { FnrsListPage } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
export default FnrsListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<FnrsListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { InstlListPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default InstlListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<InstlListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default SewerBscPlanListPage;
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
<SewerBscPlanListPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,13 @@ function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
@@ -110,7 +117,11 @@ export function createDesignCnstnColumns() {
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnApplySaveRequestDto,
|
||||
designCnstnKeys,
|
||||
@@ -103,11 +103,11 @@ function FloatingActionButtons({
|
||||
isRegister && actionButtons.some((button) => button === DESIGN_CNSTN_VIEW_BUTTON_ACTION.strg);
|
||||
|
||||
function handleGoToList() {
|
||||
router.push("/design-cnstn");
|
||||
router.push(getListHref("/design-cnstn"));
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||
router.push(withListQs(`/design-cnstn/${designCnstnDocNo}/edit`));
|
||||
}
|
||||
|
||||
function shouldShowButton(action: DesignCnstnViewButtonAction) {
|
||||
@@ -177,7 +177,7 @@ function FloatingActionButtons({
|
||||
action: DESIGN_CNSTN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
});
|
||||
router.push("/design-cnstn");
|
||||
router.push(getListHref("/design-cnstn"));
|
||||
}
|
||||
|
||||
function getReviewOpinionFormData() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import {
|
||||
designCnstnKeys,
|
||||
@@ -91,7 +91,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}/edit`);
|
||||
router.push(withListQs(`/design-cnstn/${designCnstnDocNo}/edit`));
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
@@ -115,7 +115,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
router.push(`/design-cnstn/${designCnstnDocNo}`);
|
||||
router.push(withListQs(`/design-cnstn/${designCnstnDocNo}`));
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
alert("신청 중 오류가 발생했습니다.");
|
||||
@@ -144,7 +144,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
await designCnstnMngService.delete(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.list() });
|
||||
router.push("/design-cnstn");
|
||||
router.push(getListHref("/design-cnstn"));
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
@@ -184,7 +184,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/design-cnstn")}
|
||||
onClick={() => router.push(getListHref("/design-cnstn"))}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
@@ -28,47 +29,61 @@ const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "designCnstnDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: DesignCnstnMngSearchDto = {
|
||||
bizNm: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
taskSeComCd: "",
|
||||
bizNm: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
|
||||
function DesignListPage() {
|
||||
const [bizNm, setBizNm] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useState<DesignCnstnMngSearchDto>(DEFAULT_SEARCH_PARAMS);
|
||||
const { params, setParams } = useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [bizNm, setBizNm] = useState(params.bizNm);
|
||||
const [taskSeComCd, setTaskSeComCd] = useState(params.taskSeComCd);
|
||||
const [sidoComCd, setSidoComCd] = useState(params.sidoComCd);
|
||||
const [sggComCd, setSggComCd] = useState(params.sggComCd);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const searchParams: DesignCnstnMngSearchDto = useMemo(
|
||||
() => ({
|
||||
bizNm: params.bizNm,
|
||||
taskSeComCd: params.taskSeComCd,
|
||||
admdstCd: params.sggComCd || params.sidoComCd || "",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
sort: DEFAULT_SORT,
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
setParams({
|
||||
bizNm,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePaginationChange = (next: PaginationState) => {
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
@@ -143,7 +158,7 @@ function DesignListPage() {
|
||||
<DesignList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnMngResponseDto,
|
||||
type DesignCnstnMngSearchDto,
|
||||
@@ -43,7 +43,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
const columns = useMemo(() => createDesignCnstnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: DesignCnstnMngResponseDto) => {
|
||||
router.push(`/design-cnstn/${row.designCnstnDocNo}`);
|
||||
router.push(withListQs(`/design-cnstn/${row.designCnstnDocNo}`));
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -83,7 +83,9 @@ function DesignViewPage({ id }: DesignViewPageProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<ProgressStatusSection
|
||||
columns={PROGRESS_STATUS_COLUMNS}
|
||||
|
||||
@@ -102,6 +102,13 @@ function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N")
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
@@ -152,7 +159,11 @@ export function createFnrsCnsltnColumns() {
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
AI_AUTO_SE,
|
||||
type AiAutoPrcsLatestResponseDto,
|
||||
@@ -219,11 +219,11 @@ function FloatingActionButtons({
|
||||
isRegister && actionButtons.some((button) => button === FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg);
|
||||
|
||||
function handleGoToList() {
|
||||
router.push("/fnrs-cnsltn");
|
||||
router.push(getListHref("/fnrs-cnsltn"));
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`);
|
||||
router.push(withListQs(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`));
|
||||
}
|
||||
|
||||
function shouldShowButton(action: FnrsCnsltnViewButtonAction) {
|
||||
@@ -314,7 +314,7 @@ function FloatingActionButtons({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
});
|
||||
router.push("/fnrs-cnsltn");
|
||||
router.push(getListHref("/fnrs-cnsltn"));
|
||||
}
|
||||
|
||||
async function handleEnvoSbmsnClick() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
@@ -153,7 +153,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
|
||||
alert("보완 완료 처리되었습니다.");
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
router.push(`/fnrs-cnsltn/${docNo}`);
|
||||
router.push(withListQs(`/fnrs-cnsltn/${docNo}`));
|
||||
} catch (error) {
|
||||
console.error("보완 완료 실패:", error);
|
||||
alert("보완 완료 처리 중 오류가 발생했습니다.");
|
||||
@@ -172,7 +172,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
|
||||
alert("저장되었습니다.");
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
router.push(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`);
|
||||
router.push(withListQs(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`));
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
@@ -206,7 +206,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
router.push(`/fnrs-cnsltn/${fnrsCnsltnDocNo}`);
|
||||
router.push(withListQs(`/fnrs-cnsltn/${fnrsCnsltnDocNo}`));
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
alert("신청 중 오류가 발생했습니다.");
|
||||
@@ -235,7 +235,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
await fnrsCnsltnMngService.remove(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
router.push("/fnrs-cnsltn");
|
||||
router.push(getListHref("/fnrs-cnsltn"));
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
@@ -285,7 +285,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/fnrs-cnsltn")}
|
||||
onClick={() => router.push(getListHref("/fnrs-cnsltn"))}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
@@ -28,47 +29,61 @@ const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "fnrsCnsltnDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: FnrsCnsltnMngSearchDto = {
|
||||
bizNm: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
taskSeComCd: "",
|
||||
bizNm: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
|
||||
function FnrsListPage() {
|
||||
const [bizNm, setBizNm] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useState<FnrsCnsltnMngSearchDto>(DEFAULT_SEARCH_PARAMS);
|
||||
const { params, setParams } = useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [bizNm, setBizNm] = useState(params.bizNm);
|
||||
const [taskSeComCd, setTaskSeComCd] = useState(params.taskSeComCd);
|
||||
const [sidoComCd, setSidoComCd] = useState(params.sidoComCd);
|
||||
const [sggComCd, setSggComCd] = useState(params.sggComCd);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const searchParams: FnrsCnsltnMngSearchDto = useMemo(
|
||||
() => ({
|
||||
bizNm: params.bizNm,
|
||||
taskSeComCd: params.taskSeComCd,
|
||||
admdstCd: params.sggComCd || params.sidoComCd || "",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
sort: DEFAULT_SORT,
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
setParams({
|
||||
bizNm,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePaginationChange = (next: PaginationState) => {
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
@@ -143,7 +158,7 @@ function FnrsListPage() {
|
||||
<FnrsList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
FnrsCnsltnMngResponseDto,
|
||||
type FnrsCnsltnMngSearchDto,
|
||||
@@ -43,7 +43,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
const columns = useMemo(() => createFnrsCnsltnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: FnrsCnsltnMngResponseDto) => {
|
||||
router.push(`/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`);
|
||||
router.push(withListQs(`/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`));
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -103,6 +103,13 @@ function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N")
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
@@ -153,7 +160,11 @@ export function createInstlSncnColumns() {
|
||||
columnHelper.accessor("bizNm", {
|
||||
header: "사업명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
@@ -114,11 +114,11 @@ function FloatingActionButtons({
|
||||
isRegister && actionButtons.some((button) => button === INSTL_SNCN_VIEW_BUTTON_ACTION.strg);
|
||||
|
||||
function handleGoToList() {
|
||||
router.push("/instl-sncn");
|
||||
router.push(getListHref("/instl-sncn"));
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/instl-sncn/${instlSncnDocNo}/edit`);
|
||||
router.push(withListQs(`/instl-sncn/${instlSncnDocNo}/edit`));
|
||||
}
|
||||
|
||||
function shouldShowButton(action: InstlSncnViewButtonAction) {
|
||||
@@ -212,7 +212,7 @@ function FloatingActionButtons({
|
||||
action: INSTL_SNCN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
});
|
||||
router.push("/instl-sncn");
|
||||
router.push(getListHref("/instl-sncn"));
|
||||
}
|
||||
|
||||
async function handleInstlSncnAncmntClick() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
@@ -110,7 +110,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
if (!result) return;
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
router.push(`/instl-sncn/${docNo}`);
|
||||
router.push(withListQs(`/instl-sncn/${docNo}`));
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
@@ -129,7 +129,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
if (!instlSncnDocNo) return;
|
||||
|
||||
alert("저장되었습니다.");
|
||||
router.push(`/instl-sncn/${instlSncnDocNo}/edit`);
|
||||
router.push(withListQs(`/instl-sncn/${instlSncnDocNo}/edit`));
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
@@ -158,7 +158,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
if (!instlSncnDocNo) return;
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(`/instl-sncn/${instlSncnDocNo}`);
|
||||
router.push(withListQs(`/instl-sncn/${instlSncnDocNo}`));
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
@@ -184,7 +184,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
await instlSncnService.remove(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() });
|
||||
router.push("/instl-sncn");
|
||||
router.push(getListHref("/instl-sncn"));
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
@@ -227,7 +227,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/instl-sncn")}
|
||||
onClick={() => router.push(getListHref("/instl-sncn"))}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import { useFindGugunCombo, useFindSidoCombo } from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
@@ -26,44 +27,59 @@ const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "instlSncnDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: InstlSncnMngSearchParams = {
|
||||
bizNm: "",
|
||||
const DEFAULT_PARAMS = {
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
bizNm: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
function InstlListPage() {
|
||||
const [bizNm, setBizNm] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useState<InstlSncnMngSearchParams>(DEFAULT_SEARCH_PARAMS);
|
||||
const { params, setParams } = useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [bizNm, setBizNm] = useState(params.bizNm);
|
||||
const [taskSeComCd, setTaskSeComCd] = useState(params.taskSeComCd);
|
||||
const [sidoComCd, setSidoComCd] = useState(params.sidoComCd);
|
||||
const [sggComCd, setSggComCd] = useState(params.sggComCd);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const searchParams: InstlSncnMngSearchParams = useMemo(
|
||||
() => ({
|
||||
bizNm: params.bizNm,
|
||||
taskSeComCd: params.taskSeComCd,
|
||||
admdstCd: params.sggComCd || params.sidoComCd || "",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
sort: DEFAULT_SORT,
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
setParams({
|
||||
bizNm,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePaginationChange = (next: PaginationState) => {
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
@@ -132,7 +148,7 @@ function InstlListPage() {
|
||||
<InstlList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
type InstlSncnMngResponseDto,
|
||||
type InstlSncnMngSearchParams,
|
||||
@@ -43,7 +43,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
const columns = useMemo(() => createInstlSncnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: InstlSncnMngResponseDto) => {
|
||||
router.push(`/instl-sncn/${row.instlSncnDocNo}`);
|
||||
router.push(withListQs(`/instl-sncn/${row.instlSncnDocNo}`));
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -95,7 +95,9 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<ProgressStatusSection
|
||||
columns={PROGRESS_STATUS_COLUMNS}
|
||||
|
||||
@@ -101,6 +101,13 @@ function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N")
|
||||
];
|
||||
}
|
||||
|
||||
function getProgressStatusBadge(prgrsSttsComCd?: string | null) {
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return <Badge color="orange">보완요청</Badge>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
@@ -151,7 +158,11 @@ export function createSewerBscPlanColumns() {
|
||||
columnHelper.accessor("sewerBscPlanNm", {
|
||||
header: "기본계획명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
cell: ({ row, getValue }) => (
|
||||
<>
|
||||
{getProgressStatusBadge(row.original.prgrsSttsComCd)} {formatOptional(getValue())}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
||||
header: "계획구분",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
@@ -116,11 +116,11 @@ function FloatingActionButtons({
|
||||
isRegister && actionButtons.some((button) => button === SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg);
|
||||
|
||||
function handleGoToList() {
|
||||
router.push("/sewer-bsc-plan");
|
||||
router.push(getListHref("/sewer-bsc-plan"));
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
router.push(`/sewer-bsc-plan/${sewerBscPlanDocNo}/edit?chgCycl=${chgCycl}`);
|
||||
router.push(withListQs(`/sewer-bsc-plan/${sewerBscPlanDocNo}/edit?chgCycl=${chgCycl}`));
|
||||
}
|
||||
|
||||
function shouldShowButton(action: SewerBscPlanViewButtonAction) {
|
||||
@@ -201,7 +201,7 @@ function FloatingActionButtons({
|
||||
action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del,
|
||||
successMessage: "삭제되었습니다.",
|
||||
});
|
||||
router.push("/sewer-bsc-plan");
|
||||
router.push(getListHref("/sewer-bsc-plan"));
|
||||
}
|
||||
|
||||
function getReviewOpinionFormData() {
|
||||
@@ -502,7 +502,9 @@ async function handleDmndSplmnt(
|
||||
context.request,
|
||||
);
|
||||
await context.queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
context.router.replace(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl}`);
|
||||
context.router.replace(
|
||||
withListQs(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl}`),
|
||||
);
|
||||
context.onSuccess?.();
|
||||
} catch (error) {
|
||||
context.onError?.(error as Error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
@@ -123,7 +123,9 @@ function SewerBscPlanFormPage({
|
||||
if (!result?.sewerBscPlanDocNo) return;
|
||||
|
||||
alert("저장되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}/edit?chgCycl=${result.chgCycl}`);
|
||||
router.push(
|
||||
withListQs(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}/edit?chgCycl=${result.chgCycl}`),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
@@ -162,7 +164,11 @@ function SewerBscPlanFormPage({
|
||||
if (!result?.sewerBscPlanDocNo) return;
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`);
|
||||
router.push(
|
||||
withListQs(
|
||||
`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`,
|
||||
),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
@@ -199,7 +205,7 @@ function SewerBscPlanFormPage({
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
const chgCycl = data.sewerBscPlanDtl.chgCycl ?? initialData?.detDto?.chgCycl ?? 1;
|
||||
router.push(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`);
|
||||
router.push(withListQs(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`));
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
@@ -228,7 +234,7 @@ function SewerBscPlanFormPage({
|
||||
setIsSubmitting(true);
|
||||
await sewerBscPlanService.delete(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
router.push("/sewer-bsc-plan");
|
||||
router.push(getListHref("/sewer-bsc-plan"));
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.list() });
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
@@ -240,7 +246,7 @@ function SewerBscPlanFormPage({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
@@ -274,7 +280,7 @@ function SewerBscPlanFormPage({
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/sewer-bsc-plan")}
|
||||
onClick={() => router.push(getListHref("/sewer-bsc-plan"))}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
@@ -28,51 +29,65 @@ const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "sewerBscPlanDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: SewerBscPlanSearchRequestDto = {
|
||||
sewerBscPlanNm: "",
|
||||
sewerBscPlanSeComCd: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
sewerBscPlanSeComCd: "",
|
||||
taskSeComCd: "",
|
||||
sewerBscPlanNm: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
|
||||
function SewerBscPlanListPage() {
|
||||
const [sewerBscPlanNm, setSewerBscPlanNm] = useState("");
|
||||
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<SewerBscPlanSearchRequestDto>(DEFAULT_SEARCH_PARAMS);
|
||||
const { params, setParams } = useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [sewerBscPlanNm, setSewerBscPlanNm] = useState(params.sewerBscPlanNm);
|
||||
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState(params.sewerBscPlanSeComCd);
|
||||
const [taskSeComCd, setTaskSeComCd] = useState(params.taskSeComCd);
|
||||
const [sidoComCd, setSidoComCd] = useState(params.sidoComCd);
|
||||
const [sggComCd, setSggComCd] = useState(params.sggComCd);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const searchParams: SewerBscPlanSearchRequestDto = useMemo(
|
||||
() => ({
|
||||
sewerBscPlanNm: params.sewerBscPlanNm,
|
||||
sewerBscPlanSeComCd: params.sewerBscPlanSeComCd,
|
||||
taskSeComCd: params.taskSeComCd,
|
||||
admdstCd: params.sggComCd || params.sidoComCd || "",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
sort: DEFAULT_SORT,
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
setParams({
|
||||
sewerBscPlanNm,
|
||||
sewerBscPlanSeComCd,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePaginationChange = (next: PaginationState) => {
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
@@ -163,7 +178,7 @@ function SewerBscPlanListPage() {
|
||||
<SewerBscPlanList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, withListQs } from "@/shared/lib";
|
||||
import {
|
||||
type SewerBscPlanResponseDto,
|
||||
type SewerBscPlanSearchRequestDto,
|
||||
@@ -47,7 +47,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
const columns = useMemo(() => createSewerBscPlanColumns(), []);
|
||||
|
||||
const handleRowClick = (row: SewerBscPlanResponseDto) => {
|
||||
router.push(`/sewer-bsc-plan/${row.sewerBscPlanDocNo}?chgCycl=${row.chgCycl}`);
|
||||
router.push(withListQs(`/sewer-bsc-plan/${row.sewerBscPlanDocNo}?chgCycl=${row.chgCycl}`));
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -102,7 +102,9 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<ProgressStatusSection
|
||||
columns={PROGRESS_STATUS_COLUMNS}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { dayjs } from "./dayjs";
|
||||
export { getListHref, withListQs } from "./list-return-href";
|
||||
export { matchQueryStatus } from "./match-query-status";
|
||||
export { queryClient } from "./query-client";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useDataTable } from "./use-data-table";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { useSearchParamsState } from "./use-search-params-state";
|
||||
export { getDefaultFileKey, useTus } from "./use-tus";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 목록 조회 조건을 sessionStorage에 저장하고, 목록 복귀 시 복원하기 위한 헬퍼.
|
||||
*
|
||||
* - 목록 → 상세: 현재 목록 쿼리를 `list-qs-{listPath}` 키로 저장
|
||||
* - 상세 → 수정·저장 후 이동: storage를 건드리지 않음 (같은 탭에서 유지)
|
||||
* - 목록/삭제 후 이동: storage에서 읽어 `/listPath?bizNm=...` 로 복원
|
||||
*/
|
||||
|
||||
function toListSlug(listPath: string) {
|
||||
return listPath.replace(/^\/+|\/+$/g, "").replaceAll("/", "-");
|
||||
}
|
||||
|
||||
function storageKey(listPath: string) {
|
||||
return `list-qs-${toListSlug(listPath)}`;
|
||||
}
|
||||
|
||||
/** 상세/수정 href에서 목록 slug를 추출한다. */
|
||||
function inferListPath(href: string) {
|
||||
const path = href.split("?")[0] ?? "";
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
return segments[0] ?? "";
|
||||
}
|
||||
|
||||
function isOnListPage(slug: string) {
|
||||
if (typeof window === "undefined" || !slug) return false;
|
||||
const { pathname } = window.location;
|
||||
return pathname === `/${slug}` || pathname === `/${slug}/`;
|
||||
}
|
||||
|
||||
/** 목록에 있을 때 현재 쿼리를 저장하고, href는 그대로 반환한다. */
|
||||
export function withListQs(href: string) {
|
||||
if (typeof window === "undefined") return href;
|
||||
|
||||
const slug = inferListPath(href);
|
||||
if (!isOnListPage(slug)) return href;
|
||||
|
||||
const search = window.location.search.replace(/^\?/, "");
|
||||
if (search) sessionStorage.setItem(storageKey(slug), search);
|
||||
else sessionStorage.removeItem(storageKey(slug));
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
/** sessionStorage에 저장된 목록 쿼리로 href를 만든다. */
|
||||
export function getListHref(listPath: string) {
|
||||
if (typeof window === "undefined") return listPath;
|
||||
|
||||
const listQs = sessionStorage.getItem(storageKey(listPath));
|
||||
return listQs ? `${listPath}?${listQs}` : listPath;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* 조회 조건을 URL searchParams와 동기화하는 훅.
|
||||
*
|
||||
* - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다.
|
||||
* - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다.
|
||||
* - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다.
|
||||
* - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다.
|
||||
*
|
||||
* @example
|
||||
* const { params, setParams, resetParams } = useSearchParamsState({
|
||||
* keyword: "",
|
||||
* page: 0,
|
||||
* size: 10,
|
||||
* });
|
||||
*/
|
||||
export function useSearchParamsState<T extends Record<string, ParamValue>>(
|
||||
defaults: T,
|
||||
): UseSearchParamsStateReturn<T> {
|
||||
const searchParams = useSearchParams();
|
||||
const defaultsRef = useRef(defaults);
|
||||
|
||||
const params = useMemo(
|
||||
() => parseParams(searchParams, defaultsRef.current),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const setParams = useCallback((partial: Partial<T>) => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
if (value === undefined || value === defaultsRef.current[key]) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.set(key, String(value));
|
||||
}
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
const resetParams = useCallback(() => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const key of Object.keys(defaultsRef.current)) {
|
||||
next.delete(key);
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
return { params, setParams, resetParams };
|
||||
}
|
||||
|
||||
function parseParams<T extends Record<string, ParamValue>>(
|
||||
searchParams: ReturnType<typeof useSearchParams>,
|
||||
defaults: T,
|
||||
): T {
|
||||
const result = { ...defaults };
|
||||
|
||||
for (const key of Object.keys(defaults) as (keyof T)[]) {
|
||||
const raw = searchParams?.get(key as string) ?? null;
|
||||
if (raw === null) continue;
|
||||
|
||||
if (typeof defaults[key] === "number") {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T];
|
||||
} else {
|
||||
result[key] = raw as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceUrl(searchParams: URLSearchParams) {
|
||||
const qs = searchParams.toString();
|
||||
const url = qs
|
||||
? `${window.location.pathname}?${qs}`
|
||||
: window.location.pathname;
|
||||
window.history.replaceState(null, "", url);
|
||||
}
|
||||
|
||||
type ParamValue = string | number | null;
|
||||
|
||||
interface UseSearchParamsStateReturn<T> {
|
||||
params: T;
|
||||
setParams: (partial: Partial<T>) => void;
|
||||
resetParams: () => void;
|
||||
}
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
SectionCollapsible,
|
||||
UploadedFileChip,
|
||||
} from "@/shared/ui";
|
||||
import { attachService, useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||
import {
|
||||
attachService,
|
||||
useDownloadAttachmentByAtflCd,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "@/entities/attach";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
Button,
|
||||
@@ -373,32 +377,26 @@ function AttachmentsFormSection({
|
||||
return (
|
||||
<SectionCollapsible title="첨부파일">
|
||||
<div className="space-y-6">
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="44%" />
|
||||
<col width="12%" />
|
||||
<col width="44%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" align="left">
|
||||
파일명
|
||||
</TableHead>
|
||||
<TableHead>파일 확장자</TableHead>
|
||||
<TableHead>파일첨부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{essentialFiles.length === 0 ? (
|
||||
{essentialFiles.length === 0 && (
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="44%" />
|
||||
<col width="12%" />
|
||||
<col width="44%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
<TableHead className="w-[30rem]" align="left">
|
||||
파일명
|
||||
</TableHead>
|
||||
<TableHead>파일 확장자</TableHead>
|
||||
<TableHead>파일첨부</TableHead>
|
||||
</TableRow>
|
||||
) : (
|
||||
essentialFiles.map((file) => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{essentialFiles.map((file) => {
|
||||
const atflCd = file.atchFileGroupId ?? "";
|
||||
|
||||
return (
|
||||
@@ -445,11 +443,11 @@ function AttachmentsFormSection({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -468,9 +466,7 @@ function AttachmentsFormSection({
|
||||
fileName={item.file?.name ?? item.fileName}
|
||||
isUploading={isUploading[item.fileKey] ?? false}
|
||||
onRemove={() => void removeEtcFile(idx)}
|
||||
onDownload={
|
||||
item.atflId ? () => downloadByAtflId(item.atflId!) : undefined
|
||||
}
|
||||
onDownload={item.atflId ? () => downloadByAtflId(item.atflId!) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user