420 lines
17 KiB
TypeScript
420 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import useSWR from "swr";
|
|
|
|
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";
|
|
|
|
import {
|
|
defaultSearchFormProps,
|
|
type SearchFormClsfCd,
|
|
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, string, string],
|
|
): Promise<ApiResponse<Sgg[]>> => {
|
|
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
|
return statisticsApi.getSggList(key[1], {
|
|
instSeCd: key[2],
|
|
gugunSkipYn,
|
|
});
|
|
};
|
|
|
|
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) {
|
|
return {
|
|
sido: d.sido,
|
|
gugun: d.gugun || "",
|
|
year: d.year || defaultYear.toString(),
|
|
order: d.order || "",
|
|
fctName: d.fctName || "",
|
|
bsnmCd: d.bsnmCd || "",
|
|
isDetail: false,
|
|
isSummary: d.isSummary || "",
|
|
clsfCd: initialClsfCd,
|
|
};
|
|
}
|
|
|
|
const first = sidoList?.data?.sidoList?.[0];
|
|
const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first;
|
|
|
|
return {
|
|
sido: useFirstSido ? first.sido : "",
|
|
gugun: "",
|
|
year: d?.year || defaultYear.toString(),
|
|
order: d?.order || "",
|
|
fctName: d?.fctName || "",
|
|
bsnmCd: useFirstSido ? first.bsnmCd : "",
|
|
isDetail: false,
|
|
isSummary: d?.isSummary || "",
|
|
clsfCd: initialClsfCd,
|
|
};
|
|
}
|
|
|
|
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", instSeCd],
|
|
getSidoListFetcher,
|
|
{
|
|
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.defaultValues?.sido) return;
|
|
const first = sidoList?.data?.sidoList?.[0];
|
|
if (!first || !mergedProps.sido) return;
|
|
setValues((prev) =>
|
|
prev.sido
|
|
? prev
|
|
: {
|
|
...prev,
|
|
sido: first.sido,
|
|
gugun: "",
|
|
bsnmCd: first.bsnmCd,
|
|
},
|
|
);
|
|
}, [sidoList, props.defaultValues?.sido, mergedProps.sido]);
|
|
|
|
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,
|
|
{
|
|
revalidateOnFocus: false,
|
|
revalidateOnReconnect: false,
|
|
revalidateIfStale: false,
|
|
dedupingInterval: 2000,
|
|
shouldRetryOnError: false,
|
|
onSuccess: (data) => {
|
|
if (data.status !== "success") {
|
|
alert(`시군구 목록을 불러오는데 실패했습니다.
|
|
새로고침 후 다시 시도해주세요.`);
|
|
}
|
|
},
|
|
onError: () => {
|
|
alert(`시군구 목록을 불러오는데 실패했습니다.
|
|
새로고침 후 다시 시도해주세요.`);
|
|
},
|
|
},
|
|
);
|
|
const sggOptions = sggList?.data ?? [];
|
|
|
|
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 selectedBsnm =
|
|
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || "";
|
|
setValues((prev) => ({
|
|
...prev,
|
|
sido: value,
|
|
gugun: "",
|
|
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={values.gugun}
|
|
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.isDetail && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
|
세부 데이터 포함
|
|
</Label>
|
|
<Checkbox
|
|
id="isDetail"
|
|
checked={values.isDetail}
|
|
onCheckedChange={(value) =>
|
|
setValues({
|
|
...values,
|
|
isDetail: value === true,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
{mergedProps.isSummary && (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-label cursor-pointer">
|
|
요약표 포함
|
|
</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">
|
|
<Button
|
|
color="white"
|
|
className="h-[3.2rem] w-[96px]"
|
|
type="button"
|
|
onClick={handleReset}
|
|
>
|
|
초기화
|
|
</Button>
|
|
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
|
조회
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SearchForm;
|