577 lines
23 KiB
TypeScript
577 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import useSWR from "swr";
|
|
|
|
import { useAlertStore, useUserStore } from "@/shared/lib";
|
|
import {
|
|
fetchSearchCondition,
|
|
fetchSearchConditionInput,
|
|
SEARCH_CONDITION_SWR_KEY,
|
|
} from "@/shared/lib/use-search-condition-sido-list";
|
|
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";
|
|
|
|
import { doList, doNameList } from "@/components/util/column/input/DoList";
|
|
|
|
import {
|
|
defaultSearchFormProps,
|
|
type SearchFormClsfCd,
|
|
type SearchFormProps,
|
|
type SearchFormValues,
|
|
} from "../type/SearchForm.type";
|
|
|
|
function isDoListBsnm(bsnmCd: string | undefined): boolean {
|
|
return Boolean(bsnmCd && doList.includes(bsnmCd));
|
|
}
|
|
|
|
function isDoSido(sido?: { bsnmCd?: string; sido?: string }): boolean {
|
|
if (!sido) return false;
|
|
if (isDoListBsnm(sido.bsnmCd)) return true;
|
|
return Boolean(sido.sido && doNameList.includes(sido.sido));
|
|
}
|
|
|
|
function isAllGugunValue(gugun: string | undefined): boolean {
|
|
const value = (gugun ?? "").trim();
|
|
return !value || value === "ALL" || value === "전체";
|
|
}
|
|
|
|
function firstConcreteSgg(list: Sgg[] | undefined): Sgg | undefined {
|
|
return (list ?? []).find((sgg) => !isAllGugunValue(sgg.gugun));
|
|
}
|
|
|
|
const getSggListFetcher = async (
|
|
key: [string, string, string, "Y" | "N"],
|
|
): Promise<ApiResponse<Sgg[]>> => {
|
|
const [kind, sidoName, instSeCd, gugunSkipYn] = key;
|
|
const params = { instSeCd, gugunSkipYn };
|
|
if (kind === "sgg-all") {
|
|
return statisticsApi.getSggAllList(sidoName, params);
|
|
}
|
|
return statisticsApi.getSggList(sidoName, params);
|
|
};
|
|
|
|
function buildSearchFormValues(
|
|
props: SearchFormProps,
|
|
sidoList: ApiResponse<SearchCondition> | undefined,
|
|
defaultYear: number,
|
|
): SearchFormValues {
|
|
const d = props.defaultValues;
|
|
const initialClsfCd: SearchFormClsfCd =
|
|
d?.clsfCd === "ALL" || d?.clsfCd === "INPT" || d?.clsfCd === "VRFC"
|
|
? d.clsfCd
|
|
: "ALL";
|
|
|
|
if (d?.sido) {
|
|
const sidoItem = sidoList?.data?.sidoList?.find(
|
|
(item) => item.sido === d.sido,
|
|
);
|
|
const gugun =
|
|
d.gugun ||
|
|
(sidoItem && !isDoSido(sidoItem) ? "ALL" : "");
|
|
return {
|
|
sido: d.sido,
|
|
gugun,
|
|
year: d.year || defaultYear.toString(),
|
|
order: d.order || "",
|
|
fctName: d.fctName || "",
|
|
bsnmCd: d.bsnmCd || "",
|
|
isDetail: false,
|
|
isSummary: d.isSummary || "",
|
|
clsfCd: initialClsfCd,
|
|
capacityGbn: d.capacityGbn || "ALL",
|
|
};
|
|
}
|
|
|
|
if (props.defaultRegionAll) {
|
|
return {
|
|
sido: "ALL",
|
|
gugun: "ALL",
|
|
year: d?.year || defaultYear.toString(),
|
|
order: d?.order || "",
|
|
fctName: d?.fctName || "",
|
|
bsnmCd: "",
|
|
isDetail: false,
|
|
isSummary: d?.isSummary || "",
|
|
clsfCd: initialClsfCd,
|
|
capacityGbn: d?.capacityGbn || "ALL",
|
|
};
|
|
}
|
|
|
|
const first = sidoList?.data?.sidoList?.[0];
|
|
const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first;
|
|
|
|
return {
|
|
sido: useFirstSido ? first.sido : "",
|
|
gugun:
|
|
useFirstSido && first && !isDoSido(first) ? "ALL" : "",
|
|
year: d?.year || defaultYear.toString(),
|
|
order: d?.order || "",
|
|
fctName: d?.fctName || "",
|
|
bsnmCd: useFirstSido ? first.bsnmCd : "",
|
|
isDetail: false,
|
|
isSummary: d?.isSummary || "",
|
|
clsfCd: initialClsfCd,
|
|
capacityGbn: d?.capacityGbn || "ALL",
|
|
};
|
|
}
|
|
|
|
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 isInput = Boolean(props.isInput);
|
|
const isDetailInputId = props.tabKey
|
|
? `${props.tabKey}-isDetail`
|
|
: "isDetail";
|
|
const sidoListKind = isInput ? "input" : "default";
|
|
|
|
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
|
SEARCH_CONDITION_SWR_KEY(instSeCd, sidoListKind),
|
|
isInput ? fetchSearchConditionInput : fetchSearchCondition,
|
|
{
|
|
revalidateOnFocus: false,
|
|
revalidateOnReconnect: false,
|
|
revalidateIfStale: false,
|
|
dedupingInterval: 2000,
|
|
shouldRetryOnError: false,
|
|
onSuccess: (data) => {
|
|
if (data.status !== "success") {
|
|
alert(`시도 목록을 불러오는데 실패했습니다.
|
|
새로고침 후 다시 시도해주세요.`);
|
|
}
|
|
},
|
|
onError: () => {
|
|
alert(`시도 목록을 불러오는데 실패했습니다.
|
|
새로고침 후 다시 시도해주세요.`);
|
|
},
|
|
},
|
|
);
|
|
|
|
// 마운트 시 SWR 캐시의 시도 목록으로 초기값 설정 (탭 닫았다 열 때 API 재호출 없이 동작)
|
|
const [values, setValues] = useState<SearchFormValues>(() =>
|
|
buildSearchFormValues(props, sidoList, defaultYear),
|
|
);
|
|
|
|
// 최초 진입만 시도 API가 늦게 오는 경우
|
|
useEffect(() => {
|
|
if (props.defaultRegionAll) return;
|
|
if (props.defaultValues?.sido) return;
|
|
const first = sidoList?.data?.sidoList?.[0];
|
|
if (!first || !mergedProps.sido) return;
|
|
setValues((prev) =>
|
|
prev.sido
|
|
? prev
|
|
: {
|
|
...prev,
|
|
sido: first.sido,
|
|
gugun: isDoSido(first) ? "" : "ALL",
|
|
bsnmCd: first.bsnmCd,
|
|
},
|
|
);
|
|
}, [
|
|
sidoList,
|
|
props.defaultValues?.sido,
|
|
props.defaultRegionAll,
|
|
mergedProps.sido,
|
|
]);
|
|
|
|
const selectedSido = sidoList?.data?.sidoList?.find(
|
|
(item) => item.sido === values.sido,
|
|
);
|
|
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
|
|
const useSggAllList =
|
|
gugunSkipYn === "N" && !isDoListBsnm(selectedSido?.bsnmCd);
|
|
const sggKey: [string, string, string, "Y" | "N"] | null =
|
|
values.sido && values.sido !== "ALL" && sidoList?.data
|
|
? [
|
|
useSggAllList ? "sgg-all" : "sgg-list",
|
|
values.sido,
|
|
instSeCd,
|
|
gugunSkipYn,
|
|
]
|
|
: 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 isDoSelected = isDoSido(selectedSido);
|
|
const firstConcreteSggOption = firstConcreteSgg(sggOptions);
|
|
// 도: 빈 값일 때만 첫 시군구로 보이게. ALL은 사용자가 고른 전체 유지
|
|
const gugunSelectValue =
|
|
isDoSelected && !values.gugun
|
|
? (firstConcreteSggOption?.gugun ?? undefined)
|
|
: values.gugun || undefined;
|
|
|
|
// 도: 초기/시도 변경(빈 값)만 첫 시군구. 전체(ALL)는 유지. 그 외: 전체
|
|
useEffect(() => {
|
|
if (!mergedProps.gugun) return;
|
|
if (props.defaultRegionAll) return;
|
|
if (!values.sido || values.sido === "ALL") return;
|
|
if (!selectedSido) return;
|
|
|
|
if (!isDoSelected) {
|
|
setValues((prev) => {
|
|
if (prev.gugun) return prev;
|
|
return {
|
|
...prev,
|
|
gugun: "ALL",
|
|
bsnmCd: selectedSido.bsnmCd || prev.bsnmCd,
|
|
};
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!firstConcreteSggOption) return;
|
|
|
|
setValues((prev) => {
|
|
if (prev.gugun) return prev;
|
|
return {
|
|
...prev,
|
|
gugun: firstConcreteSggOption.gugun,
|
|
bsnmCd: firstConcreteSggOption.bsnmCd,
|
|
};
|
|
});
|
|
}, [
|
|
firstConcreteSggOption,
|
|
isDoSelected,
|
|
mergedProps.gugun,
|
|
props.defaultRegionAll,
|
|
selectedSido,
|
|
values.sido,
|
|
]);
|
|
|
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
props.onSubmit?.(values);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setValues(buildSearchFormValues(props, sidoList, defaultYear));
|
|
props.onReset?.();
|
|
};
|
|
|
|
const handleSidoChange = (value: string) => {
|
|
const selected =
|
|
value === "ALL"
|
|
? undefined
|
|
: sidoList?.data?.sidoList?.find((v) => v.sido === value);
|
|
const selectedBsnm = selected?.bsnmCd || "";
|
|
const gugunAll =
|
|
props.defaultRegionAll || value === "ALL" || !isDoSido(selected);
|
|
setValues((prev) => ({
|
|
...prev,
|
|
sido: value,
|
|
gugun: gugunAll ? "ALL" : "",
|
|
bsnmCd: selectedBsnm,
|
|
}));
|
|
};
|
|
|
|
const handleSggChange = (value: string) => {
|
|
const listForBsnm = sggList?.data ?? [];
|
|
const selectedBsnm =
|
|
value === "ALL"
|
|
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
|
|
?.bsnmCd || ""
|
|
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
|
|
|
|
setValues((prev) => ({
|
|
...prev,
|
|
gugun: value,
|
|
bsnmCd: selectedBsnm,
|
|
}));
|
|
};
|
|
|
|
useEffect(() => {
|
|
props.onValuesChange?.(values);
|
|
}, [values]);
|
|
|
|
return (
|
|
<div className="card card-border">
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="flex items-center gap-8 p-2">
|
|
{mergedProps.sido && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">시도</Label>
|
|
<Select
|
|
value={values.sido}
|
|
onValueChange={handleSidoChange}
|
|
>
|
|
<SelectTrigger className="w-[12rem]">
|
|
<SelectValue
|
|
placeholder={sidoSelectText}
|
|
></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">
|
|
{sidoSelectText}
|
|
</SelectItem>
|
|
{sidoList?.data?.sidoList?.map((sido) => (
|
|
<SelectItem
|
|
key={sido.adstDstrCd}
|
|
value={sido.sido}
|
|
>
|
|
{sido.sido}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.gugun && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">시군구</Label>
|
|
<Select
|
|
value={gugunSelectValue}
|
|
onValueChange={handleSggChange}
|
|
>
|
|
<SelectTrigger className="w-[12rem]">
|
|
<SelectValue placeholder="전체"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체</SelectItem>
|
|
{sggOptions?.map((sgg) => (
|
|
<SelectItem
|
|
key={sgg.adstDstrCd}
|
|
value={sgg.gugun}
|
|
>
|
|
{sgg.gugun}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.year && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">년도</Label>
|
|
<Select
|
|
value={values.year}
|
|
onValueChange={(value) =>
|
|
setValues({ ...values, year: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[10rem]">
|
|
<SelectValue placeholder="선택"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sidoList?.data?.yearList.map((year) => (
|
|
<SelectItem
|
|
key={year}
|
|
value={String(year)}
|
|
>
|
|
{year}년
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.clsf && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">분류</Label>
|
|
<Select
|
|
value={values.clsfCd ?? "ALL"}
|
|
onValueChange={(value) =>
|
|
setValues({
|
|
...values,
|
|
clsfCd: value as SearchFormClsfCd,
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[10rem]">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체</SelectItem>
|
|
<SelectItem value="INPT">입력</SelectItem>
|
|
<SelectItem value="VRFC">검증</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.fctName && (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
htmlFor="fctNmSearchInput"
|
|
className="mr-2 text-label"
|
|
>
|
|
시설명
|
|
</Label>
|
|
<Input
|
|
className="mr-4 h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem]"
|
|
id="fctNmSearchInput"
|
|
value={values.fctName}
|
|
onChange={(e) =>
|
|
setValues({
|
|
...values,
|
|
fctName: e.target.value,
|
|
})
|
|
}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
)}
|
|
{mergedProps.capacityGbn && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">구분</Label>
|
|
<Select
|
|
value={values.capacityGbn}
|
|
onValueChange={(value) =>
|
|
setValues({ ...values, capacityGbn: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[10rem]">
|
|
<SelectValue placeholder="전체"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체</SelectItem>
|
|
<SelectItem value="500톤이상">
|
|
500톤이상
|
|
</SelectItem>
|
|
<SelectItem value="500톤미만">
|
|
500톤미만
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.isDetail && (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="cursor-pointer text-label"
|
|
htmlFor={isDetailInputId}
|
|
>
|
|
세부 데이터 포함
|
|
</Label>
|
|
<Checkbox
|
|
id={isDetailInputId}
|
|
checked={values.isDetail}
|
|
onCheckedChange={(value) =>
|
|
setValues({
|
|
...values,
|
|
isDetail: value === true,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
{mergedProps.isSummary && (
|
|
<div className="flex items-center gap-2">
|
|
<Label
|
|
className="cursor-pointer text-label"
|
|
title="시·군·구 및 읍·면·동 변경에 따라 인구 및 관로 등 공표된 통계수치와 일부 상이할 수 있으니 유의 바랍니다."
|
|
>
|
|
<i className="diveicon di-exclamation-circle mr-1" />
|
|
요약표 포함
|
|
</Label>
|
|
<Select
|
|
value={values.isSummary}
|
|
onValueChange={(value) =>
|
|
setValues({ ...values, isSummary: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[10rem]">
|
|
<SelectValue placeholder="선택 안함"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">
|
|
선택 안함
|
|
</SelectItem>
|
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(
|
|
(i) => (
|
|
<SelectItem
|
|
value={i.toString()}
|
|
key={i.toString()}
|
|
>{`${i}개년`}</SelectItem>
|
|
),
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
{mergedProps.order && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label">차수</Label>
|
|
<Select
|
|
value={values.order}
|
|
onValueChange={(value) =>
|
|
setValues({ ...values, order: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[10rem]">
|
|
<SelectValue placeholder="전체"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
<div className="ml-auto flex items-center gap-2">
|
|
{mergedProps.isReset && (
|
|
<Button
|
|
color="white"
|
|
className="h-[3.2rem] w-[96px]"
|
|
type="button"
|
|
onClick={handleReset}
|
|
>
|
|
초기화
|
|
</Button>
|
|
)}
|
|
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
|
{props.searchButtonText
|
|
? props.searchButtonText
|
|
: "조회"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SearchForm;
|