Compare commits

...
Author SHA1 Message Date
jcchoi a323c42acf 테이블에 scope="col" 및 캡션 추가
Deploy Frontend / deploy (push) Successful in 4m16s
2026-10-01 22:15:11 +09:00
jcchoi 095101ad2f 필수항목 tooltip 2026-10-01 22:14:28 +09:00
jcchoi 7ee5ebd506 글자 확대 축소 2026-10-01 20:26:45 +09:00
jcchoi f90ef6297d 상단메뉴 클릭 표준 2026-10-01 18:03:03 +09:00
kiminchan 90c1cb1f52 Merge branch 'iipbs/sn/dev' 2026-10-01 16:40:10 +09:00
mind-one001 8c614b84a7 merge(sn): preserve modular map entry with remote import cleanup 2026-10-01 16:22:02 +09:00
kiminchan 7d1a501ad8 관망도 페이지 import 병합 충돌 해소 2026-10-01 16:03:12 +09:00
mind-one001 48589274c8 merge(sn): integrate latest survey layers and upload guidance 2026-10-01 15:59:43 +09:00
kiminchan 83f7676db4 조사 레이어 표출 및 조사결과 업로드 안내 개선 2026-10-01 15:55:29 +09:00
mind-one001 c8b30810f6 style(sn): format integrated map modules 2026-10-01 15:54:43 +09:00
mind-one001 f71fa7d97a merge(sn): retain remote layer and flood updates after refactoring 2026-10-01 15:53:30 +09:00
mind-one001 4c7c4c00ab refactor(sn): modularize map and features and standardize file naming 2026-10-01 15:47:57 +09:00
mind-one005 ae341d66ba sn: 침수예측 오버레이 수정 2026-10-01 15:02:53 +09:00
kiminchan e346417c29 Merge branch 'iipbs/sn/dev' 2026-10-01 10:15:58 +09:00
kiminchan 5fcc184a88 노후관로 레이어를 DB 설정 기반으로 전환 2026-10-01 09:46:16 +09:00
mind-one005 f6d3f89b5b Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-10-01 00:26:53 +09:00
mind-one005 106e09a7a2 sn: 도시침수예방사업 지정일 수정 2026-10-01 00:26:36 +09:00
mind-one005 d1b4600392 sn: 도시침수예방사업 수정화면 사업순번 단계 수정 불가 2026-10-01 00:16:07 +09:00
kiminchan 4204dfa624 Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-30 18:15:15 +09:00
kiminchan d7fd5be068 관로분석 및 통수위험 기본 조회조건 개선 2026-09-30 18:13:31 +09:00
mind-one005 735bb397ec sn: 침수예측현황 분석시각 조건 초기화 2026-09-30 18:09:20 +09:00
mind-one005 8b5a2b713c sn: 도시침수지도 자동선택 2026-09-30 17:53:50 +09:00
chanii8 c0c2d3d1c9 Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-29 17:32:18 +09:00
chanii8 858844e81a [as] chore: 설계자문 진행상태 모델 수정 2026-09-29 16:58:01 +09:00
chanii8 1ff2731121 Merge branch 'iipbs/cs/dev' 2026-09-29 15:37:15 +09:00
chanii8 70db7662bb [cs] chore: 모듈API관리 폼 수정 2026-09-29 15:36:37 +09:00
chanii8 bb57ff72e7 [cs] feat: 모듈API관리 "API URL" 검색 조건 추가 2026-09-29 15:19:42 +09:00
chanii8 7946c08c72 Merge branch 'iipbs/as/dev' 2026-09-29 14:29:32 +09:00
chanii8 6f20ef7273 [as] feat: 보완 요청 사항 반영 (260929_1429) 2026-09-29 14:29:13 +09:00
mind-one005 657ae77dde Merge branch 'iipbs/sn/dev' 2026-09-29 14:03:36 +09:00
mind-one005 4ff80dd257 cm: 유역별 정보 수정 2026-09-29 11:38:47 +09:00
mind-one005 9bb1069d26 cm: 노후 하수관로 현황 수정 2026-09-29 11:17:52 +09:00
chanii8 78d8fea7f5 Merge branch 'iipbs/as/dev' 2026-09-28 16:44:05 +09:00
chanii8 22ed78c87b [as] feat: 행정지원 페이지 useSearchParamsState 적용 2026-09-28 16:37:20 +09:00
mind-one005 f5331d4c50 cm: 유역별 정보 div 수정 2026-09-28 16:32:16 +09:00
mind-one005 f32fe0ec5b cm: 유역별 정보 2026-09-28 16:07:52 +09:00
chanii8 3b914fbc2e [as] feat: 보완 요청 사항 반영 (20260928_1521) 2026-09-28 15:21:19 +09:00
kiminchan 533df0eb16 Merge branch 'iipbs/sn/dev' 2026-09-28 13:23:36 +09:00
mind-one001 7b506320b4 서버페이징 변경 2026-09-28 11:22:22 +09:00
chanii8 2b7c497c3e Merge branch 'iipbs/as/dev' 2026-09-28 11:20:57 +09:00
chanii8 4dee1309eb [as] feat: 보완 요청 사항 반영 (260928_1120) 2026-09-28 11:20:31 +09:00
kiminchan f3f8909e45 Merge branch 'iipbs/sn/dev' 2026-09-28 10:00:24 +09:00
mind-one001 d45cd79422 테스트코드 삭제 2026-09-23 16:00:28 +09:00
mind-one001 7cbb58ff32 노후관로 조회 기능 보완 2026-09-23 14:58:14 +09:00
chanii8 f5ce74d0ee Merge remote-tracking branch 'origin/iipbs/as/dev' 2026-09-23 14:39:57 +09:00
chanii8 3114280e83 [as] chore: 알림이력 배지 크기 조정 2026-09-23 14:39:24 +09:00
chanii8 c7b8b36f4a Merge branch 'iipbs/common/dev' 2026-09-23 14:17:15 +09:00
mind-one001 4f50593eac Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-23 13:50:59 +09:00
mind-one001 8818dd21c6 관망도 레이어 전체켜기 기능 추가 2026-09-23 13:50:50 +09:00
chanii8 db8ee298d9 Merge branch 'iipbs/cs/dev' 2026-09-23 11:40:04 +09:00
chanii8 9eef99a435 [cs] chore: 게시판 댓글 사용 여부 적용 2026-09-23 11:39:32 +09:00
mind-one005 b73b83a49f Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev 2026-09-23 09:08:05 +09:00
mind-one005 549debf8bf sn: 노후하수관로정비사업 검색조건 추가 2026-09-23 09:07:56 +09:00
423 changed files with 18354 additions and 13581 deletions
@@ -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>
);
}
+1
View File
@@ -5,6 +5,7 @@ export {
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "./api/queries";
export { useFindUserSidoGugunCombo } from "./lib/use-find-user-sido-gugun-combo";
export type {
CmCdAdstDstrComboResponseDto,
CmCdAdstDstrService,
@@ -0,0 +1,26 @@
import { useUserStore } from "@/shared/lib";
import { useFindUserGugunCombo, useFindUserSidoCombo } from "../api/queries";
import type { CmCdAdstDstrComboResponseDto } from "../model/adstdstr-interface";
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
export function useFindUserSidoGugunCombo(sidoComCd: string) {
const userInfo = useUserStore((state) => state.userInfo);
const sidoFilterUserInfo =
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
? userInfo
: undefined;
const { data: userSidoData, isPending: isUserSidoPending } =
useFindUserSidoCombo(sidoFilterUserInfo);
const { data: userGugunData, isPending: isUserGugunPending } =
useFindUserGugunCombo(sidoComCd);
return {
userSidoData: userSidoData ?? EMPTY_ADST_LIST,
userGugunData: userGugunData ?? EMPTY_ADST_LIST,
isUserSidoPending,
isUserGugunPending,
};
}
@@ -103,6 +103,8 @@ function ElaprApprovalDialog({
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(cyclDocPrgrsList, currentUserUuid);
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
const canReject =
currentUserPrgrs?.atrzSeComCd === "ATRZ03" || currentUserPrgrs?.atrzSeComCd === "ATRZ99";
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
const isBusy = isProcessing || savePrgrsSttsMutation.isPending;
@@ -392,9 +394,11 @@ function ElaprApprovalDialog({
<Button onClick={handleApprove} disabled={isBusy}>
결재
</Button>
<Button color="red" onClick={handleReject} disabled={isBusy}>
반려
</Button>
{canReject && (
<Button color="red" onClick={handleReject} disabled={isBusy}>
반려
</Button>
)}
</div>
)}
</div>
@@ -17,8 +17,8 @@ const MAIN_STEP_LABELS = {
} as const;
export const PROGRESS_STATUS_COLUMNS = [
{ label: "설계자문 신청 (시군)", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "설계자문 신청 (시군)", sttsComCd: "CT" },
{ label: "기술검토", sttsComCd: "CS" },
{ label: "기술검토 회신", sttsComCd: "CP" },
];
@@ -38,7 +38,7 @@ function buildProgressBadges(prgrsSttsComCd: string): ProgressBadge[] {
];
}
if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
if (prgrsSttsComCd === "CT" || prgrsSttsComCd === "CS" || prgrsSttsComCd === "SL") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "current" },
@@ -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,8 +8,8 @@ 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 { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import {
designCnstnKeys,
type DesignCnstnMngDetResponseDto,
@@ -26,7 +26,10 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { type DesignCnstnRegistFormData, designCnstnRegistSchema } from "../lib/design-cnstn-regist-schema";
import {
type DesignCnstnRegistFormData,
designCnstnRegistSchema,
} from "../lib/design-cnstn-regist-schema";
import {
buildFormPayload,
getCreateDefaultValues,
@@ -91,7 +94,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 +118,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 +147,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 +187,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}
>
목록
@@ -217,7 +220,6 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -1,14 +1,11 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import {
type CmCdAdstDstrComboResponseDto,
useFindGugunCombo,
useFindSidoCombo,
} from "@/entities/adstdstr";
import { useSearchParamsState } from "@/shared/lib";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { CodeSelect } from "@/entities/code";
import type { DesignCnstnMngSearchDto } from "@/entities/design-cnstn";
import {
@@ -27,48 +24,58 @@ import { DesignList } from "./design-list";
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__";
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 DEFAULT_PARAMS = {
sidoComCd: "",
sggComCd: "",
taskSeComCd: "",
bizNm: "",
page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE,
};
const { data: sidoData } = useFindSidoCombo();
const { data: gugunData } = useFindGugunCombo(sidoComCd);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
function DesignListPage() {
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 { userSidoData, userGugunData } = useFindUserSidoGugunCombo(sidoComCd);
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">
@@ -89,7 +96,7 @@ function DesignListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{sidoOptions.map((option) => (
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -111,7 +118,7 @@ function DesignListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{gugunOptions.map((option) => (
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -143,7 +150,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}
@@ -7,15 +7,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import {
Button,
Input,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
@@ -69,9 +61,9 @@ function BasicInfoFormSection() {
aria-invalid={fieldState.invalid}
className="w-[40rem]"
/>
<Button type="button" onClick={() => setIsPickerOpen(true)}>
{/* <Button type="button" onClick={() => setIsPickerOpen(true)}>
사업찾기
</Button>
</Button> */}
</div>
<FieldError errors={[fieldState.error]} />
</Field>
@@ -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: "업무구분",
@@ -48,8 +48,8 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
ENV: "환경청검토",
KECO: "기술검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
@@ -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,8 +8,8 @@ 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 { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
@@ -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}
>
목록
@@ -303,7 +303,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
onClick={handleApplyClick}
disabled={isSubmitting}
>
신청완료
신청 완료
</Button>
<Button
color="red"
@@ -350,7 +350,6 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
onClose={() => setIsPwrkCnsltnDocMngOpen(false)}
/>
) : null}
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -1,14 +1,11 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import {
type CmCdAdstDstrComboResponseDto,
useFindGugunCombo,
useFindSidoCombo,
} from "@/entities/adstdstr";
import { useSearchParamsState } from "@/shared/lib";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { CodeSelect } from "@/entities/code";
import type { FnrsCnsltnMngSearchDto } from "@/entities/fnrs-cnsltn";
import {
@@ -27,48 +24,58 @@ import { FnrsList } from "./fnrs-list";
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__";
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 DEFAULT_PARAMS = {
sidoComCd: "",
sggComCd: "",
taskSeComCd: "",
bizNm: "",
page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE,
};
const { data: sidoData } = useFindSidoCombo();
const { data: gugunData } = useFindGugunCombo(sidoComCd);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
function FnrsListPage() {
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 { userSidoData, userGugunData } = useFindUserSidoGugunCombo(sidoComCd);
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">
@@ -89,7 +96,7 @@ function FnrsListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{sidoOptions.map((option) => (
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -111,7 +118,7 @@ function FnrsListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{gugunOptions.map((option) => (
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -143,7 +150,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({
+12 -1
View File
@@ -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: "업무구분",
@@ -50,8 +50,8 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
ENV: "환경청검토",
KECO: "기술검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
@@ -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,8 +8,8 @@ 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 { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
instlSncnKeys,
@@ -30,8 +30,15 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { buildFormPayload, getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
import { type InstlSncnRegistFormData, instlSncnRegistSchema } from "../lib/instl-sncn-regist-schema";
import {
buildFormPayload,
getCreateDefaultValues,
mapDetailToFormData,
} from "../lib/form-data-mapper";
import {
type InstlSncnRegistFormData,
instlSncnRegistSchema,
} from "../lib/instl-sncn-regist-schema";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
@@ -94,7 +101,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
alert("보완 요청 내용이 있습니다.\n보완 완료 내용을 입력해 주세요.");
return;
}
@@ -109,12 +116,12 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
const result = await submitSupplementForm(data);
if (!result) return;
alert("보완완료 처리되었습니다.");
router.push(`/instl-sncn/${docNo}`);
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
alert("보완 완료 처리되었습니다.");
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
router.push(withListQs(`/instl-sncn/${docNo}`));
} catch (error) {
console.error("보완완료 실패:", error);
alert("보완완료 처리 중 오류가 발생했습니다.");
console.error("보완 완료 실패:", error);
alert("보완 완료 처리 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
@@ -129,7 +136,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 +165,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 +191,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 +234,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}
>
목록
@@ -245,7 +252,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
onClick={form.handleSubmit(onApply)}
disabled={isSubmitting}
>
신청완료
신청 완료
</Button>
<Button
color="red"
@@ -264,7 +271,7 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완완료
보완 완료
</Button>
)}
</div>
@@ -272,7 +279,6 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -1,11 +1,11 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
import { useFindGugunCombo, useFindSidoCombo } from "@/entities/adstdstr";
import { useSearchParamsState } from "@/shared/lib";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { CodeSelect } from "@/entities/code";
import type { InstlSncnMngSearchParams } from "@/entities/instl-sncn";
import {
@@ -24,46 +24,58 @@ import { InstlList } from "./instl-list";
const DEFAULT_PAGE_INDEX = 0;
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_SORT = "instlSncnDocNo,desc";
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
const ALL_SELECT_VALUE = "__ALL__";
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 { userSidoData, userGugunData } = useFindUserSidoGugunCombo(sidoComCd);
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">
@@ -75,7 +87,7 @@ function InstlListPage() {
<Select
value={sidoComCd}
onValueChange={(value) => {
setSidoComCd(value);
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
setSggComCd("");
}}
>
@@ -83,7 +95,8 @@ function InstlListPage() {
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
{sidoOptions.map((option) => (
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -95,12 +108,17 @@ function InstlListPage() {
<Label htmlFor="sggComCd" className="whitespace-nowrap">
시군구
</Label>
<Select value={sggComCd} onValueChange={setSggComCd} disabled={!sidoComCd}>
<Select
value={sggComCd}
onValueChange={(value) => setSggComCd(value === ALL_SELECT_VALUE ? "" : value)}
disabled={!sidoComCd}
>
<SelectTrigger id="sggComCd" className="w-[12rem]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
{gugunOptions.map((option) => (
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -132,7 +150,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}
@@ -55,7 +55,7 @@ export function resolveNotiStatus(
return {
label: "미조회",
badgeProps: { color: "yellow" },
badgeProps: { color: "yellow", size: "lg" },
actionLabel: "처리하기",
};
}
@@ -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: "계획구분",
@@ -44,8 +44,8 @@ export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
ENV: "환경청검토",
KECO: "기술검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
@@ -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);
@@ -4,11 +4,7 @@ import { useEffect, useState } from "react";
import { useUserStore } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
type CmCdAdstDstrComboResponseDto,
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "@/entities/adstdstr";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { useCodeList } from "@/entities/code";
import {
Button,
@@ -48,8 +44,6 @@ interface SewerBscPlanApplyDialogProps {
onConfirm: (selection: SewerBscPlanApplySelection) => void;
}
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlanApplyDialogProps) {
const userInfo = useUserStore((state) => state.userInfo);
const [sidoComCd, setSidoComCd] = useState("");
@@ -57,37 +51,48 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState("");
const [taskSeComCd, setTaskSeComCd] = useState("");
const sidoFilterUserInfo =
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
? userInfo
: undefined;
const { data: sidoData, isPending: isSidoPending } = useFindUserSidoCombo(sidoFilterUserInfo);
const { data: gugunData, isPending: isGugunPending } = useFindUserGugunCombo(sidoComCd);
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
const {
userSidoData,
userGugunData,
isUserSidoPending,
isUserGugunPending,
} = useFindUserSidoGugunCombo(sidoComCd);
const { data: planSeOptions, isPending: isPlanSePending } = useCodeList("SEWERBSCPLANSE");
const { data: taskSeOptions, isPending: isTaskSePending } = useCodeList("SEWER");
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
const isPending = isSidoPending || isPlanSePending || isTaskSePending;
const isPending = isUserSidoPending || isPlanSePending || isTaskSePending;
useEffect(() => {
if (!open) return;
setSidoComCd("");
setSggComCd("");
setSewerBscPlanSeComCd("");
setTaskSeComCd("");
}, [open]);
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
if (!canSidoGugunSelectable) return;
setSidoComCd("");
setSggComCd("");
}, [open, canSidoGugunSelectable]);
useEffect(() => {
if (canSidoGugunSelectable || userSidoData.length === 0) return;
const nextSidoComCd = userSidoData[0].adstDstrCd;
setSidoComCd((current) => (current === nextSidoComCd ? current : nextSidoComCd));
}, [canSidoGugunSelectable, userSidoData]);
useEffect(() => {
if (canSidoGugunSelectable || !sidoComCd || userGugunData.length === 0) return;
const nextSggComCd = userGugunData[0].adstDstrCd;
setSggComCd((current) => current || nextSggComCd);
}, [canSidoGugunSelectable, sidoComCd, userGugunData]);
const canConfirm =
!!sewerBscPlanSeComCd && !!taskSeComCd && (!canSidoGugunSelectable || !!sidoComCd);
function handleConfirm() {
if (!canConfirm) return;
const sidoOption = sidoOptions.find((option) => option.adstDstrCd === sidoComCd);
const gugunOption = gugunOptions.find((option) => option.adstDstrCd === sggComCd);
const sidoOption = userSidoData.find((option) => option.adstDstrCd === sidoComCd);
const gugunOption = userGugunData.find((option) => option.adstDstrCd === sggComCd);
const planSeOption = planSeOptions?.find((option) => option.lwpoCd === sewerBscPlanSeComCd);
const taskSeOption = taskSeOptions?.find((option) => option.lwpoCd === taskSeComCd);
@@ -135,7 +140,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{sidoOptions.map((option) => (
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -148,13 +153,13 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
<Select
value={sggComCd || undefined}
onValueChange={setSggComCd}
disabled={!sidoComCd || isGugunPending}
disabled={!sidoComCd || isUserGugunPending}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{gugunOptions.map((option) => (
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -8,8 +8,8 @@ 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 { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
@@ -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,9 @@ 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);
@@ -179,7 +183,7 @@ function SewerBscPlanFormPage({
(item) => item.cmptnYn === "N" && (!item.splmntCn || !item.splmntCn.trim()),
);
if (hasEmptySplmntCn) {
alert("보완요청 항목이 있습니다.\n보완완료 내용을 입력해 주세요.");
alert("보완 요청 항목이 있습니다.\n보완 완료 내용을 입력해 주세요.");
return;
}
@@ -197,13 +201,13 @@ function SewerBscPlanFormPage({
const result = await sewerBscPlanService.submitSupplement(docNo, payload);
if (!result) return;
alert("보완완료 처리되었습니다.");
alert("보완 완료 처리되었습니다.");
await queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
const chgCycl = data.sewerBscPlanDtl.chgCycl ?? initialData?.detDto?.chgCycl ?? 1;
router.push(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`);
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
router.push(withListQs(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`));
} catch (error) {
console.error("보완완료 실패:", error);
alert("보완완료 처리 중 오류가 발생했습니다.");
console.error("보완 완료 실패:", error);
alert("보완 완료 처리 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
@@ -228,7 +232,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 +244,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 +278,7 @@ function SewerBscPlanFormPage({
<Button
color="white"
type="button"
onClick={() => router.push("/sewer-bsc-plan")}
onClick={() => router.push(getListHref("/sewer-bsc-plan"))}
disabled={isSubmitting}
>
목록
@@ -292,7 +296,7 @@ function SewerBscPlanFormPage({
onClick={handleApplyClick}
disabled={isSubmitting}
>
신청완료
신청 완료
</Button>
<Button
color="red"
@@ -311,7 +315,7 @@ function SewerBscPlanFormPage({
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완완료
보완 완료
</Button>
)}
</div>
@@ -319,7 +323,6 @@ function SewerBscPlanFormPage({
</div>
</SectionCollapsibleGroup>
</form>
<RhfDevTool control={form.control} />
</FormProvider>
);
}
@@ -1,14 +1,11 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import {
type CmCdAdstDstrComboResponseDto,
useFindGugunCombo,
useFindSidoCombo,
} from "@/entities/adstdstr";
import { useSearchParamsState } from "@/shared/lib";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { CodeSelect } from "@/entities/code";
import type { SewerBscPlanSearchRequestDto } from "@/entities/sewer-bsc-plan";
import {
@@ -27,52 +24,62 @@ import { SewerBscPlanList } from "./sewer-bsc-plan-list";
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__";
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 DEFAULT_PARAMS = {
sidoComCd: "",
sggComCd: "",
sewerBscPlanSeComCd: "",
taskSeComCd: "",
sewerBscPlanNm: "",
page: DEFAULT_PAGE_INDEX,
size: DEFAULT_PAGE_SIZE,
};
const { data: sidoData } = useFindSidoCombo();
const { data: gugunData } = useFindGugunCombo(sidoComCd);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
function SewerBscPlanListPage() {
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 { userSidoData, userGugunData } = useFindUserSidoGugunCombo(sidoComCd);
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">
@@ -93,7 +100,7 @@ function SewerBscPlanListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{sidoOptions.map((option) => (
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -115,7 +122,7 @@ function SewerBscPlanListPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
{gugunOptions.map((option) => (
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -163,7 +170,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}
+2
View File
@@ -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>
@@ -4,11 +4,7 @@ import { useEffect, useState } from "react";
import { useUserStore } from "@/shared/lib";
import { Field, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
type CmCdAdstDstrComboResponseDto,
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "@/entities/adstdstr";
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
import { useCodeList } from "@/entities/code";
import {
Button,
@@ -46,8 +42,6 @@ interface PbadmsTaskApplyDialogProps {
codeUpCd?: string;
}
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
function PbadmsTaskApplyDialog({
open,
onOpenChange,
@@ -60,18 +54,15 @@ function PbadmsTaskApplyDialog({
const [sggComCd, setSggComCd] = useState("");
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const sidoFilterUserInfo =
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
? userInfo
: undefined;
const { data: sidoData, isPending: isSidoPending } = useFindUserSidoCombo(sidoFilterUserInfo);
const { data: gugunData, isPending: isGugunPending } = useFindUserGugunCombo(sidoComCd);
const {
userSidoData,
userGugunData,
isUserSidoPending,
isUserGugunPending,
} = useFindUserSidoGugunCombo(sidoComCd);
const { data: taskOptions, isPending: isTaskSePending } = useCodeList(codeUpCd);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
const isPending = isSidoPending || isTaskSePending;
const isPending = isUserSidoPending || isTaskSePending;
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
const canConfirm = selectedCodes.length > 0 && (!canSidoGugunSelectable || !!sidoComCd);
@@ -93,8 +84,8 @@ function PbadmsTaskApplyDialog({
function handleConfirm() {
if (!canConfirm) return;
const sidoOption = sidoOptions.find((option) => option.adstDstrCd === sidoComCd);
const gugunOption = gugunOptions.find((option) => option.adstDstrCd === sggComCd);
const sidoOption = userSidoData.find((option) => option.adstDstrCd === sidoComCd);
const gugunOption = userGugunData.find((option) => option.adstDstrCd === sggComCd);
onConfirm({
admdstCd: sggComCd || sidoComCd,
@@ -137,7 +128,7 @@ function PbadmsTaskApplyDialog({
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{sidoOptions.map((option) => (
{userSidoData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
@@ -150,13 +141,13 @@ function PbadmsTaskApplyDialog({
<Select
value={sggComCd || undefined}
onValueChange={setSggComCd}
disabled={!sidoComCd || isGugunPending}
disabled={!sidoComCd || isUserGugunPending}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{gugunOptions.map((option) => (
{userGugunData.map((option) => (
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
{option.adstDstrNm}
</SelectItem>
+125 -39
View File
@@ -2,7 +2,7 @@
import { client } from "@/shared/api";
import { create } from "zustand";
import { ENDPOINT } from "@/shared/config";
import type { PplLengthContent, SewerageService } from "../model/sn-interface";
import type { BasinStofcbrCountContent, PplLengthContent, SewerageService } from "../model/sn-interface";
import type { OldPplChartData, mapPplOldType, mapPplOldItem } from "../model/sn-interface";
export type SnState = {
@@ -35,26 +35,7 @@ const initialUsePplChartData: OldPplChartData = {
};
const initialOldPplChartData: OldPplChartData = {
labels: ["금강", "한강", "낙동강", "영산강"],
datasets: [
{
label: "20-25년",
data: [1140, 3181, 3100, 1662],
backgroundColor: "#3B82F6",
borderRadius: 4
},
{
label: "25-30년",
data: [1391, 3615, 1814, 1411],
backgroundColor: "#EF4444",
borderRadius: 4
},
{
label: "30년이상",
data: [2014, 10303, 9831, 2000],
backgroundColor: "#F87171",
borderRadius: 4
}
]
datasets: []
};
type OldPplStoreState = {
chartData: OldPplChartData;
@@ -70,23 +51,122 @@ export const useUsePplStore = create<OldPplStoreState>((set) => ({
}));
export const initailMapOldPplData: mapPplOldType = {
colTitle: { totalLen: "총길이", pplLen2025: "20~25년", pplLen2530: "25~30년", pplLen30: "30년 이상" }, //헤더 영역
colTitle: { stofcbrCount: "우수토실개소수", totalLen: "하수관로총연장" },
data: [
{ name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30: 2014 },
{ name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30: 10303 },
{ name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30: 9831 },
{ name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30: 2000 },
{ name: "금강", totalLen: 0, stofcbrCount: 0 },
{ name: "한강", totalLen: 0, stofcbrCount: 0 },
{ name: "낙동강", totalLen: 0, stofcbrCount: 0 },
{ name: "영산강", totalLen: 0, stofcbrCount: 0 },
]
};
type mapPplOldState = {
mapData: mapPplOldItem[];
setMapData: (mapData: mapPplOldItem[]) => void;
setStofcbrCounts: (counts: BasinStofcbrCountContent[]) => void;
};
export const useMapOldPplStore = create<mapPplOldState>((set) => ({
mapData: initailMapOldPplData.data,
setMapData: (newMapData) => set({ mapData: newMapData }),
setMapData: (newMapData) => set((state) => ({
mapData: newMapData.map((item) => ({
...item,
stofcbrCount: state.mapData.find((current) => current.name === item.name)?.stofcbrCount ?? item.stofcbrCount,
})),
})),
setStofcbrCounts: (counts) => set((state) => ({
mapData: state.mapData.map((item) => ({
...item,
stofcbrCount: counts.find((count) => count.zoneNm === item.name)?.stofcbrCount ?? item.stofcbrCount,
})),
})),
}));
const DEFAULT_BASIN_ORDER = ["금강", "한강", "낙동강", "영산강"];
const YEAR_GROUP_START_COLOR = "#FACC15";
const YEAR_GROUP_END_COLOR = "#F87171";
/**
* 시작색과 종료색 사이를 RGB 기준으로 보간해 구간 수만큼 색상을 생성합니다.
* 구간이 하나면 시작색을, 둘 이상이면 첫 구간은 파란색·마지막 구간은 빨간색으로 고정합니다.
*/
function buildYearGroupColors(count: number): string[] {
if (count <= 0) return [];
if (count === 1) return [YEAR_GROUP_START_COLOR];
const parseHex = (color: string) => [
Number.parseInt(color.slice(1, 3), 16),
Number.parseInt(color.slice(3, 5), 16),
Number.parseInt(color.slice(5, 7), 16),
];
const toHex = (value: number) => Math.round(value).toString(16).padStart(2, "0");
const start = parseHex(YEAR_GROUP_START_COLOR);
const end = parseHex(YEAR_GROUP_END_COLOR);
return Array.from({ length: count }, (_, index) => {
const ratio = index / (count - 1);
return `#${start.map((channel, channelIndex) => {
const endChannel = end[channelIndex] ?? channel;
return toHex(channel + (endChannel - channel) * ratio);
}).join("")}`;
});
}
/**
* 별도 정렬순서가 없는 MV 계약에서 구간명의 첫 숫자를 기준으로 표시순서를 결정합니다.
* 숫자가 없는 비정형 구간은 숫자 구간 뒤에 한글 오름차순으로 배치합니다.
*/
function compareYearGroups(left: string, right: string): number {
const leftNumber = left.match(/\d+/)?.[0];
const rightNumber = right.match(/\d+/)?.[0];
if (leftNumber !== undefined && rightNumber !== undefined) {
const numberDifference = Number(leftNumber) - Number(rightNumber);
if (numberDifference !== 0) return numberDifference;
} else if (leftNumber !== undefined) {
return -1;
} else if (rightNumber !== undefined) {
return 1;
}
return left.localeCompare(right, "ko");
}
/**
* MV 응답의 yearGroup 목록으로 노후관로 차트 데이터를 동적으로 생성합니다.
* 구간명이나 구간 개수가 바뀌어도 고정 매핑 없이 동일한 로직을 사용합니다.
*/
export function buildOldPplChartData(items: PplLengthContent[]): OldPplChartData {
const responseZones = [...new Set(items.map((item) => item.zoneNm).filter(Boolean))];
const labels = [
...DEFAULT_BASIN_ORDER,
...responseZones.filter((zone) => !DEFAULT_BASIN_ORDER.includes(zone)).sort((left, right) => left.localeCompare(right, "ko")),
];
const yearGroups = [
...new Set(
items
.map((item) => item.yearGroup?.trim())
.filter((yearGroup): yearGroup is string => Boolean(yearGroup)),
),
].sort(compareYearGroups);
const colors = buildYearGroupColors(yearGroups.length);
const lengthByZoneAndGroup = new Map<string, number>();
items.forEach((item) => {
const yearGroup = item.yearGroup?.trim();
if (!item.zoneNm || !yearGroup) return;
lengthByZoneAndGroup.set(`${item.zoneNm}\u0000${yearGroup}`, Number(item.sumPrlgVl) || 0);
});
return {
labels,
datasets: yearGroups.map((yearGroup, index) => ({
label: yearGroup,
data: labels.map((zone) => lengthByZoneAndGroup.get(`${zone}\u0000${yearGroup}`) ?? 0),
backgroundColor: colors[index] ?? YEAR_GROUP_START_COLOR,
borderRadius: 4,
})),
};
}
/**
* OldPplChartData를 지도 팝업 데이터 형태인 mapPplOldItem[]으로 변환합니다.
* @param chartData 차트 데이터
@@ -96,22 +176,16 @@ export function transformChartDataToMapData(chartData: OldPplChartData): mapPplO
const mapData: mapPplOldItem[] = [];
const { labels, datasets } = chartData;
const pplLen2025Data = datasets.find(d => d.label === '20-25년')?.data || [];
const pplLen2530Data = datasets.find(d => d.label === '25-30년')?.data || [];
const pplLen30Data = datasets.find(d => d.label === '30년이상')?.data || [];
labels.forEach((label, index) => {
const pplLen2025 = pplLen2025Data[index] || 0;
const pplLen2530 = pplLen2530Data[index] || 0;
const pplLen30 = pplLen30Data[index] || 0;
const totalLen = pplLen2025 + pplLen2530 + pplLen30;
const totalLen = datasets.reduce((sum, dataset) => {
const value = dataset.data[index];
return sum + (typeof value === "number" ? value : 0);
}, 0);
mapData.push({
name: label,
totalLen,
pplLen2025,
pplLen2530,
pplLen30,
stofcbrCount: 0,
});
});
@@ -171,7 +245,7 @@ export class SewerageApiService implements SewerageService {
}));
return list;
}
// 유역별 노후도별 관로 길이(km) MV조회 : yearGroup - 20년~25년미만,25년~30년미만,30년이상
// 유역별 노후도별 관로 길이(km) MV조회: yearGroup은 MV가 제공하는 동적 구간명
public async getMvOldPplLength(): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
@@ -184,6 +258,18 @@ export class SewerageApiService implements SewerageService {
return list;
}
// CM 대시보드 유역별 우수토실 개소수 조회
public async getBasinStofcbrCount(): Promise<BasinStofcbrCountContent[]> {
const response = await client.get<{
basinStofcbrCountList: BasinStofcbrCountContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.BASIN_STOFCBR_COUNT);
return (response.data.basinStofcbrCountList ?? []).map((item) => ({
...item,
stofcbrCount: Number(item.stofcbrCount),
}));
}
}
export const SewerageApi = new SewerageApiService();
@@ -12,7 +12,7 @@ import { svcInfo } from "../model/map-data";
import type { mapOperFcltType, mapReuseMapType} from "../model/fo-interface";
import type { mapPplOldType } from "../model/sn-interface";
import { useMapOperFcltStore, OperationApi, useMapReuseMapStore } from "../api/fo-service";
import { useMapOldPplStore } from "../api/sn-service";
import { SewerageApi, useMapOldPplStore } from "../api/sn-service";
import {Button} from "@common/components";
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
@@ -31,12 +31,12 @@ const mapOperFcltData: mapOperFcltType = {
]
};
const mapPplOldData: mapPplOldType = {
colTitle: {totalLen: "총길이",pplLen2025: "20~25년",pplLen2530: "25~30년",pplLen30 : "30년 이상" }, //헤더 영역
colTitle: {stofcbrCount: "우수토실개소수", totalLen: "하수관로총연장" },
data: [
{name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30 : 2014},
{name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30 : 10303},
{name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30 : 9831},
{name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30 : 2000},
{name: "금강", totalLen: 0, stofcbrCount: 0},
{name: "한강", totalLen: 0, stofcbrCount: 0},
{name: "낙동강", totalLen: 0, stofcbrCount: 0},
{name: "영산강", totalLen: 0, stofcbrCount: 0},
]
};
const mapReuseMapData: mapReuseMapType = {
@@ -62,17 +62,18 @@ export function KoreaBasinMap() {
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
const selectedBasinKey = basinKeyMap[selectedBasin];
const {mapOperFclt} = useMapOperFcltStore();
const {mapData: mapPplOld} = useMapOldPplStore();
const {mapData: mapPplOld, setStofcbrCounts} = useMapOldPplStore();
const {setMapData: setMapReuseMapData} = useMapReuseMapStore();
mapOperFcltData.data = mapOperFclt;
mapPplOldData.data = mapPplOld;
// mapReuseMapData.data = mapReuseMap;
let standardYear = '2024';
const [standardYear, setStandardYear] = useState('2024');
useEffect(() => {
const fetchData = async () => {
try {
standardYear = await OperationApi.getStandardYear();
const mapReuseMapRslt = await OperationApi.getReuseMap(standardYear);
const fetchedStandardYear = await OperationApi.getStandardYear();
setStandardYear(fetchedStandardYear);
const mapReuseMapRslt = await OperationApi.getReuseMap(fetchedStandardYear);
setMapReuseMapData(mapReuseMapRslt);
mapReuseMapData.data = mapReuseMapRslt;
} catch (e) {
@@ -80,7 +81,13 @@ export function KoreaBasinMap() {
}
};
fetchData();
}, []);
}, [setMapReuseMapData]);
useEffect(() => {
SewerageApi.getBasinStofcbrCount()
.then(setStofcbrCounts)
.catch((e) => console.error("유역별 우수토실 개소수 조회 실패", e));
}, [setStofcbrCounts]);
// 유역정보
const showFor = useCallback((g: SVGGElement) => {
const activeService = services.find(s => s.active);
@@ -104,24 +111,16 @@ export function KoreaBasinMap() {
});
} else if (activeService.svcId === 'sn') { // 하수관로
const item = mapPplOldData.data.find(d => d.name === basinName);
newPopupContent.title = `${standardYear}년도 ${basinName}유역 노후하수관로 현황`;
newPopupContent.title = `${standardYear}년도 ${basinName}유역 하수관로 현황`;
const colTitles = mapPplOldData.colTitle;
newPopupContent.items.push({
header: colTitles.stofcbrCount,
data: item ? `${fmt(item.stofcbrCount)}개소` : '-',
});
newPopupContent.items.push({
header: colTitles.totalLen,
data: item ? `${fmt(item.totalLen)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen2025,
data: item ? `${fmt(item.pplLen2025)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen2530,
data: item ? `${fmt(item.pplLen2530)}km` : '-',
});
newPopupContent.items.push({
header: colTitles.pplLen30,
data: item ? `${fmt(item.pplLen30)}km` : '-',
});
} else if (activeService.svcId === 'wr') { // 물재이용
const item = mapReuseMapData.data.find(d => d.name === basinName);
newPopupContent.title = `${standardYear}년도 ${basinName}유역 물재이용 현황`;
@@ -160,7 +159,7 @@ export function KoreaBasinMap() {
);
popupRef.current.style.left = x + "px";
}
}, [services, mapOperFclt]);
}, [services, mapOperFclt, mapPplOld, standardYear]);
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
useEffect(() => {
@@ -410,8 +409,14 @@ export function KoreaBasinMap() {
};
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
return (
<div className="col-span-3 !p-0 relative z-10" style={{ height: "765px", background: "rgba(255, 255, 255, 0.6)" }}>
<div className="card card-border" style={{ height: "100%" }}>
<div
className="absolute inset-0 z-10 col-span-3 h-full !p-0"
style={{ background: "rgba(255, 255, 255, 0.6)" }}
>
<div
className="card card-border"
style={{ height: "calc(100% - 1.2rem)" }}
>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">유역별 정보</h3>
@@ -6,11 +6,17 @@ export interface PplLengthContent {
yearGroup?: string;
}
export interface BasinStofcbrCountContent {
zoneNm: string;
stofcbrCount: number;
}
export interface SewerageService {
getPplLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
getMvUsgPplLength(): Promise<PplLengthContent[]>;
getMvOldPplLength(): Promise<PplLengthContent[]>;
getBasinStofcbrCount(): Promise<BasinStofcbrCountContent[]>;
}
export interface ChartDataSet {
@@ -37,17 +43,13 @@ type SnOldPplLength = {
export type mapPplOldItem = {
name: string;
totalLen: number;
pplLen2025: number;
pplLen2530: number;
pplLen30: number;
stofcbrCount: number;
};
export type mapPplOldType = {
colTitle: {
totalLen: string; // 총길이
pplLen2025: string; // 20~25년
pplLen2530: string; // 25~30년
pplLen30: string; // 30년 이상
stofcbrCount: string; // 우수토실개소수
totalLen: string; // 총연장
};
data: mapPplOldItem[];
};
@@ -82,4 +84,4 @@ export function buildSnOldPplLength(
});
return result;
}
}
@@ -135,7 +135,7 @@ function DashboardPage() {
<div className="p-2 sm:p-4 md:p-6">
<div className="flex flex-col gap-1">
{/* 첫번째 라인 - 좌측 6개 패널(2x3) + 우측 지도/사용자 정보 */}
<div className="flex flex-col gap-1 lg:flex-row">
<div className="flex flex-col gap-1 lg:flex-row lg:items-stretch">
{/* 좌측: 드래그 가능한 6개 패널 (2x3 그리드, 화면의 50%) */}
<div className="w-full lg:w-1/2">
{isMounted ? (
@@ -173,9 +173,9 @@ function DashboardPage() {
</div>
{/* 우측: 고정된 지도와 사용자 정보 (화면의 50%) */}
<div className="flex w-full gap-1 lg:w-1/2">
<div className="flex w-full flex-col items-stretch gap-1 lg:w-1/2 lg:flex-row">
{/* 지도영역 - 고정 (드래그 불가) */}
<div className="flex-1">
<div className="relative min-h-[765px] w-full self-stretch lg:min-h-0 lg:flex-1">
<KoreaBasinMap />
</div>
+4 -44
View File
@@ -15,8 +15,8 @@ import {
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface";
import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
import type { PplLengthContent } from "../model/sn-interface";
import { buildOldPplChartData, SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
import { useDragHandle } from "./sortable-panel";
@@ -41,45 +41,7 @@ export default function SnOldPpl() {
useEffect(() => {
if (oldPplLenData.length === 0) return;
// 유역 목록 추출 (중복 제거)
const allZones = [...new Set(oldPplLenData.map(d => d.zoneNm))];
// yearGroup 매핑: API 응답값 -> 차트 label
const yearGroupMap: { [key: string]: { label: string; color: string } } = {
"20년~25년미만": { label: "20-25년", color: "#3B82F6" },
"25년~30년미만": { label: "25-30년", color: "#EF4444" },
"30년이상": { label: "30년이상", color: "#F87171" },
};
// 유역별, 노후년도별 데이터 구조화
const dataByZone: { [zone: string]: { [yearGroup: string]: number } } = {};
allZones.forEach(zone => {
dataByZone[zone] = {};
});
oldPplLenData.forEach(item => {
let zoneData = dataByZone[item.zoneNm];
if (!zoneData) {
zoneData = {};
dataByZone[item.zoneNm] = zoneData;
}
if (item.yearGroup) {
zoneData[item.yearGroup] = item.sumPrlgVl;
}
});
// 데이터셋 생성
const newDatasets: ChartDataSet[] = Object.entries(yearGroupMap).map(([yearGroup, { label, color }]) => ({
label,
data: allZones.map(zone => dataByZone[zone]?.[yearGroup] ?? 0),
backgroundColor: color,
borderRadius: 4,
}));
const newChartData: OldPplChartData = {
labels: allZones,
datasets: newDatasets,
};
const newChartData = buildOldPplChartData(oldPplLenData);
setChartData(newChartData);
@@ -110,9 +72,7 @@ export default function SnOldPpl() {
y: {
beginAtZero: true,
min: 0,
max: 10000,
ticks: {
stepSize: 2000,
callback: function (value) {
return Number(value).toLocaleString() + 'km';
}
@@ -192,4 +152,4 @@ export default function SnOldPpl() {
</div>
</div>
);
}
}
+1
View File
@@ -80,6 +80,7 @@ export const ENDPOINT = {
OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts",
MV_USG_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvUsgPplLength",
MV_OLD_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvOldPplLength",
BASIN_STOFCBR_COUNT: "/sewerage-service/api/v1/ms/basinStofcbrCount",
},
OPERATION_SERVICE: {
MAIN_YEAR: "/operation-service/api/v1/dashBoard/main/year",
@@ -1,3 +1,7 @@
"use client";
export { PostPage as NoticePage } from "@/widgets/post";
import { PostPage } from "@/widgets/post";
export function NoticePage({ bbsId }: { bbsId: string }) {
return <PostPage bbsId={bbsId} tableCaption="공지사항 목록" />;
}
@@ -35,7 +35,13 @@ import { columns as tableColumns } from "../model/post-table-columns";
import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns";
import { SortSelect } from "./sort-select";
function PostPage({ bbsId }: { bbsId: string }) {
function PostPage({
bbsId,
tableCaption,
}: {
bbsId: string;
tableCaption?: string;
}) {
const router = useRouter();
const pathname = usePathname();
const { confirm } = useConfirm();
@@ -116,8 +122,6 @@ function PostPage({ bbsId }: { bbsId: string }) {
meta: { bbsTypeCd: bbsInfo?.bbsTypeCd },
});
const DataTableComponent = isGridViewPage ? GridViewDataTable : DataTable;
const onPostDelete = async () => {
const selectedPosts = table
.getFilteredSelectedRowModel()
@@ -245,11 +249,20 @@ function PostPage({ bbsId }: { bbsId: string }) {
{isLoading ? (
<Skeleton className="h-[36rem] w-full" />
) : (
<DataTableComponent
table={table}
onRowClick={handleRowClick}
viewPagination
/>
isGridViewPage ? (
<GridViewDataTable
table={table}
onRowClick={handleRowClick}
viewPagination
/>
) : (
<DataTable
table={table}
onRowClick={handleRowClick}
viewPagination
caption={tableCaption}
/>
)
)}
</div>
</div>
@@ -1,10 +1,12 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface AuthorizationsParams extends PageableParams {
keywordType?: string;
keyword?: string;
authorizationName?: string;
urlPatternValue?: string;
httpMethodCode?: string;
useYn?: string;
}
+19 -2
View File
@@ -23,6 +23,7 @@ import { columns } from "../model/authorizations-table-columns";
const DEFAULT_PARAMS = {
authorizationName: "",
urlPatternValue: "",
httpMethodCode: "",
sort: "regDt,desc",
page: 0,
@@ -36,6 +37,9 @@ function ModulePage() {
const [authorizationName, setAuthorizationName] = useState(
params.authorizationName,
);
const [urlPatternValue, setUrlPatternValue] = useState(
params.urlPatternValue,
);
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
const query = useQuery({
@@ -43,6 +47,7 @@ function ModulePage() {
queryFn: () =>
AuthorizationsApi.getAuthorizationsList({
authorizationName: params.authorizationName,
urlPatternValue: params.urlPatternValue,
httpMethodCode: params.httpMethodCode,
sort: params.sort,
page: params.page,
@@ -63,11 +68,12 @@ function ModulePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setParams({ authorizationName, httpMethodCode, page: 0 });
setParams({ authorizationName, urlPatternValue, httpMethodCode, page: 0 });
};
const handleReset = () => {
setAuthorizationName("");
setUrlPatternValue("");
setHttpMethodCode("");
resetParams();
};
@@ -113,12 +119,23 @@ function ModulePage() {
API명
</Label>
<Input
id="keyword"
id="authorizationName"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={authorizationName}
onChange={(e) => setAuthorizationName(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="urlPatternValue" className="text-label">
API URL
</Label>
<Input
id="urlPatternValue"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={urlPatternValue}
onChange={(e) => setUrlPatternValue(e.target.value)}
/>
</div>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
+15 -9
View File
@@ -1,10 +1,17 @@
"use client";
import dayjs from "dayjs";
import Image from "next/image";
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMenuStore, useMutationWrapper } from "@/shared/lib";
import { AttachmentApi } from "@/entities/attachment";
import { BoardApi } from "@/entities/board";
import { PostApi } from "@/entities/post";
import {
Button,
Label,
@@ -12,14 +19,11 @@ import {
RadioGroupItem,
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi } from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper, useMenuStore } from "@/shared/lib";
import { PostEvaluationApi } from "../api/post-evaluation-service";
import { PostAction } from "./post-action";
import { PostComment } from "./post-comment";
import { PostEvaluationApi } from "../api/post-evaluation-service";
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
@@ -255,7 +259,9 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)}
</div>
</PostAction>
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
{bbsInfo?.cmntUseYn === "Y" && (
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
)}
</>
);
}
+3 -1
View File
@@ -134,6 +134,8 @@ Codex 작업 루트는 가능하면 `../../../../../`로 잡고, 공통 시작
## 작업 원칙
- 일반 목록·모든 팝업은 공통 AGENTS.md의 서버 페이징·지도·Excel 계약을 따른다. 지도 전체 표출에 현재 그리드 페이지 배열을 재사용하지 않는다. Excel은 적용조건 전체를 사용하고 대량 원시 자료는 서버 생성으로 분리한다. 화면 페이징/페이징 없음 예외는 사유와 검증 범위를 메뉴 > 화면명별로 기록한다.
1. 모든 변경은 원본 요구사항정의서와 화면설계서, 2차 요구사항, 기능 명세 또는 승인된 작업 문서를 근거로 한다.
2. `apps/sn`과 `packages/common/business_components` 범위를 넘는 수정은 하지 않는다.
3. 공통 패키지 수정은 `sn` 요구사항을 충족하기 위한 최소 범위로 제한한다.
@@ -228,7 +230,7 @@ VSCode나 별도 터미널에서 `sn` Next 개발 서버가 실행 중이면 `ne
### 프론트엔드 실행 프로세스 관리
- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 임시 파일·결과 파일 정리 기준을 적용한다. 일회성 검증 스크립트·캡처·Network/서버 로그·원시 결과 파일은 최종 보고서에 요약 후 삭제한다. 정식 회귀 테스트·최종 산출물·사용자 원본/복구 백업은 유지한다. 사용자 dev server가 쓰는 .next·공유 캐시·node_modules는 임시 검증물이라는 이유로 포괄 삭제하지 않는다.
- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
- Codex 에이전트가 작업 또는 검증을 위해 직접 기동한 `next dev`, `dev:sn`, Turborepo dev와 기타 프론트엔드 장시간 실행 프로세스는 작업 종료 전에 반드시 종료한다.
- 셸, 백그라운드, 숨김 창 등 VSCode 밖에서 에이전트가 기동한 프로세스도 종료 대상이다.
+1 -1
View File
@@ -19,7 +19,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { NoFooterLayout } from "@common/components";
import type { MenuItem } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
import useStore from '../store/useStore';
import useStore from "@/entities/reference-data/model/reference-data-store";
import { authService } from "@/entities/auth";
export default function MapLayout({
@@ -2,13 +2,13 @@
"use client";
import * as XLSX from "xlsx";
import { getRawJson } from "@/components/lib/api";
import { getRawJson } from "@/shared/api/sewerage-api";
import {
getAgedPipeStatusByCtpvValidationMessage,
normalizeAgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvResponse,
} from "./agedPipeStatusByCtpvUtils";
} from "./aged-pipe-status-by-ctpv-utils";
/** 적용조건의 시도별 노후관로현황 전체를 조회해 연도 그룹 Excel로 생성한다. */
export const downloadAgedPipeStatusByCtpvExcel = async (
@@ -4,10 +4,10 @@
*/
"use client";
import { getSnRowNumber } from "@/shared/lib/row-number";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Input } from "@common/components";
import {
getStandardRowNumber,
StandardDataTable,
StandardPopupLayout,
} from "@common/business_components";
@@ -18,10 +18,10 @@ import {
type ColumnDef,
type PaginationState,
} from "@tanstack/react-table";
import { getRawJson } from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { getRawJson } from "@/shared/api/sewerage-api";
import LoadingOverlay from "@/components/ui/loading-bar";
import { useAlertStore } from "@/shared/lib";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
import {
AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
buildAgedPipeStatusYearOptions,
@@ -32,9 +32,9 @@ import {
type AgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvResponse,
type AgedPipeStatusByCtpvRow,
} from "./agedPipeStatusByCtpvUtils";
import { downloadAgedPipeStatusByCtpvExcel } from "./agedPipeStatusByCtpvExport";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
} from "./aged-pipe-status-by-ctpv-utils";
import { downloadAgedPipeStatusByCtpvExcel } from "./aged-pipe-status-by-ctpv-export";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
// CS 목록과 동일하게 한 페이지에는 최대 10개 시도만 표시한다.
const AGED_PIPE_STATUS_PAGE_SIZE = 10;
@@ -71,7 +71,7 @@ export default function AgedPipeStatusByCtpvPopupPage() {
id: "rowNumber",
header: "순번",
cell: ({ row }) =>
getStandardRowNumber({
getSnRowNumber({
total: rows.length,
rowIndex: row.index,
}),
@@ -1,13 +1,9 @@
/** 노후관로 목록의 엑셀 다운로드 데이터와 서식을 생성한다. */
"use client";
import { postBlobData } from "@/components/lib/api";
import { fileDownload } from "@/components/lib/utils/fileDownload";
import {
AGED_PIPE_EXCEL_FIELDS,
normalizeAgedPipeFilters,
type AgedPipeFilters,
} from "./agedPipeUtils";
import { postBlobData } from "@/shared/api/file-api";
import { fileDownload } from "@/components/lib/utils/file-download";
import { AGED_PIPE_EXCEL_FIELDS, normalizeAgedPipeFilters, type AgedPipeFilters } from "@/entities/facility/lib/aged-pipe-utils";
type AgedPipeExportProgress = {
fileName?: string;
@@ -6,8 +6,8 @@
import { useEffect, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { validateElapsedYears, type AgedPipeFilters } from "./agedPipeUtils";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
import { validateElapsedYears, type AgedPipeFilters } from "@/entities/facility/lib/aged-pipe-utils";
import LayerSettingCard from "../layer-setting/layer-setting-card";
type MaybePromise<T = void> = T | Promise<T>;
@@ -0,0 +1,71 @@
/** 노후관로의 지도와 목록이 사용하는 DB 레이어 설정 계약이다. */
import { getRawJson } from "@/shared/api/sewerage-api";
import {
buildAgedPipeCqlFilters,
isAgedPipeLayerNode,
type AgedPipeFilters,
} from "@/entities/facility/lib/aged-pipe-utils";
/** DB 응답을 그대로 보존하며 최초 표출 전에 필터만 해석한다. */
export const prepareAgedPipeLayerRows = (
rows: any[],
filters: AgedPipeFilters,
): any[] =>
rows.map((row) => {
const next = { ...row };
if (isAgedPipeLayerNode(row)) {
next.agedPipeFilterTemplate = row.flterSql;
try {
validateAgedPipeLayer(row);
next.flterSql = buildAgedPipeCqlFilters(filters, row.flterSql).join(
" AND ",
);
} catch (error) {
// 노후관로 DB 미전환이 다른 시설물 레이어까지 중단시키지 않도록 이 행만 차단한다.
next.agedPipeConfigError =
error instanceof Error ? error.message : "노후관로 레이어 설정 오류";
next.actlLyrYn = "N";
next.flterSql = "EXCLUDE";
}
}
if (Array.isArray(row.children))
next.children = prepareAgedPipeLayerRows(row.children, filters);
return next;
});
/** 환경마다 달라질 수 있는 일련번호 대신 DB 업무명을 통해 설정 행을 찾는다. */
export const findAgedPipeLayer = (rows: any[]): any | undefined => {
for (const row of rows) {
if (isAgedPipeLayerNode(row)) return row;
const child = Array.isArray(row.children)
? findAgedPipeLayer(row.children)
: undefined;
if (child) return child;
}
};
/** 예전 제어용 행을 실제 WFS 레이어처럼 요청하지 않도록 전환 상태를 확인한다. */
export const validateAgedPipeLayer = (row: any) => {
if (row?.agedPipeConfigError) throw new Error(row.agedPipeConfigError);
if (
!row ||
row.actlLyrYn !== "Y" ||
row.srvcNm !== "wfs" ||
!row.lyrTyNm ||
row.lyrTyNm === "tb_old_pipe"
) {
throw new Error(
"노후관로 DB 레이어 설정을 실제 WFS 레이어와 sql_filt 템플릿으로 갱신해 주세요.",
);
}
};
/** 독립 목록 팝업도 부모창의 하드코딩 없이 동일 설정 API를 조회한다. */
export const fetchAgedPipeLayer = async () => {
const rows = await getRawJson(
"/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK",
);
const row = findAgedPipeLayer(Array.isArray(rows) ? rows : []);
validateAgedPipeLayer(row);
return row;
};
@@ -3,36 +3,40 @@
* 노후관로를 검색하고 목록·엑셀 다운로드·지도 위치 확인 기능을 제공한다.
*/
"use client";
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { getData } from "@/components/lib/api";
import { getData } from "@/shared/api/sewerage-api";
import { useAlertStore } from "@/shared/lib";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/entities/reference-data/model/reference-data-store";
import LoadingOverlay from "@/components/ui/loading-bar";
import {
AGED_PIPE_DEFAULT_FILTERS,
AGED_PIPE_LAYER_TYPE,
AGED_PIPE_LIST_HEADLIST,
AGED_PIPE_PAGE_SIZE,
buildAgedPipeCqlFilters,
getAgedPipeManagementName,
normalizeAgedPipeFilters,
toDisplayPipeRow,
validateElapsedYears,
type AgedPipeFilters,
type AgedPipeRow,
} from "./agedPipeUtils";
import { downloadAgedPipeExcel } from "./agedPipeExport";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
type AgedPipeManagementAgency,
} from "@/entities/facility/lib/aged-pipe-utils";
import { fetchAgedPipeLayer } from "./aged-pipe-layer";
import { downloadAgedPipeExcel } from "./aged-pipe-export";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
export default function AgedPipePopupPage() {
const facilityCodes = useFacilityCodeCatalog();
const alert = useAlertStore((state) => state.setMessage);
const searchParams = useSearchParams();
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
@@ -53,6 +57,18 @@ export default function AgedPipePopupPage() {
const [appliedFilters, setAppliedFilters] =
useState<AgedPipeFilters>(initialFilters);
const [rows, setRows] = useState<AgedPipeRow[]>([]);
// 관리기관명 조회가 늦게 완료되어도 기존 목록을 재조회하지 않고 표시만 갱신한다.
const [managementAgencies, setManagementAgencies] = useState<
AgedPipeManagementAgency[]
>([]);
const displayRows = useMemo(
() =>
rows.map((row) => ({
...row,
mngInstNm: getAgedPipeManagementName(row, managementAgencies),
})),
[rows, managementAgencies],
);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
@@ -73,15 +89,34 @@ export default function AgedPipePopupPage() {
if (sgg.length === 0) fetchSgg();
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
useEffect(() => {
let active = true;
// 기존 GIS 관리 화면과 같은 기관 목록을 사용하고, 실패 시 관로 조회는 유지한다.
getData("v1/fcltPpl/mngInsts")
.then((response) => {
if (active)
setManagementAgencies(Array.isArray(response) ? response : []);
})
.catch(() => {
if (active)
alert("관리기관명을 불러오지 못해 관리기관 코드로 표시합니다.");
});
return () => {
active = false;
};
}, [alert]);
const fetchRows = useCallback(
async (page: number, nextFilters: AgedPipeFilters, size: number) => {
const normalized = normalizeAgedPipeFilters(nextFilters);
// 독립 팝업에서도 현재 DB의 원천 레이어와 필터 템플릿을 사용한다.
const layer = await fetchAgedPipeLayer();
const queryParams = new URLSearchParams({
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
lyrTypeNm: layer.lyrTyNm,
page: String(page - 1),
size: String(size),
});
const cqlFilters = buildAgedPipeCqlFilters(normalized);
const cqlFilters = buildAgedPipeCqlFilters(normalized, layer.flterSql);
if (cqlFilters.length > 0) {
queryParams.append("cqlFiter", cqlFilters.join(" and "));
}
@@ -92,12 +127,12 @@ export default function AgedPipePopupPage() {
const total = Number(response?.count ?? 0);
const rows = responseRows.map(
(item: Record<string, unknown>, index: number) =>
toDisplayPipeRow(item, index, page, ctpv, sgg),
toDisplayPipeRow(item, index, page, ctpv, sgg, facilityCodes),
);
return { rows, total };
},
[ctpv, sgg],
[ctpv, sgg, facilityCodes],
);
const loadRows = useCallback(
@@ -115,13 +150,18 @@ export default function AgedPipePopupPage() {
setAppliedFilters(normalizeAgedPipeFilters(nextFilters));
} catch (error) {
console.error("노후관로 목록 조회 중 오류:", error);
alert(
error instanceof Error
? error.message
: "노후관로 목록 조회 중 오류가 발생했습니다.",
);
setRows([]);
setTotal(0);
} finally {
setIsLoading(false);
}
},
[fetchRows, filters],
[alert, fetchRows, filters],
);
useEffect(() => {
@@ -247,31 +287,6 @@ export default function AgedPipePopupPage() {
) : null}
<PopupSearchPanel contentClassName="flex flex-wrap items-end gap-x-5 gap-y-3">
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">
경과년도
</span>
<Input
className="w-[12rem]"
type="text"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
value={String(filters.elapsedYears ?? "")}
onChange={(event) => {
const digits = event.target.value.replace(/[^0-9]/g, "");
setFilters({
...filters,
elapsedYears:
digits === "" ? ("" as unknown as number) : Number(digits),
});
}}
onKeyDown={(event) => {
if (event.key === "Enter") void handleSearch(1);
}}
/>
</label>
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">
시도
@@ -316,6 +331,31 @@ export default function AgedPipePopupPage() {
</select>
</label>
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">
경과년도
</span>
<Input
className="w-[12rem]"
type="text"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
value={String(filters.elapsedYears ?? "")}
onChange={(event) => {
const digits = event.target.value.replace(/[^0-9]/g, "");
setFilters({
...filters,
elapsedYears:
digits === "" ? ("" as unknown as number) : Number(digits),
});
}}
onKeyDown={(event) => {
if (event.key === "Enter") void handleSearch(1);
}}
/>
</label>
<div className="ml-auto flex items-center gap-2">
<Button
type="button"
@@ -341,7 +381,7 @@ export default function AgedPipePopupPage() {
</PopupSearchPanel>
<MapPopupDataTable
rows={rows as Array<AgedPipeRow & Record<string, unknown>>}
rows={displayRows}
headlist={AGED_PIPE_LIST_HEADLIST}
title="노후관로 목록"
total={total}
@@ -17,13 +17,13 @@ import {
Waves,
} from "lucide-react";
import { createPredictionAnalysisTimeRange } from "@/features/fldngPredcPrst/model/analysisTimeRange";
import { createPredictionAnalysisTimeRange } from "@/features/fldng-predc-prst/model/analysis-time-range";
import {
windowPopupDefinitions,
type WindowPopupKey,
} from "@/features/windowPopup/model/popupDefinitions";
} from "@/features/window-popup/model/popup-definitions";
import { getRawJson } from "@/components/lib/api";
import { getRawJson } from "@/shared/api/sewerage-api";
type Props = {
onOpenPopup: (popupKey: WindowPopupKey) => Window | null;
@@ -1,72 +0,0 @@
/** 선택한 지도 레이어 종류에 맞는 시설 상세 컴포넌트를 반환한다. */
import MsSwagePpl from "../popupComponent/MsSwagePpl"; // 하수관로
import sewagePump from "../popupComponent/sewagePump"; // 하수펌프
import smh from "../popupComponent/smh";
import Ssc from "../popupComponent/Ssc";
import StormWaterPump from "../popupComponent/stormWaterPump";
import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
import SwtrmtPrcsZoneAnalysis from "../popupComponent/SwtrmtPrcsZoneAnalysis";
type LayerComponentProps = {
featureData: any;
isMainSsc?: boolean;
isDrtwSewagePump?: boolean;
rainMeasureData?: any;
onSaveClick?: () => void;
onFormDataChange?: (data: any) => void; // 추가
chmbrXcnts: string;
chmbrYdnts: string;
gisObject?: Record<string, any>; // GIS 객체 추가
rawFeatureData?: any;
surveyResultContent?: React.ReactNode;
};
type LayerContentMap = {
[key: string]: {
component?: React.ComponentType<any>;
props?: any;
};
};
// 레이어 이름과 컴포넌트를 매핑
// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업
const layerContentMap: LayerContentMap = {
"하수처리시설": { component: SwtrmtFclt, }, // 디지털 자료관 연계정보가 없으니 일단 주석 처리
"하수처리구역": { component: SwtrmtPrcsZoneAnalysis },
"우수토실": { component: Ssc, },
"중점우수토실": { component: Ssc, }, // 중점우수토실 여부 설정
"하수관로": { component: MsSwagePpl, },
"하수맨홀": { component: smh, },
"하수펌프장": { component: sewagePump },
// "빗물펌프": { component: StormWaterPump },
// "오수펌프": { component: sewagePump },
};
// 동적 컴포넌트 반환 함수
export const getLayerContentComponent = (layerNm: string): React.FC<LayerComponentProps> | null => {
const component = layerContentMap[layerNm];
if (!component || !component.component) {
// component가 없으면 null 반환
return () => null;
}
return component.component as React.FC<LayerComponentProps>
};
// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단)
export const handleInputChange = (section: string,
index: number,
field: string,
value: string,
dataMap: {
[key: string]: {
data: { year: string; type: string, bizInfoNm: string, bizType: string }[];
setData: React.Dispatch<React.SetStateAction<{ year: string; type: string, bizInfoNm: string, bizType: string }[]>>;
};
}
) => {
const target = dataMap[section];
if (!target) return;
const updated = [...target.data];
if (updated[index]) {
updated[index][field as keyof typeof updated[number]] = value;
}
target.setData(updated);
};
@@ -1,924 +0,0 @@
/** 지도 시설 상세 및 목록에서 사용하는 컬럼·코드 정의를 제공한다. */
// 하수관로
export const MsSwagePplCol = [
{ id: 'fclt_mng_no', title: '시설관리번호' },
{ id: 'mngInstNm', title: '관리기관' },
{ id: 'inst_mng_no', title: '기관관리번호' },
{ id: 'flowDirectionBasis', title: '유하방향 근거' },
{ id: 'instl_ymd', title: '설치일자' },
{ id: 'elapsedYears', title: '경과년도' },
{ id: 'mapsht_no', title: '도엽번호' },
{ id: 'pdmt_vl', title: '관경(mm)' },
{ id: 'prlg_vl', title: '연장(m)' },
{ id: 'usg_cd', title: '용도' },
{ id: 'mqlt_cd', title: '재질' },
{ id: 'avg_grdn', title: '평균구배(m/m)' },
{ id: 'drtw_mltv_area', title: '오수배수면적(m²)' },
{ id: 'excln_mltv_area', title: '우수배수면적(m²)' },
{ id: 'tmpt_dpth', title: '시점깊이(m)' },
{ id: 'tmpt_mhl_mng_no', title: '시점맨홀관리번호' },
{ id: 'scend_dpth', title: '종점깊이(m)' },
{ id: 'scend_mhl_mng_no', title: '종점맨홀관리번호' },
{ id: 'trfln_cnt', title: '차선수' },
{ id: 'vrtc_len', title: '세로길이(m)' },
{ id: 'wdth_len', title: '가로길이(m)' },
// { id: 'rnyssn_fvlc', title: '우기 시 유속(m/s)' },
// { id: 'clar_fvlc', title: '건기 시 유속(m/s)' },
// { id: 'mng_inst_cd', title: '관리기관코드' },
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
// { id: 'fid', title: 'FID' },
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
// { id: 'lyrTypeNm', title: '레이어타입명' },
]
// 하수관로 searchModal 기본정보tab
export const MsSwagePplCol2 = [
{ id: 'rowNumber', title: '순번' },
{ id: 'fcltMngNo', title: '시설\n관리번호' },
{ id: 'instMngNo', title: '기관\n관리번호' },
{ id: 'flowDirectionBasis', title: '유하방향\n근거' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'mapshtNo', title: '도엽번호' },
{ id: 'usgCd', title: '용도' },
{ id: 'pdmtVl', title: '관경\n(mm)' },
{ id: 'prlgVl', title: '연장\n(m)' },
// { id: 'avgGrdn', title: '평균구배\n(m/m)' },
{ id: 'tmptDpth', title: '시점깊이\n(m)' },
{ id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' },
{ id: 'scendDpth', title: '종점깊이\n(m)' },
{ id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' },
{ id: 'vrtcLen', title: '세로길이\n(m)' },
{ id: 'wdthLen', title: '가로길이\n(m)' },
{ id: 'fcltyShpCd', title: '시설물형태코드' },
// { id: 'fid', title: 'FID' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'mqltCd', title: '재질코드' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
// { id: 'usgCd', title: '용도코드' },
// { id: 'lyrTypeNm', title: '레이어타입명' },
]
export const MsSwagePplExcelCol = {
'fcltMngNo': '시설 관리번호',
'instMngNo': '기관 관리번호',
'ctpvNm': '시도',
'sggNm': '시군구',
'swtrmt_zone_nm': '처리구역',
'instlYmd': '설치일자',
'usgCd': '용도',
'pdmtVl': '관경(mm)',
'prlgVl': '연장(m)',
'tmptDpth': '시점 깊이(m)',
'tmptMhlMngNo': '시점맨홀 관리번호',
'scendDpth': '종점 깊이(m)',
'scendMhlMngNo': '종점맨홀 관리번호',
'vrtcLen': '세로길이(m)',
'wdthLen': '가로길이(m)',
'fcltyShpCd': '시설물형태코드',
'mqltCd': '재질코드',
};
// 하수관로 용도 코드표
export const MS_SWAGE_PPL_USE_CODES = [
{ val: "SBA000", text: "미분류" },
{ val: "SBA001", text: "합류관" },
{ val: "SBA002", text: "차집관" },
{ val: "SBA003", text: "우수관" },
{ val: "SBA004", text: "오수관" },
{ val: "SBA999", text: "기타" },
];
// 하수관로 재질 코드표
export const MS_SWAGE_PPL_CODES = [
{ val: "MOP000", text: "미분류" },
{ val: "MOP001", text: "닥타일주철관(DCIP)" },
{ val: "MOP002", text: "강관(SP)" },
{ val: "MOP003", text: "아연도강관(GS)" },
{ val: "MOP004", text: "염화비닐관(PVC)" },
{ val: "MOP005", text: "폴리에틸렌관(PE)" },
{ val: "MOP006", text: "스테인레스관(SSP)" },
{ val: "MOP007", text: "동관(COP)" },
{ val: "MOP008", text: "P.S.콘크리트관(PC)" },
{ val: "MOP009", text: "석면시멘트관(AS)" },
{ val: "MOP010", text: "회주철관(CIP)" },
{ val: "MOP011", text: "연관(LP)" },
{ val: "MOP012", text: "흉관(HP)" },
{ val: "MOP013", text: "철근콘크리트관(RC)" },
{ val: "MOP014", text: "강관(ST)" },
{ val: "MOP015", text: "주철관(CI)" },
{ val: "MOP016", text: "화학제품류관(PV)" },
{ val: "MOP017", text: "도관(VC)" },
{ val: "MOP018", text: "석축(SE)" },
{ val: "MOP019", text: "도복장 강관(SP)" },
{ val: "MOP020", text: "흉관(EP)" },
{ val: "MOP021", text: "아연도강관(GP)" },
{ val: "MOP022", text: "청동관(BR)" },
{ val: "MOP023", text: "고무몰딩(RM)" },
{ val: "MOP024", text: "주강(CS)" },
{ val: "MOP025", text: "VR관" },
{ val: "MOP026", text: "HDPE관" },
{ val: "MOP027", text: "시멘트라이닝주철관(C)" },
{ val: "MOP028", text: "수도용 PE관(PE)" },
{ val: "MOP029", text: "석면시멘트관(AC)" },
{ val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" },
{ val: "MOP031", text: "에폭시코팅강관(EPS)" },
{ val: "MOP032", text: "HI-3P" },
{ val: "MOP033", text: "철근콘크리트박스(RCB)" },
{ val: "MOP034", text: "콘크리트중선함(CD)" },
{ val: "MOP035", text: "후랙시불전선함(FP)" },
{ val: "MOP036", text: "전선함(FD)" },
{ val: "MOP038", text: "이중보온관" },
{ val: "MOP039", text: "폴리에틸렌피복강관(PLP)" },
{ val: "MOP041", text: "FRP" },
{ val: "MOP042", text: "강재" },
{ val: "MOP999", text: "기타" },
{ val: "MOP800", text: "2중벽PE관" },
{ val: "MOP801", text: "3중벽PE관" },
{ val: "MOP802", text: "가스배관용 강관" },
{ val: "MOP803", text: "강관(피복 Double, 용접접합)" },
{ val: "MOP804", text: "강관(피복 Single, 용접접합)" },
{ val: "MOP805", text: "강관(CP)" },
{ val: "MOP806", text: "강철" },
{ val: "MOP807", text: "강화플라스틱복합관" },
{ val: "MOP808", text: "경질염화비닐관(HI-VP)" },
{ val: "MOP809", text: "고강성PVC이중벽관" },
{ val: "MOP810", text: "고광도PVC이중벽관" },
{ val: "MOP811", text: "고밀도폴리에틸렌관(THP)" },
{ val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" },
{ val: "MOP813", text: "공장제 박스관거" },
{ val: "MOP814", text: "관로없음(알케이블)" },
{ val: "MOP815", text: "나선형강관" },
{ val: "MOP816", text: "내충격수도관" },
{ val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" },
{ val: "MOP818", text: "내충격HI-VG2관" },
{ val: "MOP819", text: "내충격PVC하수관" },
{ val: "MOP820", text: "녹석(돌산호과 일종)" },
{ val: "MOP821", text: "동관(구리관, 용접)" },
{ val: "MOP822", text: "둥근잔돌" },
{ val: "MOP823", text: "무근콘크리트" },
{ val: "MOP824", text: "백관" },
{ val: "MOP825", text: "복합철판파형관" },
{ val: "MOP826", text: "분체라이닝관" },
{ val: "MOP827", text: "석축박스" },
{ val: "MOP828", text: "석축철콘" },
{ val: "MOP829", text: "수도용경질염화비관" },
{ val: "MOP830", text: "수도용경질염화비닐이음관" },
{ val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" },
{ val: "MOP832", text: "수지파형강관(PCS)" },
{ val: "MOP833", text: "수지CSP" },
{ val: "MOP834", text: "스테인레스 주름관" },
{ val: "MOP835", text: "스테인레스관(STS)" },
{ val: "MOP836", text: "스테인레스스틸파이프관" },
{ val: "MOP837", text: "아스콘" },
{ val: "MOP838", text: "아스팔트" },
{ val: "MOP839", text: "아스팔트도복장강관(ASP)" },
{ val: "MOP840", text: "아연" },
{ val: "MOP841", text: "아연도금(백관)" },
{ val: "MOP842", text: "알루미늄" },
{ val: "MOP843", text: "알루미늄합금" },
{ val: "MOP844", text: "압력배관용 탄소강관" },
{ val: "MOP845", text: "양철관" },
{ val: "MOP846", text: "양철주름관(CSP)" },
{ val: "MOP847", text: "연합인슈강판" },
{ val: "MOP848", text: "와사관(GSP)" },
{ val: "MOP849", text: "원심력철근콘크리트" },
{ val: "MOP850", text: "유리섬유관(GRP)" },
{ val: "MOP851", text: "유리섬유복합관(GRP)" },
{ val: "MOP852", text: "이음매없는동및동합금관" },
{ val: "MOP853", text: "일반배관용스테인리스강관" },
{ val: "MOP854", text: "전압 콘크리트관" },
{ val: "MOP855", text: "주름관" },
{ val: "MOP856", text: "주름주철파형강관" },
{ val: "MOP857", text: "지중 저압케이블 입상관" },
{ val: "MOP858", text: "진동및전압철근콘크리트관" },
{ val: "MOP859", text: "철근콘크리트" },
{ val: "MOP860", text: "철근콘크리트 블록" },
{ val: "MOP861", text: "철근콘크리트조립흙막이" },
{ val: "MOP862", text: "철판매입관" },
{ val: "MOP863", text: "청동" },
{ val: "MOP864", text: "코아식프리스트레스트콘크리트관" },
{ val: "MOP865", text: "콘크리트" },
{ val: "MOP866", text: "콘크리트 폴륨관" },
{ val: "MOP867", text: "콘크리트블록" },
{ val: "MOP868", text: "탄소강" },
{ val: "MOP869", text: "탄소강(카본스틸)" },
{ val: "MOP870", text: "탄소주강" },
{ val: "MOP871", text: "트러후" },
{ val: "MOP872", text: "파선형강관(RCF)" },
{ val: "MOP873", text: "파형강관(CSP)" },
{ val: "MOP874", text: "파형관" },
{ val: "MOP875", text: "파형플라스틱관" },
{ val: "MOP876", text: "폴리머관" },
{ val: "MOP877", text: "폴리에스테르수지콘크리트관" },
{ val: "MOP878", text: "폴리에틸렌 전선관" },
{ val: "MOP879", text: "폴리에틸렌 피복강관" },
{ val: "MOP880", text: "폴리에틸렌관 1호관" },
{ val: "MOP881", text: "폴리에틸렌관 2호관" },
{ val: "MOP882", text: "폴리에틸렌관(2중벽)" },
{ val: "MOP883", text: "폴리에틸렌관(3중벽)" },
{ val: "MOP884", text: "폴리에틸렌분체라이닝" },
{ val: "MOP885", text: "폴리에틸렌이중벽하수관" },
{ val: "MOP886", text: "폴리탄산에스테르" },
{ val: "MOP887", text: "프리스트레스트콘크리트실린더관" },
{ val: "MOP888", text: "플라스틱" },
{ val: "MOP889", text: "플라스틱주름관" },
{ val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" },
{ val: "MOP891", text: "현장타설관" },
{ val: "MOP892", text: "현장타설철근콘크리트관" },
{ val: "MOP893", text: "흑관" },
{ val: "MOP894", text: "흙" },
{ val: "MOP895", text: "AP" },
{ val: "MOP896", text: "API관" },
{ val: "MOP897", text: "ASP" },
{ val: "MOP898", text: "CMCIP" },
{ val: "MOP899", text: "CSP" },
{ val: "MOP900", text: "DRP" },
{ val: "MOP901", text: "DSP" },
{ val: "MOP902", text: "DTC관" },
{ val: "MOP903", text: "ELP" },
{ val: "MOP904", text: "EPP" },
{ val: "MOP905", text: "FC관" },
{ val: "MOP906", text: "H" },
{ val: "MOP907", text: "H6" },
{ val: "MOP908", text: "HCP" },
{ val: "MOP909", text: "HI_2P관" },
{ val: "MOP910", text: "HI-KP" },
{ val: "MOP911", text: "HL" },
{ val: "MOP912", text: "IP" },
{ val: "MOP913", text: "KCGP" },
{ val: "MOP914", text: "KP관" },
{ val: "MOP915", text: "P2S관" },
{ val: "MOP916", text: "PC박스(PCB)" },
{ val: "MOP917", text: "PCS" },
{ val: "MOP918", text: "PDP" },
{ val: "MOP919", text: "PE이중벽관" },
{ val: "MOP920", text: "PE적측벽1종관" },
{ val: "MOP921", text: "PFP관" },
{ val: "MOP922", text: "PG관" },
{ val: "MOP923", text: "PIP관(이중보온관)" },
{ val: "MOP924", text: "PLCSP" },
{ val: "MOP925", text: "PS" },
{ val: "MOP926", text: "PUB" },
{ val: "MOP927", text: "PVB" },
{ val: "MOP928", text: "PVC이중벽관(PVC2)" },
{ val: "MOP929", text: "PVC-DC" },
{ val: "MOP930", text: "PVC-DV" },
{ val: "MOP931", text: "SBK" },
{ val: "MOP932", text: "SCP" },
{ val: "MOP933", text: "SEB" },
{ val: "MOP934", text: "SIS관(이중보온관)" },
{ val: "MOP935", text: "SMC" },
{ val: "MOP936", text: "STW" },
{ val: "MOP937", text: "SUS" },
{ val: "MOP938", text: "THP나선관" },
{ val: "MOP939", text: "U관" },
{ val: "MOP940", text: "VG2" },
{ val: "MOP941", text: "WSP(와사피)" },
{ val: "MOP942", text: "Steel Grating" },
{ val: "MOP943", text: "모래" },
{ val: "MOP944", text: "인조석" },
{ val: "MOP945", text: "SPOL" },
{ val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" },
{ val: "MOP947", text: "PVC3(PVC3중벽관)" },
{ val: "MOP948", text: "PE다중벽관" },
{ val: "MOP977", text: "전주용입상관" },
{ val: "MOP978", text: "MD관" },
];
// 하수관로 'fcltyShpCd' '시설물형태코드'
export const MS_SWAGE_PPL_SHAPE = [
{ val: "FOR000 ", text: "미분류" },
{ val: "FOR001 ", text: "원형" },
{ val: "FOR002 ", text: "원추형" },
{ val: "FOR003 ", text: "원통형" },
{ val: "FOR004 ", text: "구형" },
{ val: "FOR005 ", text: "타원형" },
{ val: "FOR006 ", text: "난원" },
{ val: "FOR007 ", text: "반원형" },
{ val: "FOR010 ", text: "피라미드형" },
{ val: "FOR011 ", text: "사면체형" },
{ val: "FOR012 ", text: "기둥,축" },
{ val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" },
{ val: "FOR014 ", text: "아치형" },
{ val: "FOR015 ", text: "다중아치형" },
{ val: "FOR016 ", text: "돔형" },
{ val: "FOR017 ", text: "고형체형" },
{ val: "FOR020 ", text: "사각형" },
{ val: "FOR021 ", text: "평방형" },
{ val: "FOR022 ", text: "다이아몬드형" },
{ val: "FOR023 ", text: "사다리꼴" },
{ val: "FOR024 ", text: "각형" },
{ val: "FOR025 ", text: "마제형관" },
{ val: "FOR026 ", text: "태형" },
{ val: "FOR030 ", text: "A형" },
{ val: "FOR031 ", text: "H형" },
{ val: "FOR032 ", text: "I형" },
{ val: "FOR033 ", text: "Y형" },
{ val: "FOR034 ", text: "T형" },
{ val: "FOR050 ", text: "불규칙한 형태" },
{ val: "FOR900 ", text: "계란형" },
{ val: "FOR901 ", text: "구(사각)형" },
{ val: "FOR902 ", text: "높이조절철개형" },
{ val: "FOR903 ", text: "박스형(RCB)" },
{ val: "FOR904 ", text: "반월개거" },
{ val: "FOR905 ", text: "반월개거(측구)" },
{ val: "FOR906 ", text: "벽돌조" },
{ val: "FOR907 ", text: "사각철판" },
{ val: "FOR908 ", text: "석축개거" },
{ val: "FOR909 ", text: "석축개거(측구)" },
{ val: "FOR910 ", text: "승수로" },
{ val: "FOR911 ", text: "옹벽개거" },
{ val: "FOR912 ", text: "옹벽개거(측구)" },
{ val: "FOR913 ", text: "이단" },
{ val: "FOR914 ", text: "이중침전형" },
{ val: "FOR915 ", text: "자연개거" },
{ val: "FOR916 ", text: "자연개거(측구)" },
{ val: "FOR917 ", text: "조립식" },
{ val: "FOR918 ", text: "지상관" },
{ val: "FOR919 ", text: "지상단구" },
{ val: "FOR920 ", text: "지상쌍구" },
{ val: "FOR921 ", text: "지하단구" },
{ val: "FOR922 ", text: "지하쌍구" },
{ val: "FOR923 ", text: "직사각형" },
{ val: "FOR924 ", text: "타설식" },
{ val: "FOR925 ", text: "토류형" },
{ val: "FOR926 ", text: "흄관원형" },
{ val: "FOR927 ", text: "L형측구" },
{ val: "FOR928 ", text: "LO형측구" },
{ val: "FOR929 ", text: "LU형측구" },
{ val: "FOR930 ", text: "PVC이중벽관" },
{ val: "FOR931 ", text: "RCB 맨홀" },
{ val: "FOR932 ", text: "U형" },
{ val: "FOR933 ", text: "U형측구(미복개)" },
{ val: "FOR934 ", text: "U형측구(복개)" },
{ val: "FOR999 ", text: "기타" }
];
// 하수처리시설
export const SwtrmtFcltCol = [
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
{ id: 'fclt_cpct', title: '시설용량(㎥/d)' },
{ id: 'rgn_expln', title: '지역설명' },
{ id: 'cmcn_ymd', title: '준공일자' },
{ id: 'oprtng_strt_ymd', title: '가동개시일자' },
{ id: 'clsg_ymd', title: '폐쇄일자' },
// { id: 'prcs_msrc_cd', title: '처리공법코드' },
// { id: 'lae_cd', title: '지자체코드' },
{ id: 'lae_nm', title: '지자체명' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
// { id: 'sarea_cd', title: '권역코드' },
{ id: 'sarea_nm', title: '권역명' },
{ id: 'lrge_sarea_nm', title: '대권역명' },
{ id: 'rvr_nm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'dtl_addr', title: '상세주소' },
{ id: 'rdnm_addr', title: '도로명주소' },
{ id: 'dtl_rdnm_addr', title: '상세도로명주소' },
{ id: 'dschg_wtst_nm', title: '방류수계명' },
{ id: 'plln_area', title: '부지면적(m²)' },
{ id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' },
{ id: 'swtrmt_ppltn_cnt', title: '하수처리인구수(인)' },
{ id: 'prcs_zone_area_sum', title: '처리구역면적합계(m²)' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
// { id: 'oprtngSttsCd', title: '가동상태코드' },
{ id: 'rprs_msrc_cn', title: '대표공법내용' },
// { id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량(mg/L)' },
// { id: 'stty_crtr_toc', title: '법정기준TOC((mg/L))' },
// { id: 'stty_crtr_ss', title: '법정기준SS(mg/L)' },
// { id: 'stty_crtr_tn', title: '법정기준총질소(mg/L)' },
// { id: 'stty_crtr_tph', title: '법정기준총인(mg/L)' },
// { id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수(군수/100mL)' },
{ id: 'bsn_nm', title: '유역명' },
// { id: 'obsrvnSptCd', title: '관측지점코드' },
];
export const SwtrmtFcltCol2 = [
{ id: 'rowNumber', title: '순번' },
{ id: 'swtrmtFcltNm', title: '하수처리\n시설명' },
{ id: 'fcltCpct', title: '시설\n용량\n(㎥/d)' },
{ id: 'rgnExpln', title: '지역설명' },
// { id: 'cmcnYmd', title: '준공일자' },
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
// { id: 'clsgYmd', title: '폐쇄일자' },
// { id: 'prcsMsrcCd', title: '처리공법코드' },
// { id: 'laeCd', title: '지자체코드' },
{ id: 'laeNm', title: '지자체명' },
// { id: 'ctpvNm', title: '시도명' },
// { id: 'sggNm', title: '시군구명' },
// { id: 'sareaCd', title: '권역코드' },
{ id: 'sareaNm', title: '권역명' },
// { id: 'lrgeSareaNm', title: '대권역명' },
{ id: 'rvrNm', title: '하천명' },
// { id: 'addr', title: '주소' },
// { id: 'dtlAddr', title: '상세주소' },
// { id: 'rdnmAddr', title: '도로명주소' },
// { id: 'dtlRdnmAddr', title: '상세도로명주소' },
{ id: 'dschgWtstNm', title: '방류수계명' },
// { id: 'pllnArea', title: '부지면적(m²)' },
// { id: 'mngRprtvBzentyNm', title: '관리대행업체명' },
{ id: 'swtrmtPpltnCnt', title: '하수\n처리\n인구수\n(인)' },
// { id: 'prcsZoneAreaSum', title: '처리구역면적합계(m²)' },
// { id: 'xcrd', title: 'X좌표' },
// { id: 'ycrd', title: 'Y좌표' },
// { id: 'oprtngSttsCd', title: '가동상태코드' },
{ id: 'rprsMsrcCn', title: '대표\n공법\n내용' },
// { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량(mg/L)' },
// { id: 'sttyCrtrToc', title: '법정기준TOC(mg/L)' },
// { id: 'sttyCrtrSs', title: '법정기준SS(mg/L)' },
// { id: 'sttyCrtrTn', title: '법정기준총질소(mg/L)' },
// { id: 'sttyCrtrTph', title: '법정기준총인(mg/L)' },
// { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수(군수/100mL)' },
// { id: 'bsnNm', title: '유역명' },
// { id: 'obsrvnSptCd', title: '관측지점코드' },
];
export const SwtrmtFcltExcelCol = {
'swtrmtFcltNm': '하수처리시설명',
'fcltCpct': '시설 용량(㎥/d)',
'rgnExpln': '지역설명',
'oprtngStrtYmd': '가동개시 일자',
'laeNm': '지자체명',
'sareaNm': '권역명',
'rvrNm': '하천명',
'dschgWtstNm': '방류수계명',
'swtrmtPpltnCnt': '하수 처리 인구수(인)',
'rprsMsrcCn': '대표 공법 내용'
};
// 하수처리구역
export const SwtrmtPrcsZoneCol = [
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
{ id: 'swtrmt_zone_nm', title: '하수처리구역명' },
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
// { id: 'ctpv_cd', title: '시도코드' },
{ id: 'ctpv_nm', title: '시도명' },
// { id: 'sgg_cd', title: '시군구코드' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
];
export const SwtrmtPrcsZoneCol2 = [
{ id: 'rowNumber', title: '순번' },
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
// { id: 'ctpv_cd', title: '시도코드' },
{ id: 'ctpvNm', title: '시도' },
// { id: 'sgg_cd', title: '시군구코드' },
{ id: 'sggNm', title: '시군구' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
];
export const SwtrmtPrcsZoneExcelCol = {
'swtrmtZoneNm': '하수처리구역명',
'ctpvNm': '시도',
'sggNm': '시군구',
'swtrmtFcltNm': '하수처리시설명'
};
// 침수흔적도
export const FldngWtlvMapCol = [
{ id: 'fldng_yr', title: '침수년도' },
{ id: 'fldng_dstr_nm', title: '침수재해명' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'fldng_bgng_dtm', title: '침수시작일시' },
{ id: 'fldng_end_dtm', title: '침수종료일시' },
{ id: 'fldng_hr', title: '침수시간' },
{ id: 'fldng_dstrct_nm', title: '침수지구명' },
{ id: 'fldng_cs_dtl_cd', title: '침수원인상세코드' },
{ id: 'dtl_ocrn_cs_cn', title: '침수발생원인' },
{ id: 'avg_fldng_wtlv', title: '평균침수위' },
{ id: 'fldng_area', title: '침수면적' },
{ id: 'exmn_yr', title: '조사년도' },
{ id: 'dam_amt', title: '피해금액' },
{ id: 'rain_bgng_tm', title: '강우시작일시' },
{ id: 'rain_end_tm', title: '강우종료일시' },
{ id: 'rain_tot', title: '총강우량(mm)' },
{ id: 'rain_str', title: '강우강도(mm/hr)' },
];
export const FldngWtlvMapCol2 = [
{ id: 'rowNumber', title: '순번' },
{ id: 'fldngYr', title: '침수년도' },
{ id: 'fldngDstrNm', title: '침수재해명' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
{ id: 'fldngBgngDtm', title: '침수시작일시' },
{ id: 'fldngEndDtm', title: '침수종료일시' },
{ id: 'fldngHr', title: '침수시간' },
{ id: 'fldngDstrctNm', title: '침수지구명' },
{ id: 'fldngCsDtlCd', title: '침수원인상세코드' },
{ id: 'avgFldngWtlv', title: '평균침수위' },
{ id: 'fldngArea', title: '침수면적' },
{ id: 'exmnYr', title: '조사년도' },
{ id: 'damAmt', title: '피해금액' },
{ id: 'rainBgngTm', title: '강우시작일시' },
{ id: 'rainEndTm', title: '강우종료일시' },
{ id: 'rainTot', title: '총강우량(mm)' },
{ id: 'rainStr', title: '강우강도(mm/hr)' },
];
export const FldngDpthMapCol = [
{ id: 'fldng_yr', title: '침수년도' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'fldng_dpth', title: '침수깊이(m)' },
{ id: 'fldng_area', title: '침수면적(㎡)' },
// { id: 'shape_area', title: '형상면적(㎡)' },
// { id: 'shape_len', title: '형상길이' },
];
export const FldngWtlvMapExcelCol = {
'fldngYr': '침수년도',
'fldngDstrNm': '침수재해명',
'ctpvNm': '시도',
'sggNm': '시군구',
'bgngYmd': '침수시작일자',
'fldngBgngHm': '침수시작시분',
'endYmd': '침수종료일자',
'fldngEndHm': '침수종료시분',
'fldngHr': '침수시간',
'fldngDstrctNm': '침수지구명',
'fldngCsDtlCd': '침수원인상세코드',
'avgFldngWtlv': '평균침수위',
'fldngArea': '침수면적',
'exmnYr': '조사년도',
'damAmt': '피해금액'
};
// 하수맨홀
export const MsSwageMhlCol = [
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
{ id: 'fclt_mng_no', title: '시설관리번호' },
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
{ id: 'mapsht_no', title: '도엽번호' },
// { id: 'mng_inst_cd', title: '관리기관코드' },
{ id: 'instl_ymd', title: '설치일자' },
{ id: 'last_drdgn_ymd', title: '최종준설일자' },
// { id: 'usg_cd', title: '용도코드' },
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
// { id: 'mhl_knd_cd', title: '맨홀종류코드' },
{ id: 'swage_mhL_diam_vl', title: '하수맨홀구경(mm)' },
{ id: 'mqlt_cd', title: '재질' },
{ id: 'wdth_len', title: '가로길이(m)' },
{ id: 'vrtc_len', title: '세로길이(m)' },
// { id: 'ang_dir', title: '표고' },
{ id: 'altd_vl', title: '지표고(EL.m)' },
{ id: 'dpthl_vl', title: '맨홀저고(EL.m)' },
// { id: 'inst_mng_no', title: '기관관리번호' }
];
export const MsSwageMhlCol2 = [
{ id: 'rowNumber', title: '순번' },
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fcltMngNo', title: '시설관리번호' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
// { id: 'mapshtNo', title: '도엽번호' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'lastDrdgnYmd', title: '최종준설일자' },
{ id: 'usgCd', title: '용도' },
// { id: 'fcltyShpCd', title: '시설물형태코드' },
// { id: 'mhlKndCd', title: '맨홀종류코드' },
{ id: 'swageMhlDiamVl', title: '하수맨홀구경(mm)' },
// { id: 'mqltCd', title: '재질코드' },
{ id: 'wdthLen', title: '가로길이(m)' },
{ id: 'vrtcLen', title: '세로길이(m)' },
// { id: 'angDir', title: '표고' },
{ id: 'altdVl', title: '지표고(EL.m)' },
{ id: 'dpthlVl', title: '맨홀저고(EL.m)' },
// { id: 'instMngNo', title: '기관관리번호' }
];
export const MsSwageMhlExcelCol = {
'fcltMngNo': '시설관리번호',
'ctpvNm': '시도',
'sggNm': '시군구',
'instlYmd': '설치일자',
'usgCd': '용도',
'swageMhlDiamVl': '하수맨홀구경(mm)',
'wdthLen': '가로길이(m)',
'vrtcLen': '세로길이(m)',
'altdVl': '지표고(EL.m)',
'dpthlVl': '맨홀저고(EL.m)'
};
// 하수맨홀 뚜껑재질 코드표
export const MS_SWAGE_MHL_CODES = [
{ val: "SBC000", text: "미분류" },
{ val: "SBC001", text: "주철" },
{ val: "SBC002", text: "철판" },
{ val: "SBC003", text: "콘크리트" },
{ val: "SBC004", text: "그레이팅" },
{ val: "SBC091", text: "칼라" },
{ val: "SBC092", text: "S.G" },
{ val: "SBC999", text: "기타" },
{ val: "SBC900", text: "각주철" },
{ val: "SBC901", text: "강재" },
{ val: "SBC902", text: "나무" },
{ val: "SBC903", text: "나무,널판지,합판" },
{ val: "SBC904", text: "대리석" },
{ val: "SBC905", text: "뚜껑없음" },
{ val: "SBC906", text: "보도용콘크리트" },
{ val: "SBC907", text: "스틸(ST)" },
{ val: "SBC908", text: "신형 CON" },
{ val: "SBC909", text: "알루미늄" },
{ val: "SBC910", text: "염화비닐(PE)" },
{ val: "SBC911", text: "인조석" },
{ val: "SBC912", text: "인조석(주철)" },
{ val: "SBC913", text: "인조CONC" },
{ val: "SBC914", text: "철, 철근" },
{ val: "SBC915", text: "칼라콘크리트" },
{ val: "SBC916", text: "플라스틱" },
{ val: "SBC917", text: "PVC" },
{ val: "SBC918", text: "SP" },
{ val: "SBC919", text: "SS" },
{ val: "SBC920", text: "닥타일주철" },
{ val: "SBC921", text: "철근G" },
{ val: "SBC922", text: "폴리에틸렌(PE)" },
{ val: "SBC923", text: "회주철" }
];
// 하수맨홀 용도 코드표
export const MS_SWAGE_MHL_USE_CODES = [
{ val: "SMU000 ", text: "미분류" },
{ val: "SMU001 ", text: "우수" },
{ val: "SMU002 ", text: "오수" },
{ val: "SMU003 ", text: "집수" },
{ val: "SMU004 ", text: "합류" },
{ val: "SMU005 ", text: "차집" },
{ val: "SMU999 ", text: "기타" },
{ val: "SMU900 ", text: "1호" },
{ val: "SMU901 ", text: "2호" },
{ val: "SMU902 ", text: "3호" },
{ val: "SMU903 ", text: "4호" },
{ val: "SMU904 ", text: "5호" },
{ val: "SMU905 ", text: "6호" },
{ val: "SMU906 ", text: "7호" },
{ val: "SMU907 ", text: "8호" },
{ val: "SMU908 ", text: "구거용맨홀" },
{ val: "SMU909 ", text: "부관부착맨홀" },
{ val: "SMU910 ", text: "조립식맨홀" },
{ val: "SMU911 ", text: "특1호" },
{ val: "SMU912 ", text: "특2호" },
{ val: "SMU913 ", text: "특3호" },
{ val: "SMU914 ", text: "특4호" },
{ val: "SMU915 ", text: "특5호" },
{ val: "SMU916 ", text: "하수맨홀용도" },
{ val: "SMU917 ", text: "현장타설관거용" }
];
// 우수펌프장
export const ExclnSewagePumpCol = [
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fclt_mng_no', title: '시설관리번호' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
{ id: 'mapsht_no', title: '도엽번호' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'instl_ymd', title: '설치일자' },
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
{ id: 'plln_area', title: '부지면적(m²)' },
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
{ id: 'altd_vl', title: '표고(EL.m)' },
{ id: 'wtlv_vl', title: '수위(m)' },
{ id: 'excln_wpmp_ablt_vl', title: '우수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
export const ExclnSewagePumpCol2 = [
{ id: 'rowNumber', title: '순번' },
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fcltMngNo', title: '시설관리번호' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
// { id: 'mapshtNo', title: '도엽번호' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'swagePmstnNm', title: '하수펌프장명' },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'pllnArea', title: '부지면적(m²)' },
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
{ id: 'altdVl', title: '표고(EL.m)' },
{ id: 'wtlvVl', title: '수위(m)' },
{ id: 'exclnWpmpAbltVl', title: '우수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
export const ExclnSewagePumpExcelCol = {
'fcltMngNo': '시설관리번호',
'ctpvNm': '시도',
'sggNm': '시군구',
'swagePmstnNm': '하수펌프장명',
'instlYmd': '설치일자',
'dailyPrcsCpct': '일일처리용량(㎥)',
'maxImpndCpct': '최대저수용량(㎥)',
'altdVl': '표고(EL.m)',
'wtlvVl': '수위(m)',
'exclnWpmpAbltVl': '우수양수능력(m²/d)'
};
// 오수펌프장
export const DrtwSewagePumpCol = [
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fclt_mng_no', title: '시설관리번호' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
{ id: 'mapsht_no', title: '도엽번호' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'instl_ymd', title: '설치일자' },
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
{ id: 'plln_area', title: '부지면적(m²)' },
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
{ id: 'altd_vl', title: '표고(EL.m)' },
{ id: 'wtlv_vl', title: '수위(m)' },
{ id: 'excln_wpmp_ablt_vl', title: '오수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
export const DrtwSewagePumpCol2 = [
{ id: 'rowNumber', title: '순번' },
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fcltMngNo', title: '시설관리번호' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
// { id: 'mapshtNo', title: '도엽번호' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'swagePmstnNm', title: '하수펌프장명' },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'pllnArea', title: '부지면적(m²)' },
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
{ id: 'altdVl', title: '표고(EL.m)' },
{ id: 'wtlvVl', title: '수위(m)' },
{ id: 'exclnWpmpAbltVl', title: '오수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
// 펌프장 용도 코드표
export const DRTW_SEWAGE_PUMP_USE_CODES = [
{ val: "SBE000", text: "미분류" },
{ val: "SBE001", text: "빗물펌프장" },
{ val: "SBE002", text: "오수펌프장" },
{ val: "SBE003", text: "처리장내펌프장" },
{ val: "SBE900", text: "오수배제" },
{ val: "SBE901", text: "우수배제" },
{ val: "SBE999", text: "기타" },
];
// 우수토실
export const stofcbrCol = [
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
{ id: 'fclt_mng_no', title: '시설관리번호' },
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
{ id: 'mapsht_no', title: '도엽번호' },
// { id: 'mng_inst_cd', title: '관리기관코드' },
{ id: 'instl_ymd', title: '설치일자' },
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
{ id: 'worflw_wall_hgt', title: '월류벽높이(m)' },
// { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' },
// { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' },
{ id: 'swage_mhl_mng_no', title: '하수맨홀관리번호' },
{ id: 'drtn_angl', title: '방향각도(°)' },
]
export const stofcbrCol2 = [
{ id: 'rowNumber', title: '순번' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fcltMngNo', title: '시설관리번호' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
{ id: 'mapshtNo', title: '도엽번호' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'fcltyShpCd', title: '시설물형태코드' },
{ id: 'worflwWallHgt', title: '월류벽높이(m)' },
// { id: 'mhlExstYnCd', title: '맨홀존재여부코드' },
// { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' },
{ id: 'swageMhlMngNo', title: '하수맨홀관리번호' },
{ id: 'drtnAngl', title: '방향각도(°)' },
]
export const stofcbrExcelCol = {
'fcltMngNo': '시설관리번호',
'mapshtNo': '도엽번호',
'ctpvNm': '시도',
'sggNm': '시군구',
'instlYmd': '설치일자',
'worflwWallHgt': '월류벽높이(m)',
'swageMhlMngNo': '하수맨홀관리번호',
'drtnAngl': '방향각도(°)'
};
// 중점우수토실 측정지점
export const untrtSwgRainCol1 = [
{ id: 'manage_no', title: '관리번호' },
{ id: 'swr_ppl_eqp_nm', title: '처리구역' },
{ id: 'swr_ppl_trt_nm', title: '처리분구' },
{ id: 'addr', title: '인근주소' },
{ id: 'chmbr_xcnts', title: '토실 위도' },
{ id: 'chmbr_ydnts', title: '토실 경도' }
];
// 중점우수토실 측정지점 시설현황
export const untrtSwgRainCol2 = [
{ id: 'mesure_spot_se', title: '구분' }, // 유량계측정방식 기타
{ id: 'discharge_pipe_spec', title: '방류관로 규격(m)' },
{ id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' },
{ id: 'swr_trt_popl', title: '하수처리 인구(인)' },
{ id: 'screen_instl_at', title: '스크린설치 여부' },
{ id: 'weir_hg', title: '위어 높이(m)' },
{ id: 'weir_lt', title: '위어 길이(m)' }
];
// 중점우수토실 강우정보 (id값 틀릴 수 있음)
export const untrtSwgRainCol3 = [
{ id: 'rainBeginDt', title: '강우시작일시' },
{ id: 'rainEndDt', title: '강우종료일시' },
{ id: 'precdngDry', title: '선행건기(h)' },
{ id: 'rainInfo', title: '강우정보' },
{ id: 'rainFall', title: '누적강우량(mm)' },
{ id: 'cntncTime', title: '지속시간(h)' },
{ id: 'rainIn', title: '강우강도(mm/h)' }
];
// 중점우수토실 측정정보 (id값 틀릴 수 있음)
export const untrtSwgRainCol4 = [
{ id: 'mesureBeginDt', title: '측정시작일시' },
{ id: 'mesureEndDt', title: '측정종료일시' },
{ id: 'mesureIntrvl', title: '측정간격(분)' },
{ id: 'mesureMhrlsInstlLc', title: '측정기기 설치위치' },
{ id: 'flwmtrCombined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼
{ id: 'qltwtrBeginDt', title: '채수시작일시' },
{ id: 'qltwtrEndDt', title: '채수종료일시' },
{ id: 'qltwtrCo', title: '채수횟수' }
];
// 중점우수토실 수량/수질측정 정보
export const untrtSwgRainCol5 = [
{ id: 'odr', title: '회차' },
{ id: 'mesureDt', title: '측정일자' },
{ id: 'mesureQy', title: '수량(m³)' },
{ id: 'mesureBod', title: 'BOD(mg/L)' },
{ id: 'mesureToc', title: 'TOC(mg/L)' },
{ id: 'mesureLoad', title: '오염부하량(kg)' }
];
// 🎯 날짜 필드들 (yyyymmdd -> yyyy년 mm월 dd일)
export const dateFields = [
'instl_ymd', 'last_drdgn_ymd', 'cmcn_ymd', 'oprtng_strt_ymd', 'clsg_ymd',
'mesure_begin_dt', 'mesure_end_dt', 'qltwtr_begin_dt', 'qltwtr_end_dt',
'rain_begin_dt', 'rain_end_dt', 'mesureDt',
// 🎯 camelCase 버전 추가
'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd',
'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt',
'rainBeginDt', 'rainEndDt',
'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr',
'bgng_ymd', 'end_ymd', 'bgngYmd', 'endYmd'
];
// 🎯 연월 필드들 (yyyymm -> yyyy년 m월)
export const yearMonthFields = ['aplcnYm',];
// 🎯 날짜시간 필드들 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm)
export const dateTimeFields = ['regDt',];
// 🎯 숫자 필드들 (천 단위 구분자)
export const numberFields = [
'pdmt', 'prlg', 'avg_grdn', 'drtw_mltv_area', 'excln_mltv_area', 'tmpt_dpth', 'scend_dpth', 'trfln_cnt', 'vrtc_len', 'wdth_len',
'fclt_cpct', 'plln_area', 'prcs_zone_area_sum', 'swtrmt_ppltn_cnt', 'stty_crtr_bod', 'stty_crtr_toc', 'stty_crtr_ss', 'stty_crtr_tn', 'stty_crtr_tph', 'stty_crtr_clbcl_cnt',
'swage_mh_diam', 'wdth_len', 'vrtc_len', 'altd_vl', 'dpthlVl',
'plln_area', 'daily_prcs_cpct', 'max_impnd_cpct', 'altd_vl', 'wtlv_vl', 'excln_wpmp_ablt',
'worflw_wall_hgt',
'swr_ppl_eqp_ar', 'swr_trt_popl', 'weir_hg', 'weir_lt',
'precdng_dry', 'rain_fall', 'cntnc_time', 'rain_in',
'mesure_intrvl', 'qltwtr_co', 'mesureQy', 'mesureBod', 'mesureToc', 'mesureLoad',
// 🎯 camelCase 버전 추가
'avgGrdn', 'drtwMltvArea', 'exclnMltvArea', 'tmptDpth', 'scendDpth', 'trflnCnt', 'vrtcLen', 'wdthLen',
'fcltCpct', 'pllnArea', 'prcsZoneAreaSum', 'swtrmtPpltnCnt', 'sttyCltrBod', 'sttyCltrToc', 'sttyCltrSs', 'sttyCltrTn', 'sttyCltrTph', 'sttyCltrClbclCnt',
'swageMhDiam', 'altdVl', 'dpthlVl',
'dailyPrcsCpct', 'maxImpndCpct', 'wtlvVl', 'exclnWpmpAblt',
'worflwWallHgt',
'swrPplEqpAr', 'swrTrtPopl', 'weirHg', 'weirLt',
'precdngDry', 'rainFall', 'cntncTime', 'rainIn',
'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc',
'bizAmt', 'count',
'chmbr_xcnts', 'chmbr_ydnts',
'fldng_hr', 'avg_fldng_wtlv', 'fldng_area', 'dam_amt',
'fldngHr', 'avgFldngWtlv', 'fldngArea', 'damAmt'
];
@@ -4,8 +4,8 @@
*/
"use client";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
import LayerSettingCard from "../layer-setting/layer-setting-card";
type Props = {
jobType: ExmnMapJobType;
@@ -14,6 +14,7 @@ type Props = {
enabled?: boolean;
onEnabledChange?: (enabled: boolean) => void;
onClose?: () => void;
statusMessage?: string;
};
const GRADE_OPTIONS = ["", "1", "2", "3", "4", "5"];
@@ -26,6 +27,7 @@ export default function ExmnLayerSettingPanel({
enabled = true,
onEnabledChange = () => {},
onClose = () => {},
statusMessage = "",
}: Props) {
const updateFilter = (key: keyof ExmnMapFilters, value: string) => {
onFiltersChange({
@@ -46,6 +48,7 @@ export default function ExmnLayerSettingPanel({
onClose={onClose}
>
<div className="space-y-[6px]">
{statusMessage ? <p role="status" className="text-[1.1rem] text-muted-foreground">{statusMessage}</p> : null}
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
<span className={`flex items-center gap-1 ${labelClass}`}>
<span
@@ -1,11 +1,13 @@
/** 관로조사 지도 데이터 조회 API와 응답 변환 함수를 제공한다. */
import { getRawJson } from "../../../../components/lib/api";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes";
import { getRawJson } from "@/shared/api/sewerage-api";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
type ExmnLayerScope = {
sggCd?: string;
ctpvNm?: string;
sggNm?: string;
exmnBizId?: string;
trtdstId?: string;
};
/** 구버전 응답이 하류맨홀을 포함해도 업로드 관로조사의 상류맨홀 피처만 지도에 전달한다. */
@@ -36,16 +38,19 @@ export const retainUpstreamManholeFeatures = (response: any) => {
export const getExmnMapLayer = async (
jobType: ExmnMapJobType,
filters: ExmnMapFilters,
scope: ExmnLayerScope,
scope: ExmnLayerScope = {},
signal?: AbortSignal,
) => {
const query = new URLSearchParams({ jobType });
if (scope.sggCd?.trim()) query.set("sggCd", scope.sggCd.trim());
if (scope.ctpvNm?.trim()) query.set("ctpvNm", scope.ctpvNm.trim());
if (scope.sggNm?.trim()) query.set("sggNm", scope.sggNm.trim());
if (scope.exmnBizId?.trim()) query.set("exmnBizId", scope.exmnBizId.trim());
if (scope.trtdstId?.trim()) query.set("trtdstId", scope.trtdstId.trim());
if (filters.bizCycl) query.set("bizCycl", filters.bizCycl);
if (filters.structuralGrade) query.set("structuralGrade", filters.structuralGrade);
if (filters.operationalGrade) query.set("operationalGrade", filters.operationalGrade);
const response = await getRawJson(`/sewerage-service/api/v1/exmnMap/layers?${query.toString()}`);
const response = await getRawJson(`/sewerage-service/api/v1/exmnMap/layers?${query.toString()}`, { signal });
return jobType === "MNHL" ? retainUpstreamManholeFeatures(response) : response;
};
@@ -21,6 +21,7 @@ export type ExmnMapFilters = {
export type ExmnMapEntryContext = {
exmnBizId: string;
trtdstId: string;
ctpvNm: string;
sggNm: string;
bizCycl: string;
@@ -291,6 +292,7 @@ export const buildExmnMapEntryContext = (
const rawJobType = String(searchParams.get("exmnJobType") ?? "").toUpperCase();
return {
exmnBizId: String(searchParams.get("exmnBizId") ?? "").trim(),
trtdstId: String(searchParams.get("trtdstId") ?? "").trim(),
ctpvNm: String(searchParams.get("ctpvNm") ?? "").trim(),
sggNm: String(searchParams.get("sggNm") ?? "").trim(),
bizCycl: String(searchParams.get("bizCycl") ?? "").replace(/[^0-9]/g, ""),
@@ -0,0 +1,261 @@
"use client";
import {
useCallback,
useEffect,
useRef,
useState,
type RefObject,
} from "react";
import GeoJSON from "ol/format/GeoJSON";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { getExmnMapLayer } from "./exmn-map-api";
import {
EMPTY_EXMN_MAP_FILTERS,
EXMN_RESULT_PPL_LAYER_ID,
EXMN_RESULT_MNHL_LAYER_ID,
type ExmnMapEntryContext,
type ExmnMapFilters,
type ExmnMapJobType,
} from "./exmn-map-types";
type Options = {
gisApp: any;
mapRef: RefObject<any>;
ready: boolean;
treeData: any[];
entry: ExmnMapEntryContext;
pipeChecked: boolean;
manholeChecked: boolean;
pipeEnabled: boolean;
manholeEnabled: boolean;
pipeFilters: ExmnMapFilters;
manholeFilters: ExmnMapFilters;
pipeStyle: any;
manholeStyle: any;
};
type LayerState = {
layer?: VectorLayer<any>;
controller?: AbortController;
sequence: number;
};
/** 전체 조사 레이어와 상세현황 위치 이동을 분리하며 유형별 요청을 취소/무효화한다. */
export const useExmnResultMap = (options: Options) => {
const {
gisApp,
mapRef,
ready,
treeData,
entry,
pipeChecked,
manholeChecked,
pipeEnabled,
manholeEnabled,
pipeFilters,
manholeFilters,
pipeStyle,
manholeStyle,
} = options;
// 지도 객체/요청 수명은 React 렌더링과 분리하고 관로·맨홀을 독립적으로 관리한다.
const layers = useRef<Record<ExmnMapJobType, LayerState>>({
PPL: { sequence: 0 },
MNHL: { sequence: 0 },
});
const fittedContext = useRef("");
const [messages, setMessages] = useState<Record<ExmnMapJobType, string>>({
PPL: "",
MNHL: "",
});
/** 체크 해제/언마운트 시 늦게 도착한 응답도 다시 표시되지 않게 한다. */
const removeLayer = useCallback(
(job: ExmnMapJobType) => {
const state = layers.current[job];
state.sequence++;
state.controller?.abort();
state.controller = undefined;
if (state.layer) {
gisApp.Module.select?.removeLayer?.(state.layer);
mapRef.current?.removeLayer?.(state.layer);
}
state.layer = undefined;
},
[gisApp, mapRef],
);
/** 지역 검색값 없이 전체를 요청하고, 선택 진입 context는 위치 이동 범위 조회에만 사용한다. */
const loadLayer = useCallback(
async (job: ExmnMapJobType, filters: ExmnMapFilters) => {
const map = mapRef.current;
if (!map) return;
removeLayer(job);
const state = layers.current[job];
const sequence = state.sequence;
const controller = new AbortController();
state.controller = controller;
const active = () =>
state.sequence === sequence && !controller.signal.aborted;
setMessages((current) => ({
...current,
[job]: "조사결과를 불러오는 중입니다.",
}));
try {
const response = await getExmnMapLayer(
job,
filters,
{},
controller.signal,
);
if (!active()) return;
const data = response?.data ?? response;
const features = new GeoJSON().readFeatures(data, {
dataProjection: "EPSG:5179",
featureProjection: gisApp.TO_PROJ,
});
const source = new VectorSource({ features });
const layer = new VectorLayer({
source,
zIndex: job === "PPL" ? 9988 : 9989,
style: job === "PPL" ? pipeStyle : manholeStyle,
});
layer.setProperties({
id:
job === "PPL"
? EXMN_RESULT_PPL_LAYER_ID
: EXMN_RESULT_MNHL_LAYER_ID,
lyrNm: job === "PPL" ? "관로조사" : "맨홀조사",
lyrTyNm: job === "PPL" ? "tb_ms_swage_ppl" : "tb_ms_swage_mhl",
clickPsblYn: "Y",
chcYn: "Y",
});
state.layer = layer;
map.addLayer(layer);
gisApp.Module.select?.addLayer?.(layer);
const missing = Number(data?.missingGeometryCount ?? 0);
const diagnostics = data?.diagnostics ?? {};
const reasons = [
["지역 확인 불가", diagnostics.regionUnresolvedCount],
["시설번호 없음", diagnostics.identifierMissingCount],
["도형 없음", diagnostics.geometryNotFoundCount],
["도형 중복", diagnostics.ambiguousGeometryCount],
]
.filter(([, count]) => Number(count) > 0)
.map(([label, count]) => `${label} ${count}건`)
.join(", ");
const summary = features.length
? `${features.length.toLocaleString()}개 표시`
: "표시할 조사결과가 없습니다.";
const status = missing
? `${summary} · 공간정보 미연결 ${missing}건${reasons ? ` (${reasons})` : ""}`
: summary;
setMessages((current) => ({ ...current, [job]: status }));
if (entry.jobType !== job || !entry.exmnBizId || !entry.trtdstId)
return;
// 위치 조회 실패가 정상적으로 조회된 기본 레이어를 지우지 않도록 별도 처리한다.
try {
const selectedResponse = await getExmnMapLayer(
job,
filters,
{ exmnBizId: entry.exmnBizId, trtdstId: entry.trtdstId },
controller.signal,
);
if (!active()) return;
const selectedData = selectedResponse?.data ?? selectedResponse;
const selectedFeatures = new GeoJSON().readFeatures(selectedData, {
dataProjection: "EPSG:5179",
featureProjection: gisApp.TO_PROJ,
});
if (!selectedFeatures.length) {
setMessages((current) => ({
...current,
[job]: `${status} · 선택한 처리분구에 현재 조건으로 표시할 공간정보가 없습니다.`,
}));
return;
}
const selectedSource = new VectorSource({
features: selectedFeatures,
});
// 별도 강조선을 추가하지 않고 기존 조사 레이어 색상으로 표시한다.
const contextKey = `${entry.exmnBizId}:${entry.trtdstId}:${job}`;
const extent = selectedSource.getExtent();
if (
fittedContext.current !== contextKey &&
extent.every(Number.isFinite)
) {
map
.getView()
.fit(extent, {
duration: 300,
padding: [80, 420, 80, 80],
maxZoom: 18,
constrainResolution: true,
nearest: true,
});
fittedContext.current = contextKey;
}
} catch (error) {
if (active())
setMessages((current) => ({
...current,
[job]: `${status} · 선택 영역 조회 실패: ${error instanceof Error ? error.message : "다시 조회해 주세요."}`,
}));
}
} catch (error) {
if (active())
setMessages((current) => ({
...current,
[job]: `조회 실패: ${error instanceof Error ? error.message : "다시 조회해 주세요."}`,
}));
}
},
[
entry.exmnBizId,
entry.trtdstId,
entry.jobType,
gisApp,
mapRef,
pipeStyle,
manholeStyle,
removeLayer,
],
);
useEffect(() => {
if (ready && pipeChecked)
void loadLayer("PPL", pipeEnabled ? pipeFilters : EMPTY_EXMN_MAP_FILTERS);
else removeLayer("PPL");
return () => removeLayer("PPL");
}, [
ready,
pipeChecked,
pipeEnabled,
pipeFilters,
treeData,
loadLayer,
removeLayer,
]);
useEffect(() => {
if (ready && manholeChecked)
void loadLayer(
"MNHL",
manholeEnabled ? manholeFilters : EMPTY_EXMN_MAP_FILTERS,
);
else removeLayer("MNHL");
return () => removeLayer("MNHL");
}, [
ready,
manholeChecked,
manholeEnabled,
manholeFilters,
treeData,
loadLayer,
removeLayer,
]);
return { removeLayer, messages };
};
@@ -1,8 +1,8 @@
/** 침수흔적 강우분석과 연결된 심선·위선 목록 조회를 담당한다. */
import { getData } from "@/components/lib/api";
import { getData } from "@/shared/api/sewerage-api";
import type { FldngHstryDetailCacheValue } from "./fldngHstryTypes";
import type { FldngHstryDetailCacheValue } from "@/entities/flood-history/model/flood-history-types";
const normalizeFldngHstryRainAnalysisPayload = (data: any) => {
if (!data || typeof data !== "object") return {};
@@ -5,8 +5,8 @@
"use client";
import { useMemo, useState } from "react";
import type { FldngHstryItem } from "./fldngHstryTypes";
import { formatFldngHstryArea, formatFldngHstryDepth } from "./fldngHstryUtils";
import type { FldngHstryItem } from "@/entities/flood-history/model/flood-history-types";
import { formatFldngHstryArea, formatFldngHstryDepth } from "@/entities/flood-history/lib/flood-history-utils";
type Props = {
detail: FldngHstryItem;
@@ -10,12 +10,8 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
import type Feature from "ol/Feature.js";
import VectorSource from "ol/source/Vector.js";
import { loadFldngHstryDetail } from "./fldngHstryClient";
import {
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
FLDNG_HSTRY_LAYER_TYPE,
type FldngHstryDetailCacheValue,
} from "./fldngHstryTypes";
import { loadFldngHstryDetail } from "./fldng-hstry-client";
import { FLDNG_HSTRY_DEPTH_LAYER_TYPE, FLDNG_HSTRY_LAYER_TYPE, type FldngHstryDetailCacheValue } from "@/entities/flood-history/model/flood-history-types";
type FldngHstryGisLayer = {
searchLayer?: { getSource?: () => VectorSource | null } | null;
@@ -5,12 +5,9 @@
"use client";
import { SelectBox } from "@common/business_components/ui";
import {
FLDNG_HSTRY_MIN_YEAR,
type FldngHstryYearRange,
} from "./fldngHstryTypes";
import { normalizeFldngHstryYearRange } from "./fldngHstryUtils";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
import { FLDNG_HSTRY_MIN_YEAR, type FldngHstryYearRange } from "@/entities/flood-history/model/flood-history-types";
import { normalizeFldngHstryYearRange } from "@/entities/flood-history/lib/flood-history-utils";
import LayerSettingCard from "../layer-setting/layer-setting-card";
type Props = {
yearRange: FldngHstryYearRange;
@@ -7,34 +7,16 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { getRawJson } from "@/components/lib/api";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { getRawJson } from "@/shared/api/sewerage-api";
import { requestGridExcel } from "@/shared/api/grid-export";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
import { useAlertStore } from "@/shared/lib";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/LoadingBar";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import {
FLDNG_HSTRY_LAYER_TYPE,
FLDNG_HSTRY_DEFAULT_FILTERS,
FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
FLDNG_HSTRY_DOWNLOAD_HEADERS,
FLDNG_HSTRY_LIST_HEADLIST,
FLDNG_HSTRY_PAGE_SIZE,
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
type FldngHstryApiItem,
type FldngHstryFilters,
type FldngHstryTableRow,
} from "./fldngHstryTypes";
import {
attachFldngHstryGeometry,
buildFldngHstryListQueryString,
buildFldngHstryYearOptions,
normalizeFldngHstryFilters,
toFldngHstryTableRows,
} from "./fldngHstryUtils";
import useStore from "@/entities/reference-data/model/reference-data-store";
import LoadingOverlay from "@/components/ui/loading-bar";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import { FLDNG_HSTRY_LAYER_TYPE, FLDNG_HSTRY_DEFAULT_FILTERS, FLDNG_HSTRY_LIST_HEADLIST, FLDNG_HSTRY_PAGE_SIZE, FLDNG_HSTRY_ROW_SELECT_MESSAGE, type FldngHstryApiItem, type FldngHstryFilters, type FldngHstryTableRow } from "@/entities/flood-history/model/flood-history-types";
import { attachFldngHstryGeometry, buildFldngHstryListQueryString, buildFldngHstryYearOptions, normalizeFldngHstryFilters, toFldngHstryTableRows } from "@/entities/flood-history/lib/flood-history-utils";
const unwrapPayload = <T,>(response: any): T =>
(response?.data ?? response) as T;
@@ -58,26 +40,6 @@ const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => {
return "";
};
const buildDownloadRows = (rows: FldngHstryTableRow[]) =>
rows.map((row) => ({
rowNo: row.rowNo,
floodYearDisplay: row.floodYearDisplay,
ctpvNm: row.ctpvNm,
sggNm: row.sggNm,
disasterName: row.disasterName,
rainBgngTmDisplay: row.rainBgngTmDisplay,
rainEndTmDisplay: row.rainEndTmDisplay,
rainTotDisplay: row.rainTotDisplay,
rainStrDisplay: row.rainStrDisplay,
floodHourDisplay: row.floodHourDisplay,
districtName: row.districtName,
causeDetailCode: row.causeDetailCode,
causeDetailName: row.causeDetailName,
avgFloodDepthDisplay: row.avgFloodDepthDisplay,
floodAreaDisplay: row.floodAreaDisplay,
exmnYearDisplay: row.exmnYearDisplay,
damageAmountDisplay: row.damageAmountDisplay,
}));
const resolveRegionNames = (
row: FldngHstryTableRow,
@@ -265,19 +227,13 @@ export default function FldngHstryPopupPage() {
setIsLoading(true);
try {
const response = await fetchAllPages({
batchSize: FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
fetchPage: (page, size) => fetchRows(page + 1, appliedFilters, size),
getItems: (page) => page.rows,
getTotal: (page) => page.total,
});
await downloadExcelWithKoreanHeaders({
data: buildDownloadRows(response.items) as unknown as JSON[],
fileName: `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`,
sheetNameArray: ["침수발생이력"],
headerRange: "A1:Q1",
headerMapping: FLDNG_HSTRY_DOWNLOAD_HEADERS,
});
const params = new URLSearchParams(buildFldngHstryListQueryString(appliedFilters, 1, FLDNG_HSTRY_PAGE_SIZE));
params.delete("page"); params.delete("size");
await requestGridExcel("v1/spatialGridExports/floodHistories", {
history: Object.fromEntries(params),
ctpvNames: Object.fromEntries(ctpv.map((item) => [String(item.val).trim(), item.text])),
sggNames: Object.fromEntries(sgg.map((item) => [String(item.val).trim(), item.text])),
}, `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`);
} catch (error) {
console.error("침수발생이력 다운로드 중 오류:", error);
alert(
@@ -7,12 +7,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { getData } from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import useStore from "@/store/useStore";
import { getData } from "@/shared/api/sewerage-api";
import LoadingOverlay from "@/components/ui/loading-bar";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import useStore from "@/entities/reference-data/model/reference-data-store";
import {
FLOOD_DNGR_MAP_DEFAULT_FILTERS,
FLOOD_DNGR_MAP_DEPTH_OPTIONS,
@@ -25,7 +25,7 @@ import {
type FloodDngrMapExtent,
type FloodDngrMapFilters,
type FloodDngrMapTableRow,
} from "./floodDngrMapTypes";
} from "./flood-dngr-map-types";
const FLOOD_DNGR_MAP_REQUEST_TIMEOUT_MS = 10_000;
const FLOOD_DNGR_MAP_LOADING_WATCHDOG_MS = 12_000;
@@ -159,7 +159,7 @@ export const toFloodDngrMapTableRows = (
): FloodDngrMapTableRow[] =>
items.map((item, index) => ({
...item,
rowNo: total - page * pageSize - index,
rowNo: page * pageSize + index + 1,
ctpvNm: resolveAdministrativeName(item.ctpvCd, ctpvOptions, 2),
sggNm: resolveAdministrativeName(item.sggCd, sggOptions, 5),
sbmrgDpthlDisplay: toDepthRangeDisplay(
@@ -4,15 +4,19 @@
*/
"use client";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState, useRef } from "react";
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
import { Button } from "@common/components/ui";
import { getData } from "@/components/lib/api";
import { VerticalTable } from "@/components/ui/verticalTable";
import { getData } from "@/shared/api/sewerage-api";
import { VerticalTable } from "@/components/ui/vertical-table";
import type { GisEditChange, GisEditPipeBasicInfo } from "./gisEditTypes";
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
import type { GisEditChange, GisEditPipeBasicInfo } from "./gis-edit-types";
import {
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_USE_CODES,
} from "@/entities/facility/model/facility-code-fallbacks";
type Props = {
change: GisEditChange;
@@ -206,20 +210,25 @@ const normalizeBasicFormValues = (values: Record<string, any>) => {
return nextValues;
};
const buildBaseFeatureValues = (featureData: any) => {
const buildBaseFeatureValues = (
featureData: any,
catalog?: ReturnType<typeof useFacilityCodeCatalog>,
) => {
const resolveUsageCode = (value: unknown) => {
const raw = String(value ?? "").trim();
const matched = MS_SWAGE_PPL_USE_CODES.find(
(item) => item.val === raw || item.text === raw,
);
const matched = [
...(catalog?.MS_SWAGE_PPL_USE_CODES ?? []),
...MS_SWAGE_PPL_USE_CODES,
].find((item) => item.val === raw || item.text === raw);
return matched?.val.trim() ?? raw;
};
const resolveMaterialCode = (value: unknown) => {
const raw = String(value ?? "").trim();
const matched = MS_SWAGE_PPL_CODES.find(
(item) => item.val === raw || item.text === raw,
);
const matched = [
...(catalog?.MS_SWAGE_PPL_CODES ?? []),
...MS_SWAGE_PPL_CODES,
].find((item) => item.val === raw || item.text === raw);
return matched?.val.trim() ?? raw;
};
@@ -243,13 +252,12 @@ const buildBaseFeatureValues = (featureData: any) => {
}, {});
};
const getPipeUsageLabel = (value: unknown) => {
const getPipeUsageLabel = (value: unknown, codes = MS_SWAGE_PPL_USE_CODES) => {
const raw = String(value ?? "").trim();
if (!raw) return "";
return (
MS_SWAGE_PPL_USE_CODES.find(
(item) => item.val.trim() === raw || item.text === raw,
)?.text ?? raw
codes.find((item) => item.val.trim() === raw || item.text === raw)?.text ??
raw
);
};
@@ -269,10 +277,17 @@ export default function GisEditPipeSessionPopup({
onClose,
onSave,
}: Props) {
const catalog = useFacilityCodeCatalog();
// CMS 명칭 갱신은 입력 중인 초안을 초기화할 이유가 아니다.
const initializedSource = useRef<{
featureData: any;
changeId: string;
isNewPipe: boolean;
} | null>(null);
const isNewPipe = change.editType === "ADD";
const baseFeatureValues = useMemo(
() => buildBaseFeatureValues(featureData),
[featureData],
() => buildBaseFeatureValues(featureData, catalog),
[featureData, catalog],
);
const [basicInfoTableData, setBasicInfoTableData] = useState<
Record<string, any>
@@ -322,12 +337,36 @@ export default function GisEditPipeSessionPopup({
PIPE_BASIC_INFO_FIELDS.map((field) =>
field.id === "mngInstCd"
? { ...field, options: mngInstSelectOptions }
: field,
: field.id === "usgCd" || field.id === "mqltCd"
? {
...field,
options: [
EMPTY_SELECT_OPTION,
...(field.id === "usgCd"
? catalog.MS_SWAGE_PPL_USE_CODES
: catalog.MS_SWAGE_PPL_CODES
).map((item) => ({ value: item.val, label: item.text })),
],
}
: field,
),
[mngInstSelectOptions],
[mngInstSelectOptions, catalog],
);
useEffect(() => {
const previous = initializedSource.current;
if (
previous &&
previous.featureData === featureData &&
previous.changeId === change.changeId &&
previous.isNewPipe === isNewPipe
)
return;
initializedSource.current = {
featureData,
changeId: change.changeId,
isNewPipe,
};
const initialBasicValues: Record<string, any> = {
...baseFeatureValues,
...normalizeBasicFormValues(change.basicInfo ?? {}),
@@ -335,7 +374,7 @@ export default function GisEditPipeSessionPopup({
setBasicInfoTableData(initialBasicValues);
setInputInfoTableData(buildInitialInputDraft(change));
}, [baseFeatureValues, change.changeId, isNewPipe]);
}, [baseFeatureValues, featureData, change.changeId, isNewPipe]);
useEffect(() => {
if (isNewPipe) return;
@@ -429,7 +468,7 @@ export default function GisEditPipeSessionPopup({
<h3 className="whitespace-nowrap text-2xl font-medium text-primary-foreground">
하수관로
{basicInfoTableData?.usgCd
? `(${getPipeUsageLabel(basicInfoTableData.usgCd)})`
? `(${getPipeUsageLabel(basicInfoTableData.usgCd, catalog.MS_SWAGE_PPL_USE_CODES)})`
: ""}{" "}
정보 <br />
관리번호 : {displayManagementNo || "신규"}
@@ -6,8 +6,8 @@
import { Button } from "@common/components";
import type { GisEditSessionSnapshot, GisEditTool } from "./gisEditTypes";
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
import type { GisEditSessionSnapshot, GisEditTool } from "./gis-edit-types";
import { useGisEditOperationLabels } from "./use-gis-edit-common-codes";
type Props = GisEditSessionSnapshot & {
onRemoveFocusedChange: () => void;
@@ -7,8 +7,8 @@
import { Check, Move, PenTool, Plus, Trash2, X } from "lucide-react";
import { Button } from "@common/components/ui";
import type { GisEditTool } from "./gisEditTypes";
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
import type { GisEditTool } from "./gis-edit-types";
import { useGisEditOperationLabels } from "./use-gis-edit-common-codes";
type Props = {
isEditMode: boolean;
@@ -1,2 +1,2 @@
/** GIS 편집 모델은 기능이 소유하고 기존 화면 import 경로는 유지한다. */
export * from "@/features/gisEdit/model/types";
export * from "@/features/gis-edit/model/types";
@@ -3,9 +3,9 @@
import { useEffect, useState } from "react";
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
import { getCommonCodes } from "@/entities/code/api/common-code-api";
import type { GisEditTool } from "./gisEditTypes";
import type { GisEditTool } from "./gis-edit-types";
const GIS_EDIT_OPERATION_UP_CD = "GISEDITOPRT";
@@ -22,15 +22,15 @@ import {
getFeatureProperties,
normalizePipeGeometry,
createChangeId,
} from "@/features/gisEdit";
export { EMPTY_GIS_EDIT_PIPE_BASIC_INFO } from "@/features/gisEdit";
} from "@/features/gis-edit";
export { EMPTY_GIS_EDIT_PIPE_BASIC_INFO } from "@/features/gis-edit";
import type {
GisEditChange,
GisEditPipeBasicInfo,
GisEditSavePayload,
GisEditTool,
} from "./gisEditTypes";
} from "./gis-edit-types";
export const useGisEdit = (treeData: any[]) => {
const editableLayers = useMemo(() => collectEditableLayers(treeData), [treeData]);
@@ -12,26 +12,22 @@ import {
} from "@common/business_components";
import { Button, Input } from "@common/components";
import {
getBlobDataWithHeaders,
getData,
postMutiPart,
postRawJson,
} from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import { getBlobDataWithHeaders, postMutiPart } from "@/shared/api/file-api";
import { getData, postRawJson } from "@/shared/api/sewerage-api";
import LoadingOverlay from "@/components/ui/loading-bar";
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import { useAlertStore } from "@/shared/lib";
import { useUserStore } from "@/shared/lib/use-user-store";
import GisUploadPreviewMap from "./GisUploadPreviewMap";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { useGisManagementCommonCodes } from "./useGisManagementCommonCodes";
import type { UploadFacilityType, MngInstOption, UploadHistoryItem, GisUploadResponse, ValidationErrorItem, ViewMode } from "@/features/gisManagement/model/upload-types";
import { getLocalizedValidationErrorMessage, getFileExtension, formatDateTime, toScopeKey, buildManagementLabel, getErrorMessage } from "@/features/gisManagement/lib/upload-display";
import { createGisDownloadClient, saveBlob } from "@/features/gisManagement/api/gis-download";
import { postRefreshMessageToOpener } from "@/features/gisManagement/lib/popup-refresh";
import GisUploadPreviewMap from "./gis-upload-preview-map";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import { useGisManagementCommonCodes } from "./use-gis-management-common-codes";
import type { UploadFacilityType, MngInstOption, UploadHistoryItem, GisUploadResponse, ValidationErrorItem, ViewMode } from "@/features/gis-management/model/upload-types";
import { getLocalizedValidationErrorMessage, getFileExtension, formatDateTime, toScopeKey, buildManagementLabel, getErrorMessage } from "@/features/gis-management/lib/upload-display";
import { createGisDownloadClient, saveBlob } from "@/features/gis-management/api/gis-download";
import { postRefreshMessageToOpener } from "@/features/gis-management/lib/popup-refresh";
import { createGisUploadRetryGuard, getGisPartialSuccessNotice } from "@/features/gisManagement";
import { createGisUploadRetryGuard, getGisPartialSuccessNotice } from "@/features/gis-management";
// 기존 전송 경로를 유지하며 다운로드 작업의 비동기 처리는 기능 모듈에 위임한다.
const { downloadGeoPackage, startDownloadJob, waitForDownloadJob } =
@@ -57,7 +53,7 @@ const GIS_UPLOAD_HISTORY_HEADLIST: UiHeadProps[] = [
];
/** 오류 원본 행번호와 공통 표의 역순 순번을 별도 열로 표시한다. */
/** 오류 원본 행번호와 공통 표의 오름차순 표시 순번을 별도 열로 표시한다. */
const GIS_VALIDATION_ERROR_HEADLIST: UiHeadProps[] = [
{ id: "no", title: "순번", widthPercent: 8 },
{ id: "sourceRowNo", title: "행번호", widthPercent: 10 },
@@ -20,7 +20,7 @@ import { get as getProjection } from "ol/proj";
import { register } from "ol/proj/proj4";
import proj4 from "proj4";
import { getRawJson } from "@/components/lib/api";
import { getRawJson } from "@/shared/api/sewerage-api";
type GisUploadPreviewMapProps = {
uploadId: string;
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
import { getCommonCodes } from "@/entities/code/api/common-code-api";
type CodeLabelMap = Record<string, string>;
@@ -11,26 +11,25 @@ import {
type UiHeadProps,
type UiTableSort,
} from "@common/business_components";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { useAlertStore } from "@/shared/lib";
import { Button } from "@common/components";
import { getData } from "@/components/lib/api";
import { getData } from "@/shared/api/sewerage-api";
import { appendApiSearchParams } from "@/shared/util/util";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import useStore from "@/store/useStore";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
import useStore from "@/entities/reference-data/model/reference-data-store";
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import LoadingOverlay from "@/components/ui/loading-bar";
import {
INFTWT_RESULT_ROW_SELECT_MESSAGE,
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
type InftwtResultRow,
} from "./inftwtResultTypes";
} from "./inftwt-result-types";
const INFTWT_RESULT_PAGE_SIZE = 10;
@@ -160,7 +159,7 @@ export default function InftwtResultPopupPage() {
return {
...(formattedItem as Record<string, unknown>),
rowNumber: Math.max(
totalElements - ((page - 1) * INFTWT_RESULT_PAGE_SIZE + index),
(page - 1) * INFTWT_RESULT_PAGE_SIZE + index + 1,
0,
),
ctpvNm: ctpvOption ? ctpvOption.text : String(item?.ctpvCd ?? "-"),
@@ -241,29 +240,7 @@ export default function InftwtResultPopupPage() {
try {
setIsLoading(true);
const response = await fetchAllPages<any, Record<string, unknown>>({
fetchPage: (page, size) =>
getData(
`v1/inftwtAnlsRslt/ppl/${selectedRow.inftwtAnlsSn}?page=${page}&size=${size}`,
),
getItems: (page) =>
Array.isArray(page?.content) ? page.content : [],
getTotal: (page) => Number(page?.totalElements ?? 0),
});
const excelRows = response.items;
await downloadExcelWithKoreanHeaders({
data: excelRows as unknown as JSON[],
fileName: `침입수분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.inftwtAnlsSn}`,
sheetNameArray: ["침입수 분석결과"],
headerRange: "A1:D1",
headerMapping: {
inftwtAnlsSn: "침입수분석\n분석번호",
pplMngNo: "관로\n관리번호",
anlsTm: "분석시각",
predcOilq: "예측유량\n(m³/h)",
},
});
await downloadAnalysisExcel("inftwt", selectedRow.inftwtAnlsSn, `침입수분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.inftwtAnlsSn}`);
} catch (error) {
console.error("침입수 분석결과 다운로드 중 오류:", error);
alert("침입수 분석결과 다운로드 중 오류가 발생했습니다.");
@@ -10,8 +10,8 @@ import Fill from "ol/style/Fill.js";
import Style from "ol/style/Style.js";
import { intersects as intersectsExtent } from "ol/extent";
import { getRawJson } from "@/components/lib/api";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { getRawJson } from "@/shared/api/sewerage-api";
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
import { useAlertStore } from "@/shared/lib";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
@@ -23,7 +23,7 @@ import {
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
type InftwtResultMapViewResponse,
type InftwtResultRow,
} from "./inftwtResultTypes";
} from "./inftwt-result-types";
type UseInftwtResultMapParams = {
gisMaps: MutableRefObject<any | null>;
@@ -8,20 +8,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { getData } from "@/components/lib/api";
import { requestGridExcel } from "@/shared/api/grid-export";
import { getData } from "@/shared/api/sewerage-api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/loading-bar";
import useStore from "@/entities/reference-data/model/reference-data-store";
import { useAlertStore } from "@/shared/lib";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { useUserStore } from "@/shared/lib/use-user-store";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
import {
PIPE_ANALYSIS_DETAIL_DEFAULT_FILTERS,
PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS,
PIPE_ANALYSIS_DETAIL_HEADLIST,
PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE,
applyScopeToDetailFilters,
@@ -36,24 +34,11 @@ import {
toPipeAnalysisDetailViewRows,
type PipeAnalysisDetailFilters,
type PipeAnalysisDetailRow,
} from "./pipeAnalysisUtils";
} from "./pipe-analysis-utils";
type DetailViewRow = ReturnType<typeof toPipeAnalysisDetailViewRows>[number];
const PIPE_ANALYSIS_DETAIL_PAGE_SIZE = 10;
const PIPE_ANALYSIS_MAP_WINDOW_NAME = "sn-sewerage-map-root";
const PIPE_ANALYSIS_DETAIL_EXPORT_KEYS = [
"rowNo",
"ctpvNm",
"sggNm",
"fcltMngNo",
"instMngNo",
"swtrmtZoneNm",
"instlYmd",
"useNm",
"pdmtVl",
"prlgVl",
"materialName",
] as const;
const PIPE_ANALYSIS_DETAIL_STANDARD_HEADLIST = PIPE_ANALYSIS_DETAIL_HEADLIST;
const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
@@ -62,6 +47,8 @@ const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
: fallbackMessage;
export default function PipeAnalysisDetailPopupPage() {
const { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } =
useFacilityCodeCatalog();
const alert = useAlertStore((state) => state.setMessage);
const searchParams = useSearchParams();
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
@@ -158,11 +145,20 @@ export default function PipeAnalysisDetailPopupPage() {
responseRows as PipeAnalysisDetailRow[],
ctpv,
sgg,
{ materials: MS_SWAGE_PPL_CODES, uses: MS_SWAGE_PPL_USE_CODES },
),
total: Number(response?.count ?? 0),
};
},
[baseAnalysisType, baseElapsedYearBucket, ctpv, scope.regionCode, sgg],
[
baseAnalysisType,
baseElapsedYearBucket,
ctpv,
scope.regionCode,
sgg,
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_USE_CODES,
],
);
const loadRows = useCallback(
@@ -240,33 +236,31 @@ export default function PipeAnalysisDetailPopupPage() {
try {
setIsLoading(true);
const response = await fetchAllPages({
fetchPage: (page, size) => fetchRows(page + 1, appliedFilters, size),
getItems: (page) => page.rows,
getTotal: (page) => page.total,
});
const exportRows = response.items.map((row) => ({
rowNo: row.rowNo,
ctpvNm: row.ctpvNm,
sggNm: row.sggNm,
fcltMngNo: row.fcltMngNo,
instMngNo: row.instMngNo,
swtrmtZoneNm: row.swtrmtZoneNm,
instlYmd: row.instlYmd,
useNm: row.useNm,
pdmtVl: row.pdmtVl,
prlgVl: row.prlgVl,
materialName:
resolveMaterialKoreanName(row.materialCode) || row.materialName,
}));
const detailHeaderRange = `A1:${String.fromCharCode(64 + PIPE_ANALYSIS_DETAIL_EXPORT_KEYS.length)}1`;
await downloadExcelWithKoreanHeaders({
data: exportRows as unknown as JSON[],
fileName: `관로분석_관로현황_${buildPipeAnalysisFileDate()}`,
sheetNameArray: ["관로현황"],
headerRange: detailHeaderRange,
headerMapping: PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS,
const params = new URLSearchParams({ analysisType: baseAnalysisType });
appendApiSearchParams(params, {
ctpvCd: appliedFilters.ctpvCd,
sggCd: appliedFilters.sggCd,
swtrmtZoneNm: appliedFilters.swtrmtZoneNm,
elapsedYearBucket: baseElapsedYearBucket,
materialCode: appliedFilters.materialCode,
useCd: appliedFilters.useCd,
diameterFrom: appliedFilters.diameterFrom,
diameterTo: appliedFilters.diameterTo,
});
if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
await requestGridExcel(
"v1/spatialGridExports/pipes",
{
pipe: Object.fromEntries(params),
ctpvNames: Object.fromEntries(
ctpv.map((item) => [String(item.val).trim(), item.text]),
),
sggNames: Object.fromEntries(
sgg.map((item) => [String(item.val).trim(), item.text]),
),
},
`관로분석_관로현황_${buildPipeAnalysisFileDate()}.xlsx`,
);
} catch (error) {
console.error("관로현황 다운로드 중 오류:", error);
alert(
@@ -293,12 +287,12 @@ export default function PipeAnalysisDetailPopupPage() {
) {
options.push({
value: baseMaterialCode,
label: resolveMaterialName(baseMaterialCode),
label: resolveMaterialName(baseMaterialCode, MS_SWAGE_PPL_CODES),
});
}
return options;
}, [baseMaterialCode]);
}, [baseMaterialCode, MS_SWAGE_PPL_CODES]);
const titleSuffix = useMemo(() => {
const locationParts: string[] = [];
@@ -329,7 +323,7 @@ export default function PipeAnalysisDetailPopupPage() {
if (baseElapsedYearBucket)
parts.push(resolveElapsedYearBucketLabel(baseElapsedYearBucket));
if (baseMaterialCode && baseMaterialCode !== "0")
parts.push(resolveMaterialName(baseMaterialCode));
parts.push(resolveMaterialName(baseMaterialCode, MS_SWAGE_PPL_CODES));
return parts.length > 0 ? ` (${parts.join(" / ")})` : "";
}, [
baseAnalysisType,
@@ -339,6 +333,7 @@ export default function PipeAnalysisDetailPopupPage() {
filters.ctpvCd,
filters.sggCd,
filters.swtrmtZoneNm,
MS_SWAGE_PPL_CODES,
rows,
sgg,
]);
@@ -414,7 +409,9 @@ export default function PipeAnalysisDetailPopupPage() {
if (event.key === "Enter") handleSearch();
}}
/>
<span className="shrink-0 text-xl font-semibold text-foreground">~</span>
<span className="shrink-0 text-xl font-semibold text-foreground">
~
</span>
<Input
className="sn-popup-input !h-[3.2rem] w-[12rem] !text-xl"
aria-label="관경 최대값"
@@ -8,16 +8,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { getData } from "@/components/lib/api";
import { getData } from "@/shared/api/sewerage-api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/loading-bar";
import useStore from "@/entities/reference-data/model/reference-data-store";
import { useAlertStore } from "@/shared/lib";
import { useUserStore } from "@/shared/lib/use-user-store";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import PipeAnalysisSummaryTable from "./PipeAnalysisSummaryTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
import PipeAnalysisSummaryTable from "./pipe-analysis-summary-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import {
PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS,
PIPE_ANALYSIS_TYPE_OPTIONS,
@@ -35,7 +35,7 @@ import {
type PipeAnalysisSummaryResponse,
type PipeAnalysisSummaryViewRow,
type PipeAnalysisYearMaterialGroup,
} from "./pipeAnalysisUtils";
} from "./pipe-analysis-utils";
const SUMMARY_FILE_PREFIX: Record<string, string> = {
YEAR: "관로분석_경과년도별",
@@ -83,8 +83,12 @@ export default function PipeAnalysisPopupPage() {
Array<{ value: string; label: string }>
>([]);
const [summaryCurrentPage, setSummaryCurrentPage] = useState(1);
// 복합표는 물리 행 수가 아니라 서버가 반환하는 처리구역 그룹 수를 사용한다.
const [summaryTotal, setSummaryTotal] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const initializedRef = useRef(false);
// 늦게 완료된 이전 페이지 응답이 최신 조회조건의 표를 덮어쓰지 않게 한다.
const summaryRequestId = useRef(0);
useEffect(() => {
if (ctpv.length === 0) fetchCtpv();
@@ -124,9 +128,13 @@ export default function PipeAnalysisPopupPage() {
}, [draftFilters.ctpvCd, draftFilters.sggCd, scope.regionCode, sgg]);
const fetchSummary = useCallback(
async (nextFilters: PipeAnalysisSummaryFilters) => {
async (nextFilters: PipeAnalysisSummaryFilters, page = 1) => {
const requestId = ++summaryRequestId.current;
setIsLoading(true);
const params = new URLSearchParams({
analysisType: nextFilters.analysisType,
page: String(page - 1),
size: String(PIPE_ANALYSIS_SUMMARY_PAGE_SIZE),
});
appendApiSearchParams(params, {
@@ -137,22 +145,27 @@ export default function PipeAnalysisPopupPage() {
if (nextFilters.scopeRegionCode)
params.set("scopeRegionCode", nextFilters.scopeRegionCode);
const response = (await getData(
`v1/fclts/pplAnalysis/summaries?${params.toString()}`,
)) as PipeAnalysisSummaryResponse;
const nextMaterialColumns = Array.isArray(response?.materialColumns)
? response.materialColumns
: [];
setMaterialColumns(nextMaterialColumns);
setRows(buildPipeAnalysisSummaryViewRows(response, ctpv, sgg));
try {
const response = (await getData(
`v1/fclts/pplAnalysis/summaryPage?${params.toString()}`,
)) as PipeAnalysisSummaryResponse & { count: number };
if (requestId !== summaryRequestId.current) return;
const nextMaterialColumns = Array.isArray(response?.materialColumns) ? response.materialColumns : [];
setMaterialColumns(nextMaterialColumns);
setRows(buildPipeAnalysisSummaryViewRows(response, ctpv, sgg));
setSummaryTotal(response.count);
setSummaryCurrentPage(page);
} catch (error) {
if (requestId === summaryRequestId.current) throw error;
} finally {
if (requestId === summaryRequestId.current) setIsLoading(false);
}
},
[ctpv, sgg],
);
const handleSearch = useCallback(
async (nextFilters = draftFilters) => {
setIsLoading(true);
try {
const scopedFilters = applyScopeToSummaryFilters(nextFilters, scope);
setDraftFilters(scopedFilters);
@@ -168,9 +181,8 @@ export default function PipeAnalysisPopupPage() {
),
);
setRows([]);
setSummaryTotal(0);
setMaterialColumns([]);
} finally {
setIsLoading(false);
}
},
[alert, draftFilters, fetchSummary, scope],
@@ -191,6 +203,18 @@ export default function PipeAnalysisPopupPage() {
}
try {
// 화면은 한 페이지뿐이므로 Excel만 별도 전체 집계 계약으로 조회한다.
const params = new URLSearchParams({ analysisType: appliedFilters.analysisType });
appendApiSearchParams(params, {
ctpvCd: appliedFilters.ctpvCd, sggCd: appliedFilters.sggCd,
swtrmtZoneNm: appliedFilters.swtrmtZoneNm,
});
if (appliedFilters.scopeRegionCode) params.set("scopeRegionCode", appliedFilters.scopeRegionCode);
const response = await getData(`v1/fclts/pplAnalysis/summaries?${params}`) as PipeAnalysisSummaryResponse;
const materialColumns = response.materialColumns ?? [];
const rows = buildPipeAnalysisSummaryViewRows(response, ctpv, sgg);
const orderedYearMaterialGroups = buildPipeAnalysisYearMaterialGroups(rows, materialColumns)
.map((group, index) => ({ ...group, rowNo: index + 1 }));
const exportHeadList = buildPipeAnalysisSummaryHeadList(
appliedFilters.analysisType,
materialColumns,
@@ -334,31 +358,11 @@ export default function PipeAnalysisPopupPage() {
() =>
yearMaterialGroups.map((group, index) => ({
...group,
rowNo: yearMaterialGroups.length - index,
rowNo: (summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE + index + 1,
})),
[yearMaterialGroups],
[yearMaterialGroups, summaryCurrentPage],
);
const summaryPageOffset =
(summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE;
const pagedRows = useMemo(
() =>
appliedFilters.analysisType === "YEAR_MATERIAL"
? rows
: rows.slice(
summaryPageOffset,
summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE,
),
[appliedFilters.analysisType, rows, summaryPageOffset],
);
const visibleYearMaterialGroups = useMemo(
() =>
orderedYearMaterialGroups.slice(
summaryPageOffset,
summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE,
),
[orderedYearMaterialGroups, summaryPageOffset],
);
const sggOptions = useMemo(
() =>
draftFilters.ctpvCd === "0"
@@ -393,8 +397,8 @@ export default function PipeAnalysisPopupPage() {
}))
}
>
<option value="0">전체</option>
{ctpv.map((item) => (
{/* 시도는 전체 조회 없이 개별 지역만 선택한다. */}
{ctpv.filter((item) => String(item.val).trim() !== "0").map((item) => (
<option key={item.val} value={String(item.val)}>
{item.text}
</option>
@@ -498,19 +502,18 @@ export default function PipeAnalysisPopupPage() {
</PopupSearchPanel>
<PipeAnalysisSummaryTable
rows={pagedRows}
groups={visibleYearMaterialGroups}
rows={rows}
groups={orderedYearMaterialGroups}
grouped={appliedFilters.analysisType === "YEAR_MATERIAL"}
heads={headList}
title={`${resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 목록`}
total={
appliedFilters.analysisType === "YEAR_MATERIAL"
? orderedYearMaterialGroups.length
: rows.length
}
total={summaryTotal}
page={summaryCurrentPage}
pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE}
onPageChange={setSummaryCurrentPage}
onPageChange={(page) => {
void fetchSummary(appliedFilters, page)
.catch(() => alert("관로분석 페이지를 불러오지 못했습니다."));
}}
onCellClick={handleCellClick}
onGroupedCellClick={handleYearMaterialCellClick}
actions={
@@ -15,7 +15,7 @@ import type {
PipeAnalysisSummaryViewRow,
PipeAnalysisYearMaterialGroup,
PipeAnalysisYearMaterialGroupRow,
} from "./pipeAnalysisUtils";
} from "./pipe-analysis-utils";
type SummaryRow =
| {
@@ -158,7 +158,7 @@ export default function PipeAnalysisSummaryTable({
if (summaryRow.kind === "flat") {
const value =
head.id === "rowNo"
? total - ((page - 1) * pageSize + summaryRow.sourceIndex)
? (page - 1) * pageSize + summaryRow.sourceIndex + 1
: summaryRow.value[head.id];
const onClick =
summaryRow.value.__cellFilters[head.id] && Number(value) > 0
@@ -3,7 +3,10 @@
import type { SelectDataType } from "@common/business_components";
import type { IUserInfo } from "@/shared/store";
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
import {
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_USE_CODES,
} from "@/entities/facility/model/facility-code-fallbacks";
export const PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE =
"SN_PIPE_ANALYSIS_DETAIL_ROW_SELECT";
@@ -125,13 +128,6 @@ export type PipeAnalysisYearMaterialGroup = {
rows: PipeAnalysisYearMaterialGroupRow[];
};
const materialMap = new Map(
MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]),
);
const useMap = new Map(
MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]),
);
export const PIPE_ANALYSIS_TYPE_OPTIONS: Array<{
value: PipeAnalysisType;
label: string;
@@ -144,7 +140,8 @@ export const PIPE_ANALYSIS_TYPE_OPTIONS: Array<{
export const PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS: PipeAnalysisSummaryFilters =
{
analysisType: "YEAR",
ctpvCd: "0",
// 관로분석 최초 조회·초기화의 기본 시도는 부산광역시다. 계정 지역 제한은 별도 적용한다.
ctpvCd: "26",
sggCd: "0",
swtrmtZoneNm: "0",
scopeRegionCode: "",
@@ -486,13 +483,17 @@ export const toPipeAnalysisDetailViewRows = (
rows: PipeAnalysisDetailRow[],
ctpvOptions: SelectDataType[],
sggOptions: SelectDataType[],
catalog?: {
materials: { val: string; text: string }[];
uses: { val: string; text: string }[];
},
) =>
rows.map((row) => ({
...row,
ctpvNm: row.ctpvNm || resolveRegionName(row.ctpvCd, ctpvOptions, 2),
sggNm: row.sggNm || resolveRegionName(row.sggCd, sggOptions, 5),
useNm: resolveUseName(row.useCd),
materialName: resolveMaterialName(row.materialCode),
useNm: resolveUseName(row.useCd, catalog?.uses),
materialName: resolveMaterialName(row.materialCode, catalog?.materials),
}));
export const buildPipeAnalysisSummaryDownloadHeaders = (
@@ -538,11 +539,14 @@ export const PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS = {
materialName: "관재질",
};
export const resolveMaterialName = (materialCode?: string) => {
export const resolveMaterialName = (
materialCode?: string,
codes = MS_SWAGE_PPL_CODES,
) => {
const code = String(materialCode ?? "").trim();
if (!code) return "-";
if (code === "OTHER") return "기타";
return materialMap.get(code) ?? code;
return codes.find((item) => item.val.trim() === code)?.text ?? code;
};
/**
@@ -627,10 +631,13 @@ export const resolvePipeAnalysisTypeLabel = (analysisType?: string) => {
);
};
export const resolveUseName = (useCode?: string) => {
export const resolveUseName = (
useCode?: string,
codes = MS_SWAGE_PPL_USE_CODES,
) => {
const code = String(useCode ?? "").trim();
if (!code) return "-";
return useMap.get(code) ?? code;
return codes.find((item) => item.val.trim() === code)?.text ?? code;
};
export const resolveRegionName = (
@@ -13,24 +13,23 @@ import {
} from "@common/business_components";
import { Button } from "@common/components";
import { getData } from "@/components/lib/api";
import { getData } from "@/shared/api/sewerage-api";
import { appendApiSearchParams } from "@/shared/util/util";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
import { useAlertStore } from "@/shared/lib";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import useStore from "@/store/useStore";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
import useStore from "@/entities/reference-data/model/reference-data-store";
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import LoadingOverlay from "@/components/ui/loading-bar";
import {
RDII_RESULT_ROW_SELECT_MESSAGE,
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
type RdiiResultRow,
} from "./rdiiResultTypes";
} from "./rdii-result-types";
const RDII_RESULT_PAGE_SIZE = 10;
@@ -162,7 +161,7 @@ export default function RdiiResultPopupPage() {
return {
...(formattedItem as Record<string, unknown>),
rowNumber: Math.max(
totalElements - ((page - 1) * RDII_RESULT_PAGE_SIZE + index),
(page - 1) * RDII_RESULT_PAGE_SIZE + index + 1,
0,
),
ctpvNm: ctpvOption ? ctpvOption.text : String(item?.ctpvCd ?? "-"),
@@ -243,29 +242,7 @@ export default function RdiiResultPopupPage() {
try {
setIsLoading(true);
const response = await fetchAllPages<any, Record<string, unknown>>({
fetchPage: (page, size) =>
getData(
`v1/rdiiAnlsRslt/ppl/${selectedRow.rainInfwtAnlsSn}?page=${page}&size=${size}`,
),
getItems: (page) =>
Array.isArray(page?.content) ? page.content : [],
getTotal: (page) => Number(page?.totalElements ?? 0),
});
const excelRows = response.items;
await downloadExcelWithKoreanHeaders({
data: excelRows as unknown as JSON[],
fileName: `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`,
sheetNameArray: ["RDII 분석결과"],
headerRange: "A1:D1",
headerMapping: {
rainInfwtAnlsSn: "RDII분석\n분석번호",
pplMngNo: "관로\n관리번호",
anlsTm: "분석시각",
predcOilq: "예측유량\n(m³/h)",
},
});
await downloadAnalysisExcel("rdii", selectedRow.rainInfwtAnlsSn, `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`);
} catch (error) {
console.error("RDII 분석결과 다운로드 중 오류:", error);
alert("RDII 분석결과 다운로드 중 오류가 발생했습니다.");
@@ -8,8 +8,8 @@ import VectorSource from "ol/source/Vector.js";
import Stroke from "ol/style/Stroke.js";
import Style from "ol/style/Style.js";
import { getRawJson } from "@/components/lib/api";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { getRawJson } from "@/shared/api/sewerage-api";
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
import { useAlertStore } from "@/shared/lib";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
@@ -21,7 +21,7 @@ import {
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
type RdiiResultMapViewResponse,
type RdiiResultRow,
} from "./rdiiResultTypes";
} from "./rdii-result-types";
type UseRdiiResultMapParams = {
gisMaps: MutableRefObject<any | null>;
@@ -4,29 +4,30 @@
*/
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, Input } from "@common/components";
import {
StandardPopupLayout,
type UiHeadProps,
} from "@common/business_components";
import LoadingOverlay from "@/components/ui/LoadingBar";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { getRawJson } from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/loading-bar";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import { getRawJson } from "@/shared/api/sewerage-api";
import {
WINDOW_POPUP_MESSAGE_TYPE,
type WindowPopupContentProps,
type WindowPopupMessageType,
} from "@/features/windowPopup/model/types";
import { MS_SWAGE_PPL_CODES } from "../common/table";
} from "@/features/window-popup/model/types";
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
import {
downloadRepairHistoriesByZoneId,
fetchAllRepairHistoriesByZoneId,
fetchRepairHistoryPage,
fetchRepairHistoryMapFeatures,
type RepairHistoryFilters,
type RepairHistoryRow,
} from "../popupComponent/swtrmtPrcsZoneAnalysisApi";
} from "@/features/facility-detail/api/treatment-zone-api";
const REPAIR_HISTORY_PAGE_SIZE = 10;
const MAP_LAYER_SETTING_PATH =
@@ -162,6 +163,7 @@ const formatYear = (value: unknown) => {
export default function RepairHistoryPopupPage({
searchParams,
}: WindowPopupContentProps) {
const { MS_SWAGE_PPL_CODES } = useFacilityCodeCatalog();
const zoneId = useMemo(
() => firstSearchParam(searchParams.zoneId),
[searchParams.zoneId],
@@ -175,17 +177,21 @@ export default function RepairHistoryPopupPage({
useState<RepairHistoryFilters>(buildEmptyFilters);
const [appliedFilters, setAppliedFilters] =
useState<RepairHistoryFilters>(buildEmptyFilters);
// 전체 조회 결과는 부모 지도 강조와 팝업 내 10건 페이지 표가 함께 사용한다.
// 목록에는 현재 서버 페이지의 행만 저장하고 지도 전체 도형은 별도로 전달한다.
const [rows, setRows] = useState<RepairHistoryRow[]>([]);
const [total, setTotal] = useState(0);
// 페이지 요청과 지도 요청을 분리해 페이지 이동이 진행 중인 전체 지도 조회를 취소하지 않는다.
const pageRequestId = useRef(0);
const mapRequestId = useRef(0);
// 용도 선택지는 별도 하드코딩하지 않고 관망도 트리 설정 API의 sortOrdr를 따른다.
const [pipeUseOptions, setPipeUseOptions] = useState<PipeUseOption[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
/** 조회 결과 전체를 부모 지도 레이어와 팝업 표에 동시에 반영한다. */
/** 목록은 한 페이지만 조회하고 검색조건 변경 시에만 지도 전체 도형을 갱신한다. */
const searchRepairHistories = useCallback(
async (nextFilters: RepairHistoryFilters) => {
async (nextFilters: RepairHistoryFilters, page = 1, refreshMap = true) => {
if (!zoneId) {
setErrorMessage("처리구역 식별값이 없습니다.");
return;
@@ -193,38 +199,59 @@ export default function RepairHistoryPopupPage({
setIsLoading(true);
setErrorMessage("");
const requestId = ++pageRequestId.current;
if (refreshMap) {
const mapId = ++mapRequestId.current;
postRepairMessage(
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
{ zoneId, rows: [] },
);
void fetchRepairHistoryMapFeatures(zoneId, nextFilters)
.then((features) => {
if (mapId !== mapRequestId.current) return;
postRepairMessage(
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
{ zoneId, rows: features },
);
})
.catch(() => {
if (mapId === mapRequestId.current)
setErrorMessage(
"보수이력 지도 도형을 불러오지 못했습니다. 조회를 다시 실행해주세요.",
);
});
}
try {
const response = await fetchAllRepairHistoriesByZoneId(
const response = await fetchRepairHistoryPage(
zoneId,
nextFilters,
page - 1,
REPAIR_HISTORY_PAGE_SIZE,
);
if (requestId !== pageRequestId.current) return;
const nextRows = response.data ?? [];
setRows(nextRows);
setTotal(response.totalElements);
setAppliedFilters({ ...nextFilters });
setCurrentPage(1);
postRepairMessage(
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
{
zoneId,
rows: nextRows,
},
);
setCurrentPage(page);
} catch (error) {
if (requestId !== pageRequestId.current) return;
setRows([]);
postRepairMessage(
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
{
zoneId,
rows: [],
},
);
setTotal(0);
if (refreshMap) {
mapRequestId.current += 1;
postRepairMessage(
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
{ zoneId, rows: [] },
);
}
setErrorMessage(
error instanceof Error
? error.message
: "보수이력을 불러오지 못했습니다.",
);
} finally {
setIsLoading(false);
if (requestId === pageRequestId.current) setIsLoading(false);
}
},
[zoneId],
@@ -270,6 +297,8 @@ export default function RepairHistoryPopupPage({
};
window.addEventListener("beforeunload", clearParentLayer);
return () => {
pageRequestId.current += 1;
mapRequestId.current += 1;
window.removeEventListener("beforeunload", clearParentLayer);
clearParentLayer();
};
@@ -277,7 +306,7 @@ export default function RepairHistoryPopupPage({
/** 화면에서 적용한 조건과 동일한 9개 목록 항목의 XLSX를 내려받는다. */
const handleDownload = async () => {
if (!zoneId || rows.length === 0) return;
if (!zoneId || total === 0) return;
setIsLoading(true);
setErrorMessage("");
try {
@@ -306,18 +335,10 @@ export default function RepairHistoryPopupPage({
});
};
const visibleRows = useMemo(
() =>
rows.slice(
(currentPage - 1) * REPAIR_HISTORY_PAGE_SIZE,
currentPage * REPAIR_HISTORY_PAGE_SIZE,
),
[currentPage, rows],
);
// 공통 표에 전달할 표시값은 원본 API 행과 분리해 지도 연계 식별값을 보존한다.
const tableRows = useMemo<RepairHistoryTableRow[]>(
() =>
visibleRows.map((row) => ({
rows.map((row) => ({
fid: row.fid,
fcltMngNo: formatCellValue(row.fcltMngNo),
useName: formatCellValue(row.useNm),
@@ -329,7 +350,7 @@ export default function RepairHistoryPopupPage({
installationYear: formatYear(row.installationYear),
burialDepth: formatCellValue(row.burialDepth),
})),
[visibleRows],
[rows],
);
return (
<StandardPopupLayout title="보수이력 목록">
@@ -459,12 +480,14 @@ export default function RepairHistoryPopupPage({
}
headlist={REPAIR_HISTORY_HEADLIST}
title={`${zoneName || "처리구역"} 보수이력 목록`}
total={rows.length}
total={total}
page={currentPage}
pageSize={REPAIR_HISTORY_PAGE_SIZE}
onPageChange={setCurrentPage}
onPageChange={(page) =>
void searchRepairHistories(appliedFilters, page, false)
}
onRowClick={(_, index) => {
const row = visibleRows[index];
const row = rows[index];
if (row) handleRowClick(row);
}}
rowId={(row, index) => String(row.fid ?? index)}
@@ -1,6 +1,6 @@
/** 우측 상세 탭의 표시·분류 모델을 지도 조회 수명에서 분리한다. */
import type { ExmnMapJobType } from "../exmn/exmnMapTypes";
import type { SinkholeDetail, SinkholeNearbyPipe } from "../sinkhole/sinkholeTypes";
import type { ExmnMapJobType } from "../exmn/exmn-map-types";
import type { SinkholeDetail, SinkholeNearbyPipe } from "../sinkhole/sinkhole-types";
export type RightPanelTabKind = "feature" | "sinkhole" | "gisEdit";
export type RightPanelTabCategory =
@@ -7,13 +7,13 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { getRawJson } from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { getRawJson } from "@/shared/api/sewerage-api";
import LoadingOverlay from "@/components/ui/loading-bar";
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
import { useAlertStore } from "@/shared/lib";
import useStore from "@/store/useStore";
import useStore from "@/entities/reference-data/model/reference-data-store";
import {
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
SBMRG_DNGR_MAP_FREQUENCY_OPTIONS,
@@ -27,7 +27,7 @@ import {
type SbmrgDngrMapApiItem,
type SbmrgDngrMapFilters,
type SbmrgDngrMapTableRow,
} from "./sbmrgDngrMapTypes";
} from "./sbmrg-dngr-map-types";
const unwrapPayload = <T,>(response: any): T =>
(response?.data ?? response) as T;
@@ -139,7 +139,7 @@ export const toSbmrgDngrMapTableRows = (
): SbmrgDngrMapTableRow[] =>
items.map((item, index) => ({
...item,
rowNo: total - page * pageSize - index,
rowNo: page * pageSize + index + 1,
ctpvNm: resolveAdministrativeNames(item.ctpvCd, ctpvOptions, 2),
sggNm: resolveAdministrativeNames(item.sggCd, sggOptions, 5),
floodWtlvFrqVlDisplay: toDisplay(item.floodWtlvFrqVl),
@@ -6,10 +6,10 @@
import { useEffect, useMemo, useState } from "react";
import { Button } from "@common/components";
import { VerticalTable } from "@/components/ui/verticalTable";
import { getCmsData } from "@/components/lib/api";
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkholeTypes";
import { useSinkholeCommonCodes } from "./useSinkholeCommonCodes";
import { VerticalTable } from "@/components/ui/vertical-table";
import { getCmsData } from "@/shared/api/external-service-api";
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkhole-types";
import { useSinkholeCommonCodes } from "./use-sinkhole-common-codes";
type Props = {
detail: SinkholeDetail;

Some files were not shown because too many files have changed in this diff Show More