Merge iipbs/sn/dev_popup into iipbs/sn/dev2_kic
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_BASE_URL=
|
||||
+17
-8
@@ -1,21 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
CLAIM_NAME,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
PORT,
|
||||
PROXY_HOST,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
const linkBackendApiHost =
|
||||
process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;");
|
||||
response.headers.set(
|
||||
"Content-Security-Policy",
|
||||
"img-src 'self' data: https: blob:;",
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -66,7 +70,7 @@ export function middleware(req: NextRequest) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
|
||||
// /api/link 경로를 제거하고 백엔드 API 경로로 변환
|
||||
const apiPath = pathname.replace("/api/link", "");
|
||||
const queryParams = searchParams.toString();
|
||||
@@ -94,11 +98,16 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
// console.log(logMessages.join("\n"));
|
||||
|
||||
|
||||
const response = NextResponse.next();
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"],
|
||||
matcher: [
|
||||
"/server/:path*",
|
||||
"/attach-service/:path*",
|
||||
"/api/link/:path*",
|
||||
"/",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SERVER_API_URL } from "@/shared/config";
|
||||
import { TINY_BASE_URL } from "@/shared/config";
|
||||
|
||||
export const editorBaseConfig = {
|
||||
// 폐쇄망 환경에서는 API 키 없이 자체 호스팅 모드 사용
|
||||
@@ -7,7 +7,7 @@ export const editorBaseConfig = {
|
||||
licenseKey: "gpl",
|
||||
init: {
|
||||
language: "ko_KR",
|
||||
language_url: '/tinymce/langs/ko_KR.js', // 직접 호스팅한 경우 경로 지정
|
||||
language_url: "/tinymce/langs/ko_KR.js", // 직접 호스팅한 경우 경로 지정
|
||||
height: 500,
|
||||
// 폐쇄망 환경을 위한 설정
|
||||
base_url: "/tinymce",
|
||||
@@ -38,7 +38,7 @@ export const editorBaseConfig = {
|
||||
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
|
||||
"link image",
|
||||
],
|
||||
images_upload_base_path: SERVER_API_URL,
|
||||
images_upload_base_path: TINY_BASE_URL,
|
||||
file_picker_types: "image",
|
||||
// 폐쇄망 환경을 위한 추가 설정
|
||||
content_css: "/tinymce/skins/content/default/content.min.css",
|
||||
|
||||
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
|
||||
const { data: group } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||
enabled: !isRegistration,
|
||||
});
|
||||
|
||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||
@@ -92,8 +92,9 @@ const GroupsDialog = memo(
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) form.reset();
|
||||
}, [open]);
|
||||
// 그룹 등록 및 취소 시 폼 초기화
|
||||
if (isRegistration && !open) form.reset();
|
||||
}, [groupId, open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||
|
||||
@@ -16,7 +16,13 @@ type RoleSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
function RoleSelect({ mngrYn = "N", ...props }: RoleSelectProps) {
|
||||
const { data: roles } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES],
|
||||
queryFn: () => RoleApi.getRoles(),
|
||||
queryFn: () =>
|
||||
RoleApi.getRoles({
|
||||
keywordType: "useYn",
|
||||
keyword: "Y",
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
const options = roles?.content.filter((role) => role.mngrYn === mngrYn);
|
||||
|
||||
@@ -3,13 +3,8 @@ import { useState } from "react";
|
||||
import type { Role } from "@/entities/role";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
export const createColumns = ({
|
||||
deleteRole,
|
||||
}: {
|
||||
deleteRole: (roleId: string) => void;
|
||||
}): ColumnDef<Role>[] => [
|
||||
export const createColumns = (): ColumnDef<Role>[] => [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
@@ -44,18 +39,7 @@ export const createColumns = ({
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MappingRoleButton roleId={row.original.roleId} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={() => deleteRole(row.original.roleId)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -24,13 +24,6 @@ function ModifyPage({ roleId }: { roleId: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
||||
mutationFn: () => RoleApi.deleteRole(roleId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", () => router.push("/role"));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<RoleFormData>({
|
||||
resolver: zodResolver(roleFormSchema),
|
||||
values: {
|
||||
@@ -47,12 +40,7 @@ function ModifyPage({ roleId }: { roleId: string }) {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">권한 수정</h3>
|
||||
</div>
|
||||
<RoleForm
|
||||
roleId={roleId}
|
||||
form={form}
|
||||
handleSave={updateRole}
|
||||
handleDelete={deleteRole}
|
||||
/>
|
||||
<RoleForm roleId={roleId} form={form} handleSave={updateRole} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ type RoleFormProps = {
|
||||
roleId?: string;
|
||||
form: UseFormReturn<RoleFormData>;
|
||||
handleSave: (data: RoleFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
function RolePage() {
|
||||
@@ -16,6 +16,7 @@ function RolePage() {
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
@@ -34,13 +35,6 @@ function RolePage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
||||
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", query.refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRowClick = (row: Role) => {
|
||||
router.push(`/role/register/${row.roleId}`);
|
||||
};
|
||||
@@ -48,9 +42,7 @@ function RolePage() {
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns({
|
||||
deleteRole,
|
||||
}),
|
||||
columns: createColumns(),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const SERVER_API_URL = process.env.SERVER_API_URL;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
@@ -11,4 +11,5 @@ export const ACCESS_TOKEN =
|
||||
export const REFRESH_TOKEN =
|
||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
export const TINY_BASE_URL = process.env.NEXT_PUBLIC_TINY_BASE_URL;
|
||||
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://localhost:8883
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
* - 위경도로 강우시 미처리 하수 목록 조회: GET /api/v1/onsemiro/getSwgRainsByGeomety (sewerage-service)
|
||||
* - 강우시 월류 하수 관리 모니터링 조회: GET /api/v1/onsemiro/swgRain/{adstDstrCd}/{manageNo}/mntrngs (sewerage-service)
|
||||
*/
|
||||
import { UiModal, UiTable } from '@common/business_components/ui';
|
||||
'use client';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
@@ -21,21 +22,21 @@ import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const inputColumn = [
|
||||
{ id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] },
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
];
|
||||
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick, renderMode = 'inline' }: {
|
||||
featureData: any,
|
||||
isMainSsc?: boolean,
|
||||
chmbrXcnts?: string,
|
||||
chmbrYdnts?: string,
|
||||
onSaveClick?: () => void
|
||||
onSaveClick?: () => void,
|
||||
renderMode?: 'inline' | 'popup'
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [rainMeasureData, setRainMeasureData] = useState<any>({}); // 강우, 측정 정보
|
||||
const [mntrngsData, setMntrngsData] = useState<any[]>([]); // 수량/수질측정 정보
|
||||
@@ -151,14 +152,15 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
}
|
||||
}, [chmbrXcnts, chmbrYdnts]);
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
const openDetailPopup = () => {
|
||||
const popupWindow = openWindowPopup("sscDetail", {
|
||||
chmbrXcnts,
|
||||
chmbrYdnts,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
if (!isDataLoaded) {
|
||||
setTimeout(() => setIsDataLoaded(true), 100);
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,33 +294,24 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isMainSsc && (
|
||||
{isMainSsc && renderMode !== 'popup' && (
|
||||
<div>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
|
||||
</div>
|
||||
<Button
|
||||
className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600'
|
||||
onClick={openModal}
|
||||
onClick={openDetailPopup}
|
||||
>
|
||||
상세보기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-none
|
||||
max-h-none
|
||||
overflow-y-auto
|
||||
z-[101]
|
||||
`}
|
||||
body={renderModalBody()}
|
||||
noHeaderYn='Y'
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
{isMainSsc && renderMode === 'popup' && (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">중점우수토실 운영정보</h1>
|
||||
{renderModalBody()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
* - 조회 버튼을 통한 모달 표시 및 ReportTab 연동
|
||||
*/
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { UiModal } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import ReportTab from '../../searchModal/reportTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
// 하수처리시설
|
||||
const headList = [
|
||||
@@ -21,16 +20,26 @@ const headList = [
|
||||
];
|
||||
|
||||
const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const ctpvNm = gisObject?.ctpv_nm;
|
||||
const sggNm = gisObject?.sgg_nm;
|
||||
|
||||
const handleOpenReportPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VerticalTable
|
||||
children={{
|
||||
title: <Button className="gray" onClick={() => setIsModalOpen(true)}>조회</Button>
|
||||
title: <Button className="gray" onClick={handleOpenReportPopup}>조회</Button>
|
||||
}}
|
||||
headName='디지털 자료관 연계정보'
|
||||
tableData={[]}
|
||||
@@ -43,31 +52,8 @@ const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
/>
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle="하수도정비 기본계획보고서"
|
||||
headerTitleClass="text-3xl font-semibold text-gray-700"
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-[60vw]
|
||||
max-h-[100vh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={
|
||||
<ReportTab
|
||||
open={isModalOpen}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm}
|
||||
sggNm={sggNm}
|
||||
/>
|
||||
}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default SwtrmtFclt
|
||||
export default SwtrmtFclt
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TInftwtAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useInfilChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -28,7 +26,6 @@ type DataType = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore();
|
||||
|
||||
const [selectedAplcnYm, setSelectedAplcnYm] = useState('');
|
||||
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
|
||||
@@ -66,12 +63,6 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setInftwtQty('0');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState(); // 모달이 닫힐 때 리셋
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -276,21 +267,10 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1000px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">침입수 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TRdiiAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useRdiiChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -29,7 +27,6 @@ type LineBarChartData = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore();
|
||||
|
||||
const [aplcnYm, setAplcnYm] = useState('');
|
||||
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
||||
@@ -72,12 +69,6 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setTotalVolume('0');// 총체적 m³/mm 초기화
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState();
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -336,21 +327,10 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1100px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalStore {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useInfilChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
|
||||
export const useRdiiChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -34,14 +33,12 @@ import Stroke from "ol/style/Stroke.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import { getLayerContentComponent } from "../common";
|
||||
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import InfilChart from './chart/infilChart';
|
||||
import RdiiChart from './chart/rdiiChart';
|
||||
import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { searchUpperPpls } from "./smh";
|
||||
import { getElapsedYearsDisplayText, isInvalidInstallYmd } from "../agedPipe/agedPipeUtils";
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
@@ -74,12 +71,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
const { setIsOpen: setIsModalInfil } = useInfilChartModalStore();
|
||||
const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore();
|
||||
const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태
|
||||
const [isLoading, setIsLoading] = useState(false); // 로딩 상태
|
||||
const [chmbrXcnts, setChmbrXcnts] = useState<string>(''); // 로딩 상태
|
||||
@@ -469,16 +463,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -565,14 +552,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
|
||||
{layerNm === '하수관로' && (
|
||||
<>
|
||||
<Button onClick={() => setIsModalInfil(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("infilChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700"
|
||||
color="white"
|
||||
size="sm"
|
||||
>
|
||||
침입수분석
|
||||
</Button>
|
||||
<Button onClick={() => setIsModalRdii(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("rdiiChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
|
||||
color="white"
|
||||
size="sm"
|
||||
@@ -629,7 +628,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
@@ -642,9 +640,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<InfilChart pplMngNo={pplMngNo} />
|
||||
<RdiiChart pplMngNo={pplMngNo} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
type AgedPipeRow,
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
} from "./components/agedPipe/agedPipeUtils";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
|
||||
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
@@ -102,6 +103,7 @@ const Map = () => {
|
||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||
|
||||
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
||||
const [layerLoadError, setLayerLoadError] = useState<string | null>(null);
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState<any>(null); // 선택된 피쳐 데이터
|
||||
@@ -135,17 +137,56 @@ const Map = () => {
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
const handlePopupMessage = async (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
const data = event.data;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fidsRaw = data.payload?.fids;
|
||||
if (!layerNm || !fidsRaw || !core?.showMultipleSearchResultsFromGeoJSON) return;
|
||||
|
||||
let fids: string[] = [];
|
||||
try {
|
||||
fids = JSON.parse(fidsRaw);
|
||||
} catch (error) {
|
||||
console.error("다중 시설 메시지 파싱 실패:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handlePopupMessage);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("message", handlePopupMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(appendAgedPipeControlNode(data));
|
||||
setLayerLoadError(null);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
|
||||
throw new Error('Invalid JSON response from server');
|
||||
const message =
|
||||
error instanceof Error ? error.message : "레이어 설정 조회 중 알 수 없는 오류가 발생했습니다.";
|
||||
console.error("레이어 설정 조회 실패:", error);
|
||||
setTreeData([]);
|
||||
setLayerLoadError(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -488,6 +529,11 @@ const Map = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
|
||||
{layerLoadError ? (
|
||||
<div className="absolute left-4 top-4 z-[60] max-w-[520px] rounded border border-red-200 bg-white px-4 py-3 text-sm text-red-700 shadow">
|
||||
레이어 설정 조회 실패: {layerLoadError}
|
||||
</div>
|
||||
) : null}
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 (셀 클릭)
|
||||
*
|
||||
* 📄 Report Files Modal
|
||||
* Next.js App Router (app/) 기반 보고서 파일 모달 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 파일 목록 표시
|
||||
* - 파일명 및 등록일자 정보 테이블 출력
|
||||
* - 페이지네이션 및 로딩 오버레이 지원
|
||||
* - 파일 클릭 이벤트 처리 및 상세 조회
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 모달 열림/닫힘 시 데이터 초기화 기능
|
||||
*/
|
||||
'use client'
|
||||
import React, { useEffect } from 'react';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import useReportTab from './useReportTab';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { UiModal } from "@common/business_components";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호
|
||||
const ReportFilesModal: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
|
||||
// 하수도정비 기본계획 보고서 파일목록
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
const {
|
||||
curPage, setCurPage,
|
||||
isLoading,
|
||||
isResetPage,
|
||||
isResetSelection,
|
||||
setIsResetPage,
|
||||
setIsResetSelection
|
||||
} = useSearchModalStore();
|
||||
const {
|
||||
isOpen: isModalOpen,
|
||||
files,
|
||||
dcFiles: filesOrigin,
|
||||
setIsOpen: setIsModalOpen,
|
||||
setFiles,
|
||||
fetchDcFiles
|
||||
} = useReportTabStore();
|
||||
const { getFiles, handleCellClick } = useReportTab();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcFiles(clsfSystmSn);
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen === true) {
|
||||
getFiles(); // 파일 목록 조회
|
||||
} else {
|
||||
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}
|
||||
}, [filesOrigin, isModalOpen, curPage]);
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={(() => {
|
||||
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
|
||||
})()
|
||||
}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={filesOrigin.length}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
min-h-[50vh] max-h-[90dvh]
|
||||
max-w-[750px]
|
||||
h-auto w-auto
|
||||
overflow-auto
|
||||
z-[5000]
|
||||
flex items-start
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportFilesModal);
|
||||
@@ -22,7 +22,7 @@ import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import ReportFilesModal from './reportFilesModal'; // 파일목록모달
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
@@ -56,7 +56,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
const [ctpvCd, setCtpvCd] = useState('0');
|
||||
const [sggCd, setSggCd] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState("0");
|
||||
const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호
|
||||
|
||||
const {
|
||||
tableData,
|
||||
@@ -72,8 +71,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
const {
|
||||
isOpen: isReportFilesModalOpen,
|
||||
setIsOpen: setIsReportFilesModalOpen,
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore(); // 모달 상태 관리 스토어
|
||||
@@ -139,8 +136,14 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
if (typeof index === "number" && tableData[index]) {
|
||||
const reportData = tableData[index];
|
||||
if (reportData.fileSn !== "") {
|
||||
setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정
|
||||
setIsReportFilesModalOpen(true); // 모달 열기
|
||||
const popupWindow = openWindowPopup("reportFiles", {
|
||||
clsfSystmSn: reportData.clsfSystmSn,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -263,10 +266,8 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 모달 컴포넌트 */}
|
||||
{isReportFilesModalOpen && <ReportFilesModal clsfSystmSn={clsfSystmSn} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportTab);
|
||||
export default React.memo(ReportTab);
|
||||
|
||||
@@ -11,21 +11,26 @@
|
||||
* - Zustand를 통한 검색 모달 상태 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 지원
|
||||
*/
|
||||
'use client';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { UiModal, UITab, UiTable } from '@common/business_components/ui';
|
||||
import { UITab, UiTable } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const SearchModal: React.FC = () => {
|
||||
type SearchModalProps = {
|
||||
renderMode?: "launcher" | "popup";
|
||||
};
|
||||
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher" }) => {
|
||||
const {
|
||||
showTable,
|
||||
isSearchModalOpen, setIsSearchModalOpen,
|
||||
@@ -59,6 +64,7 @@ const SearchModal: React.FC = () => {
|
||||
operationResultExcelDownload,
|
||||
handleOperationPageNation
|
||||
} = useOperationTab();
|
||||
const hasAutoOpenedReportRef = useRef(false);
|
||||
|
||||
|
||||
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
|
||||
@@ -87,6 +93,42 @@ const SearchModal: React.FC = () => {
|
||||
return [];
|
||||
}, [activeTab, workType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderMode !== "launcher" || !isSearchModalOpen) return;
|
||||
|
||||
const popupWindow = openWindowPopup("mapSearch", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
|
||||
setIsSearchModalOpen(false);
|
||||
}, [renderMode, isSearchModalOpen]);
|
||||
|
||||
const openReportTabPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂.');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'report') {
|
||||
hasAutoOpenedReportRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAutoOpenedReportRef.current) return;
|
||||
|
||||
openReportTabPopup();
|
||||
hasAutoOpenedReportRef.current = true;
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [activeTab, basicType, workType]);
|
||||
@@ -119,7 +161,23 @@ const SearchModal: React.FC = () => {
|
||||
/>
|
||||
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
|
||||
{activeTab === 'operation' && (<OperationTab />)} {/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'report' && (<ReportTab open={isSearchModalOpen} />)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
{activeTab === 'report' && (
|
||||
<div className="mt-4 flex h-[79dvh] w-full items-center justify-center rounded-lg border bg-white p-6">
|
||||
<div className="flex max-w-[520px] flex-col items-center text-center">
|
||||
<h2 className="mb-3 text-2xl font-semibold text-gray-900">
|
||||
하수도정비 기본계획보고서
|
||||
</h2>
|
||||
<p className="mb-5 text-lg leading-8 text-gray-600">
|
||||
보고서 상세는 모달 내부 탭 대신
|
||||
<br />
|
||||
전용 윈도우 팝업으로 열리도록 전환되었습니다.
|
||||
</p>
|
||||
<Button className="px-6 py-2 text-white" onClick={openReportTabPopup}>
|
||||
전용 팝업 열기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
|
||||
|
||||
{/* 📋 결과 리스트 */}
|
||||
@@ -214,22 +272,16 @@ const SearchModal: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
if (renderMode === "launcher") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto overflow-y-hidden'>
|
||||
<UiModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[1220px]
|
||||
max-h-[970px]
|
||||
overflow-y-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={renderBody()}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
<div className='h-dvh overflow-x-auto overflow-y-hidden bg-white p-4'>
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">관망도 검색</h1>
|
||||
<div className="h-[calc(100dvh-5.6rem)] w-full overflow-y-hidden rounded-lg border bg-white p-4">
|
||||
{renderBody()}
|
||||
</div>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,13 @@ import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
|
||||
import { swageMhlCqlFilters } from './swageMhl';
|
||||
import { swagePmstnCqlFilters } from './swagePmstn';
|
||||
import { stofcbrCqlFilters } from './stofcbr';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useBasicTab() {
|
||||
@@ -115,9 +122,21 @@ export default function useBasicTab() {
|
||||
// 기본정보 테이블 cellClick 핸들러
|
||||
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = tableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
@@ -23,6 +23,13 @@ import { confirmToast } from '@/components/ui/toast'
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import useStore from '@/store/useStore';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useOperationTab() {
|
||||
@@ -200,9 +207,21 @@ export default function useOperationTab() {
|
||||
// 유지관리 정보 디테일 테이블 cellClick 핸들러
|
||||
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = detailTableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
@@ -434,6 +453,20 @@ export default function useOperationTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const emitMultipleFeaturesToMap = () => {
|
||||
if (detailTableData.length === 0 || !isPopupWindow()) return false;
|
||||
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
WORK_TYPES,
|
||||
@@ -446,7 +479,8 @@ export default function useOperationTab() {
|
||||
handleOperationCellClick,
|
||||
handleOperationResultCellClick,
|
||||
handleOperationPageNation,
|
||||
emitMultipleFeaturesToMap,
|
||||
operationExcelDownload,
|
||||
operationResultExcelDownload
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,13 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import useSearchModalStore from '../../../store/useSearchModalStore';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
export default function useSearchModal() {
|
||||
const {
|
||||
@@ -132,6 +139,18 @@ export default function useSearchModal() {
|
||||
const viewMultipleFeature = async () => {
|
||||
if (activeTab === 'basic') {
|
||||
if (!(tableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = tableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(tableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[0];
|
||||
@@ -144,6 +163,18 @@ export default function useSearchModal() {
|
||||
}
|
||||
} else if (activeTab === 'operation') {
|
||||
if (!(detailTableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[0];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function PopupLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-dvh bg-white text-foreground">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isWindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { windowPopupRegistry } from "@/features/windowPopup/registry";
|
||||
|
||||
type PopupPageProps = {
|
||||
params: Promise<{
|
||||
popupKey: string;
|
||||
}>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
};
|
||||
|
||||
export default async function PopupPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PopupPageProps) {
|
||||
const { popupKey } = await params;
|
||||
const resolvedSearchParams = await searchParams;
|
||||
|
||||
if (!isWindowPopupKey(popupKey)) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">등록되지 않은 팝업입니다.</h1>
|
||||
<p className="text-base text-gray-600">popupKey: {popupKey}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const PopupComponent = windowPopupRegistry[popupKey].component;
|
||||
|
||||
return <PopupComponent searchParams={resolvedSearchParams} />;
|
||||
}
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlist } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -51,8 +49,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1) //현재 페이지
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -67,6 +63,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -140,27 +147,24 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 현황 등록
|
||||
const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileName
|
||||
}
|
||||
const data = await postData('v1/sbmrgPrevnt', json)
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsUploadModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("floodPreUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("floodPreTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +250,7 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
@@ -254,40 +258,8 @@ const Page = () => {
|
||||
resetPageNo={isResetPage}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isUploadModalOpen}
|
||||
onOpenChange={setIsUploadModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='도시침수 예방사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -55,6 +55,12 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
@@ -110,6 +116,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
const filteredResultData = filterEmptyRows(resultData);
|
||||
setResultTableData(filteredResultData);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('FloodPre attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlistOldPp } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -49,8 +47,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1)
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isModalOpen, setIsModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -65,6 +61,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -137,27 +144,25 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 등록
|
||||
const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm
|
||||
}
|
||||
const data = await postData('v1/dcrpPpl', json);
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("oldPpUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsModalOpen(true);
|
||||
// 엑셀 데이터는 브라우저 윈도우 팝업에서 독립 조회
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("oldPpTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,47 +247,15 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
resetSelection={resetSelection}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='노후 하수관로 정비사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -64,6 +64,12 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
@@ -242,6 +248,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
}));
|
||||
setTableData6(filteredData6);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('OldPp attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
|
||||
@@ -153,7 +153,10 @@ export const getRawJson = async (url: string): Promise<any> => {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch data: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length');
|
||||
@@ -170,7 +173,7 @@ export const getRawJson = async (url: string): Promise<any> => {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
throw new Error('Invalid JSON response from server');
|
||||
throw new Error(`Invalid JSON response from server: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,7 +187,10 @@ export const getBlobData = async (url: string): Promise<Blob> => {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`,
|
||||
);
|
||||
}
|
||||
|
||||
return res.blob();
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 📄 Modal Component
|
||||
* 모달 컴포넌트
|
||||
*
|
||||
* - 모달 열기/닫기 상태 관리
|
||||
* - 제목 및 내용 표시
|
||||
* - 저장 및 닫기 버튼 지원
|
||||
* - 반응형 크기 조정 (최대/최소 너비 및 높이)
|
||||
* - 오버레이 및 z-index를 통한 최상위 레이어 배치
|
||||
* - 접근성 지원 (aria-describedby)
|
||||
*/
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogOverlay } from '@common/components/ui'
|
||||
import React from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean;
|
||||
setIsModalOpen: (open: boolean) => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
closeModal: () => void;
|
||||
saveModal?: () => void;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal, saveModal }) => {
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogOverlay className='z-[200]' />
|
||||
<DialogContent className="w-auto max-w-[1200px] min-w-[400px] min-h-[150px] h-auto max-h-[450px] overflow-auto z-[1000]" aria-describedby={undefined}>
|
||||
{title ? <DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader> : null}
|
||||
{children}
|
||||
<DialogFooter>
|
||||
<div className='flex justify-end gap-2'>
|
||||
{saveModal ? <button className="px-4 py-2 bg-primary text-white rounded" onClick={saveModal}>저장</button> : null}
|
||||
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
+4
-14
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -74,7 +73,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
@@ -459,18 +457,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
};
|
||||
}, [featureData, gisMap]);
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
// Use the browser confirm dialog for lightweight save confirmation.
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
void btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -617,7 +608,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
|
||||
+18
-2
@@ -12,7 +12,11 @@
|
||||
* - 앱 전체의 공통 스타일 및 스크립트 적용
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
import { cookies } from "next/headers";
|
||||
import { AppProvider } from "./providers";
|
||||
import "../src/app/styles/base.css";
|
||||
import "../src/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 하수관로",
|
||||
@@ -24,4 +28,16 @@ export const metadata: Metadata = {
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { AppProvider } from "../src/app/providers";
|
||||
@@ -1,7 +1,3 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export { openWindowPopup } from "./lib/openWindowPopup";
|
||||
export {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "./model/popupDefinitions";
|
||||
export { windowPopupRegistry } from "./registry";
|
||||
export type {
|
||||
OpenWindowPopupParams,
|
||||
WindowPopupContentProps,
|
||||
WindowPopupMessageType,
|
||||
WindowPopupSearchParams,
|
||||
} from "./model/types";
|
||||
export { WINDOW_POPUP_MESSAGE_TYPE } from "./model/types";
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "../model/popupDefinitions";
|
||||
import type { OpenWindowPopupParams } from "../model/types";
|
||||
|
||||
const buildQueryString = (params: OpenWindowPopupParams = {}) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === null || value === undefined || value === "") return;
|
||||
searchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
return queryString ? `?${queryString}` : "";
|
||||
};
|
||||
|
||||
const buildPopupFeatures = (width: number, height: number) => {
|
||||
const screenLeft = window.screenX ?? window.screenLeft ?? 0;
|
||||
const screenTop = window.screenY ?? window.screenTop ?? 0;
|
||||
const outerWidth = window.outerWidth || window.screen.width;
|
||||
const outerHeight = window.outerHeight || window.screen.height;
|
||||
const left = Math.max(0, screenLeft + Math.round((outerWidth - width) / 2));
|
||||
const top = Math.max(0, screenTop + Math.round((outerHeight - height) / 2));
|
||||
|
||||
return [
|
||||
`width=${width}`,
|
||||
`height=${height}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
"resizable=yes",
|
||||
"scrollbars=yes",
|
||||
"status=no",
|
||||
"location=no",
|
||||
"menubar=no",
|
||||
"toolbar=no",
|
||||
].join(",");
|
||||
};
|
||||
|
||||
export const openWindowPopup = (
|
||||
popupKey: WindowPopupKey,
|
||||
params?: OpenWindowPopupParams,
|
||||
) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!isWindowPopupKey(popupKey)) return null;
|
||||
|
||||
const definition = windowPopupDefinitions[popupKey];
|
||||
const popupUrl = `/popup/${popupKey}${buildQueryString(params)}`;
|
||||
const popupFeatures = buildPopupFeatures(definition.width, definition.height);
|
||||
const popupWindow = window.open(
|
||||
popupUrl,
|
||||
`sn-popup-${popupKey}`,
|
||||
popupFeatures,
|
||||
);
|
||||
|
||||
popupWindow?.focus();
|
||||
|
||||
return popupWindow;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { WindowPopupMessageType } from "../model/types";
|
||||
|
||||
export const postPopupMessage = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
if (typeof window === "undefined" || !window.opener) return;
|
||||
|
||||
window.opener.postMessage(
|
||||
{
|
||||
type,
|
||||
payload,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
export const postPopupMessageAndClose = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
postPopupMessage(type, payload);
|
||||
window.close();
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { WindowPopupDefinition } from "./types";
|
||||
|
||||
export const windowPopupDefinitions = {
|
||||
oldPpUpload: {
|
||||
key: "oldPpUpload",
|
||||
title: "노후 하수관로 정비사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
oldPpTable: {
|
||||
key: "oldPpTable",
|
||||
title: "노후 하수관로 정비사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
floodPreUpload: {
|
||||
key: "floodPreUpload",
|
||||
title: "도시침수 예방사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
floodPreTable: {
|
||||
key: "floodPreTable",
|
||||
title: "도시침수 예방사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
reportTab: {
|
||||
key: "reportTab",
|
||||
title: "하수도정비 기본계획보고서",
|
||||
width: 1280,
|
||||
height: 920,
|
||||
},
|
||||
reportFiles: {
|
||||
key: "reportFiles",
|
||||
title: "보고서 파일 목록",
|
||||
width: 860,
|
||||
height: 720,
|
||||
requiredParams: ["clsfSystmSn"],
|
||||
},
|
||||
mapSearch: {
|
||||
key: "mapSearch",
|
||||
title: "관망도 검색",
|
||||
width: 1280,
|
||||
height: 980,
|
||||
},
|
||||
infilChart: {
|
||||
key: "infilChart",
|
||||
title: "침입수 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
rdiiChart: {
|
||||
key: "rdiiChart",
|
||||
title: "RDII 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
sscDetail: {
|
||||
key: "sscDetail",
|
||||
title: "중점우수토실 운영정보",
|
||||
width: 1180,
|
||||
height: 980,
|
||||
requiredParams: ["chmbrXcnts", "chmbrYdnts"],
|
||||
},
|
||||
} as const satisfies Record<string, WindowPopupDefinition>;
|
||||
|
||||
export type WindowPopupKey = keyof typeof windowPopupDefinitions;
|
||||
|
||||
export const isWindowPopupKey = (key: string): key is WindowPopupKey => {
|
||||
return key in windowPopupDefinitions;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export type WindowPopupSearchParams = Record<
|
||||
string,
|
||||
string | string[] | undefined
|
||||
>;
|
||||
|
||||
export type WindowPopupContentProps = {
|
||||
searchParams: WindowPopupSearchParams;
|
||||
};
|
||||
|
||||
export type WindowPopupDefinition = {
|
||||
key: string;
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
requiredParams?: string[];
|
||||
};
|
||||
|
||||
export type WindowPopupRegistryItem = WindowPopupDefinition & {
|
||||
component: ComponentType<WindowPopupContentProps>;
|
||||
};
|
||||
|
||||
export type OpenWindowPopupParams = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
export const WINDOW_POPUP_MESSAGE_TYPE = {
|
||||
OLD_PP_UPLOAD_COMPLETED: "sn:popup:oldPpUploadCompleted",
|
||||
FLOOD_PRE_UPLOAD_COMPLETED: "sn:popup:floodPreUploadCompleted",
|
||||
MAP_SEARCH_SELECT_FEATURE: "sn:popup:mapSearchSelectFeature",
|
||||
MAP_SEARCH_SHOW_MULTIPLE: "sn:popup:mapSearchShowMultiple",
|
||||
} as const;
|
||||
|
||||
export type WindowPopupMessageType =
|
||||
(typeof WINDOW_POPUP_MESSAGE_TYPE)[keyof typeof WINDOW_POPUP_MESSAGE_TYPE];
|
||||
@@ -0,0 +1,55 @@
|
||||
import { windowPopupDefinitions, type WindowPopupKey } from "./model/popupDefinitions";
|
||||
import type { WindowPopupRegistryItem } from "./model/types";
|
||||
import FloodPreTablePopupContent from "./ui/FloodPreTablePopupContent";
|
||||
import FloodPreUploadPopupContent from "./ui/FloodPreUploadPopupContent";
|
||||
import InfilChartPopupContent from "./ui/InfilChartPopupContent";
|
||||
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
||||
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
||||
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
||||
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
||||
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
||||
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
||||
import SscDetailPopupContent from "./ui/SscDetailPopupContent";
|
||||
|
||||
export const windowPopupRegistry = {
|
||||
oldPpUpload: {
|
||||
...windowPopupDefinitions.oldPpUpload,
|
||||
component: OldPpUploadPopupContent,
|
||||
},
|
||||
oldPpTable: {
|
||||
...windowPopupDefinitions.oldPpTable,
|
||||
component: OldPpTablePopupContent,
|
||||
},
|
||||
floodPreUpload: {
|
||||
...windowPopupDefinitions.floodPreUpload,
|
||||
component: FloodPreUploadPopupContent,
|
||||
},
|
||||
floodPreTable: {
|
||||
...windowPopupDefinitions.floodPreTable,
|
||||
component: FloodPreTablePopupContent,
|
||||
},
|
||||
reportTab: {
|
||||
...windowPopupDefinitions.reportTab,
|
||||
component: ReportTabPopupContent,
|
||||
},
|
||||
reportFiles: {
|
||||
...windowPopupDefinitions.reportFiles,
|
||||
component: ReportFilesPopupContent,
|
||||
},
|
||||
mapSearch: {
|
||||
...windowPopupDefinitions.mapSearch,
|
||||
component: SearchModalPopupContent,
|
||||
},
|
||||
infilChart: {
|
||||
...windowPopupDefinitions.infilChart,
|
||||
component: InfilChartPopupContent,
|
||||
},
|
||||
rdiiChart: {
|
||||
...windowPopupDefinitions.rdiiChart,
|
||||
component: RdiiChartPopupContent,
|
||||
},
|
||||
sscDetail: {
|
||||
...windowPopupDefinitions.sscDetail,
|
||||
component: SscDetailPopupContent,
|
||||
},
|
||||
} as const satisfies Record<WindowPopupKey, WindowPopupRegistryItem>;
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/floodPre/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function FloodPreTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.floodPreTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.floodPreTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function FloodPreUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/sbmrgPrevnt", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.floodPreUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import InfilChart from "@/app/(map-layout)/map/components/popupComponent/chart/infilChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function InfilChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.infilChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfilChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/oldPp/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function OldPpTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.oldPpTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.oldPpTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function OldPpUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/dcrpPpl", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.oldPpUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function PopupMissingParams({
|
||||
title,
|
||||
missingParams,
|
||||
}: {
|
||||
title: string;
|
||||
missingParams: string[];
|
||||
}) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">{title}</h1>
|
||||
<p className="text-base text-gray-600">
|
||||
팝업을 여는 데 필요한 값이 누락되었습니다.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
누락된 값: {missingParams.join(", ")}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import RdiiChart from "@/app/(map-layout)/map/components/popupComponent/chart/rdiiChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function RdiiChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.rdiiChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <RdiiChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { getDcData } from "@/components/lib/api";
|
||||
import { UiTable } from "@common/business_components/ui";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
type ReportDataFile = {
|
||||
no?: number;
|
||||
fileTitle: string;
|
||||
regDt: string;
|
||||
fileSn: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
export default function ReportFilesPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const clsfSystmSn = getPopupParamValue(searchParams.clsfSystmSn);
|
||||
const [files, setFiles] = useState<ReportDataFile[]>([]);
|
||||
const [allFiles, setAllFiles] = useState<ReportDataFile[]>([]);
|
||||
const [curPage, setCurPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clsfSystmSn) return;
|
||||
|
||||
const fetchFiles = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getDcData(`/api/v1/file?clsfSystmSn=${clsfSystmSn}`);
|
||||
const normalized = (result ?? []).map((item: ReportDataFile, index: number) => ({
|
||||
...item,
|
||||
no: index + 1,
|
||||
}));
|
||||
setAllFiles(normalized);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 목록 조회 실패:", error);
|
||||
setAllFiles([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFiles();
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
const paged = allFiles.slice((curPage - 1) * 10, curPage * 10);
|
||||
setFiles(paged);
|
||||
}, [allFiles, curPage]);
|
||||
|
||||
const handleDownload = async (fileSn: string, fileTitle: string, filePath: string) => {
|
||||
if (!filePath) {
|
||||
alert("filePath 정보가 없으므로 다운로드가 불가합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const responseBlob = await getDcData(
|
||||
`/api/v1/fileDownload?filePath=${encodeURIComponent(filePath)}`,
|
||||
"blob",
|
||||
);
|
||||
|
||||
const objectUrl = URL.createObjectURL(responseBlob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = fileTitle;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 다운로드 실패:", error);
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
if (!clsfSystmSn) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.reportFiles.title}
|
||||
missingParams={["clsfSystmSn"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportFiles.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={`조회 결과 (${allFiles.length.toLocaleString("ko-KR")})건`}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={allFiles.length}
|
||||
onPageChange={setCurPage}
|
||||
cellClick={(index) => {
|
||||
if (typeof index !== "number" || !files[index]) return;
|
||||
const target = files[index];
|
||||
handleDownload(target.fileSn, target.fileTitle, target.filePath);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ReportTab from "@/app/(map-layout)/map/searchModal/reportTab";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function ReportTabPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const ctpvNm = getPopupParamValue(searchParams.ctpvNm);
|
||||
const sggNm = getPopupParamValue(searchParams.sggNm);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportTab.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<ReportTab
|
||||
open={true}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm || undefined}
|
||||
sggNm={sggNm || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SearchModal from "@/app/(map-layout)/map/searchModal/searchModal";
|
||||
|
||||
export default function SearchModalPopupContent() {
|
||||
return <SearchModal renderMode="popup" />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Ssc from "@/app/(map-layout)/map/components/popupComponent/Ssc";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function SscDetailPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const chmbrXcnts = getPopupParamValue(searchParams.chmbrXcnts);
|
||||
const chmbrYdnts = getPopupParamValue(searchParams.chmbrYdnts);
|
||||
|
||||
const missingParams = [
|
||||
!chmbrXcnts ? "chmbrXcnts" : "",
|
||||
!chmbrYdnts ? "chmbrYdnts" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.sscDetail.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Ssc
|
||||
featureData={null}
|
||||
isMainSsc={true}
|
||||
chmbrXcnts={chmbrXcnts}
|
||||
chmbrYdnts={chmbrYdnts}
|
||||
renderMode="popup"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const getPopupParamValue = (
|
||||
value: string | string[] | undefined,
|
||||
): string => {
|
||||
if (Array.isArray(value)) return value[0] ?? "";
|
||||
return value ?? "";
|
||||
};
|
||||
+74
-74
@@ -1,76 +1,76 @@
|
||||
{
|
||||
"name": "st",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@fullcalendar/core": "^6.1.17",
|
||||
"@fullcalendar/daygrid": "^6.1.17",
|
||||
"@fullcalendar/interaction": "^6.1.17",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"realgrid": "^2.9.5",
|
||||
"realgrid-react": "^0.9.7",
|
||||
"remixicon": "^4.7.0",
|
||||
"sonner": "^2.0.1",
|
||||
"swr": "^2.3.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"name": "st",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@fullcalendar/core": "^6.1.17",
|
||||
"@fullcalendar/daygrid": "^6.1.17",
|
||||
"@fullcalendar/interaction": "^6.1.17",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"realgrid": "^2.9.5",
|
||||
"realgrid-react": "^0.9.7",
|
||||
"remixicon": "^4.7.0",
|
||||
"sonner": "^2.0.1",
|
||||
"swr": "^2.3.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
|
||||
import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
|
||||
|
||||
@@ -36,6 +37,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
<SelectPromptDialog />
|
||||
<GlobalProgressOverlay />
|
||||
<Toaster position="top-center" />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface ReviewOpinionButtonProps {
|
||||
reviewOpinionFunc: () => void;
|
||||
reviewOpinionFunc: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ReviewOpinionButton = ({ reviewOpinionFunc }: ReviewOpinionButtonProps) => {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface VerificationResultButtonProps {
|
||||
VerificationResultButtonFunc: () => void;
|
||||
}
|
||||
|
||||
const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => {
|
||||
return (
|
||||
<Button color="white" className="h-[3.2rem]" onClick={VerificationResultButtonFunc}>
|
||||
<i className="diveicon di-list" />
|
||||
<span>지난 검증 결과</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationResultButton;
|
||||
@@ -16,12 +16,6 @@
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
.rg-grid .rg-header .rg-table tr td,
|
||||
.rg-grid .rg-head .rg-table tr td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-white;
|
||||
}
|
||||
@@ -138,4 +132,10 @@
|
||||
.rg-grid .rg-body .rg-table tr.rg-alternate-row td,
|
||||
.rg-grid .rg-body .rg-table tr.rg-alternate-row .rg-data-cell {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.rg-grid .rg-header .rg-table tr td,
|
||||
.rg-grid .rg-head .rg-table tr td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
@@ -311,6 +311,7 @@ function buildFieldsFromColumns(
|
||||
const DataGrid = ({
|
||||
columnInfo,
|
||||
rowStartNum,
|
||||
excelExportFileSuffix,
|
||||
isExcelImport = true,
|
||||
isExcelExport = true,
|
||||
fixedColumns,
|
||||
@@ -318,13 +319,19 @@ const DataGrid = ({
|
||||
rowData,
|
||||
setRowData,
|
||||
getGridDataRef,
|
||||
getGridRef,
|
||||
cellStyleCallback,
|
||||
cellTooltipCallback,
|
||||
onCellEdited,
|
||||
onRowClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
onScrollToBottom,
|
||||
statusInfo,
|
||||
highlightedRowId,
|
||||
idFieldName = "id",
|
||||
enableSort = false,
|
||||
gridHeight = 65, // vh 단위
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
@@ -340,9 +347,20 @@ const DataGrid = ({
|
||||
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
|
||||
cellTooltipCallback,
|
||||
);
|
||||
const onCellEditedRef = useRef<typeof onCellEdited>(onCellEdited);
|
||||
const onScrollToBottomRef = useRef<(() => void) | undefined>(
|
||||
onScrollToBottom,
|
||||
);
|
||||
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
|
||||
const onItemAllCheckedRef =
|
||||
useRef<typeof onItemAllChecked>(onItemAllChecked);
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
cellTooltipCallbackRef.current = cellTooltipCallback;
|
||||
onScrollToBottomRef.current = onScrollToBottom;
|
||||
onItemCheckedRef.current = onItemChecked;
|
||||
onItemAllCheckedRef.current = onItemAllChecked;
|
||||
onCellEditedRef.current = onCellEdited;
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
@@ -470,10 +488,14 @@ const DataGrid = ({
|
||||
gridView.setColumnLayout?.(normalizedLayout);
|
||||
}
|
||||
|
||||
const exportBaseName = excelExportFileSuffix
|
||||
? `${menuName}_${excelExportFileSuffix}`
|
||||
: menuName;
|
||||
|
||||
gridView.exportGrid({
|
||||
type: "excel",
|
||||
target: "local",
|
||||
fileName: `${menuName}.xlsx`,
|
||||
fileName: `${exportBaseName}.xlsx`,
|
||||
footer: "hidden",
|
||||
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
|
||||
done: () => {
|
||||
@@ -558,10 +580,10 @@ const DataGrid = ({
|
||||
) {
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
];
|
||||
if (
|
||||
cell &&
|
||||
@@ -637,7 +659,7 @@ const DataGrid = ({
|
||||
const col = range.s.c + colIndex;
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
@@ -761,13 +783,13 @@ const DataGrid = ({
|
||||
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
|
||||
(gridView.displayOptions as DisplayOptions).fitStyle =
|
||||
GridFitStyle.EVEN;
|
||||
@@ -937,7 +959,7 @@ const DataGrid = ({
|
||||
) {
|
||||
(
|
||||
column.footer[
|
||||
index
|
||||
index
|
||||
] as FooterItemWithCallback
|
||||
).valueCallback =
|
||||
footerItem.valueCallback;
|
||||
@@ -990,11 +1012,11 @@ const DataGrid = ({
|
||||
getGridDataRef.current = () => {
|
||||
const gv = gridRef.current?.gridView as
|
||||
| {
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
@@ -1090,6 +1112,74 @@ const DataGrid = ({
|
||||
};
|
||||
}, [onRowClick, cellTooltipCallback, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || !onScrollToBottom) return;
|
||||
|
||||
gridView.onScrollToBottom = () => {
|
||||
onScrollToBottomRef.current?.();
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (gridView.onScrollToBottom)
|
||||
gridView.onScrollToBottom = undefined;
|
||||
};
|
||||
}, [onScrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
if (onItemChecked) {
|
||||
gridView.onItemChecked = (_grid, itemIndex, checked) => {
|
||||
onItemCheckedRef.current?.(itemIndex, checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemChecked = undefined;
|
||||
}
|
||||
|
||||
if (onItemAllChecked) {
|
||||
gridView.onItemAllChecked = (_grid, checked) => {
|
||||
onItemAllCheckedRef.current?.(checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemAllChecked = undefined;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
|
||||
if (gridView.onItemAllChecked)
|
||||
gridView.onItemAllChecked = undefined;
|
||||
};
|
||||
}, [onItemChecked, onItemAllChecked]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
gridView.onCellEdited = (grid, itemIndex, _row, field) => {
|
||||
const fieldName = columns[field]?.fieldName;
|
||||
if (!fieldName) return;
|
||||
|
||||
const newValue = grid.getValue(itemIndex, fieldName);
|
||||
const prevRow = rowDataRef.current[itemIndex];
|
||||
const oldValue = prevRow?.[fieldName] ?? "";
|
||||
|
||||
onCellEditedRef.current?.({
|
||||
itemIndex,
|
||||
fieldName,
|
||||
oldValue,
|
||||
newValue,
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (gridView.onCellEdited) {
|
||||
gridView.onCellEdited = undefined;
|
||||
}
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
|
||||
<input
|
||||
@@ -1105,23 +1195,23 @@ const DataGrid = ({
|
||||
<span>
|
||||
진행상황:{" "}
|
||||
{statusInfo.laeSbmsnSttsCd === null ||
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
? "미작성"
|
||||
: statusInfo.laeSbmsnSttsCd === "1"
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
</span>
|
||||
<span>
|
||||
제출일:{" "}
|
||||
{statusInfo.laeSbmsnYmd
|
||||
? statusInfo.laeSbmsnYmd.length === 8 &&
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
|
||||
: statusInfo.laeSbmsnYmd
|
||||
: "미제출"}
|
||||
@@ -1159,7 +1249,7 @@ const DataGrid = ({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[65vh]">
|
||||
<div style={{ height: `${gridHeight}vh` }}>
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
@@ -1176,33 +1266,33 @@ const DataGrid = ({
|
||||
) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
const dataProvider = gridRef.current
|
||||
?.dataProvider as unknown as
|
||||
| {
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(
|
||||
buildFieldsFromColumns(columns),
|
||||
);
|
||||
}
|
||||
if (dataProvider?.setRows && rowData?.length) {
|
||||
if (dataProvider?.setRows)
|
||||
dataProvider.setRows(rowData);
|
||||
}
|
||||
if (getGridRef) getGridRef.current = gridRef.current;
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
@@ -1346,14 +1436,14 @@ const DataGrid = ({
|
||||
})}
|
||||
footer={
|
||||
column.footer &&
|
||||
!Array.isArray(column.footer)
|
||||
!Array.isArray(column.footer)
|
||||
? {
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
mergeRule={
|
||||
|
||||
@@ -1,52 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
type SearchFormProps,
|
||||
type SearchFormValues,
|
||||
defaultSearchFormProps,
|
||||
} from "../type/SearchForm.type";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
Sgg,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
import {
|
||||
defaultSearchFormProps,
|
||||
type SearchFormProps,
|
||||
type SearchFormValues,
|
||||
} from "../type/SearchForm.type";
|
||||
import { doList } from "../util/column/input/DoList";
|
||||
|
||||
const getSidoListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsApi.getSidoList({ instSeCd: key[1] });
|
||||
};
|
||||
|
||||
const getSggListFetcher = async (
|
||||
key: [string, string],
|
||||
key: [string, string, string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggList(key[1]);
|
||||
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
||||
return statisticsApi.getSggAllList(key[1], {
|
||||
instSeCd: key[2],
|
||||
gugunSkipYn,
|
||||
});
|
||||
};
|
||||
|
||||
const SearchForm = (props: SearchFormProps = {}) => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const mergedProps = { ...defaultSearchFormProps, ...props };
|
||||
const instSeCd = userInfo?.instSeCd ?? "";
|
||||
const sidoSelectText =
|
||||
props.sidoSelectText && props.sidoSelectText.trim() !== ""
|
||||
? props.sidoSelectText
|
||||
: "선택";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const defaultYear = currentYear - 1;
|
||||
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
||||
"sido-list",
|
||||
["sido-list", instSeCd],
|
||||
getSidoListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
@@ -77,8 +91,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const sggKey: [string, string] | null =
|
||||
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null;
|
||||
const sggKey: [string, string, string, string] | null =
|
||||
values.sido && values.sido !== "ALL"
|
||||
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""]
|
||||
: null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
sggKey,
|
||||
getSggListFetcher,
|
||||
@@ -102,7 +118,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
);
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
props.onSubmit?.(values);
|
||||
};
|
||||
@@ -163,10 +179,14 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택"></SelectValue>
|
||||
<SelectValue
|
||||
placeholder={sidoSelectText}
|
||||
></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">선택</SelectItem>
|
||||
<SelectItem value="ALL">
|
||||
{sidoSelectText}
|
||||
</SelectItem>
|
||||
{sidoList?.data?.sidoList?.map((sido) => (
|
||||
<SelectItem
|
||||
key={sido.adstDstrCd}
|
||||
|
||||
@@ -19,7 +19,7 @@ import CompactDataGrid from "./CompactDataGrid";
|
||||
import SaveButton from "../button/SaveButton";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
@@ -91,13 +91,13 @@ const adminRegionColumnInfo: ColumnInfo = {
|
||||
};
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
return statisticsApi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggAllListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggAllList(key[1]);
|
||||
return statisticsApi.getSggAllList(key[1]);
|
||||
};
|
||||
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
|
||||
export interface VerificationResultDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function VerificationResultDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: VerificationResultDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[640px]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검증 결과</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="min-h-[120px]" />
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
type VerificationResultGridRow = Record<string, string | number>;
|
||||
|
||||
const PRCS_YN_LABEL: Record<string, string> = {
|
||||
"0": "미통과",
|
||||
"1": "통과",
|
||||
"2": "예외처리",
|
||||
"3": "반려",
|
||||
};
|
||||
|
||||
const verificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt"],
|
||||
};
|
||||
|
||||
export interface VerificationResultHistoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
queryContext?: {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
} | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
export function VerificationResultHistoryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
queryContext,
|
||||
title = "검증 결과 내역",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
}: VerificationResultHistoryDialogProps) {
|
||||
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
const fetchVerificationResultPage = useCallback(
|
||||
async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
|
||||
if (!queryContext) return null;
|
||||
const param: VerificationResultListRequest = {
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
ctpvNm: queryContext.ctpvNm,
|
||||
sggNm: queryContext.sggNm,
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
pbcpAprvYn: 'N',
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0,2,3",
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
return statisticsApi.getVerificationResultList(param);
|
||||
},
|
||||
[queryContext],
|
||||
);
|
||||
|
||||
const loadInitial = useCallback(async () => {
|
||||
if (!queryContext) {
|
||||
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes] = await Promise.all([
|
||||
fetchVerificationResultPage("NV", 0),
|
||||
fetchVerificationResultPage("IMR", 0),
|
||||
]);
|
||||
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
|
||||
setImrEntries(imrRes?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
}
|
||||
}, [fetchVerificationResultPage, queryContext]);
|
||||
|
||||
const handleNormalPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === normalPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("NV", pageIndex);
|
||||
setNormalEntries(response?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setNormalPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, normalPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleImrPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === imrPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setImrLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("IMR", pageIndex);
|
||||
setImrEntries(response?.data?.verifyResultList ?? []);
|
||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setImrLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, imrPageIndex, queryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
void loadInitial();
|
||||
}, [loadInitial, open, resetState]);
|
||||
|
||||
const mapRows = (entries: VerificationResultReportItem[]): VerificationResultGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
}));
|
||||
|
||||
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
|
||||
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
|
||||
|
||||
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
|
||||
SetStateAction<Record<string, string | number>[]>
|
||||
>;
|
||||
|
||||
const renderGrid = (
|
||||
rows: VerificationResultGridRow[],
|
||||
pageIndex: number,
|
||||
pageCount: number,
|
||||
onPageIndexChange: (page: number) => void,
|
||||
loading: boolean,
|
||||
) => {
|
||||
if (!loading && rows.length === 0) {
|
||||
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-2">
|
||||
<DataGrid
|
||||
columnInfo={verificationResultColumnInfo}
|
||||
rowData={rows}
|
||||
setRowData={noopSetRows}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
/>
|
||||
<VerificationResultHistoryPagination
|
||||
pageCount={Math.max(pageCount, 1)}
|
||||
pageIndex={Math.max(pageIndex, 0)}
|
||||
onPageIndexChange={onPageIndexChange}
|
||||
className="mx-2 mb-2"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 rounded-md [data-theme='black']:bg-transparent bg-white/40 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
||||
{errorMessage && (
|
||||
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
|
||||
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
|
||||
</section>
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
|
||||
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button, cn } from "@common/components";
|
||||
|
||||
export type VerificationResultHistoryPaginationProps = {
|
||||
pageCount: number;
|
||||
pageIndex: number;
|
||||
onPageIndexChange: (pageIndex: number) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function VerificationResultHistoryPagination({
|
||||
pageCount,
|
||||
pageIndex: currentPage,
|
||||
onPageIndexChange,
|
||||
className,
|
||||
}: VerificationResultHistoryPaginationProps) {
|
||||
const maxVisiblePages = 10;
|
||||
let startPage = Math.max(0, currentPage - maxVisiblePages / 2);
|
||||
const endPage = Math.min(pageCount, startPage + maxVisiblePages);
|
||||
|
||||
if (endPage - startPage < maxVisiblePages) {
|
||||
startPage = Math.max(0, endPage - maxVisiblePages);
|
||||
}
|
||||
|
||||
const pageNumbers = Array(endPage - startPage)
|
||||
.fill(0)
|
||||
.map((_, i) => startPage + i);
|
||||
|
||||
const canPrevious = currentPage > 0;
|
||||
const canNext = pageCount > 0 && currentPage < pageCount - 1;
|
||||
|
||||
return (
|
||||
<div className={cn("mt-2 flex items-center justify-center gap-1 rounded-md border border-border/70 bg-muted/20 px-2 py-2", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-8 p-0 lg:flex"
|
||||
onClick={() => onPageIndexChange(0)}
|
||||
disabled={!canPrevious}
|
||||
>
|
||||
<span className="sr-only">첫 페이지로</span>
|
||||
<DoubleArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-8 p-0"
|
||||
onClick={() => onPageIndexChange(currentPage - 1)}
|
||||
disabled={!canPrevious}
|
||||
>
|
||||
<span className="sr-only">이전 페이지로</span>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{pageNumbers.map((pageNumber) => (
|
||||
<Button
|
||||
key={pageNumber}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"inline-block h-8 min-w-8 border border-transparent !bg-transparent px-2 py-1 text-foreground hover:!bg-accent",
|
||||
currentPage === pageNumber &&
|
||||
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
|
||||
)}
|
||||
onClick={() => onPageIndexChange(pageNumber)}
|
||||
>
|
||||
{pageNumber + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-8 p-0"
|
||||
onClick={() => onPageIndexChange(currentPage + 1)}
|
||||
disabled={!canNext}
|
||||
>
|
||||
<span className="sr-only">다음 페이지로</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-8 p-0 lg:flex"
|
||||
onClick={() => onPageIndexChange(Math.max(0, pageCount - 1))}
|
||||
disabled={!canNext}
|
||||
>
|
||||
<span className="sr-only">마지막 페이지로</span>
|
||||
<DoubleArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SplitDataGrid from "@/components/module/SplitDataGrid";
|
||||
import GridTopActionButton from "@/components/module/GridTopActionButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
|
||||
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
|
||||
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
@@ -17,20 +19,13 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||
import type {
|
||||
SeweragePipeInputAdminAreaConnectionItem,
|
||||
SeweragePipeInputFacilityConnectionItem,
|
||||
SeweragePipeInputFactInstallStatusItem,
|
||||
SeweragePipeInputManholeStatusItem,
|
||||
SeweragePipeInputPipeDiameterStatusItem,
|
||||
SeweragePipeInputPipeTypeStatusItem,
|
||||
SeweragePipeInputRegionalFactStatusItem,
|
||||
SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
SeweragePipeInputStatusInfo,
|
||||
SeweragePipeRequestParams,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
||||
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
const applyEnterableFromStatus = (
|
||||
@@ -62,6 +57,23 @@ function buildSewagePipeSearchParam(
|
||||
};
|
||||
}
|
||||
|
||||
/** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */
|
||||
function tabRowDataExists(
|
||||
m: Map<string, Record<string, string | number>[]>,
|
||||
tabKey: string,
|
||||
): boolean {
|
||||
if (tabKey === "ST00020006") {
|
||||
const left = m.get("treatmentPlantConnectionInp");
|
||||
const right = m.get("adminAreaConnectionInp");
|
||||
return (
|
||||
(Array.isArray(left) && left.length > 0) ||
|
||||
(Array.isArray(right) && right.length > 0)
|
||||
);
|
||||
}
|
||||
const rows = m.get(tabKey);
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 02. 하수관로
|
||||
const SewagePipeInp = () => {
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
@@ -128,7 +140,7 @@ const SewagePipeInp = () => {
|
||||
});
|
||||
}, [isEnterable, isAdminInfo]);
|
||||
|
||||
/** mergeIntoExistingMap: true면 지난연도(PRE) 등 — 현재 탭만 갱신, 나머지 탭 캐시 유지 */
|
||||
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
|
||||
const loadSewagePipeActiveTabData = async (
|
||||
tabKey: string,
|
||||
searchParam: SeweragePipeRequestParams,
|
||||
@@ -290,7 +302,6 @@ const SewagePipeInp = () => {
|
||||
const checkEnterable = periodResponse.data;
|
||||
periodEnterableRef.current = checkEnterable;
|
||||
|
||||
setRowDataMap(new Map());
|
||||
lazyTabLoggedRef.current.clear();
|
||||
|
||||
setIsLoading(true);
|
||||
@@ -301,7 +312,7 @@ const SewagePipeInp = () => {
|
||||
setHasSearched(true);
|
||||
setLastSearchedYear(values.year ?? null);
|
||||
|
||||
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, false, true);
|
||||
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, true, true);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -312,8 +323,7 @@ const SewagePipeInp = () => {
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const activeTabData = rowDataMap.get(tabKey);
|
||||
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
|
||||
const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey);
|
||||
|
||||
const canLazyLoad =
|
||||
searchParamRef.current &&
|
||||
@@ -324,86 +334,20 @@ const SewagePipeInp = () => {
|
||||
if (!canLazyLoad) return;
|
||||
|
||||
const searchParam = searchParamRef.current;
|
||||
if (!searchParam) return;
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!searchParam || !values) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let response;
|
||||
|
||||
if (tabKey === 'ST00020001') {
|
||||
response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam);
|
||||
} else if (tabKey === 'ST00020002') {
|
||||
response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam);
|
||||
} else if (tabKey === 'ST00020003') {
|
||||
response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam);
|
||||
} else if (tabKey === 'ST00020004') {
|
||||
response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam);
|
||||
} else if (tabKey === 'ST00020005') {
|
||||
response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam);
|
||||
} else if (tabKey === 'ST00020006') {
|
||||
response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam);
|
||||
}
|
||||
lazyTabLoggedRef.current.add(tabKey);
|
||||
|
||||
if (response?.status === 'success') {
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(response.data.commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, periodEnterableRef.current, setIsEnterable);
|
||||
setRowDataMap(prev => {
|
||||
const next = new Map(prev);
|
||||
if (tabKey === 'ST00020001') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = (response.data.regionalFactStatusList ?? []).map((d: SeweragePipeInputRegionalFactStatusItem) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d
|
||||
}));
|
||||
next.set(tabKey, toGridRows(convertedData));
|
||||
} else if (tabKey === 'ST00020002') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = (response.data.factInstallStatusList ?? []).map((d: SeweragePipeInputFactInstallStatusItem) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d
|
||||
}));
|
||||
next.set(tabKey, toGridRows(convertedData));
|
||||
} else if (tabKey === 'ST00020003') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = (response.data.pipeTypeStatusList ?? []).map((d: SeweragePipeInputPipeTypeStatusItem) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d
|
||||
}));
|
||||
next.set(tabKey, toGridRows(convertedData));
|
||||
} else if (tabKey === 'ST00020004') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = (response.data.pipeDiameterStatusList ?? []).map((d: SeweragePipeInputPipeDiameterStatusItem) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d
|
||||
}));
|
||||
next.set(tabKey, toGridRows(convertedData));
|
||||
} else if (tabKey === 'ST00020005') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData = (response.data.manholeStatusList ?? []).map((d: SeweragePipeInputManholeStatusItem) => ({
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d
|
||||
}));
|
||||
next.set(tabKey, toGridRows(convertedData));
|
||||
} else if (tabKey === 'ST00020006') {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const facilityConvertedData = (response.data.facilityConnectionList ?? []).map((d: SeweragePipeInputFacilityConnectionItem) => ({
|
||||
...d
|
||||
}));
|
||||
const adminConvertedData = (response.data.adminAreaConnectionList ?? []).map((d: SeweragePipeInputAdminAreaConnectionItem) => ({
|
||||
...d,
|
||||
ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
|
||||
}));
|
||||
next.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
|
||||
next.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response?.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
const ok = await loadSewagePipeActiveTabData(
|
||||
tabKey,
|
||||
searchParam,
|
||||
values,
|
||||
periodEnterableRef.current,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
if (ok) lazyTabLoggedRef.current.add(tabKey);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -543,8 +487,9 @@ const SewagePipeInp = () => {
|
||||
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
|
||||
/** TabDataGrid는 rowDataMap 사용 — 행이 하나라도 있을 때만 툴바 카드 의미 있음 */
|
||||
/** rowDataMap — 행이 하나라도 있을 때만 툴바·엑셀 의미 있음 */
|
||||
const hasRowData = Array.from(rowDataMap.values()).some((rows) => rows.length > 0);
|
||||
const excelToolbarEnabled = hasRowData && isEnterable && gugunOkOrNotDoList;
|
||||
|
||||
const showActionToolbarCard =
|
||||
(hasRowData || hasSearched) &&
|
||||
@@ -573,16 +518,132 @@ const SewagePipeInp = () => {
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
isExcelImport={hasRowData && isEnterable && gugunOkOrNotDoList}
|
||||
facilitySearchStatsYr={lastSearchedYear}
|
||||
getGridDataRef={getGridDataRef}
|
||||
/>
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
|
||||
if (info.isSplitGrid && info.left && info.right) {
|
||||
const leftKey = info.left.tabKey || `${tabKey}_left`;
|
||||
const rightKey = info.right.tabKey || `${tabKey}_right`;
|
||||
const leftRowData = rowDataMap.get(leftKey) ?? [];
|
||||
const rightRowData = rowDataMap.get(rightKey) ?? [];
|
||||
const splitTopButton = info.right.topButton;
|
||||
const showSplitTopButton =
|
||||
Boolean(splitTopButton?.text) &&
|
||||
(splitTopButton?.visible ?? true) &&
|
||||
excelToolbarEnabled;
|
||||
const leftColumnInfo: ColumnInfo = {
|
||||
columns: info.left.columns ?? [],
|
||||
columnLayout: info.left.columnLayout ?? [],
|
||||
};
|
||||
const rightColumnInfo: ColumnInfo = {
|
||||
columns: info.right.columns ?? [],
|
||||
columnLayout: info.right.columnLayout ?? [],
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<div className="flex flex-col gap-[0.6rem]">
|
||||
<div className="flex justify-end gap-[0.6rem]">
|
||||
{showSplitTopButton && (
|
||||
<GridTopActionButton
|
||||
text={splitTopButton?.text ?? ""}
|
||||
onClick={splitTopButton?.onClick}
|
||||
statsYr={lastSearchedYear ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={leftColumnInfo}
|
||||
rightColumnInfo={rightColumnInfo}
|
||||
leftFixedColumns={info.left.fixedColumns}
|
||||
rightFixedColumns={info.right.fixedColumns}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
leftRowData={leftRowData}
|
||||
rightRowData={rightRowData}
|
||||
setLeftRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(leftKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function"
|
||||
? updater(cur)
|
||||
: updater;
|
||||
next.set(leftKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
setRightRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(rightKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function"
|
||||
? updater(cur)
|
||||
: updater;
|
||||
next.set(rightKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={excelToolbarEnabled}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
getGridDataRef={tabKey === "ST00020001" ? getGridDataRef : undefined}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,16 +6,18 @@ import DataGrid from "@/components/module/DataGrid";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import SubmitDailog from "./SubmitDailog";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { getFooterSummary } from "@/components/util/grid/footerSummary";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
@@ -30,7 +32,11 @@ import type {
|
||||
SewerageSupplyRateSaveRequest,
|
||||
SewerageSupplyRateStatusInfo,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import type { cancelSubmitRequest, SubmitExceptionRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
cancelSubmitRequest,
|
||||
ImrStatusResponse,
|
||||
SubmitExceptionRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
@@ -45,6 +51,32 @@ const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResu
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
};
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/** IMR 상태가 COMPLETED 이고 job_prog 가 100일 때까지 폴링 */
|
||||
async function waitForImrJobCompleted(
|
||||
jobId: string,
|
||||
getStatus: (id: string) => Promise<ImrStatusResponse>,
|
||||
options?: {
|
||||
pollIntervalMs?: number;
|
||||
maxAttempts?: number;
|
||||
onProgress?: (jobProgress: number) => void;
|
||||
},
|
||||
): Promise<void> {
|
||||
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
|
||||
const maxAttempts = options?.maxAttempts ?? 150;
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const res = await getStatus(jobId);
|
||||
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
|
||||
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
|
||||
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
|
||||
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
||||
}
|
||||
|
||||
function getSewerageSupplyRateParams(
|
||||
values: SearchFormValues,
|
||||
overrides?: { yr?: string; demoYr?: string; mode?: string }
|
||||
@@ -69,11 +101,17 @@ const SeweragePenetrationRateInp = () => {
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
|
||||
const [showDailog, setShowDailog] = useState<boolean>(false);
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
||||
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
|
||||
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
@@ -88,6 +126,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
});
|
||||
|
||||
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
|
||||
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
|
||||
@@ -212,7 +251,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsAPi.submitException(params);
|
||||
const response = await statisticsApi.submitException(params);
|
||||
|
||||
if (response.status === 'success') {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
||||
@@ -271,7 +310,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
showGlobalProgress({ label: "제출 진행 중...", progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const currentData = getGridDataRef.current?.();
|
||||
|
||||
@@ -348,39 +387,62 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
|
||||
if (response.status === 'success') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
||||
stats_cd: statsCd,
|
||||
stats_yr: searchValues?.year ?? '',
|
||||
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
|
||||
};
|
||||
setGlobalProgress(50);
|
||||
|
||||
if (response.data.finalStatus === '5') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
||||
stats_cd: statsCd,
|
||||
stats_yr: searchValues?.year ?? '',
|
||||
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
|
||||
gen_verify_stts_cd: response.data.nvpassed ? '1' : '2',
|
||||
};
|
||||
|
||||
try {
|
||||
await statisticsAPi.callImrService(imrParam);
|
||||
} catch {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setIsLoading(false);
|
||||
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
|
||||
return;
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(imrResponse.job_id, (id) =>
|
||||
statisticsApi.getImrStatus(id),
|
||||
{
|
||||
onProgress: (jobProgress) => {
|
||||
setGlobalProgress(50 + Math.round(jobProgress / 2));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
setGlobalProgress(100);
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setVerificationResultDialogOpen(true);
|
||||
} catch (e) {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setIsLoading(false);
|
||||
const msg =
|
||||
e instanceof Error && e.message
|
||||
? e.message
|
||||
: 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.';
|
||||
alert(msg);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setGlobalProgress(100);
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
||||
alert('제출되었습니다.');
|
||||
}
|
||||
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
||||
alert('제출되었습니다.');
|
||||
} else {
|
||||
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 제출 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
try {
|
||||
if (await confirm('저장 하시겠습니까?')) {
|
||||
setIsLoading(true);
|
||||
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const currentData = getGridDataRef.current?.();
|
||||
@@ -465,23 +527,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
||||
stats_cd: statsCd,
|
||||
stats_yr: searchValues?.year ?? '',
|
||||
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'),
|
||||
};
|
||||
|
||||
try {
|
||||
await statisticsAPi.callImrService(imrParam);
|
||||
} catch {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setIsLoading(false);
|
||||
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalProgress(50);
|
||||
setGlobalProgress(100);
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
alert('저장되었습니다.');
|
||||
} else {
|
||||
@@ -493,7 +540,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
} catch {
|
||||
alert('데이터 저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -512,7 +559,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
statsCd: statsCd,
|
||||
};
|
||||
|
||||
const response = await statisticsAPi.cancelSubmit(requestData);
|
||||
const response = await statisticsApi.cancelSubmit(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
@@ -526,6 +573,14 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openVerificationHistoryDialog = () => {
|
||||
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !statsCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
setVerificationHistoryOpen(true);
|
||||
};
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
||||
: true;
|
||||
@@ -547,12 +602,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
{isAdminInfo && statusInfo && statusInfo.laeSbmsnSttsCd !== '0' && (
|
||||
<SubmitExceptionButton SubmitExceptionFunc={() => submitFunc(true)} />
|
||||
)}
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
||||
<SubmitButton submitFunc={() => submitFunc(false)} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
|
||||
@@ -560,7 +614,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
{isLoading && !globalProgressVisible && <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>
|
||||
@@ -593,7 +647,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - ';
|
||||
return `${prefix}${e.rsnExpln}`;
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
@@ -614,6 +668,25 @@ const SeweragePenetrationRateInp = () => {
|
||||
submitMode={isExceptionSubmitFlow ? 'exception' : 'normal'}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<VerificationResultHistoryDialog
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={
|
||||
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
||||
? {
|
||||
sewerBzmnCd: searchValues.bsnmCd,
|
||||
ctpvNm: searchValues.sido,
|
||||
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
||||
statsYr: searchValues.year,
|
||||
statsCd,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import RejectButton from "@/components/button/RejectButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { StatsInputStatusListApiResponse } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
const getStatsInputStatusFetcher = async (key: [string, StatsInputStatusListRequestParams]): Promise<StatsInputStatusListApiResponse> => {
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import RejectButton from "@/components/button/RejectButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
const getStatsInputStatusFetcher = async (
|
||||
key: [string, StatsInputStatusListRequestParams],
|
||||
): Promise<StatsInputStatusListApiResponse> => {
|
||||
return StatisticsInputStatusApi.getStatisticsInputStatusList(key[1]);
|
||||
};
|
||||
|
||||
@@ -24,10 +27,15 @@ const getStatsInputStatusFetcher = async (key: [string, StatsInputStatusListRequ
|
||||
const StatsInputStatusInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [searchParams, setSearchParams] = useState<StatsInputStatusListRequestParams | null>(null);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
);
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<StatsInputStatusListRequestParams | null>(null);
|
||||
|
||||
const swrKey = searchParams ? (["stats-input-status", searchParams] as const) : null;
|
||||
const swrKey = searchParams
|
||||
? (["stats-input-status", searchParams] as const)
|
||||
: null;
|
||||
const { mutate } = useSWRConfig();
|
||||
const { isLoading } = useSWR<StatsInputStatusListApiResponse>(
|
||||
swrKey,
|
||||
@@ -49,7 +57,7 @@ const StatsInputStatusInp = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId === 'StatsInputStatusInp' && swrKey) {
|
||||
if (activeTabId === "StatsInputStatusInp" && swrKey) {
|
||||
mutate(swrKey);
|
||||
}
|
||||
}, [activeTabId]);
|
||||
@@ -63,7 +71,7 @@ const StatsInputStatusInp = () => {
|
||||
ctpvNm: values.sido,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
statsYr: values.year,
|
||||
statsYr: values.year ?? "",
|
||||
};
|
||||
setSearchParams(params);
|
||||
mutate(["stats-input-status", params]);
|
||||
@@ -87,22 +95,24 @@ const StatsInputStatusInp = () => {
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<RejectButton rejectFunc={rejectFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
{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" />
|
||||
{isLoading && (
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
columnInfo={ StatsInputStatusInpColumn }
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
)}
|
||||
<DataGrid
|
||||
columnInfo={StatsInputStatusInpColumn}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
/>
|
||||
@@ -111,4 +121,4 @@ const StatsInputStatusInp = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsInputStatusInp;
|
||||
export default StatsInputStatusInp;
|
||||
|
||||
@@ -2,13 +2,30 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
Sgg,
|
||||
StatisticsLog,
|
||||
StatisticsLogRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { Button, Label, LoadingWithAnimation } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { LogHistoryDetailDialog } from "@/components/statistics/management/LogHistoryDetailDialog";
|
||||
@@ -18,9 +35,23 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsApi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsApi.getSggList(key[1]);
|
||||
};
|
||||
|
||||
const LogHistory = () => {
|
||||
// const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [statsCd, setStatsCd] = useState<string>("");
|
||||
const [sido, setSido] = useState<string>("ALL");
|
||||
const [gugun, setGugun] = useState<string>("ALL");
|
||||
const [bsnmCd, setBsnmCd] = useState<string>("");
|
||||
const [statsDataChgComCd, setStatsDataChgComCd] = useState<string>("ALL");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
@@ -32,49 +63,148 @@ const LogHistory = () => {
|
||||
string,
|
||||
string | number
|
||||
> | null>(null);
|
||||
const [startDate, setStartDate] = useState<Date | undefined>(() =>
|
||||
dayjs().subtract(1, "month").startOf("day").toDate(),
|
||||
);
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(() =>
|
||||
dayjs().endOf("day").toDate(),
|
||||
);
|
||||
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
||||
"log-history-sido-list",
|
||||
getSidoListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const sggKey: [string, string] | null =
|
||||
sido && sido !== "ALL" ? ["log-history-sgg-list", sido] : null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
sggKey,
|
||||
getSggListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.
|
||||
새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
},
|
||||
);
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
const handleSidoChange = (value: string) => {
|
||||
const selectedBsnm =
|
||||
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd ?? "";
|
||||
setSido(value);
|
||||
setGugun("ALL");
|
||||
setBsnmCd(selectedBsnm);
|
||||
};
|
||||
|
||||
const handleSggChange = (value: string) => {
|
||||
const listForBsnm = sggList?.data ?? [];
|
||||
const selectedBsnm =
|
||||
value === "ALL"
|
||||
? (sidoList?.data.sidoList.find((v) => v.sido === sido)
|
||||
?.bsnmCd ?? "")
|
||||
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? "");
|
||||
setGugun(value);
|
||||
setBsnmCd(selectedBsnm);
|
||||
};
|
||||
|
||||
const loadLogs = useCallback(
|
||||
async (page1Based: number) => {
|
||||
const param: StatisticsLogRequest = {
|
||||
statsCd: statsCd === "" || statsCd === "ALL" ? "" : statsCd,
|
||||
page: page1Based,
|
||||
size: PAGE_SIZE,
|
||||
startDate: startDate
|
||||
? dayjs(startDate).format("YYYY-MM-DD")
|
||||
: "",
|
||||
endDate: endDate ? dayjs(endDate).format("YYYY-MM-DD") : "",
|
||||
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
|
||||
...(sido && sido !== "ALL" && { ctpvNm: sido }),
|
||||
...(gugun && gugun !== "ALL" && { sggNm: gugun }),
|
||||
...(bsnmCd && { sewerBzmnCd: bsnmCd }),
|
||||
...(statsDataChgComCd &&
|
||||
statsDataChgComCd !== "ALL" && {
|
||||
statsDataChgComCd,
|
||||
}),
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsAPi.getLogList(param);
|
||||
if (response.status === "success") {
|
||||
const lostList = response.data?.statsLogList ?? [];
|
||||
const convertedData: StatisticsLog[] = lostList.map(
|
||||
(item: StatisticsLog) => ({
|
||||
...item,
|
||||
dmndFwkNm:
|
||||
item.dmndFwkNm === "save"
|
||||
? "저장"
|
||||
: item.dmndFwkNm === "submit"
|
||||
? "제출"
|
||||
: "예외 제출",
|
||||
}),
|
||||
);
|
||||
const response = await statisticsApi.getLogList(param);
|
||||
|
||||
setRowData(toGridRows(convertedData));
|
||||
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
|
||||
setPageIndex(Math.max(0, page1Based - 1));
|
||||
} else {
|
||||
alert(
|
||||
response.message || "데이터 조회 중 오류가 발생했습니다.",
|
||||
);
|
||||
if (response.status === "success") {
|
||||
const lostList = response.data?.statsLogList ?? [];
|
||||
|
||||
setRowData(toGridRows(lostList));
|
||||
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
|
||||
setPageIndex(Math.max(0, page1Based - 1));
|
||||
} else {
|
||||
alert(
|
||||
response.message ||
|
||||
"데이터 조회 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[statsCd],
|
||||
[
|
||||
statsCd,
|
||||
startDate,
|
||||
endDate,
|
||||
sido,
|
||||
gugun,
|
||||
bsnmCd,
|
||||
statsDataChgComCd,
|
||||
alert,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSearch = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSearch = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await loadLogs(1);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setStatsCd("");
|
||||
setSido("ALL");
|
||||
setGugun("ALL");
|
||||
setBsnmCd("");
|
||||
setStatsDataChgComCd("ALL");
|
||||
setStartDate(dayjs().subtract(1, "month").startOf("day").toDate());
|
||||
setEndDate(dayjs().endOf("day").toDate());
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Record<string, string | number>) => {
|
||||
setSelectedLogRow(row);
|
||||
setDetailDialogOpen(true);
|
||||
@@ -86,14 +216,58 @@ const LogHistory = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs(1);
|
||||
void loadLogs(1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-8 p-2">
|
||||
<div className="flex flex-wrap items-center gap-8 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">시도</Label>
|
||||
<Select
|
||||
value={sido}
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sidoList?.data?.sidoList?.map((item) => (
|
||||
<SelectItem
|
||||
key={item.adstDstrCd}
|
||||
value={item.sido}
|
||||
>
|
||||
{item.sido}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">시군구</Label>
|
||||
<Select
|
||||
value={gugun}
|
||||
onValueChange={handleSggChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggOptions.map((sgg) => (
|
||||
<SelectItem
|
||||
key={sgg.adstDstrCd}
|
||||
value={sgg.gugun}
|
||||
>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">통계항목</Label>
|
||||
<CodeSelectWithAll
|
||||
@@ -104,11 +278,36 @@ const LogHistory = () => {
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">요청 기능</Label>
|
||||
<CodeSelectWithAll
|
||||
upCd="STATSDATACHG"
|
||||
value={statsDataChgComCd}
|
||||
onValueChange={setStatsDataChgComCd}
|
||||
placeholder="전체"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mx-2 text-label">시작일</Label>
|
||||
<InputDatePicker
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
className="w-[14rem]"
|
||||
/>
|
||||
<Label className="mx-2 text-label">종료일</Label>
|
||||
<InputDatePicker
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
className="w-[14rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem] w-[96px]"
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
|
||||
export interface LogHistoryDetailDialogProps {
|
||||
open: boolean;
|
||||
@@ -30,7 +30,7 @@ export function LogHistoryDetailDialog({
|
||||
|
||||
const sn = row.statsLogSn;
|
||||
void (async () => {
|
||||
const response = await statisticsAPi.getLogDetail(
|
||||
const response = await statisticsApi.getLogDetail(
|
||||
sn !== undefined && sn !== null ? String(sn) : "",
|
||||
);
|
||||
console.log(response.data);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
export interface ReturnReasonDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave?: (reason: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ReturnReasonDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: ReturnReasonDialogProps) {
|
||||
const [reason, setReason] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [reasonError, setReasonError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setReason("");
|
||||
setReasonError(false);
|
||||
}, [open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (reason.trim() === "") {
|
||||
setReasonError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await onSave?.(reason);
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>반려 사유</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">반려 사유</Label>
|
||||
<Textarea
|
||||
value={reason}
|
||||
onChange={(e) => {
|
||||
setReason(e.target.value);
|
||||
if (reasonError) {
|
||||
setReasonError(false);
|
||||
}
|
||||
}}
|
||||
className="min-h-[12rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="반려 사유를 입력하세요."
|
||||
disabled={isSaving}
|
||||
/>
|
||||
{reasonError && (
|
||||
<p className="text-[1.2rem] text-red-500">
|
||||
반려 사유를 작성해주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray" disabled={isSaving}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export type ReviewOpinionDialogContext = Pick<
|
||||
ReviewOpinionSaveRequest,
|
||||
"statsYr" | "sewerBzmnCd" | "statsCd"
|
||||
>;
|
||||
|
||||
export interface ReviewOpinionDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** 조회 성공 후의 통계연도·사업자코드·통계코드 (없으면 저장 불가) */
|
||||
context: ReviewOpinionDialogContext | null;
|
||||
/** 다이얼로그 열릴 때 검토의견 초기값 */
|
||||
initialReviewOpinion?: string;
|
||||
/** 다이얼로그 열릴 때 처리내역 초기값 */
|
||||
initialProcessHistory?: string;
|
||||
/** 저장 시 생성된 param 전달 (API 호출 등) */
|
||||
onSave?: (param: ReviewOpinionSaveRequest) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ReviewOpinionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
context,
|
||||
initialReviewOpinion = "",
|
||||
initialProcessHistory = "",
|
||||
onSave,
|
||||
}: ReviewOpinionDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [reviewOpinion, setReviewOpinion] = useState("");
|
||||
const [processHistory, setProcessHistory] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [reviewOpinionError, setReviewOpinionError] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setReviewOpinion("");
|
||||
setProcessHistory("");
|
||||
setReviewOpinionError(false);
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) resetForm();
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setReviewOpinion(initialReviewOpinion);
|
||||
setProcessHistory(initialProcessHistory);
|
||||
}, [open, initialReviewOpinion, initialProcessHistory]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (reviewOpinion.trim() === "") {
|
||||
setReviewOpinionError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
context == null ||
|
||||
context.statsYr === "" ||
|
||||
context.sewerBzmnCd === ""
|
||||
) {
|
||||
alert("시도·시군구·년도로 조회한 뒤 검토의견을 저장할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
let userInfo: Awaited<ReturnType<typeof getUserInfo>>;
|
||||
try {
|
||||
userInfo = await getUserInfo();
|
||||
} catch {
|
||||
alert("사용자 정보를 불러오지 못했습니다. 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
const param: ReviewOpinionSaveRequest = {
|
||||
statsYr: context.statsYr,
|
||||
sewerBzmnCd: context.sewerBzmnCd,
|
||||
statsCd: context.statsCd,
|
||||
splmntOpnnCn: reviewOpinion,
|
||||
wrterOpnnCn: processHistory,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
};
|
||||
await onSave?.(param);
|
||||
resetForm();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
/* 저장 실패 시 부모에서 alert 후 throw — 다이얼로그는 유지 */
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토의견작성</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">검토의견</Label>
|
||||
<Textarea
|
||||
value={reviewOpinion}
|
||||
onChange={(e) => {
|
||||
setReviewOpinion(e.target.value);
|
||||
if (reviewOpinionError) {
|
||||
setReviewOpinionError(false);
|
||||
}
|
||||
}}
|
||||
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="검토의견을 입력하세요."
|
||||
disabled={isSaving}
|
||||
/>
|
||||
{reviewOpinionError && (
|
||||
<p className="text-[1.2rem] text-red-500">
|
||||
검토의견을 작성해주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">처리내역</Label>
|
||||
<Textarea
|
||||
value={processHistory}
|
||||
onChange={(e) => setProcessHistory(e.target.value)}
|
||||
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="처리내역을 입력하세요."
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray" disabled={isSaving}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useMemo, useRef,useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import type { ColumnConfig,ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
@@ -79,8 +79,8 @@ const SewagePipeMaintainRvw = () => {
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
if (!values.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,26 +91,27 @@ const SewagePipeMaintainRvw = () => {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
};
|
||||
|
||||
searchParamRef.current = searchParam;
|
||||
searchDoneRef.current = true;
|
||||
lazyTabLoggedRef.current.clear();
|
||||
setRowDataMap(new Map());
|
||||
|
||||
const response = await fetchSewagePipeMaintainReviewTab(activeTab, searchParam);
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
if (response?.status === "success" && response.data) {
|
||||
const listData = getListDataByTab(activeTab, response.data);
|
||||
setRowDataMap(
|
||||
new Map([[activeTab, toGridRows(listData)]]),
|
||||
);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(activeTab, toGridRows(listData));
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -177,14 +178,60 @@ const SewagePipeMaintainRvw = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
/>
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useMemo, useRef,useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { SewagePipeReviewRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import type { ColumnConfig,ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
@@ -48,17 +48,6 @@ const SewagePipeRvw = () => {
|
||||
const searchDoneRef = useRef(false);
|
||||
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
return SewagePipeColumnInfoListRvw.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
@@ -86,14 +75,15 @@ const SewagePipeRvw = () => {
|
||||
searchParamRef.current = searchParam;
|
||||
searchDoneRef.current = true;
|
||||
lazyTabLoggedRef.current.clear();
|
||||
setRowDataMap(new Map());
|
||||
|
||||
const response = await fetchSewagePipeReviewTab(activeTab, searchParam);
|
||||
|
||||
if (response?.status === "success" && response.data) {
|
||||
setRowDataMap(
|
||||
new Map([[activeTab, toGridRows(response.data as object[])]]),
|
||||
);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(activeTab, toGridRows(response.data as object[]));
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
@@ -163,14 +153,60 @@ const SewagePipeRvw = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
/>
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{SewagePipeColumnInfoListRvw.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{SewagePipeColumnInfoListRvw.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,13 +5,22 @@ import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
|
||||
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
|
||||
import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface";
|
||||
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import {
|
||||
ReviewOpinionDialog,
|
||||
type ReviewOpinionDialogContext,
|
||||
} from "@/components/statistics/review/ReviewOpinionDialog";
|
||||
|
||||
// 하수도 통계 검토 01.하수도 보급률
|
||||
const SeweragePenetrationRateRvw = () => {
|
||||
@@ -19,6 +28,14 @@ const SeweragePenetrationRateRvw = () => {
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [statsStatusInfo, setStatsStatusInfo] = useState<statsStatusInfo>({});
|
||||
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
|
||||
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList;
|
||||
@@ -35,14 +52,14 @@ const SeweragePenetrationRateRvw = () => {
|
||||
}, []);
|
||||
|
||||
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
|
||||
const lae = statsStatusInfo.laeSbmsnSttsCd != null ? String(statsStatusInfo.laeSbmsnSttsCd) : "";
|
||||
const pic = statsStatusInfo.statPicSbmsnSttsCd != null ? String(statsStatusInfo.statPicSbmsnSttsCd) : "";
|
||||
const laeIs3 = lae === "3";
|
||||
const reviewSubmitVisible = laeIs3 && (pic === "0" || pic === "1");
|
||||
const submit = statsStatusInfo.submitStatusCd != null ? String(statsStatusInfo.submitStatusCd) : "";
|
||||
const prvn = statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
|
||||
const submitIs3 = submit === "3";
|
||||
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
|
||||
return {
|
||||
showReviewOpinionButton: reviewSubmitVisible,
|
||||
showSubmitButton: reviewSubmitVisible,
|
||||
showReturnButton: laeIs3 && pic !== "3",
|
||||
showReturnButton: submitIs3 && prvn !== "3",
|
||||
};
|
||||
}, [statsStatusInfo]);
|
||||
|
||||
@@ -52,6 +69,10 @@ const SeweragePenetrationRateRvw = () => {
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
@@ -60,6 +81,7 @@ const SeweragePenetrationRateRvw = () => {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
setLastSearchValues(values);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -80,9 +102,23 @@ const SeweragePenetrationRateRvw = () => {
|
||||
}));
|
||||
|
||||
setStatsStatusInfo(response.data.statsStatusInfo);
|
||||
setReviewOpinionContext({
|
||||
statsYr: values.year ?? "",
|
||||
sewerBzmnCd: values.bsnmCd ?? "",
|
||||
statsCd: response.data.statsStatusInfo.statsCd ?? "",
|
||||
});
|
||||
setLatestReviewOpinion(
|
||||
response.data.statsStatusInfo.splmntOpnnCn ?? "",
|
||||
);
|
||||
setLatestProcessHistory(
|
||||
response.data.statsStatusInfo.wrterOpnnCn ?? "",
|
||||
);
|
||||
setRowData(toGridRows(convertedData));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -93,11 +129,120 @@ const SeweragePenetrationRateRvw = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitFunc = () => console.log('submit');
|
||||
const submitFunc = async () => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 제출할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
|
||||
if (!(await confirm("제출 하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnFunc = () => console.log('returnFunc');
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await statisticsApi.submitReview({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
});
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "제출에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert(response.message || "제출되었습니다.");
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
} catch {
|
||||
alert("제출 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = async () => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 검토의견을 작성할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const detailRes = await statisticsApi.getReviewOpinionDetail({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
});
|
||||
|
||||
if (detailRes.status !== "success") {
|
||||
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.wrterOpnnCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
|
||||
try {
|
||||
const response = await statisticsApi.saveReviewOpinion(param);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert(response.message || "저장되었습니다.");
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
wrterOpnnCn: param.wrterOpnnCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.wrterOpnnCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const returnFunc = () => setReturnDialogOpen(true);
|
||||
|
||||
const handleSaveReturnReason = async (reason: string) => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 반려할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm('반려 처리 하시겠습니까?')) {
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await statisticsApi.saveReviewReturn({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
rjctOpnnCn: reason,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
});
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert(response.message || "반려 처리 되었습니다.");
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
|
||||
} catch {
|
||||
alert("반려 처리 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -131,6 +276,20 @@ const SeweragePenetrationRateRvw = () => {
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
/>
|
||||
|
||||
<ReviewOpinionDialog
|
||||
open={reviewOpinionOpen}
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReturnReasonDialog
|
||||
open={returnDialogOpen}
|
||||
onOpenChange={setReturnDialogOpen}
|
||||
onSave={handleSaveReturnReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
DataTable
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { ReviewHistoryResponse } from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
|
||||
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
|
||||
|
||||
const checkPrvnStatusCd = (code: string) => {
|
||||
return (code === '0' || code === '1') ? '미작성' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '반려'));
|
||||
};
|
||||
|
||||
const checkSubmitStatusCd = (code: string) => {
|
||||
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
|
||||
};
|
||||
|
||||
// 하수도 통계 검토 00. 통계 입력 현황
|
||||
const StatsInputStatusRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [searchedYear, setSearchedYear] = useState<string>("");
|
||||
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
|
||||
const [historySearchInfo, setHistorySearchInfo] = useState<{
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
tabNm: string;
|
||||
}>({
|
||||
ctpvNm: '',
|
||||
sggNm: '',
|
||||
tabNm: '',
|
||||
});
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout } = StatsInputStatusInfoList;
|
||||
@@ -30,8 +61,30 @@ const StatsInputStatusRvw = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const reviewHistoryColumns = useMemo(() => {
|
||||
const col = createColumnHelper<ReviewHistoryRow>();
|
||||
return [
|
||||
col.accessor("logDt", {
|
||||
header: "등록일",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
col.accessor("historyContent", {
|
||||
header: "내용",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
];
|
||||
}, []);
|
||||
|
||||
const historyTable = useReactTable({
|
||||
data: historyRowData,
|
||||
columns: reviewHistoryColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (_row, index) => String(index),
|
||||
});
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setSearchedYear("");
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
@@ -42,24 +95,29 @@ const StatsInputStatusRvw = () => {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
setSearchedYear(values.year ?? "");
|
||||
|
||||
const response = await statisticsManagementApi.getStatsInputStatusList({
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
statsYr: values.year,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data.sewerageInputList ?? [];
|
||||
const convertedData = rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || '0'),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || '0')
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(rawList));
|
||||
setRowData(toGridRows(convertedData));
|
||||
} else {
|
||||
setRowData([]);
|
||||
alert(response.message || "조회된 데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("입력현황 조회 중 에러 발생:", error);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
} finally {
|
||||
@@ -68,20 +126,40 @@ const StatsInputStatusRvw = () => {
|
||||
};
|
||||
|
||||
const approveFunc = () => console.log('approveFunc');
|
||||
const saveFunc = () => console.log('submit');
|
||||
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
|
||||
const returnFunc = () => console.log('returnFunc');
|
||||
|
||||
const handleRowClick = async (row: Record<string, string | number>) => {
|
||||
setHistorySearchInfo({
|
||||
ctpvNm: String(row.ctpvNm ?? ""),
|
||||
sggNm: String(row.sggNm ?? ""),
|
||||
tabNm: String(row.tabNm ?? ""),
|
||||
});
|
||||
const params = {
|
||||
statsYr: searchedYear,
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsManagementApi.getReviewHistory(params);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setHistoryRowData(response.data);
|
||||
setReviewHistoryOpen(true);
|
||||
} else {
|
||||
alert(response.message || '검토 이력 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('검토 이력 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<ReturnButton returnFunc={returnFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,12 +174,43 @@ const StatsInputStatusRvw = () => {
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
rowStartNum={2}
|
||||
showHeaderCheckbox={true}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
|
||||
<DialogContent
|
||||
className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>
|
||||
{(historySearchInfo.sggNm ? `${historySearchInfo.ctpvNm} ${historySearchInfo.sggNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력` : `${historySearchInfo.ctpvNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력`)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{historyRowData.length === 0 ?
|
||||
<div className="text-2xl h-40 flex items-center justify-center">
|
||||
검토 이력이 없습니다.
|
||||
</div>
|
||||
:
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-auto">
|
||||
<DataTable table={historyTable} />
|
||||
</div>
|
||||
}
|
||||
<DialogFooter className="shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={() => setReviewHistoryOpen(false)}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,20 +2,22 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001";
|
||||
|
||||
const SewagePipeTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("ST00020001");
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
|
||||
const columnInfo = useMemo(
|
||||
() =>
|
||||
@@ -32,11 +34,11 @@ const SewagePipeTot = () => {
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab("ST00020001");
|
||||
setActiveTab(firstTabKey);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
setActiveTab("ST00020001");
|
||||
setActiveTab(firstTabKey);
|
||||
setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
@@ -76,20 +78,66 @@ const SewagePipeTot = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ const SeweragePenetrationRateTot = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} />
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
{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" />
|
||||
|
||||
@@ -1,10 +1,123 @@
|
||||
import SearchForm from "../../module/SearchForm";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
|
||||
import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
|
||||
|
||||
const SewerageServiceLevelInpTabList = [
|
||||
ComplaintInpColumn,
|
||||
ResidentFriendlyFactTotColumn,
|
||||
];
|
||||
|
||||
const SewerageServiceLevelTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey);
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(
|
||||
() =>
|
||||
SewerageServiceLevelInpTabList.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(ComplaintInpColumn.tabKey);
|
||||
setLastSearchValues(null);
|
||||
};
|
||||
|
||||
const fetchByTab = async (tabKey: string, values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const param = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (tabKey === ResidentFriendlyFactTotColumn.tabKey) {
|
||||
response =
|
||||
await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param);
|
||||
} else {
|
||||
response =
|
||||
await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param);
|
||||
}
|
||||
|
||||
if (response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, toGridRows(response.data as object[]));
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
setLastSearchValues(values);
|
||||
setRowDataMap(new Map());
|
||||
await fetchByTab(activeTab, values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSearchValues == null) return;
|
||||
if (rowDataMap.has(activeTab)) return;
|
||||
void fetchByTab(activeTab, lastSearchValues);
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
{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>
|
||||
)}
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
export interface ApprovalFormData {
|
||||
rjctOpnnCn: string;
|
||||
lastCfmtnPicNm: string;
|
||||
lastCfmtnPicTelNo: string;
|
||||
lastCfmtnPicDeptNm: string;
|
||||
lastCfmtnPicJbgdNm: string;
|
||||
}
|
||||
|
||||
export interface ApprovalInputDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm?: (data: ApprovalFormData) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ApprovalInputDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: ApprovalInputDialogProps) {
|
||||
const [rjctOpnnCn, setRjctOpnnCn] = useState("");
|
||||
const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState("");
|
||||
const [lastCfmtnPicTelNo, setLastCfmtnPicTelNo] = useState("");
|
||||
const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState("");
|
||||
const [lastCfmtnPicJbgdNm, setLastCfmtnPicJbgdNm] = useState("");
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setRjctOpnnCn("");
|
||||
setLastCfmtnPicNm("");
|
||||
setLastCfmtnPicTelNo("");
|
||||
setLastCfmtnPicDeptNm("");
|
||||
setLastCfmtnPicJbgdNm("");
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) resetForm();
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetForm();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const formData: ApprovalFormData = {
|
||||
rjctOpnnCn: rjctOpnnCn.trim(),
|
||||
lastCfmtnPicNm: lastCfmtnPicNm.trim(),
|
||||
lastCfmtnPicTelNo: lastCfmtnPicTelNo.trim(),
|
||||
lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(),
|
||||
lastCfmtnPicJbgdNm: lastCfmtnPicJbgdNm.trim(),
|
||||
};
|
||||
await onConfirm?.(formData);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent
|
||||
className="max-w-[56rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>승인 정보 입력</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">최종 확인 담당자명</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="담당자명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 전화번호
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicTelNo}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicTelNo(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="전화번호를 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 부서명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicDeptNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicDeptNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="부서명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
최종 확인 담당자 직급명
|
||||
</Label>
|
||||
<Input
|
||||
value={lastCfmtnPicJbgdNm}
|
||||
onChange={(e) => {
|
||||
setLastCfmtnPicJbgdNm(e.target.value);
|
||||
}}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="직급명을 입력하세요."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">반려 의견 내용</Label>
|
||||
<Textarea
|
||||
value={rjctOpnnCn}
|
||||
onChange={(e) => setRjctOpnnCn(e.target.value)}
|
||||
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="반려 의견 내용을 입력하세요. (선택사항)"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => void handleConfirm()}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { ValueType } from "realgrid";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
@@ -18,30 +19,30 @@ export type RealGridColumnInfo = {
|
||||
mergeRule?: { criteria: "value" | string };
|
||||
sortable?: boolean;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** setColumns 메서드가 있는 GridView */
|
||||
@@ -110,21 +111,21 @@ export type DataFieldValueCallback = (
|
||||
export type ComputedFieldConfig =
|
||||
| { fields: string[]; type: "sum" | "avg" }
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
|
||||
export type ColumnConfig = {
|
||||
name: string;
|
||||
@@ -147,31 +148,31 @@ export type ColumnConfig = {
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
};
|
||||
|
||||
export type FooterUserSpan = {
|
||||
@@ -183,16 +184,16 @@ export type FooterUserSpan = {
|
||||
export type ColumnLayoutItem =
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
| {
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
|
||||
export type ColumnInfo = {
|
||||
columns: ColumnConfig[];
|
||||
@@ -202,7 +203,10 @@ export type ColumnInfo = {
|
||||
export type GridTopButtonConfig = {
|
||||
visible?: boolean;
|
||||
text: string;
|
||||
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
||||
onClick?: (helpers: {
|
||||
openDialog: (title?: string) => void;
|
||||
closeDialog: () => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type ColumnInfoList = {
|
||||
@@ -225,15 +229,35 @@ export type CellStyleCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
};
|
||||
|
||||
export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined;
|
||||
export type CellStyleCallback = (
|
||||
params: CellStyleCallbackParams,
|
||||
) => string | undefined;
|
||||
|
||||
/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */
|
||||
export type CellTooltipCallbackParams = { row: Record<string, string | number>; fieldName: string };
|
||||
export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined;
|
||||
export type CellTooltipCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
fieldName: string;
|
||||
};
|
||||
export type CellTooltipCallback = (
|
||||
params: CellTooltipCallbackParams,
|
||||
) => string | undefined;
|
||||
|
||||
/** 셀 편집 완료 시 호출되는 콜백 (RealGrid onEditRowChanged) */
|
||||
export type CellEditedCallbackParams = {
|
||||
itemIndex: number;
|
||||
fieldName: string;
|
||||
oldValue: string | number;
|
||||
newValue: string | number;
|
||||
};
|
||||
export type CellEditedCallback = (
|
||||
params: CellEditedCallbackParams,
|
||||
) => void;
|
||||
|
||||
export type DataGridProps = {
|
||||
columnInfo: ColumnInfo;
|
||||
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
|
||||
/** 있으면보내기 파일명: `{메뉴명}_{suffix}.xlsx` (TabDataGrid의 탭 라벨 접미사와 동일) */
|
||||
excelExportFileSuffix?: string;
|
||||
isExcelImport?: boolean; // 엑셀 가져오기 유무
|
||||
isExcelExport?: boolean;
|
||||
onRowsChange?: (rows: Record<string, string | number>[]) => void;
|
||||
@@ -247,11 +271,20 @@ export type DataGridProps = {
|
||||
cellStyleCallback?: CellStyleCallback;
|
||||
/** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */
|
||||
cellTooltipCallback?: CellTooltipCallback;
|
||||
/** 셀 편집 완료 시 호출 (RealGrid onEditRowChanged) */
|
||||
onCellEdited?: CellEditedCallback;
|
||||
/** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
|
||||
getGridDataRef?: {
|
||||
current: (() => Record<string, string | number>[]) | null;
|
||||
};
|
||||
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
|
||||
getGridRef?: React.MutableRefObject<RealGridReact | null>;
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
/** CheckBar 행 체크 변경 */
|
||||
onItemChecked?: (itemIndex: number, checked: boolean) => void;
|
||||
/** CheckBar 헤더 전체 선택/해제 */
|
||||
onItemAllChecked?: (checked: boolean) => void;
|
||||
onScrollToBottom?: () => void;
|
||||
statusInfo?: SewerageSupplyRateStatusInfo | null;
|
||||
/** 강조 및 스크롤할 행의 ID */
|
||||
highlightedRowId?: string | number | null;
|
||||
@@ -263,6 +296,7 @@ export type DataGridProps = {
|
||||
compactFullWidth?: boolean;
|
||||
/** CompactDataGrid 전용: 그리드 영역 높이 Tailwind 클래스 */
|
||||
compactGridHeightClass?: string;
|
||||
gridHeight?: number;
|
||||
};
|
||||
|
||||
export type TabDataGridProps = {
|
||||
|
||||
@@ -10,6 +10,7 @@ export type SearchFormValues = {
|
||||
|
||||
export type SearchFormProps = {
|
||||
sido?: boolean;
|
||||
sidoSelectText?: string;
|
||||
gugun?: boolean;
|
||||
year?: boolean;
|
||||
order?: boolean;
|
||||
@@ -22,7 +23,9 @@ export type SearchFormProps = {
|
||||
defaultValues?: SearchFormValues;
|
||||
};
|
||||
|
||||
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'onValuesChange' | 'defaultValues'>> = {
|
||||
export const defaultSearchFormProps: Required<
|
||||
Omit<SearchFormProps, "onSubmit" | "onReset" | "onValuesChange" | "defaultValues" | "sidoSelectText">
|
||||
> = {
|
||||
sido: true,
|
||||
gugun: true,
|
||||
year: true,
|
||||
|
||||
@@ -2,20 +2,26 @@ import { ValueType } from "realgrid";
|
||||
|
||||
export const LogHistoryColumn = {
|
||||
columns: [
|
||||
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsYr', fieldName: 'statsYr', header: { text: '연도' }, editable: false },
|
||||
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false },
|
||||
{ name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false },
|
||||
{ name: 'dmndFwkNm', fieldName: 'dmndFwkNm', header: { text: '요청 기능명' }, editable: false },
|
||||
{ name: 'statsDataChgComNm', fieldName: 'statsDataChgComNm', header: { text: '요청 기능명' }, editable: false },
|
||||
{ name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false, visible: false },
|
||||
{ name: 'regDt', fieldName: 'regDt', header: { text: '등록일' }, editable: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
'statsLogSn',
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'statsYr',
|
||||
'menuNm',
|
||||
'tblNm',
|
||||
'dmndFwkNm',
|
||||
'statsDataChgComNm',
|
||||
'rgtrId',
|
||||
'regDt'
|
||||
'regDt',
|
||||
'statsLogSn',
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원
|
||||
export const ComplaintInpColumn = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } },
|
||||
{
|
||||
name: "swtrmtPpltnCnt",
|
||||
fieldName: "swtrmtPpltnCnt",
|
||||
header: { text: "하수처리장민원건수(건)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "whol",
|
||||
fieldName: "whol",
|
||||
header: { text: "합계" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{ name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관거" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "방류수수질" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "ldsCnt", fieldName: "ldsCnt", header: { text: "퇴적토" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "오염사고" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{
|
||||
name: "crtrPpltnCvlcptEvntcsCnt",
|
||||
fieldName: "crtrPpltnCvlcptEvntcsCnt",
|
||||
header: { text: "인구10만명당 민원건수(건)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
|
||||
{ name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false },
|
||||
{ name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false },
|
||||
{ name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false },
|
||||
].map((col) => ({ ...col, editable: false })),
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: "groupA",
|
||||
hideChildHeaders: false,
|
||||
header: { text: "행정구역" },
|
||||
items: ["ctpvNm", "sggNm", "admdstNm"],
|
||||
},
|
||||
{
|
||||
name: "groupB",
|
||||
hideChildHeaders: false,
|
||||
header: { text: "민원건수(건)" },
|
||||
items: [
|
||||
"whol",
|
||||
"odorCnt",
|
||||
"swagePplCnt",
|
||||
"dschgWqCnt",
|
||||
"ldsCnt",
|
||||
"plltnAcdntCnt",
|
||||
"sewerCrgAmt",
|
||||
"etcCnt",
|
||||
],
|
||||
},
|
||||
"swtrmtPpltnCnt",
|
||||
"crtrPpltnCvlcptEvntcsCnt",
|
||||
"rmrk",
|
||||
"statsYr",
|
||||
"admdstCd",
|
||||
"rowType",
|
||||
],
|
||||
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 1,
|
||||
tabKey: "ST00130001",
|
||||
tabLabel: "민원",
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설
|
||||
export const ResidentFriendlyFactTotColumn = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } },
|
||||
{ name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" } },
|
||||
{
|
||||
name: "fcltCpct",
|
||||
fieldName: "fcltCpct",
|
||||
header: { text: "시설용량(m³/일)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "tmplntArea",
|
||||
fieldName: "tmplntArea",
|
||||
header: { text: "처리장면적(㎡)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "whol",
|
||||
fieldName: "whol",
|
||||
header: { text: "총계" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{ name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및 문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
|
||||
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
|
||||
{ name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false },
|
||||
{ name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false },
|
||||
{ name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false },
|
||||
].map((col) => ({ ...col, editable: false })),
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: "groupA",
|
||||
hideChildHeaders: false,
|
||||
header: { text: "행정구역" },
|
||||
items: ["ctpvNm", "sggNm", "admdstNm"],
|
||||
},
|
||||
"swtrmtFcltNm",
|
||||
"fcltCpct",
|
||||
"tmplntArea",
|
||||
{
|
||||
name: "groupB",
|
||||
hideChildHeaders: false,
|
||||
header: { text: "주민친화시설 면적(㎡)" },
|
||||
items: [
|
||||
"whol",
|
||||
"brkFcltArea",
|
||||
"childFrshpSpacArea",
|
||||
"bdySportsFcltArea",
|
||||
"sportsFcltArea",
|
||||
"pkltArea",
|
||||
"eduMultiFcltArea",
|
||||
"wtlndMakeArea",
|
||||
"etcCnt",
|
||||
],
|
||||
},
|
||||
"rmrk",
|
||||
"statsYr",
|
||||
"admdstCd",
|
||||
"rowType",
|
||||
],
|
||||
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 3,
|
||||
tabKey: "ST00130002",
|
||||
tabLabel: "주민친화시설",
|
||||
};
|
||||
@@ -1,127 +1,169 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
import type {
|
||||
CheckVerificationNameRequest,
|
||||
CheckVerificationNameResponse,
|
||||
CopyVerifyDataRequest,
|
||||
DeleteVrfcMasterResponse,
|
||||
GeneralverificationDetailResponse,
|
||||
GeneralverificationInfoResponse,
|
||||
GeneralverificationRequest,
|
||||
GeneralverificationResponse,
|
||||
GeneralverificationService,
|
||||
GeneralverificationInfoResponse,
|
||||
CheckVerificationNameRequest,
|
||||
CheckVerificationNameResponse,
|
||||
UpdateVrfcMasterRequest,
|
||||
UpdateVrfcMasterResponse,
|
||||
RegistVrfcMasterRequest,
|
||||
RegistVrfcMasterResponse,
|
||||
DeleteVrfcMasterResponse,
|
||||
VrfcOperatorsResponse,
|
||||
GeneralverificationDetailResponse,
|
||||
UpdateVrfcMasterRequest,
|
||||
UpdateVrfcMasterResponse,
|
||||
VerifyColumnListRequest,
|
||||
VerifyColumnListResponse,
|
||||
VerifyDetailRegistRequest,
|
||||
VerifyDetailUpdateRequest,
|
||||
CopyVerifyDataRequest,
|
||||
VerifyResultListRequest,
|
||||
VerifyResultListResponse,
|
||||
VrfcOperatorsResponse,
|
||||
} from "../model/general-verification-interface";
|
||||
|
||||
export class GeneralverificationApiService implements GeneralverificationService {
|
||||
public async getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse> {
|
||||
const response = await stClient.get<GeneralverificationResponse> (
|
||||
public async getGeneralverificationList(
|
||||
param: GeneralverificationRequest,
|
||||
): Promise<GeneralverificationResponse> {
|
||||
const response = await stClient.get<GeneralverificationResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse> {
|
||||
const response = await stClient.get<GeneralverificationInfoResponse> (
|
||||
public async getGeneralverificationInfo(
|
||||
param: number,
|
||||
): Promise<GeneralverificationInfoResponse> {
|
||||
const response = await stClient.get<GeneralverificationInfoResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse> {
|
||||
const response = await stClient.get<CheckVerificationNameResponse> (
|
||||
public async checkVerificationName(
|
||||
param: CheckVerificationNameRequest,
|
||||
): Promise<CheckVerificationNameResponse> {
|
||||
const response = await stClient.get<CheckVerificationNameResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse> {
|
||||
const response = await stClient.put<UpdateVrfcMasterResponse> (
|
||||
public async UpdateVrfcMaster(
|
||||
param: UpdateVrfcMasterRequest,
|
||||
): Promise<UpdateVrfcMasterResponse> {
|
||||
const response = await stClient.put<UpdateVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse> (
|
||||
|
||||
public async RegistVrfcMaster(
|
||||
param: RegistVrfcMasterRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse> {
|
||||
const response = await stClient.delete<DeleteVrfcMasterResponse> (
|
||||
public async DeleteVrfcMaster(
|
||||
param: number,
|
||||
): Promise<DeleteVrfcMasterResponse> {
|
||||
const response = await stClient.delete<DeleteVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVrfcOperators(): Promise<VrfcOperatorsResponse> {
|
||||
const response = await stClient.get<VrfcOperatorsResponse> (
|
||||
const response = await stClient.get<VrfcOperatorsResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse> {
|
||||
const response = await stClient.get<GeneralverificationDetailResponse> (
|
||||
public async getVrfcDetail(
|
||||
param: number,
|
||||
): Promise<GeneralverificationDetailResponse> {
|
||||
const response = await stClient.get<GeneralverificationDetailResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse> (
|
||||
param.refStatsCd ?
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
|
||||
:
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
|
||||
public async getVerifyColumnList(
|
||||
param: VerifyColumnListRequest,
|
||||
): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse>(
|
||||
param.refStatsCd
|
||||
? `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
|
||||
: `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse> (
|
||||
public async getSelectedVerifyColumnList(
|
||||
param: string,
|
||||
): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse> (
|
||||
public async RegistVrfcDetail(
|
||||
param: VerifyDetailRegistRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
param
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.put<RegistVrfcMasterResponse> (
|
||||
public async UpdateVrfcDetail(
|
||||
param: VerifyDetailUpdateRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.put<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
param
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse> (
|
||||
public async CopyVrfcData(
|
||||
param: CopyVerifyDataRequest,
|
||||
): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVerifyResultList(
|
||||
param: VerifyResultListRequest,
|
||||
): Promise<VerifyResultListResponse> {
|
||||
const response = await stClient.get<VerifyResultListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
|
||||
{
|
||||
params: {
|
||||
...param.searchDto,
|
||||
page: param.page,
|
||||
size: param.size,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const generalverificationApiService = new GeneralverificationApiService();
|
||||
export const generalverificationApiService =
|
||||
new GeneralverificationApiService();
|
||||
|
||||
+6
-5
@@ -7,7 +7,8 @@ import {
|
||||
type SewerageSupplyRateApiResponse,
|
||||
type CheckInputPeriodResponse,
|
||||
type SewerageSupplyRateSaveRequest,
|
||||
type SewerageSupplyRateSaveApiResponse,
|
||||
type SewerageSupplyRateSubmitResponse,
|
||||
type SewerageSupplyRateSaveResponse,
|
||||
} from "../../model/input/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSewSupplyRateService {
|
||||
@@ -26,16 +27,16 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
|
||||
const response = await stClient.post<SewerageSupplyRateSaveApiResponse>(
|
||||
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveResponse> {
|
||||
const response = await stClient.post<SewerageSupplyRateSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`,
|
||||
params,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
|
||||
const response = await stClient.post<SewerageSupplyRateSaveApiResponse>(
|
||||
public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSubmitResponse> {
|
||||
const response = await stClient.post<SewerageSupplyRateSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`,
|
||||
params,
|
||||
);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
StatisticsInputStatusService,
|
||||
StatsInputStatusListRequestParams,
|
||||
StatsInputStatusListApiResponse,
|
||||
StatsInputStatusListRequestParams,
|
||||
} from "../../model/input/statistics-input-status-interface";
|
||||
|
||||
export class StatisticsInputStatusApiService implements StatisticsInputStatusService {
|
||||
public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse> {
|
||||
public async getStatisticsInputStatusList(
|
||||
param: StatsInputStatusListRequestParams,
|
||||
): Promise<StatsInputStatusListApiResponse> {
|
||||
const response = await stClient.get<StatsInputStatusListApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`,
|
||||
{ params: param },
|
||||
@@ -17,4 +19,4 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer
|
||||
}
|
||||
}
|
||||
|
||||
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
|
||||
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
SeweragePipeInputPipeDiameterResponse,
|
||||
SeweragePipeInputPipeTypeResponse,
|
||||
SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
SeweragePipeInputRegionalFactStatusSaveApiResponse,
|
||||
SeweragePipeInputRegionalFactStatusSaveResponse,
|
||||
SeweragePipeInputRegionalResponse,
|
||||
SeweragePipeRequestParams,
|
||||
StatisticsInputSewPipeService,
|
||||
@@ -82,8 +82,8 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveApiResponse> {
|
||||
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveApiResponse>(
|
||||
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse> {
|
||||
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`,
|
||||
param,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import type { VerifyResultPageData } from "@/entities/statistics/model/general-verification-interface";
|
||||
import type {
|
||||
StatisticsVerificationFilterState,
|
||||
StatisticsVerificationSubFilterState,
|
||||
} from "@/entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
|
||||
|
||||
import { stAnlsMstApi } from "./statistics-analysis-master-api";
|
||||
import { stAnlsRsltApi } from "./statistics-analysis-result-api";
|
||||
|
||||
const DETAIL_PAGE_SIZE = 100;
|
||||
|
||||
interface StatisticsVerificationDetailSubmittedParams {
|
||||
main: StatisticsVerificationFilterState;
|
||||
sub: StatisticsVerificationSubFilterState;
|
||||
}
|
||||
|
||||
export const useStAnlsSidoCombo = () => {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
|
||||
@@ -58,3 +72,88 @@ export const useStAnlsChartData = (
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStatsVrfcRptData = (
|
||||
submittedMainFilters: StatisticsVerificationFilterState | null,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.ST_SERVICE.INPUT,
|
||||
"statistics-input-status",
|
||||
submittedMainFilters?.statsYr ?? "",
|
||||
submittedMainFilters?.ctpvNm ?? "",
|
||||
submittedMainFilters?.sggNm ?? "",
|
||||
submittedMainFilters?.sewerBzmnCd ?? "",
|
||||
submittedMainFilters?.statsCd ?? "",
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!submittedMainFilters) return [];
|
||||
const response =
|
||||
await StatisticsInputStatusApi.getStatisticsInputStatusList(
|
||||
submittedMainFilters,
|
||||
);
|
||||
if (response.status !== "success")
|
||||
throw new Error(response.message);
|
||||
return response.data ?? [];
|
||||
},
|
||||
enabled: Boolean(submittedMainFilters),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStatsVrfcRsltData = (
|
||||
submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null,
|
||||
) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.ST_SERVICE.INPUTVERIFY,
|
||||
"verify-results",
|
||||
submittedDetailParams?.main.statsYr ?? "",
|
||||
submittedDetailParams?.main.ctpvNm ?? "",
|
||||
submittedDetailParams?.main.sggNm ?? "",
|
||||
submittedDetailParams?.main.sewerBzmnCd ?? "",
|
||||
submittedDetailParams?.main.statsCd ?? "",
|
||||
submittedDetailParams?.sub.prcsYnList ?? "",
|
||||
submittedDetailParams?.sub.pbcpAprvYn ?? "",
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!submittedDetailParams)
|
||||
return {
|
||||
verifyResultList: [],
|
||||
pageInfo: {
|
||||
currentPage: 0,
|
||||
size: DETAIL_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
},
|
||||
} satisfies VerifyResultPageData;
|
||||
|
||||
const response =
|
||||
await generalverificationApiService.getVerifyResultList({
|
||||
page: pageParam,
|
||||
size: DETAIL_PAGE_SIZE,
|
||||
searchDto: {
|
||||
statsYr: submittedDetailParams.main.statsYr,
|
||||
ctpvNm: submittedDetailParams.main.ctpvNm,
|
||||
sggNm: submittedDetailParams.main.sggNm,
|
||||
sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd,
|
||||
statsCd: submittedDetailParams.main.statsCd,
|
||||
prcsYnList: submittedDetailParams.sub.prcsYnList,
|
||||
pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== "success")
|
||||
throw new Error(response.message);
|
||||
return response.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const { currentPage, totalPages } = lastPage.pageInfo;
|
||||
if (currentPage + 1 <= totalPages) return currentPage + 1;
|
||||
return undefined;
|
||||
},
|
||||
enabled: Boolean(submittedDetailParams),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ApprovalResponse,
|
||||
ApproveRequest,
|
||||
RejectRequest,
|
||||
StatisticsApprovalService,
|
||||
} from "../model/statistics-approval-interface";
|
||||
|
||||
class StatisticsApprovalApiService implements StatisticsApprovalService {
|
||||
public async approveStats(
|
||||
params: ApproveRequest[],
|
||||
): Promise<ApprovalResponse> {
|
||||
const response = await stClient.post<ApprovalResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.APPROVAL}/approve?type=VRFC`,
|
||||
params,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async rejectStats(
|
||||
params: RejectRequest[],
|
||||
): Promise<ApprovalResponse> {
|
||||
const response = await stClient.post<ApprovalResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.APPROVAL}/reject?type=VRFC`,
|
||||
params,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsApprovalApi = new StatisticsApprovalApiService();
|
||||
@@ -2,8 +2,10 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { stClient } from "@/shared/api/client";
|
||||
|
||||
|
||||
import {
|
||||
type StatsInputStatusReviewApiResponse
|
||||
import type {
|
||||
StatsInputStatusReviewApiResponse,
|
||||
ReviewHistoryRequest,
|
||||
ReviewHistoryResponse,
|
||||
} from "../model/statistics-input-review-interface";
|
||||
import {
|
||||
type SewerageSupplyRateReviewRequestParams,
|
||||
@@ -190,6 +192,14 @@ export class StatisticsManagementApiService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getReviewHistory(params: ReviewHistoryRequest): Promise<ReviewHistoryResponse> {
|
||||
const response = await stClient.get<ReviewHistoryResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/history`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 통합 인스턴스 수출
|
||||
|
||||
@@ -1,72 +1,106 @@
|
||||
import { client, stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stClient, client } from "@/shared/api/client";
|
||||
|
||||
import {
|
||||
type StatisticsService,
|
||||
type SearchCondition,
|
||||
type ApiResponse,
|
||||
type Sgg,
|
||||
type cancelSubmitRequest,
|
||||
type cancelSubmitResponse,
|
||||
type getReviewOpinionDetailRequest,
|
||||
type getReviewOpinionDetailResponse,
|
||||
type ImrRequest,
|
||||
type SubmitExceptionRequest,
|
||||
type SubmitExceptionResponse,
|
||||
type ReviewOpinionSaveRequest,
|
||||
type ReviewOpinionSaveResponse,
|
||||
type ReviewReturnSaveRequest,
|
||||
type ReviewReturnSaveResponse,
|
||||
type ReviewSubmitRequest,
|
||||
type ReviewSubmitResponse,
|
||||
type VerificationResultListRequest,
|
||||
type VerificationResultListResponse,
|
||||
type SearchCondition,
|
||||
type Sgg,
|
||||
type StatisticsLogDetailResponse,
|
||||
type StatisticsLogRequest,
|
||||
type StatisticsLogResponse,
|
||||
type StatisticsLogDetailResponse,
|
||||
type StatisticsService,
|
||||
type SubmitExceptionRequest,
|
||||
type SubmitExceptionResponse,
|
||||
type ImrResponse,
|
||||
type ImrStatusResponse,
|
||||
} from "../model/statistics-interface";
|
||||
|
||||
export class StatisticsAPiService implements StatisticsService {
|
||||
public async getSidoList(): Promise<ApiResponse<SearchCondition>> {
|
||||
export class StatisticsApiService implements StatisticsService {
|
||||
public async getSidoList(params?: {
|
||||
instSeCd?: string;
|
||||
}): Promise<ApiResponse<SearchCondition>> {
|
||||
const response = await stClient.get<ApiResponse<SearchCondition>>(
|
||||
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSggList(sidoName: string): Promise<ApiResponse<Sgg[]>> {
|
||||
public async getSggList(
|
||||
sidoName: string,
|
||||
params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" },
|
||||
): Promise<ApiResponse<Sgg[]>> {
|
||||
const response = await stClient.get<ApiResponse<Sgg[]>>(
|
||||
`${ENDPOINT.ST_SERVICE.COMMON}/sgg`,
|
||||
{ params: { sidoName } },
|
||||
{ params: { sidoName, ...params } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSggAllList(sidoName: string): Promise<ApiResponse<Sgg[]>> {
|
||||
public async getSggAllList(
|
||||
sidoName: string,
|
||||
params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" },
|
||||
): Promise<ApiResponse<Sgg[]>> {
|
||||
const response = await stClient.get<ApiResponse<Sgg[]>>(
|
||||
`${ENDPOINT.ST_SERVICE.COMMON}/sgg-all`,
|
||||
{ params: { sidoName } },
|
||||
{ params: { sidoName, ...params } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 입력 - 제출 취소
|
||||
public async cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse> {
|
||||
public async cancelSubmit(
|
||||
param: cancelSubmitRequest,
|
||||
): Promise<cancelSubmitResponse> {
|
||||
const response = await stClient.put<cancelSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/cancel-submit`,
|
||||
param
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async callImrService(param: ImrRequest): Promise<null> {
|
||||
const response = await client.post<null>(
|
||||
public async callImrService(param: ImrRequest): Promise<ImrResponse> {
|
||||
const response = await client.post<ImrResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
|
||||
param
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getImrStatus(param: string): Promise<ImrStatusResponse> {
|
||||
const response = await client.get<ImrStatusResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 예외 제출
|
||||
public async submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse> {
|
||||
public async submitException(
|
||||
param: SubmitExceptionRequest,
|
||||
): Promise<SubmitExceptionResponse> {
|
||||
const response = await stClient.post<SubmitExceptionResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/submit-exception`,
|
||||
param
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse> {
|
||||
public async getLogList(
|
||||
param: StatisticsLogRequest,
|
||||
): Promise<StatisticsLogResponse> {
|
||||
const response = await stClient.get<StatisticsLogResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs`,
|
||||
{ params: param },
|
||||
@@ -74,12 +108,62 @@ export class StatisticsAPiService implements StatisticsService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLogDetail(param: string): Promise<StatisticsLogDetailResponse> {
|
||||
public async getLogDetail(
|
||||
param: string,
|
||||
): Promise<StatisticsLogDetailResponse> {
|
||||
const response = await stClient.get<StatisticsLogDetailResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveReviewOpinion(
|
||||
param: ReviewOpinionSaveRequest,
|
||||
): Promise<ReviewOpinionSaveResponse> {
|
||||
const response = await stClient.post<ReviewOpinionSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/opinion-save`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getReviewOpinionDetail(
|
||||
param: getReviewOpinionDetailRequest,
|
||||
): Promise<getReviewOpinionDetailResponse> {
|
||||
const response = await stClient.get<getReviewOpinionDetailResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/opinion-select`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveReviewReturn(
|
||||
param: ReviewReturnSaveRequest,
|
||||
): Promise<ReviewReturnSaveResponse> {
|
||||
const response = await stClient.post<ReviewReturnSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/return`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitReview(
|
||||
param: ReviewSubmitRequest,
|
||||
): Promise<ReviewSubmitResponse> {
|
||||
const response = await stClient.post<ReviewSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/submit`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse> {
|
||||
const response = await stClient.get<VerificationResultListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsAPi = new StatisticsAPiService();
|
||||
export const statisticsApi = new StatisticsApiService();
|
||||
|
||||
@@ -3,6 +3,11 @@ import { stClient } from "@/shared/api/client";
|
||||
|
||||
import type { SewerageSupplyRateRequest, SewerageSupplyRateResponse } from "../model/statistics-total/statistics-sewage-supply-rate-total-interface";
|
||||
import type { SewagePipeTotalRequest, SewagePipeTotalResponse } from "../model/statistics-total/statistics-sewage-pipe-total-interface";
|
||||
import type {
|
||||
SewerageServiceLevelComplaintResponse,
|
||||
SewerageServiceLevelRequest,
|
||||
SewerageServiceLevelResidentFriendlyFactResponse
|
||||
} from "../model/statistics-total/statistics-service-level-total-interface";
|
||||
|
||||
export class StatisticsTotalApiService {
|
||||
public async getSewerageSupplyRateList(params: SewerageSupplyRateRequest): Promise<SewerageSupplyRateResponse> {
|
||||
@@ -20,6 +25,22 @@ export class StatisticsTotalApiService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise<SewerageServiceLevelComplaintResponse> {
|
||||
const response = await stClient.get<SewerageServiceLevelComplaintResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`,
|
||||
{ params: params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise<SewerageServiceLevelResidentFriendlyFactResponse> {
|
||||
const response = await stClient.get<SewerageServiceLevelResidentFriendlyFactResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`,
|
||||
{ params: params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
|
||||
@@ -1,3 +1,10 @@
|
||||
export {
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
useStatsVrfcRptData,
|
||||
useStatsVrfcRsltData,
|
||||
} from "./api/queries";
|
||||
export { statisticsApprovalApi } from "./api/statistics-approval-service";
|
||||
export {
|
||||
type AnalysisHistoryListApiResponse,
|
||||
type AnalysisRequestPayload,
|
||||
@@ -7,6 +14,7 @@ export {
|
||||
type JobStatusDto,
|
||||
type OutlierRecordDto,
|
||||
} from "./model/analysis/analysis-interface";
|
||||
export { type VerifyResultItem } from "./model/general-verification-interface";
|
||||
export {
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT,
|
||||
@@ -25,6 +33,7 @@ export {
|
||||
type ImrLogResponse,
|
||||
} from "./model/imr/imr-interface";
|
||||
export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns";
|
||||
export { type StatsInputStatusList } from "./model/input/statistics-input-status-interface";
|
||||
export {
|
||||
type StAnlsDtlListApiResponse,
|
||||
type StAnlsDtlResponse,
|
||||
@@ -54,6 +63,19 @@ export {
|
||||
} from "./model/statistics-analysis/statistics-analysis-result-interface";
|
||||
export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns";
|
||||
export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns";
|
||||
export {
|
||||
type ApprovalResponse,
|
||||
type ApproveRequest,
|
||||
type RejectRequest,
|
||||
} from "./model/statistics-approval-interface";
|
||||
export {
|
||||
statisticsVerificationReportColumnInfo,
|
||||
statisticsVerificationResultColumnInfo,
|
||||
} from "./model/statistics-verification-report/statistics-verification-report-columns";
|
||||
export {
|
||||
type StatisticsVerificationFilterState,
|
||||
type StatisticsVerificationSubFilterState,
|
||||
} from "./model/statistics-verification-report/statistics-verification-report-types";
|
||||
export {
|
||||
hbArticleColumnInfo,
|
||||
imrArticleColumnInfo,
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface VrfcOperatorsResponse {
|
||||
opertSn: string;
|
||||
opertSymbl: string;
|
||||
opertKornNm: string;
|
||||
}[]
|
||||
}[];
|
||||
}
|
||||
|
||||
/** 검증 상세 - 공통 정보 */
|
||||
@@ -165,48 +165,133 @@ export interface SelectedVerifyColumnListResponseData {
|
||||
export interface SelectedVerifyColumnListResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: VerifyColumnListResponseData
|
||||
data: VerifyColumnListResponseData;
|
||||
}
|
||||
|
||||
export interface VerifyDetailRegistData {
|
||||
trgtStatsCd: string;
|
||||
refStatsCd: string | null;
|
||||
kornArtopnEqtnCn: string,
|
||||
engArtopnEqtnCn: string,
|
||||
kornArtopnEqtnCn: string;
|
||||
engArtopnEqtnCn: string;
|
||||
cmprOpert: string | null;
|
||||
eqtnCombSeq: number
|
||||
eqtnCombSeq: number;
|
||||
}
|
||||
|
||||
export interface VerifyDetailRegistRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[]
|
||||
detailList: VerifyDetailRegistData[];
|
||||
rgtrId: string;
|
||||
}
|
||||
|
||||
export interface VerifyDetailUpdateRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[]
|
||||
mdfrId: string;
|
||||
detailList: VerifyDetailRegistData[];
|
||||
mdfrId: string;
|
||||
}
|
||||
|
||||
export interface CopyVerifyDataRequest {
|
||||
vrfcSn?: number | null;
|
||||
targetStatsCd : string;
|
||||
targetStatsCd: string;
|
||||
rgtrId: string;
|
||||
}
|
||||
|
||||
export interface VerifyResultSearchRequest {
|
||||
sewerBzmnCd?: string | null;
|
||||
ctpvNm?: string | null;
|
||||
sggNm?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
vrfcMthdCd?: string;
|
||||
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
|
||||
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
|
||||
}
|
||||
|
||||
export interface VerifyResultListRequest {
|
||||
searchDto: VerifyResultSearchRequest;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface VerifyResultItem {
|
||||
statsYr?: string;
|
||||
statsCd?: string;
|
||||
statsNm?: string;
|
||||
sewerBzmnCd?: string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
admdstNm?: string;
|
||||
vrfcSn?: number;
|
||||
vrfcMthdCd?: string;
|
||||
vrfcMthdNm?: string;
|
||||
vrfcTrgtNm?: string;
|
||||
regDt?: string;
|
||||
rsnExpln?: string;
|
||||
nowVlCn?: string;
|
||||
compVlCn?: string;
|
||||
prcsYn?: string;
|
||||
prcsOpnnExpln?: string;
|
||||
prcrId?: string;
|
||||
rjctOpnnCn?: string;
|
||||
pbcpAprvYn?: string;
|
||||
mdfrId?: string;
|
||||
mdfcnDt?: string;
|
||||
}
|
||||
|
||||
export interface VerifyResultPageInfo {
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface VerifyResultPageData {
|
||||
verifyResultList: VerifyResultItem[];
|
||||
pageInfo: VerifyResultPageInfo;
|
||||
}
|
||||
|
||||
export interface VerifyResultListResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: VerifyResultPageData;
|
||||
code?: string;
|
||||
httpStatus?: number;
|
||||
}
|
||||
|
||||
export interface GeneralverificationService {
|
||||
getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse>;
|
||||
getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse>;
|
||||
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
|
||||
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
|
||||
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
|
||||
getGeneralverificationList(
|
||||
param: GeneralverificationRequest,
|
||||
): Promise<GeneralverificationResponse>;
|
||||
getGeneralverificationInfo(
|
||||
param: number,
|
||||
): Promise<GeneralverificationInfoResponse>;
|
||||
checkVerificationName(
|
||||
param: CheckVerificationNameRequest,
|
||||
): Promise<CheckVerificationNameResponse>;
|
||||
UpdateVrfcMaster(
|
||||
param: UpdateVrfcMasterRequest,
|
||||
): Promise<UpdateVrfcMasterResponse>;
|
||||
RegistVrfcMaster(
|
||||
param: RegistVrfcMasterRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse>;
|
||||
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
|
||||
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
|
||||
getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse>;
|
||||
getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse>;
|
||||
RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse>;
|
||||
UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse>;
|
||||
CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse>;
|
||||
}
|
||||
getVerifyColumnList(
|
||||
param: VerifyColumnListRequest,
|
||||
): Promise<VerifyColumnListResponse>;
|
||||
getSelectedVerifyColumnList(
|
||||
param: string,
|
||||
): Promise<VerifyColumnListResponse>;
|
||||
RegistVrfcDetail(
|
||||
param: VerifyDetailRegistRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
UpdateVrfcDetail(
|
||||
param: VerifyDetailUpdateRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
CopyVrfcData(
|
||||
param: CopyVerifyDataRequest,
|
||||
): Promise<RegistVrfcMasterResponse>;
|
||||
getVerifyResultList(
|
||||
param: VerifyResultListRequest,
|
||||
): Promise<VerifyResultListResponse>;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
|
||||
header: { text: "당해년도" },
|
||||
width: 100,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
dataFormat: "#,##0.##",
|
||||
styleName: "right-column",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { ApiResponse } from "../statistics-interface";
|
||||
|
||||
export interface StatsInputStatusListRequestParams {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
sewerBzmnCd?: string;
|
||||
statsYr?: string;
|
||||
mode?: string;
|
||||
statsCd?: string;
|
||||
ctpvNm?: string | null;
|
||||
sggNm?: string | null;
|
||||
sewerBzmnCd?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusList {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
statsCd?: string;
|
||||
sewerBzmnCd?: string;
|
||||
menuTabName?: string;
|
||||
locgStatusNm?: string;
|
||||
inputRate?: number;
|
||||
|
||||
+6
-5
@@ -1,5 +1,3 @@
|
||||
import type { SewerageSupplyRateSaveApiResponse } from "./statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
export interface SeweragePipeRequestParams {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
@@ -119,8 +117,11 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest {
|
||||
saveList: SeweragePipeInputRegionalFactStatusSaveItem[];
|
||||
}
|
||||
|
||||
/** 지역별 시설현황 저장 API 응답 — 보급률 저장과 동일 래핑 */
|
||||
export type SeweragePipeInputRegionalFactStatusSaveApiResponse = SewerageSupplyRateSaveApiResponse;
|
||||
export interface SeweragePipeInputRegionalFactStatusSaveResponse {
|
||||
data: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 시설별 설치 현황 — 목록 항목 */
|
||||
export interface SeweragePipeInputFactInstallStatusItem {
|
||||
@@ -519,5 +520,5 @@ export interface StatisticsInputSewPipeService {
|
||||
getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise<SeweragePipeFacilityAdminAreaListResponse>;
|
||||
saveRegionalFactStatus(
|
||||
param: SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
): Promise<SeweragePipeInputRegionalFactStatusSaveApiResponse>;
|
||||
): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user