st: feat: 그리드 해더 색상 통일, 통계입력현황 조회 기능 추가 & 검색폼 컴포넌트 로직 수정(통계입력현황 조회시 도를 제외한 시는 행정구역(sggNm)을 조회하지 않도록)
This commit is contained in:
@@ -4,9 +4,24 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#466995] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
.rg-grid .rg-header .rg-table {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
/* 상태바·체크바 헤더 */
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@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;
|
||||
}
|
||||
@@ -40,6 +55,12 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-[#545556] !text-[#fff];
|
||||
}
|
||||
@@ -52,6 +73,12 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
@@ -61,5 +88,11 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,6 +449,7 @@ const DataGrid = ({
|
||||
target: "local",
|
||||
fileName: `${menuName}.xlsx`,
|
||||
footer: "hidden",
|
||||
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
@@ -852,6 +853,7 @@ const DataGrid = ({
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
...(col.width != null && { width: col.width }),
|
||||
...(col.styleName != null && {
|
||||
styleName: col.styleName,
|
||||
}),
|
||||
@@ -1146,6 +1148,7 @@ const DataGrid = ({
|
||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
|
||||
header={column.header}
|
||||
editable={column.editable}
|
||||
{...(column.width != null && { width: column.width })}
|
||||
{...(baseStyleName != null && { styleName: baseStyleName })}
|
||||
{...(column.visible === false && { visible: false })}
|
||||
{...(cellTooltipCallback && { renderer: { showTooltip: true } })}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import useSWR from 'swr';
|
||||
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
|
||||
import { doList } from "../util/column/input/DoList";
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
@@ -66,8 +67,13 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
bsnmCd: props.defaultValues?.bsnmCd || '',
|
||||
});
|
||||
|
||||
// isStatusMenu면 bsnmCd가 doList에 있을 때만 시군구 조회. 선택 후 옵션 유지를 위해 마지막 목록 ref 사용
|
||||
const sggKey: [string, string] | null =
|
||||
values.sido && values.sido !== "ALL" && (!mergedProps.isStatusMenu || doList.includes(values.bsnmCd ?? ""))
|
||||
? ["sgg-list", values.sido]
|
||||
: null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null,
|
||||
sggKey,
|
||||
getSggListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
@@ -87,7 +93,9 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const lastSggListRef = useRef<Sgg[]>([]);
|
||||
if (sggList?.data) lastSggListRef.current = sggList.data;
|
||||
const sggOptions = sggList?.data ?? (values.sido && values.sido !== "ALL" ? lastSggListRef.current : []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -95,6 +103,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
lastSggListRef.current = [];
|
||||
const resetValues: SearchFormValues = {
|
||||
sido: '',
|
||||
gugun: '',
|
||||
@@ -109,7 +118,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
|
||||
const handleSidoChange = (value: string) => {
|
||||
const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || '';
|
||||
|
||||
lastSggListRef.current = []; // 시도 변경 시 이전 시군구 목록 제거(조건 안 맞는 시도로 바꿀 때 옵션 안 남도록)
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
sido: value,
|
||||
@@ -119,9 +128,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
};
|
||||
|
||||
const handleSggChange = (value: string) => {
|
||||
const listForBsnm = sggList?.data ?? lastSggListRef.current;
|
||||
const selectedBsnm = value === 'ALL'
|
||||
? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '')
|
||||
: (sggList?.data?.find((v) => v.gugun === value)?.bsnmCd || '');
|
||||
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd || '');
|
||||
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
@@ -169,7 +179,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggList?.data?.map((sgg) => (
|
||||
{sggOptions?.map((sgg) => (
|
||||
<SelectItem key={sgg.adstDstrCd} value={sgg.gugun}>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
|
||||
@@ -419,6 +419,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
target: 'local',
|
||||
fileName: `${menuName}_${tabLabel}.xlsx`,
|
||||
footer: 'hidden',
|
||||
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
@@ -772,7 +773,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" variant="outline">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" variant="default">
|
||||
<TabsList className="mb-4">
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
|
||||
@@ -13,7 +13,7 @@ import SubmitDailog from "./SubmitDailog";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service";
|
||||
import { StatisticsInputSewSupplyRateApi } from "@/entities/statistics/api/statistics-input-sew-supply-rate-service";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import type {
|
||||
SewerageSupplyRateItem,
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
SewerageSupplyRateVerificationResult,
|
||||
SewerageSupplyRateSaveRequest,
|
||||
SewerageSupplyRateStatusInfo,
|
||||
} from "@/entities/statistics/model/statistics-input-interface";
|
||||
} from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface";
|
||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
@@ -101,7 +101,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputAPi.getSewerageSupplyRateList(params);
|
||||
const response = await StatisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params);
|
||||
if (response.status === 'success' && response.data) {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const statusInfo = response.data.statusInfo;
|
||||
@@ -126,7 +126,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
totalPopl: commonInfo.sewerBzmnTotalPopl,
|
||||
poplSm: commonInfo.sewerBzmnPoplSm,
|
||||
frgnrPoplSm: commonInfo.sewerBzmnFrgnrPoplSm,
|
||||
refArea: commonInfo.sewerBzmnArea,
|
||||
refArea: commonInfo.sewerBzmnArea,
|
||||
}
|
||||
: null
|
||||
);
|
||||
@@ -161,7 +161,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await statisticsInputAPi.checkInputPeriod(values.year || '');
|
||||
const response = await StatisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
|
||||
|
||||
if (response.status === 'success') {
|
||||
const checkEnterable = response.data;
|
||||
@@ -199,7 +199,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
const submitFunc = async () => {
|
||||
const isError = rowData.filter((d) => d.verificationResult);
|
||||
if (isError.length > 0) {
|
||||
alert('검증 오류 데이터가 있습니다.');
|
||||
alert(`데이터에 오류가 있습니다.
|
||||
수정 후 제출해주세요.`);
|
||||
return;
|
||||
}
|
||||
if (await confirm('제출 하시겠습니까?')) {
|
||||
@@ -276,7 +277,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
locgCnfirmDept: submitInfo.locgCnfirmDept,
|
||||
locgCnfirmPh: submitInfo.locgCnfirmPh,
|
||||
};
|
||||
const response = await statisticsInputAPi.submitSewerageSupplyRate(requestData);
|
||||
|
||||
const response = await StatisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
|
||||
if (response.status === 'success') {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
||||
alert('제출되었습니다.');
|
||||
@@ -294,15 +296,15 @@ const SeweragePenetrationRateInp = () => {
|
||||
try {
|
||||
if (await confirm('저장 하시겠습니까?')) {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const currentData = getGridDataRef.current?.();
|
||||
|
||||
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 컬럼 정의에서 dataFormat 추출 (소수점 자리수 맞추기 위해)
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
|
||||
@@ -316,10 +318,10 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (value === null || value === undefined || value === '') return 0;
|
||||
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
|
||||
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
|
||||
|
||||
// #,##0.000 → 소수점 3자리, #,##0.0 → 소수점 1자리, #,##0 → 정수
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
@@ -370,8 +372,12 @@ const SeweragePenetrationRateInp = () => {
|
||||
sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계
|
||||
};
|
||||
|
||||
const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData);
|
||||
|
||||
if (isAdminInfo) {
|
||||
requestData.superAdmin = 'superAdmin';
|
||||
}
|
||||
|
||||
const response = await StatisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
alert('저장되었습니다.');
|
||||
@@ -390,57 +396,57 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
{rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
{isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && (
|
||||
<>
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && <SaveButton saveFunc={saveFunc} />}
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
{rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
{isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && (
|
||||
<>
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && <SaveButton saveFunc={saveFunc} />}
|
||||
</div>
|
||||
</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" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
|
||||
rowStartNum={3}
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
statusInfo={statusInfo}
|
||||
getGridDataRef={getGridDataRef}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return 'rg-cell-verify-error';
|
||||
}
|
||||
}}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
<SubmitDailog open={showDailog} onOpenChange={setShowDailog} statusInfo={statusInfo} onConfirm={executeSubmit} />
|
||||
)}
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
|
||||
rowStartNum={3}
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
statusInfo={statusInfo}
|
||||
getGridDataRef={getGridDataRef}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return 'rg-cell-verify-error';
|
||||
}
|
||||
}}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
<SubmitDailog open={showDailog} onOpenChange={setShowDailog} statusInfo={statusInfo} onConfirm={executeSubmit} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,59 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { DataGrid } from "@rsone/components";
|
||||
// import { DataGrid } from "@rsone/components";
|
||||
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 { StatisticsInputStatusApi } from "@/entities/statistics/api/statistics-input-status-service";
|
||||
import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/statistics-input-status-interface";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 하수도 통계 관리 > 00. 통계 입력 현황
|
||||
const StatsInputStatusInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (values.sido === '' || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParam: StatsInputStatusListRequestParams = {
|
||||
ctpvNm: values.sido,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
statsYr: values.year,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await StatisticsInputStatusApi.getStatisticsInputStatusList(searchParam);
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
setRowData(toGridRows(response.data));
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
const rejectFunc = () => {
|
||||
// 보완 요청 로직
|
||||
@@ -18,25 +63,31 @@ const StatsInputStatusInp = () => {
|
||||
// 승인 요청 로직
|
||||
};
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<RejectButton rejectFunc={rejectFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} isStatusMenu={true} />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[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" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
columnInfo={ StatsInputStatusInpColumn }
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={ StatsInputStatusInpColumn }
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useEffect } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
/** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */
|
||||
function formatPhoneWithHyphen(value: string): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { ValueType } from "realgrid";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
// RealGrid 타입 정의
|
||||
/** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */
|
||||
@@ -11,6 +11,7 @@ export type RealGridColumnInfo = {
|
||||
editable?: boolean;
|
||||
valueType?: import("realgrid").ValueType;
|
||||
dataFormat?: string;
|
||||
width?: number;
|
||||
styleName?: string;
|
||||
mergeRule?: { criteria: "value" | string };
|
||||
sortable?: boolean;
|
||||
@@ -132,6 +133,8 @@ export type ColumnConfig = {
|
||||
editable?: boolean; // 셀 편집 가능 여부
|
||||
/** 그리드에서 컬럼 숨김 (수식용 필드 등) */
|
||||
visible?: boolean;
|
||||
/** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */
|
||||
width?: number;
|
||||
/** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */
|
||||
styleName?: string;
|
||||
mergeCells?: boolean;
|
||||
|
||||
@@ -18,6 +18,7 @@ export type SearchFormProps = {
|
||||
onReset?: () => void;
|
||||
onValuesChange?: (values: SearchFormValues) => void;
|
||||
defaultValues?: SearchFormValues;
|
||||
isStatusMenu?: boolean;
|
||||
};
|
||||
|
||||
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'onValuesChange' | 'defaultValues'>> = {
|
||||
@@ -27,4 +28,5 @@ export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' |
|
||||
order: false,
|
||||
fctName: false,
|
||||
userMenual: false,
|
||||
isStatusMenu: false,
|
||||
};
|
||||
@@ -6,40 +6,42 @@ import { ValueType } from "realgrid";
|
||||
*/
|
||||
export const StatsInputStatusInpColumn = {
|
||||
columns: [
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'menu_nm', fieldName: '구분', header: { text: '구분' } },
|
||||
{ name: 'locg_status_nm', fieldName: '진행상황', header: { text: '진행상황' } },
|
||||
{ name: 'locg_status', fieldName: '작성률', header: { text: '작성률' }, valueType: ValueType.NUMBER },
|
||||
{ name: 'locg_charger', fieldName: '작성자', header: { text: '작성자' } },
|
||||
{ name: 'locg_cttpc', fieldName: '작성자연락처', header: { text: '작성자연락처' } },
|
||||
{ name: 'locg_dept', fieldName: '부서', header: { text: '부서' } },
|
||||
{ name: 'locg_job', fieldName: '직제', header: { text: '직제' } },
|
||||
{ name: 'locg_cnfirm_nm', fieldName: '승인자', header: { text: '승인자' } },
|
||||
{ name: 'locg_cnfirm_dept', fieldName: '부서', header: { text: '부서' } },
|
||||
{ name: 'locg_cnfirm_job', fieldName: '직제', header: { text: '직제' } },
|
||||
{ name: 'return_opinion_sm', fieldName: '반려사유', header: { text: '반려사유' } },
|
||||
{ name: 'sido', fieldName: 'sido', header: { text: '시도' }, editable: false },
|
||||
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'menuCd', fieldName: 'menuCd', header: { text: '메뉴코드' }, visible: false, editable: false },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '구분' }, editable: false, width: 220 },
|
||||
{ name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false },
|
||||
{ name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'locgCharger', fieldName: 'locgCharger', header: { text: '작성자' }, editable: false },
|
||||
{ name: 'locgCttpc', fieldName: 'locgCttpc', header: { text: '작성자연락처' }, editable: false },
|
||||
{ name: 'locgDept', fieldName: 'locgDept', header: { text: '부서' }, editable: false },
|
||||
{ name: 'locgJob', fieldName: 'locgJob', header: { text: '직제' }, editable: false },
|
||||
{ name: 'locgCnfirmNm', fieldName: 'locgCnfirmNm', header: { text: '승인자' }, editable: false },
|
||||
{ name: 'locgCnfirmDept', fieldName: 'locgCnfirmDept', header: { text: '부서' }, editable: false },
|
||||
{ name: 'locgCnfirmJob', fieldName: 'locgCnfirmJob', header: { text: '직제' }, editable: false },
|
||||
{ name: 'returnOpinionSm', fieldName: 'returnOpinionSm', header: { text: '반려사유' }, editable: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
'sido',
|
||||
'sgg',
|
||||
'menu_nm',
|
||||
'gugun',
|
||||
'menuCd',
|
||||
'menuNm',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수도사업자' },
|
||||
items: [
|
||||
'locg_status_nm',
|
||||
'locg_status',
|
||||
'locg_charger',
|
||||
'locg_cttpc',
|
||||
'locg_dept',
|
||||
'locg_job',
|
||||
'locg_cnfirm_nm',
|
||||
'locg_cnfirm_dept',
|
||||
'locg_cnfirm_job',
|
||||
'return_opinion_sm'
|
||||
'locgStatusNm',
|
||||
'inputRate',
|
||||
'locgCharger',
|
||||
'locgCttpc',
|
||||
'locgDept',
|
||||
'locgJob',
|
||||
'locgCnfirmNm',
|
||||
'locgCnfirmDept',
|
||||
'locgCnfirmJob',
|
||||
'returnOpinionSm'
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
+4
-4
@@ -2,15 +2,15 @@ import axios from "axios";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import {
|
||||
type StatisticsInputService,
|
||||
type StatisticsInputSewSupplyRateService,
|
||||
type SewerageSupplyRateRequestParams,
|
||||
type SewerageSupplyRateApiResponse,
|
||||
type CheckInputPeriodResponse,
|
||||
type SewerageSupplyRateSaveRequest,
|
||||
type SewerageSupplyRateSaveApiResponse,
|
||||
} from "../model/statistics-input-interface";
|
||||
} from "../model/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
export class StatisticsInputAPiService implements StatisticsInputService {
|
||||
export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSewSupplyRateService {
|
||||
public async getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise<SewerageSupplyRateApiResponse> {
|
||||
const response = await axios.get<SewerageSupplyRateApiResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`,
|
||||
@@ -49,4 +49,4 @@ export class StatisticsInputAPiService implements StatisticsInputService {
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsInputAPi = new StatisticsInputAPiService();
|
||||
export const StatisticsInputSewSupplyRateApi = new StatisticsInputSewSupplyRateApiService();
|
||||
@@ -0,0 +1,23 @@
|
||||
import axios from "axios";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
StatisticsInputStatusService,
|
||||
StatsInputStatusListRequestParams,
|
||||
StatsInputStatusListApiResponse,
|
||||
} from "../model/statistics-input-status-interface";
|
||||
|
||||
export class StatisticsInputStatusApiService implements StatisticsInputStatusService {
|
||||
public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse> {
|
||||
const response = await axios.get<StatsInputStatusListApiResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`,
|
||||
{
|
||||
params: param
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
|
||||
@@ -2,7 +2,6 @@ import axios from "axios";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
|
||||
import { type CheckInputPeriodResponse } from "../model/statistics-input-interface";
|
||||
import {
|
||||
type StatsInputStatusReviewApiResponse
|
||||
} from "../model/statistics-input-review-interface";
|
||||
@@ -46,18 +45,6 @@ export class StatisticsManagementApiService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 기간 확인 (공통)
|
||||
* @param year 연도
|
||||
*/
|
||||
public async checkInputPeriod(year: string | number): Promise<CheckInputPeriodResponse> {
|
||||
const response = await axios.get<CheckInputPeriodResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period`,
|
||||
{ params: { year } }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 01. 하수도 보급률 검토 목록 조회
|
||||
* @param params (sido, gugun, yr, bsnmCd)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ApiResponse } from "./statistics-interface";
|
||||
|
||||
export interface StatsInputStatusListRequestParams {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
sewerBzmnCd?: string;
|
||||
statsYr?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusList {
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
menuCd?: string;
|
||||
menuNm?: string;
|
||||
locgStatusNm?: string;
|
||||
inputRate?: number;
|
||||
locgCharger?: string;
|
||||
locgCttpc?: string;
|
||||
locgDept?: string;
|
||||
locgJob?: string;
|
||||
locgCnfirmNm?: string;
|
||||
locgCnfirmDept?: string;
|
||||
locgCnfirmJob?: string;
|
||||
returnOpinionSm?: string;
|
||||
}
|
||||
|
||||
export type StatsInputStatusListApiResponse = ApiResponse<StatsInputStatusList[]> & {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface StatisticsInputStatusService {
|
||||
getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse>;
|
||||
}
|
||||
+2
-1
@@ -146,6 +146,7 @@ export interface SewerageSupplyRateSaveRequest {
|
||||
locgCnfirmJob?: string;
|
||||
locgCnfirmDept?: string;
|
||||
locgCnfirmPh?: string;
|
||||
superAdmin?: string;
|
||||
}
|
||||
|
||||
export interface SewerageSupplyRateSaveData {
|
||||
@@ -162,7 +163,7 @@ export interface CheckInputPeriodResponse {
|
||||
data: boolean;
|
||||
}
|
||||
|
||||
export interface StatisticsInputService {
|
||||
export interface StatisticsInputSewSupplyRateService {
|
||||
getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise<SewerageSupplyRateApiResponse>;
|
||||
checkInputPeriod(params: string): Promise<CheckInputPeriodResponse>;
|
||||
saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse>;
|
||||
Reference in New Issue
Block a user