Files
frontend/apps/st/src/components/module/SearchForm.tsx
T

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;