Merge remote-tracking branch 'origin/iipbs/st/dev'
This commit is contained in:
@@ -565,7 +565,7 @@ export function ExceptionProcessingHistoryDialog({
|
||||
onClick={handleOpenRejectReasonDialog}
|
||||
disabled={rejectSubmitting}
|
||||
>
|
||||
반려
|
||||
일괄 반려
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
|
||||
@@ -36,27 +37,43 @@ export interface MainFacilityMappingSaveDialogProps {
|
||||
onConfirm?: (values: MainFacilityMappingSaveDialogValues) => void | Promise<void>;
|
||||
}
|
||||
|
||||
function isValidYmd(ymd: string): boolean {
|
||||
if (!/^\d{8}$/.test(ymd)) return false;
|
||||
const y = Number(ymd.slice(0, 4));
|
||||
const m = Number(ymd.slice(4, 6));
|
||||
const d = Number(ymd.slice(6, 8));
|
||||
const dt = new Date(y, m - 1, d);
|
||||
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
|
||||
function formatDateToYmd(d: Date | undefined): string | null {
|
||||
if (!d || Number.isNaN(d.getTime())) return null;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}${m}${day}`;
|
||||
}
|
||||
|
||||
function validatePrxyCtrtCn(value: string): string | null {
|
||||
const t = value.trim();
|
||||
if (!t) return "위탁계약기간을 입력해 주세요.";
|
||||
/** `YYYYMMDD` → `Date` (로컬 자정) */
|
||||
function parseYmdToDate(ymd: string): Date | undefined {
|
||||
const s = ymd.replace(/\D/g, "");
|
||||
if (s.length !== 8) return undefined;
|
||||
const y = Number(s.slice(0, 4));
|
||||
const mo = Number(s.slice(4, 6)) - 1;
|
||||
const day = Number(s.slice(6, 8));
|
||||
const d = new Date(y, mo, day);
|
||||
if (d.getFullYear() !== y || d.getMonth() !== mo || d.getDate() !== day) return undefined;
|
||||
return d;
|
||||
}
|
||||
|
||||
function parseInitialPrxyCtrtCnToDates(initial: string): { start: Date | undefined; end: Date | undefined } {
|
||||
const t = initial.trim();
|
||||
if (!/^\d{8}~\d{8}$/.test(t)) {
|
||||
return "위탁계약기간은 YYYYMMDD~YYYYMMDD 형식으로 입력해 주세요. (예: 20240101~20281231)";
|
||||
return { start: undefined, end: undefined };
|
||||
}
|
||||
const start = t.slice(0, 8);
|
||||
const end = t.slice(9, 17);
|
||||
if (!isValidYmd(start) || !isValidYmd(end)) {
|
||||
return "시작일 또는 종료일이 유효한 날짜가 아닙니다.";
|
||||
const start = parseYmdToDate(t.slice(0, 8));
|
||||
const end = parseYmdToDate(t.slice(9, 17));
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function validateContractRange(start: Date | undefined, end: Date | undefined): string | null {
|
||||
const startYmd = formatDateToYmd(start);
|
||||
const endYmd = formatDateToYmd(end);
|
||||
if (!startYmd || !endYmd) {
|
||||
return "시작일과 종료일을 선택해 주세요.";
|
||||
}
|
||||
if (start > end) {
|
||||
if (startYmd > endYmd) {
|
||||
return "시작일이 종료일보다 늦을 수 없습니다.";
|
||||
}
|
||||
return null;
|
||||
@@ -75,7 +92,8 @@ export function MainFacilityMappingSaveDialog({
|
||||
const [prxyBzentyNmCn, setPrxyBzentyNmCn] = useState("");
|
||||
const [cnsgnCstInput, setCnsgnCstInput] = useState("");
|
||||
const [wholNopeInput, setWholNopeInput] = useState("");
|
||||
const [prxyCtrtCn, setPrxyCtrtCn] = useState("");
|
||||
const [prxyCtrtStart, setPrxyCtrtStart] = useState<Date | undefined>(undefined);
|
||||
const [prxyCtrtEnd, setPrxyCtrtEnd] = useState<Date | undefined>(undefined);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,7 +112,9 @@ export function MainFacilityMappingSaveDialog({
|
||||
? ""
|
||||
: String(initialWholNope),
|
||||
);
|
||||
setPrxyCtrtCn((initialPrxyCtrtCn ?? "").trim());
|
||||
const { start, end } = parseInitialPrxyCtrtCnToDates(initialPrxyCtrtCn ?? "");
|
||||
setPrxyCtrtStart(start);
|
||||
setPrxyCtrtEnd(end);
|
||||
}
|
||||
}, [open, initialPrxyBzentyNmCn, initialCnsgnCstVl, initialWholNope, initialPrxyCtrtCn]);
|
||||
|
||||
@@ -103,7 +123,8 @@ export function MainFacilityMappingSaveDialog({
|
||||
setPrxyBzentyNmCn("");
|
||||
setCnsgnCstInput("");
|
||||
setWholNopeInput("");
|
||||
setPrxyCtrtCn("");
|
||||
setPrxyCtrtStart(undefined);
|
||||
setPrxyCtrtEnd(undefined);
|
||||
}
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
@@ -121,18 +142,21 @@ export function MainFacilityMappingSaveDialog({
|
||||
alert("전체 인원은 0 이상의 정수로 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
const prxyCtrtErr = validatePrxyCtrtCn(prxyCtrtCn);
|
||||
const prxyCtrtErr = validateContractRange(prxyCtrtStart, prxyCtrtEnd);
|
||||
if (prxyCtrtErr) {
|
||||
alert(prxyCtrtErr);
|
||||
return;
|
||||
}
|
||||
const startYmd = formatDateToYmd(prxyCtrtStart)!;
|
||||
const endYmd = formatDateToYmd(prxyCtrtEnd)!;
|
||||
const prxyCtrtCnParam = `${startYmd}~${endYmd}`;
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onConfirm?.({
|
||||
prxyBzentyNmCn: prxyBzentyNmCn.trim(),
|
||||
cnsgnCstVl,
|
||||
wholNope,
|
||||
prxyCtrtCn: prxyCtrtCn.trim(),
|
||||
prxyCtrtCn: prxyCtrtCnParam,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -193,14 +217,26 @@ export function MainFacilityMappingSaveDialog({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">위탁계약기간</Label>
|
||||
<Input
|
||||
value={prxyCtrtCn}
|
||||
onChange={(e) => setPrxyCtrtCn(e.target.value)}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)"
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">시작일</Label>
|
||||
<InputDatePicker
|
||||
value={prxyCtrtStart}
|
||||
onChange={setPrxyCtrtStart}
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">종료일</Label>
|
||||
<InputDatePicker
|
||||
value={prxyCtrtEnd}
|
||||
onChange={setPrxyCtrtEnd}
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from '@/shared/lib';
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib';
|
||||
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
|
||||
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
|
||||
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
|
||||
@@ -26,7 +26,7 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '
|
||||
|
||||
import { getUserInfo } from '../../../../../../packages/common/components/src/api';
|
||||
|
||||
import { MainFacilityMappingSaveDialog } from './MainFacilityMappingSaveDialog';
|
||||
import { MainFacilityMappingSaveDialog, type MainFacilityMappingSaveDialogValues } from './MainFacilityMappingSaveDialog';
|
||||
import SubmitDailog from './SubmitDailog';
|
||||
|
||||
import CancelSubmitButton from '@/components/button/CancelSubmitButton';
|
||||
@@ -145,6 +145,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
||||
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
|
||||
@@ -415,6 +416,13 @@ const PublicSewageFacilityInp = () => {
|
||||
.filter((sn) => sn !== '');
|
||||
|
||||
const wholNopeFromRow = Number(selectedMain.wholNope ?? 0);
|
||||
const deleteOnly = saveList.length === 0 && deleteList.length > 0;
|
||||
const nothingToPersist = saveList.length === 0 && deleteList.length === 0;
|
||||
if (nothingToPersist) {
|
||||
alert('저장할 변경 사항이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
mainFacilityMappingSaveDraftRef.current = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
|
||||
@@ -429,6 +437,16 @@ const PublicSewageFacilityInp = () => {
|
||||
initialWholNope: Number.isFinite(wholNopeFromRow) ? wholNopeFromRow : 0,
|
||||
initialPrxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''),
|
||||
};
|
||||
|
||||
if (deleteOnly) {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
const ok = await runMainFacilityMappingSave(draft, null);
|
||||
if (ok) {
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMainFacilityMappingSaveDialogKey((k) => k + 1);
|
||||
setMainFacilityMappingSaveDialogOpen(true);
|
||||
return;
|
||||
@@ -671,42 +689,40 @@ const PublicSewageFacilityInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainFacilityMappingSaveConfirm = async (input: {
|
||||
prxyBzentyNmCn: string;
|
||||
cnsgnCstVl: number;
|
||||
wholNope: number;
|
||||
prxyCtrtCn: string;
|
||||
}) => {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
if (!draft || !searchParamRef.current || !lastSearchedValues) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
return;
|
||||
const runMainFacilityMappingSave = async (
|
||||
draft: MainFacilityMappingSaveDraft,
|
||||
dialogInput: MainFacilityMappingSaveDialogValues | null,
|
||||
): Promise<boolean> => {
|
||||
if (!searchParamRef.current || !lastSearchedValues) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const param: PublicSewageMainFacilityMappingSaveRequest = {
|
||||
const base = {
|
||||
statsYr: draft.statsYr,
|
||||
sewerBzmnCd: draft.sewerBzmnCd,
|
||||
admdstCd: draft.admdstCd,
|
||||
admdstNm: draft.admdstNm,
|
||||
mainFcltSn: draft.mainFcltSn,
|
||||
mainFcltNm: draft.mainFcltNm,
|
||||
prxyBzentyNmCn: input.prxyBzentyNmCn,
|
||||
cnsgnCstVl: input.cnsgnCstVl,
|
||||
wholNope: input.wholNope,
|
||||
prxyCtrtCn: input.prxyCtrtCn,
|
||||
userId: userInfo.userUnqId,
|
||||
saveList: draft.saveList,
|
||||
deleteList: draft.deleteList,
|
||||
};
|
||||
const param: PublicSewageMainFacilityMappingSaveRequest = dialogInput
|
||||
? {
|
||||
...base,
|
||||
prxyBzentyNmCn: dialogInput.prxyBzentyNmCn,
|
||||
cnsgnCstVl: dialogInput.cnsgnCstVl,
|
||||
wholNope: dialogInput.wholNope,
|
||||
prxyCtrtCn: dialogInput.prxyCtrtCn,
|
||||
}
|
||||
: { ...base };
|
||||
const response =
|
||||
await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param);
|
||||
if (response.status === 'success') {
|
||||
setGlobalProgress(50);
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
await loadPublicSewageFacilityActiveTabData(
|
||||
'ST00060002',
|
||||
searchParamRef.current,
|
||||
@@ -714,16 +730,32 @@ const PublicSewageFacilityInp = () => {
|
||||
);
|
||||
setGlobalProgress(100);
|
||||
alert('저장되었습니다.');
|
||||
} else {
|
||||
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
||||
return false;
|
||||
} catch {
|
||||
alert('데이터 저장 중 오류가 발생했습니다.');
|
||||
return false;
|
||||
} finally {
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainFacilityMappingSaveConfirm = async (input: MainFacilityMappingSaveDialogValues) => {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
if (!draft || !searchParamRef.current || !lastSearchedValues) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
return;
|
||||
}
|
||||
const ok = await runMainFacilityMappingSave(draft, input);
|
||||
if (ok) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
@@ -928,7 +960,14 @@ const PublicSewageFacilityInp = () => {
|
||||
inputPeriodEnterable,
|
||||
isAdminInfo,
|
||||
);
|
||||
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar;
|
||||
const {
|
||||
showSaveInToolbar: baseShowSaveInToolbar,
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard = baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
const isMainFacilityTab = activeTab === 'ST00060002';
|
||||
|
||||
const openVerificationHistoryDialog = () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
@@ -375,6 +375,7 @@ const SewagePipeInp = () => {
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const getFactInstallGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
@@ -1783,10 +1784,12 @@ const SewagePipeInp = () => {
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
const showSaveInToolbar = !hideToolbarForConnectionTab && baseShowSaveInToolbar;
|
||||
const showSaveInToolbar =
|
||||
!hideToolbarForConnectionTab && baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons =
|
||||
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons;
|
||||
const showActionToolbarCard = !hideToolbarForConnectionTab && baseShowActionToolbarCard;
|
||||
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard =
|
||||
!hideToolbarForConnectionTab && baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
@@ -192,6 +192,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
}, [activeTabId]);
|
||||
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
|
||||
@@ -710,12 +711,15 @@ const SeweragePenetrationRateInp = () => {
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
||||
: true;
|
||||
|
||||
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
/** 카드는 저장/제출줄/제출취소 중 하나라도 보일 때만 (관리자만으로 빈 카드 방지) */
|
||||
const showActionToolbarCard =
|
||||
rowData.length > 0 &&
|
||||
(showSaveInToolbar ||
|
||||
toolbarGroupAllowed &&
|
||||
(baseShowSaveInToolbar ||
|
||||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { RealGridReact } from "realgrid-react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAlertStore, useStatisticsInputStatusConfirmGroupGate } from "@/shared/lib";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
@@ -17,8 +18,8 @@ import type {
|
||||
StatsInputStatusListRequestParams,
|
||||
} from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
@@ -41,7 +42,9 @@ const getStatsInputStatusFetcher = async (
|
||||
|
||||
// 하수도 통계 관리 > 00. 통계 입력 현황
|
||||
const StatsInputStatusInp = () => {
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
@@ -58,6 +61,8 @@ const StatsInputStatusInp = () => {
|
||||
const [exceptionHistoryContext, setExceptionHistoryContext] = useState<
|
||||
ExceptionProcessingHistoryQueryContext | null
|
||||
>(null);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const confirmGroupToolbarAllowed = useStatisticsInputStatusConfirmGroupGate();
|
||||
|
||||
const swrKey = searchParams
|
||||
? (["stats-input-status", searchParams] as const)
|
||||
@@ -140,6 +145,7 @@ const StatsInputStatusInp = () => {
|
||||
inputPeriodCurrentRef.current = false;
|
||||
setExceptionHistoryOpen(false);
|
||||
setExceptionHistoryContext(null);
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const approveFunc = async () => {
|
||||
@@ -152,15 +158,20 @@ const StatsInputStatusInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// const hasInvalidStatus = rowData.some((row) =>
|
||||
// ["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
|
||||
// );
|
||||
const hasInvalidStatus = rowData.some((row) =>
|
||||
["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
|
||||
);
|
||||
|
||||
// if (hasInvalidStatus) {
|
||||
// alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
|
||||
// return;
|
||||
// }
|
||||
if (hasInvalidStatus) {
|
||||
alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproveLoading(true);
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
@@ -186,7 +197,7 @@ const StatsInputStatusInp = () => {
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "INPUT");
|
||||
if (response.status === "success") {
|
||||
if (response.resultCode.toLocaleLowerCase() === "success") {
|
||||
if (searchParams) {
|
||||
await mutate(["stats-input-status", searchParams]);
|
||||
}
|
||||
@@ -196,6 +207,8 @@ const StatsInputStatusInp = () => {
|
||||
}
|
||||
} catch {
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsApproveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -239,8 +252,21 @@ const StatsInputStatusInp = () => {
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
|
||||
: true;
|
||||
|
||||
const allRowsLaeSbmsnApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.laeSbmsnSttsCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
const showActionToolbarCard =
|
||||
hasCompletedSearch && rowData.length > 0 && gugunOkOrNotDoList && inputPeriodCurrentRef.current;
|
||||
hasCompletedSearch &&
|
||||
rowData.length > 0 &&
|
||||
gugunOkOrNotDoList &&
|
||||
inputPeriodCurrentRef.current &&
|
||||
!allRowsLaeSbmsnApproved &&
|
||||
confirmGroupToolbarAllowed;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -253,7 +279,7 @@ const StatsInputStatusInp = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
{(isLoading || isApproveLoading) && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { ColumnConfig, DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
@@ -53,6 +53,8 @@ function formatYmdForDisplay(ymd: string): string {
|
||||
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
const PERIOD_DETAIL_FIELD = "detailInfo";
|
||||
|
||||
const StatisticsInputManagement = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -88,6 +90,7 @@ const StatisticsInputManagement = () => {
|
||||
seType: seTypeToDisplayLabel(row.seType),
|
||||
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
|
||||
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
|
||||
[PERIOD_DETAIL_FIELD]: "상세정보",
|
||||
}));
|
||||
setRowData(toGridRows(rowsForGrid));
|
||||
} else {
|
||||
@@ -119,48 +122,59 @@ const StatisticsInputManagement = () => {
|
||||
setPeriodDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleRowClick = async (row: Record<string, string | number>) => {
|
||||
const seCdRaw = row.seCd;
|
||||
const seCdNum =
|
||||
typeof seCdRaw === "number"
|
||||
? seCdRaw
|
||||
: parseInt(String(seCdRaw ?? ""), 10);
|
||||
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
|
||||
const statsYr = String(row.statsYr ?? "");
|
||||
const seType = rowSeTypeToCode(row);
|
||||
const openPeriodDetailForRow = useCallback(
|
||||
async (row: Record<string, string | number>) => {
|
||||
const seCdRaw = row.seCd;
|
||||
const seCdNum =
|
||||
typeof seCdRaw === "number"
|
||||
? seCdRaw
|
||||
: parseInt(String(seCdRaw ?? ""), 10);
|
||||
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
|
||||
const statsYr = String(row.statsYr ?? "");
|
||||
const seType = rowSeTypeToCode(row);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsApi.getStatsInputPeriodDetail({
|
||||
statsYr,
|
||||
seType,
|
||||
seCd,
|
||||
});
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsApi.getStatsInputPeriodDetail({
|
||||
statsYr,
|
||||
seType,
|
||||
seCd,
|
||||
});
|
||||
|
||||
if (res.status !== "success" || !res.data) {
|
||||
alert(res.message || "상세 정보를 조회하지 못했습니다.");
|
||||
return;
|
||||
if (res.status !== "success" || !res.data) {
|
||||
alert(res.message || "상세 정보를 조회하지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = res.data;
|
||||
setPeriodDialogMode("edit");
|
||||
setPeriodDialogInitial({
|
||||
statsYr: String(detail.statsYr ?? statsYr),
|
||||
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
|
||||
seCd:
|
||||
typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
|
||||
? detail.seCd
|
||||
: seCd,
|
||||
inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
|
||||
inptEndYmd: parseYmdToDate(detail.inptEndYmd),
|
||||
});
|
||||
setPeriodDialogOpen(true);
|
||||
} catch {
|
||||
alert("상세 정보 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[alert],
|
||||
);
|
||||
|
||||
const detail = res.data;
|
||||
setPeriodDialogMode("edit");
|
||||
setPeriodDialogInitial({
|
||||
statsYr: String(detail.statsYr ?? statsYr),
|
||||
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
|
||||
seCd:
|
||||
typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
|
||||
? detail.seCd
|
||||
: seCd,
|
||||
inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
|
||||
inptEndYmd: parseYmdToDate(detail.inptEndYmd),
|
||||
});
|
||||
setPeriodDialogOpen(true);
|
||||
} catch {
|
||||
alert("상세 정보 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handlePeriodDetailCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== PERIOD_DETAIL_FIELD) return;
|
||||
void openPeriodDetailForRow(row);
|
||||
},
|
||||
[openPeriodDetailForRow],
|
||||
);
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{
|
||||
@@ -193,6 +207,14 @@ const StatisticsInputManagement = () => {
|
||||
header: { text: "종료일" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: PERIOD_DETAIL_FIELD,
|
||||
fieldName: PERIOD_DETAIL_FIELD,
|
||||
header: { text: "상세정보" },
|
||||
editable: false,
|
||||
width: 118,
|
||||
styleName: "rg-cell-outline-action-btn",
|
||||
},
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
@@ -201,6 +223,7 @@ const StatisticsInputManagement = () => {
|
||||
"seCd",
|
||||
"inptBgngYmd",
|
||||
"inptEndYmd",
|
||||
PERIOD_DETAIL_FIELD,
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -232,7 +255,7 @@ const StatisticsInputManagement = () => {
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
onRowClick={handleRowClick}
|
||||
onDataCellClick={handlePeriodDetailCellClick}
|
||||
/>
|
||||
<StatisticsInputPeriodDialog
|
||||
open={periodDialogOpen}
|
||||
|
||||
@@ -3,37 +3,92 @@ import { useState } from "react";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
const StatisticsReviewHistory = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: '구분', fieldName: '구분', header: { text: '구분' }, editable: false, sortdisable: true },
|
||||
{ name: '광역자치단체', fieldName: '광역자치단체', header: { text: '광역자치단체' }, editable: false, sortdisable: true },
|
||||
{ name: '공단', fieldName: '공단', header: { text: '공단' }, editable: false, sortdisable: true },
|
||||
{ name: '처리내역', fieldName: '처리내역', header: { text: '처리내역' }, editable: false, sortdisable: true },
|
||||
{ name: "statsYr", fieldName: "statsYr", header: { text: "연도" }, editable: false },
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "sewerBzmnCd", fieldName: "sewerBzmnCd", header: { text: "하수사업자코드" }, editable: false },
|
||||
{ name: "statsCd", fieldName: "statsCd", header: { text: "통계코드" }, editable: false },
|
||||
{ name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false },
|
||||
{ name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false },
|
||||
{ name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false },
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
'sido',
|
||||
'sgg',
|
||||
'구분',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '검토내역' },
|
||||
items: ['광역자치단체', '공단', '처리내역'],
|
||||
},
|
||||
"statsYr",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"sewerBzmnCd",
|
||||
"statsCd",
|
||||
"menuNm",
|
||||
"chckerOpinionSm",
|
||||
"wrterOpinionSm",
|
||||
];
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.year?.trim()) {
|
||||
alert("년도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsApi.getStatiscicsManagementReviewHistory({
|
||||
statsYr: values.year.trim(),
|
||||
...(values.bsnmCd?.trim() ? { sewerBzmnCd: values.bsnmCd.trim() } : {}),
|
||||
...(values.sido && values.sido !== "ALL" ? { ctpvNm: values.sido } : {}),
|
||||
});
|
||||
|
||||
if (res.status === "success" && Array.isArray(res.data)) {
|
||||
setRowData(toGridRows(res.data));
|
||||
} else {
|
||||
setRowData([]);
|
||||
alert(res.message || "조회된 데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
setRowData([]);
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<DataGrid columnInfo={{ columns, columnLayout }} rowStartNum={2} fixedColumns={3} rowData={rowData} setRowData={setRowData} isExcelImport={false} />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataGrid
|
||||
columnInfo={{ columns, columnLayout }}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
enableSort={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticsReviewHistory;
|
||||
export default StatisticsReviewHistory;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
@@ -41,7 +42,7 @@ const checkSubmitStatusCd = (code: string) => {
|
||||
};
|
||||
|
||||
const isStatsInputStatusRowApprovable = (row: Record<string, string | number>) =>
|
||||
String(row.submitStatusCd ?? "") === "3" && String(row.prvnStatusCd ?? "") === "2";
|
||||
String(row.submitStatusCd ?? "") === "3" && (String(row.prvnStatusCd ?? "") === "2" || String(row.prvnStatusCd ?? "") === "3");
|
||||
|
||||
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
||||
rawList.map((r) => ({
|
||||
@@ -54,9 +55,11 @@ const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
||||
// 하수도 통계 검토 00. 통계 입력 현황
|
||||
const StatsInputStatusRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [searchedYear, setSearchedYear] = useState<string>("");
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
|
||||
@@ -108,6 +111,7 @@ const StatsInputStatusRvw = () => {
|
||||
setSearchedYear("");
|
||||
setLastSearchedValues(null);
|
||||
setHasCompletedSearch(false);
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
@@ -158,11 +162,16 @@ const StatsInputStatusRvw = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!rowData.every(isStatsInputStatusRowApprovable)) {
|
||||
// alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
|
||||
// return;
|
||||
// }
|
||||
if (!rowData.every(isStatsInputStatusRowApprovable)) {
|
||||
alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproveLoading(true);
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
@@ -188,7 +197,7 @@ const StatsInputStatusRvw = () => {
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "REVIEW");
|
||||
if (response.status === "success") {
|
||||
if (response.resultCode.toLocaleLowerCase() === "success") {
|
||||
try {
|
||||
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
|
||||
ctpvNm: lastSearchedValues.sido,
|
||||
@@ -209,6 +218,8 @@ const StatsInputStatusRvw = () => {
|
||||
}
|
||||
} catch {
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsApproveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,11 +234,20 @@ const StatsInputStatusRvw = () => {
|
||||
[rowData],
|
||||
);
|
||||
|
||||
/** 진행상태(prvn) 코드가 모두 승인완료(3) */
|
||||
const allPrvnStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.prvnStatusCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
const showActionToolbarCard =
|
||||
hasCompletedSearch &&
|
||||
rowData.length > 0 &&
|
||||
gugunOkOrNotDoList &&
|
||||
!allSubmitStatusApproved;
|
||||
allSubmitStatusApproved &&
|
||||
!allPrvnStatusApproved;
|
||||
|
||||
const openReviewHistoryForRow = useCallback(
|
||||
async (row: Record<string, string | number>) => {
|
||||
@@ -282,7 +302,7 @@ const StatsInputStatusRvw = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
{(isLoading || isApproveLoading) && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
|
||||
/**
|
||||
* 하수도 통계 관리 > 02. 하수관로 (합계)
|
||||
* 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외.
|
||||
@@ -976,16 +978,18 @@ export const getST00020003Config = (statsYr: string | number) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const SewagePipeColumnInfoListTot = rawSewagePipeTotList.map((tab) => ({
|
||||
export const SewagePipeColumnInfoListTot: ColumnInfoList[] = rawSewagePipeTotList.map((tab) => ({
|
||||
...tab,
|
||||
columns: tab.columns.map((col) => ({ ...col, editable: false as const })),
|
||||
}));
|
||||
|
||||
SewagePipeColumnInfoListTot.splice(2, 0, {
|
||||
const st00020003PlaceholderTab: ColumnInfoList = {
|
||||
tabKey: "ST00020003",
|
||||
tabLabel: "관종별 현황",
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 2,
|
||||
columns: [],
|
||||
columnLayout: [],
|
||||
} as any);
|
||||
columns: [],
|
||||
columnLayout: [],
|
||||
};
|
||||
|
||||
SewagePipeColumnInfoListTot.splice(2, 0, st00020003PlaceholderTab);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
GroupUserService,
|
||||
GroupUserSearchParams,
|
||||
GroupUserListResponseDto,
|
||||
GroupUserDetailResponse,
|
||||
PageGroupUserList,
|
||||
GroupUserSaveData,
|
||||
GroupUserUpdateData,
|
||||
} from "../model/group-user-interface";
|
||||
|
||||
export class GroupUsersApiService implements GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
public async findPageGroupUser(
|
||||
params: GroupUserSearchParams,
|
||||
): Promise<PageGroupUserList> {
|
||||
const response = await client.get<PageGroupUserList>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
public async findByGroupAndUserId(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.get<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장
|
||||
public async saveGroupUser(
|
||||
data: GroupUserSaveData[],
|
||||
): Promise<GroupUserListResponseDto[]> {
|
||||
const response = await client.post<GroupUserListResponseDto[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정
|
||||
public async updateGroupUser(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.put<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
public async deleteGroupUser(
|
||||
groupId: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${groupId}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupUsersApi = new GroupUsersApiService();
|
||||
@@ -0,0 +1,5 @@
|
||||
export { GroupUsersApi } from "./api/group-user-service";
|
||||
export {
|
||||
type GroupUserListResponseDto,
|
||||
type GroupUserSaveData,
|
||||
} from "./model/group-user-interface";
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { GroupUsersApi } from "../api/group-user-service";
|
||||
|
||||
export const GRP_ST_INPUT = "GRP_ST_INPUT";
|
||||
export const GRP_ST_CONFIRM = "GRP_ST_CONFIRM";
|
||||
export const GRP_ADMIN = "GRP_ADMIN";
|
||||
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
/**
|
||||
* 지정한 그룹(`keyword`)마다 `findPageGroupUser`를 호출하고,
|
||||
* 응답 목록이 비어 있지 않으며 로그인 사용자 `userUnqId`와 일치하는 행이 있으면 true.
|
||||
*/
|
||||
export async function checkPageGroupUserMembershipForUser(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
groupKeywords: readonly string[],
|
||||
): Promise<boolean> {
|
||||
const uid = userUnqId.trim();
|
||||
const nm = userName.trim();
|
||||
if (!uid || !nm) return false;
|
||||
|
||||
const paramsBase = { userNm: nm, page: 0, size: PAGE_SIZE };
|
||||
|
||||
for (const keyword of groupKeywords) {
|
||||
const page = await GroupUsersApi.findPageGroupUser({
|
||||
...paramsBase,
|
||||
keyword,
|
||||
});
|
||||
const rows = page.content ?? [];
|
||||
if (rows.length > 0 && rows.some((r) => String(r.userUnqId ?? "").trim() === uid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 입력 화면 상단 액션 — `GRP_ST_INPUT` / `GRP_ADMIN`.
|
||||
*/
|
||||
export async function checkStatisticsInputToolbarGroupMembership(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_INPUT, GRP_ADMIN]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 입력 현황 승인 등 — `GRP_ST_CONFIRM` / `GRP_ADMIN`.
|
||||
*/
|
||||
export async function checkStatisticsInputStatusConfirmGroupMembership(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_CONFIRM, GRP_ADMIN]);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 사용자 검색 파라미터 인터페이스
|
||||
export interface GroupUserSearchParams extends PageableParams {
|
||||
keyword?: string;
|
||||
userNm?: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 목록 응답 인터페이스
|
||||
export interface GroupUserListResponseDto {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
mphn: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 페이지 응답 인터페이스
|
||||
export type PageGroupUserList = PageableList<GroupUserListResponseDto>;
|
||||
|
||||
// 그룹 사용자 상세 응답 인터페이스
|
||||
export interface GroupUserDetailResponse {
|
||||
grpSn: number;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
mphn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
rgtrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장 데이터 인터페이스
|
||||
export interface GroupUserSaveData {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정 데이터 인터페이스
|
||||
export interface GroupUserUpdateData {
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 서비스 인터페이스
|
||||
export interface GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
findPageGroupUser: (
|
||||
params: GroupUserSearchParams,
|
||||
) => Promise<PageGroupUserList>;
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
findByGroupAndUserId: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 저장
|
||||
saveGroupUser: (
|
||||
data: GroupUserSaveData[],
|
||||
) => Promise<GroupUserListResponseDto[]>;
|
||||
|
||||
// 그룹 사용자 수정
|
||||
updateGroupUser: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
deleteGroupUser: (groupId: string, userUnqId: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
type ReviewSubmitResponse,
|
||||
type SearchCondition,
|
||||
type Sgg,
|
||||
type StatiscicsManagementReviewHistoryRequest,
|
||||
type StatiscicsManagementReviewHistoryResponse,
|
||||
type StatisticsColumnGuideListResponse,
|
||||
type StatisticsLogDetailResponse,
|
||||
type StatisticsLogRequest,
|
||||
@@ -301,6 +303,14 @@ export class StatisticsApiService implements StatisticsService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise<StatiscicsManagementReviewHistoryResponse> {
|
||||
const response = await stClient.get<StatiscicsManagementReviewHistoryResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/stats-review`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsApi = new StatisticsApiService();
|
||||
|
||||
+5
-4
@@ -187,10 +187,11 @@ export interface PublicSewageMainFacilityMappingSaveRequest {
|
||||
admdstNm: string;
|
||||
mainFcltSn: string;
|
||||
mainFcltNm: string;
|
||||
prxyBzentyNmCn: string;
|
||||
cnsgnCstVl: number;
|
||||
wholNope: number;
|
||||
prxyCtrtCn: string;
|
||||
/** 매핑 추가(saveList 있음) 시 입력. 매핑 삭제만 할 때는 요청에서 생략 가능 */
|
||||
prxyBzentyNmCn?: string;
|
||||
cnsgnCstVl?: number;
|
||||
wholNope?: number;
|
||||
prxyCtrtCn?: string;
|
||||
userId: string;
|
||||
saveList: PublicSewageMainFacilityMappingSaveItem[];
|
||||
deleteList: string[];
|
||||
|
||||
@@ -393,8 +393,10 @@ export interface StatsInputStatusApproveBatchRequest {
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 API 응답 */
|
||||
export interface StatsInputStatusApproveResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
exceptionCnt: number;
|
||||
message: string;
|
||||
resultCode: string;
|
||||
updatedStts: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerifyExceptionItem {
|
||||
@@ -434,6 +436,32 @@ export interface ReviewerInfoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatiscicsManagementReviewHistoryRequest {
|
||||
statsYr: string;
|
||||
sewerBzmnCd?: string;
|
||||
ctpvNm?: string;
|
||||
statsCd?: string;
|
||||
}
|
||||
|
||||
/** 통계관리 검토 이력 조회 API `data` 배열 요소 */
|
||||
export interface StatiscicsManagementReviewHistoryItem {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
menuNm: string;
|
||||
wrterOpinionSm: string | null;
|
||||
chckerOpinionSm: string | null;
|
||||
returnOpinionSm: string | null;
|
||||
}
|
||||
|
||||
export interface StatiscicsManagementReviewHistoryResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: StatiscicsManagementReviewHistoryItem[];
|
||||
}
|
||||
|
||||
export interface StatisticsService {
|
||||
getSidoList(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>>;
|
||||
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
|
||||
@@ -461,4 +489,5 @@ export interface StatisticsService {
|
||||
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
|
||||
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
|
||||
getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse>;
|
||||
getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise<StatiscicsManagementReviewHistoryResponse>;
|
||||
}
|
||||
@@ -5,5 +5,9 @@ export { useDataTable } from "./use-data-table";
|
||||
export { useGlobalProgressStore } from "./use-global-progress-store";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export {
|
||||
useStatisticsInputStatusConfirmGroupGate,
|
||||
useStatisticsInputToolbarGroupGate,
|
||||
} from "./use-statistics-input-toolbar-group-gate";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
export { useSelectPrompt } from "./use-select-prompt";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
checkStatisticsInputStatusConfirmGroupMembership,
|
||||
checkStatisticsInputToolbarGroupMembership,
|
||||
} from "@/entities/group-user/lib/check-statistics-input-toolbar-group-membership";
|
||||
|
||||
import { useUserStore } from "./use-user-store";
|
||||
|
||||
/** 통계 입력 상단 툴바(저장·제출·검증결과 등) — 그룹 멤버십 만족 시에만 true */
|
||||
export function useStatisticsInputToolbarGroupGate(): boolean {
|
||||
const userInfo = useUserStore((s) => s.userInfo);
|
||||
const [allowed, setAllowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const name = userInfo?.userName?.trim() ?? "";
|
||||
const uid = userInfo?.userUnqId?.trim() ?? "";
|
||||
if (!name || !uid) {
|
||||
setAllowed(false);
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const ok = await checkStatisticsInputToolbarGroupMembership(name, uid);
|
||||
if (!cancelled) setAllowed(ok);
|
||||
} catch {
|
||||
if (!cancelled) setAllowed(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userInfo?.userName, userInfo?.userUnqId]);
|
||||
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 — `GRP_ST_CONFIRM` / `GRP_ADMIN` 멤버일 때만 true */
|
||||
export function useStatisticsInputStatusConfirmGroupGate(): boolean {
|
||||
const userInfo = useUserStore((s) => s.userInfo);
|
||||
const [allowed, setAllowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const name = userInfo?.userName?.trim() ?? "";
|
||||
const uid = userInfo?.userUnqId?.trim() ?? "";
|
||||
if (!name || !uid) {
|
||||
setAllowed(false);
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const ok = await checkStatisticsInputStatusConfirmGroupMembership(name, uid);
|
||||
if (!cancelled) setAllowed(ok);
|
||||
} catch {
|
||||
if (!cancelled) setAllowed(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userInfo?.userName, userInfo?.userUnqId]);
|
||||
|
||||
return allowed;
|
||||
}
|
||||
Reference in New Issue
Block a user