This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
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,
|
||||
@@ -217,7 +220,6 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
</div>
|
||||
</SectionCollapsibleGroup>
|
||||
</form>
|
||||
<RhfDevTool control={form.control} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { DesignCnstnMngSearchDto } from "@/entities/design-cnstn";
|
||||
import {
|
||||
@@ -28,7 +24,6 @@ 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 ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
@@ -47,10 +42,7 @@ function DesignListPage() {
|
||||
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,
|
||||
@@ -104,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>
|
||||
@@ -126,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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { FnrsCnsltnMngSearchDto } from "@/entities/fnrs-cnsltn";
|
||||
import {
|
||||
@@ -28,7 +24,6 @@ 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 ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
@@ -47,10 +42,7 @@ function FnrsListPage() {
|
||||
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,
|
||||
@@ -104,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>
|
||||
@@ -126,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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
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("보완완료 처리되었습니다.");
|
||||
alert("보완 완료 처리되었습니다.");
|
||||
await queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
router.push(withListQs(`/instl-sncn/${docNo}`));
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.all });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
console.error("보완 완료 실패:", error);
|
||||
alert("보완 완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import { useFindGugunCombo, useFindSidoCombo } from "@/entities/adstdstr";
|
||||
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { InstlSncnMngSearchParams } from "@/entities/instl-sncn";
|
||||
import {
|
||||
@@ -25,7 +24,7 @@ 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_PARAMS = {
|
||||
sidoComCd: "",
|
||||
@@ -43,10 +42,7 @@ function InstlListPage() {
|
||||
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,
|
||||
@@ -91,7 +87,7 @@ function InstlListPage() {
|
||||
<Select
|
||||
value={sidoComCd}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
@@ -99,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>
|
||||
@@ -111,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>
|
||||
|
||||
@@ -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("");
|
||||
@@ -58,19 +52,16 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
|
||||
const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
|
||||
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: 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;
|
||||
@@ -83,16 +74,16 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
}, [open, canSidoGugunSelectable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canSidoGugunSelectable || sidoOptions.length === 0) return;
|
||||
const nextSidoComCd = sidoOptions[0].adstDstrCd;
|
||||
if (canSidoGugunSelectable || userSidoData.length === 0) return;
|
||||
const nextSidoComCd = userSidoData[0].adstDstrCd;
|
||||
setSidoComCd((current) => (current === nextSidoComCd ? current : nextSidoComCd));
|
||||
}, [canSidoGugunSelectable, sidoOptions]);
|
||||
}, [canSidoGugunSelectable, userSidoData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canSidoGugunSelectable || !sidoComCd || gugunOptions.length === 0) return;
|
||||
const nextSggComCd = gugunOptions[0].adstDstrCd;
|
||||
if (canSidoGugunSelectable || !sidoComCd || userGugunData.length === 0) return;
|
||||
const nextSggComCd = userGugunData[0].adstDstrCd;
|
||||
setSggComCd((current) => current || nextSggComCd);
|
||||
}, [canSidoGugunSelectable, sidoComCd, gugunOptions]);
|
||||
}, [canSidoGugunSelectable, sidoComCd, userGugunData]);
|
||||
|
||||
const canConfirm =
|
||||
!!sewerBscPlanSeComCd && !!taskSeComCd && (!canSidoGugunSelectable || !!sidoComCd);
|
||||
@@ -100,8 +91,8 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
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);
|
||||
|
||||
@@ -149,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>
|
||||
@@ -162,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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getListHref, useAlertStore, useUserStore, withListQs } from "@/shared/lib";
|
||||
import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
|
||||
import {
|
||||
@@ -165,9 +165,7 @@ function SewerBscPlanFormPage({
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(
|
||||
withListQs(
|
||||
`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`,
|
||||
),
|
||||
withListQs(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}?chgCycl=${result.chgCycl ?? 1}`),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
} catch (error) {
|
||||
@@ -185,7 +183,7 @@ function SewerBscPlanFormPage({
|
||||
(item) => item.cmptnYn === "N" && (!item.splmntCn || !item.splmntCn.trim()),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청 항목이 있습니다.\n보완완료 내용을 입력해 주세요.");
|
||||
alert("보완 요청 항목이 있습니다.\n보완 완료 내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,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(withListQs(`/sewer-bsc-plan/${docNo}?chgCycl=${chgCycl}`));
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
console.error("보완 완료 실패:", error);
|
||||
alert("보완 완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -298,7 +296,7 @@ function SewerBscPlanFormPage({
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
신청 완료
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
@@ -317,7 +315,7 @@ function SewerBscPlanFormPage({
|
||||
onClick={form.handleSubmit(onSplmntCmptn)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
보완완료
|
||||
보완 완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -325,7 +323,6 @@ function SewerBscPlanFormPage({
|
||||
</div>
|
||||
</SectionCollapsibleGroup>
|
||||
</form>
|
||||
<RhfDevTool control={form.control} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { useMemo, useState } from "react";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useFindUserSidoGugunCombo } from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SewerBscPlanSearchRequestDto } from "@/entities/sewer-bsc-plan";
|
||||
import {
|
||||
@@ -28,7 +24,6 @@ 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 ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
@@ -49,10 +44,7 @@ function SewerBscPlanListPage() {
|
||||
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,
|
||||
@@ -108,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>
|
||||
@@ -130,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>
|
||||
|
||||
@@ -377,7 +377,7 @@ function AttachmentsFormSection({
|
||||
return (
|
||||
<SectionCollapsible title="첨부파일">
|
||||
<div className="space-y-6">
|
||||
{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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user