Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein

This commit is contained in:
2026-02-05 10:50:15 +09:00
7 changed files with 146 additions and 66 deletions
+21 -20
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import useSWR from 'swr';
import {
@@ -18,9 +18,9 @@ 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 type { ApiResponse, Sido, Sgg } from "@/entities/statistics/model/statistics-interface";
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
const getSidoListFetcher = async (): Promise<ApiResponse<Sido[]>> => {
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
};
@@ -31,23 +31,11 @@ const getSggListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg
const SearchForm = (props: SearchFormProps = {}) => {
const { setMessage: alert } = useAlertStore((state) => state);
const mergedProps = { ...defaultSearchFormProps, ...props };
const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1;
const startYear = currentYear - 10;
const yearOptions = Array.from({ length: currentYear - startYear + 1 }, (_, i) => currentYear - i);
const [values, setValues] = useState<SearchFormValues>({
sido: props.defaultValues?.sido || "",
gugun: props.defaultValues?.gugun || '',
year: props.defaultValues?.year || String(defaultYear),
order: props.defaultValues?.order || '',
fctName: props.defaultValues?.fctName || '',
bsnmCd: props.defaultValues?.bsnmCd || '',
});
const { data: sidoList } = useSWR<ApiResponse<Sido[]>>(
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"sido-list",
getSidoListFetcher,
{
@@ -69,6 +57,15 @@ const SearchForm = (props: SearchFormProps = {}) => {
}
);
const [values, setValues] = useState<SearchFormValues>({
sido: props.defaultValues?.sido || '',
gugun: props.defaultValues?.gugun || '',
year: props.defaultValues?.year || defaultYear.toString(),
order: props.defaultValues?.order || '',
fctName: props.defaultValues?.fctName || '',
bsnmCd: props.defaultValues?.bsnmCd || '',
});
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null,
getSggListFetcher,
@@ -111,7 +108,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
};
const handleSidoChange = (value: string) => {
const selectedBsnm = sidoList?.data.find((v) => v.sido === value)?.bsnmCd || '';
const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || '';
setValues((prev) => ({
...prev,
@@ -121,6 +118,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
}));
};
useEffect(() => {
props.onValuesChange?.(values);
}, [values]);
return (
<div className="card card-border">
<form onSubmit={handleSubmit}>
@@ -136,7 +137,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">선택</SelectItem>
{sidoList?.data?.map((sido) => (
{sidoList?.data?.sidoList?.map((sido) => (
<SelectItem key={sido.adstDstrCd} value={sido.sido}>
{sido.sido}
</SelectItem>
@@ -175,7 +176,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
<SelectValue placeholder="선택"></SelectValue>
</SelectTrigger>
<SelectContent>
{yearOptions.map((year) => (
{sidoList?.data?.yearList.map((year) => (
<SelectItem key={year} value={String(year)}>
{year}년
</SelectItem>
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import SearchForm from "@/components/module/SearchForm";
// import { DataGrid } from "@rsone/components";
@@ -12,47 +12,68 @@ import { LoadingWithAnimation } from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import type { SewerageSupplyRateItem } from "@/entities/statistics/model/statistics-input-interface";
import type {
SewerageSupplyRateItem,
SewerageSupplyRateRequestParams,
} from "@/entities/statistics/model/statistics-input-interface";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
function getSewerageSupplyRateParams(
values: SearchFormValues,
overrides?: { yr?: string; demoYr?: string; mode?: string }
): SewerageSupplyRateRequestParams {
return {
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: overrides?.yr ?? values.year,
demoYr: overrides?.demoYr ?? values.year,
...(overrides?.mode && { mode: overrides.mode }),
};
}
// 하수도 통계 관리 > 01. 하수도 보급률
const SeweragePenetrationRateInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [isEnterable, setIsEnterable] = useState<boolean>(true);
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
const columnInfo = useMemo(() => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
return {
columnLayout,
columns: isEnterable
? columns
: columns.map((col) => ({ ...col, editable: false as const })),
};
}, [isEnterable]);
const handleReset = () => {
setRowData([]);
setLastSearchedYear(null);
};
const handleSearch = async (values: SearchFormValues) => {
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => {
try {
if (values.sido !== '' && values.sido !== 'ALL') {
setIsLoading(true);
setIsLoading(true);
const response = await statisticsInputAPi.getSewerageSupplyRateList(params);
if (response.status === 'success' && response.data) {
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
...d,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
}));
const response = await statisticsInputAPi.getSewerageSupplyRateList({
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: values.year,
demoYr: values.year,
});
if (response.status === 'success' && response.data) {
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
...d,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
}));
setRowData(toGridRows(convertedData));
} else {
alert(response.message);
}
setRowData(toGridRows(convertedData));
} else {
alert('시도를 선택해주세요.');
alert(response.message);
}
} catch (error) {
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
setRowData([]);
} finally {
@@ -60,12 +81,45 @@ const SeweragePenetrationRateInp = () => {
}
};
const SubmitExceptionFunc = () => {
// 예외 제출 로직
const handleSearch = async (values: SearchFormValues) => {
if (values.sido === '' || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
try {
const response = await statisticsInputAPi.checkInputPeriod(values.year || '');
if (response.status === 'success') {
setIsEnterable(response.data);
setLastSearchedYear(values.year || null);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values));
} else {
alert(response.message);
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
}
};
const getLastYearDataFunc = () => {
// 지난연도 데이터 가져오기
const SubmitExceptionFunc = () => {
// 예외 제출 로직
};
const getLastYearDataFunc = async (values: SearchFormValues) => {
if (values.sido === '' || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
// 실제 조회된 연도가 없으면 실행 불가
if (!lastSearchedYear) {
alert('먼저 데이터를 조회해주세요.');
return;
}
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }));
};
const submitFunc = () => {
@@ -78,22 +132,29 @@ const SeweragePenetrationRateInp = () => {
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
<SubmitButton submitFunc={submitFunc} />
<SaveButton saveFunc={saveFunc} />
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
{rowData.length > 0 &&
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{isEnterable && <SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />}
{isEnterable && <LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />}
{isEnterable && <SubmitButton submitFunc={submitFunc} />}
{
// 최고 관리자일땐 저장 가능하게
<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
columnInfo={SeweragePenetrationRateInpColumn}
key={isEnterable ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable}
rowStartNum={3}
fixedColumns={1}
rowData={rowData}
@@ -16,10 +16,11 @@ export type SearchFormProps = {
userMenual?: boolean;
onSubmit?: (values: SearchFormValues) => void;
onReset?: () => void;
onValuesChange?: (values: SearchFormValues) => void;
defaultValues?: SearchFormValues;
};
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'defaultValues'>> = {
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'onValuesChange' | 'defaultValues'>> = {
sido: true,
gugun: true,
year: true,
@@ -5,6 +5,7 @@ import {
type StatisticsInputService,
type SewerageSupplyRateRequestParams,
type SewerageSupplyRateApiResponse,
type CheckInputPeriodResponse,
} from "../model/statistics-input-interface";
export class StatisticsInputAPiService implements StatisticsInputService {
@@ -18,6 +19,14 @@ export class StatisticsInputAPiService implements StatisticsInputService {
return response.data;
}
public async checkInputPeriod(params: string): Promise<CheckInputPeriodResponse> {
const response = await axios.get<CheckInputPeriodResponse> (
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period?year=${params}`,
);
return response.data;
}
}
export const statisticsInputAPi = new StatisticsInputAPiService();
@@ -3,16 +3,16 @@ import { ENDPOINT } from "@/shared/config";
import {
type StatisticsService,
type Sido,
type SearchCondition,
type ApiResponse,
type Sgg
} from "../model/statistics-interface";
export class StatisticsAPiService implements StatisticsService {
public async getSidoList(): Promise<ApiResponse<Sido[]>> {
const response = await axios.get<ApiResponse<Sido[]>>(
`${ENDPOINT.ST_SERVICE.COMMON}/sido`,
public async getSidoList(): Promise<ApiResponse<SearchCondition>> {
const response = await axios.get<ApiResponse<SearchCondition>>(
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition`,
);
return response.data;
@@ -1,6 +1,6 @@
import type { ApiResponse } from "./statistics-interface";
// 하수도 보급률 API 응답의 상태 정보
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateStatusInfo {
locgStatus: string;
locgDt: string;
@@ -13,7 +13,6 @@ export interface SewerageSupplyRateStatusInfo {
returnOpinionSm: string | null;
}
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateItem {
sido: string;
gugun: string;
@@ -103,7 +102,7 @@ export interface SewerageSupplyRateResponseData {
export type SewerageSupplyRateApiResponse = ApiResponse<SewerageSupplyRateResponseData> & {
message: string;
};
}
export interface SewerageSupplyRateRequestParams {
sido?: string;
@@ -111,6 +110,14 @@ export interface SewerageSupplyRateRequestParams {
bsnmCd?: string;
yr?: string;
demoYr?: string;
mode?: string;
}
// 하수도 통계 입력 기간 조회
export interface CheckInputPeriodResponse {
status: string;
message: string;
data: boolean;
}
export interface StatisticsInputService {
+1
View File
@@ -68,6 +68,7 @@ export const ENDPOINT = {
COMMON: "/st-management-service/api/v1/common",
INPUT: "/st-input-service/api/v1/input",
REVIEW: "/st-management-service/api/v1/review",
INPUTCOMMON: "/st-input-service/api/v1/common",
}
};