feat, fix : SidoSigunguSelectBox 및 YearRangeSelectBox, SelectBox2 컴포넌트 추가, SearchModal.tsx 스타일 수정

- 선택한 시도에 따라 동적으로 시군구 옵션이 바뀌는 SidoSigunguSelectBox 구현
- 시작/종료년도 유효성 검증이 포함된 YearRangeSelectBox 구현
- 드롭다운 z-index 개선 및 재사용 가능한 SelectBox2 컴포넌트 도입
- SearchModal.tsx 태그들 공통 컴포넌트로 변환하여 스타일 통일
This commit is contained in:
swcho
2025-05-14 17:53:38 +09:00
parent 037a04f1c9
commit 7a9038bbe2
4 changed files with 568 additions and 524 deletions
@@ -0,0 +1,159 @@
import React, { useMemo } from "react";
import { SelectBox2 } from "@common/business_components/ui/select/index-2";
interface Option {
id: number;
name: string;
}
interface SidoSigunguSelectBoxProps {
sido: number;
sigungu: number;
onSidoChange: (id: number) => void;
onSigunguChange: (id: number) => void;
className?: string;
}
const sidoList: { id: number, name: string }[] = [
{ id: 0, name: '선택' },
{ id: 26, name: '부산광역시' },
{ id: 28, name: '인천광역시' },
{ id: 30, name: '대전광역시' },
{ id: 41, name: '경기도' },
{ id: 42, name: '강원도' },
{ id: 44, name: '충청남도' },
{ id: 45, name: '전라북도' },
{ id: 46, name: '전라남도' },
{ id: 47, name: '경상북도' },
{ id: 48, name: '경상남도' },
];
const sigunguMap: Record<number, { id: number; name: string }[]> = {
26: [
{ id: 0, name: '선택' },
// ...부산산광역시의 시군구 추가
],
28: [
{ id: 0, name: '선택' },
// ...인천광역시의 시군구 추가
],
30: [
{ id: 0, name: '선택' },
// ...대전광역시의 시군구 추가
],
41: [
{ id: 0, name: '선택' },
{ id: 41820, name: '가평군' },
{ id: 41550, name: '안성시' },
{ id: 41830, name: '양평군' },
{ id: 41670, name: '여주시' },
{ id: 41500, name: '이천시' },
{ id: 41430, name: '의왕시' },
{ id: 41150, name: '의정부시' },
{ id: 41480, name: '파주시' },
{ id: 41650, name: '포천시' },
// ...경기도의 시군구 추가
],
42: [
{ id: 0, name: '선택' },
{ id: 42170, name: '동해시' },
{ id: 42110, name: '춘천시' },
{ id: 42720, name: '홍천군' },
// ...강원도의 시군구 추가
],
44: [
{ id: 0, name: '선택' },
{ id: 44770, name: '서천군' },
// ...충청남도의 시군구 추가
],
45: [
{ id: 0, name: '선택' },
{ id: 45130, name: '군산시' },
{ id: 45190, name: '남원시' },
{ id: 45730, name: '무주군' },
{ id: 45800, name: '부안군' },
{ id: 45770, name: '순창군' },
{ id: 45140, name: '익산시' },
{ id: 45750, name: '임실군' },
{ id: 45710, name: '완주군' },
{ id: 45110, name: '전주시' },
// ...전라북도의 시군구 추가
],
46: [
{ id: 0, name: '선택' },
{ id: 46810, name: "강진군" },
{ id: 46770, name: "고흥군" },
{ id: 46720, name: "곡성군" },
{ id: 46730, name: "구례군" },
{ id: 46230, name: "광양시" },
{ id: 46170, name: "나주시" },
{ id: 46110, name: "목포시" },
{ id: 46840, name: "무안군" },
{ id: 46150, name: "순천시" },
{ id: 46910, name: "신안군" },
{ id: 46130, name: "여수시" },
{ id: 46890, name: "완도군" },
// ...전라남도의 시군구 추가
],
47: [
{ id: 0, name: '선택' },
{ id: 47130, name: "경주시" },
{ id: 47830, name: "고령군" },
{ id: 47190, name: "구미시" },
{ id: 47720, name: "군위군" },
{ id: 47150, name: "김천시" },
{ id: 47280, name: "문경시" },
{ id: 47920, name: "봉화군" },
{ id: 47250, name: "상주시" },
{ id: 47770, name: "영덕군" },
{ id: 47760, name: "영양군" },
{ id: 47210, name: "영주시" },
{ id: 47900, name: "예천군" },
{ id: 47930, name: "울진군" },
{ id: 47730, name: "의성군" },
{ id: 47750, name: "청송군" },
{ id: 47850, name: "칠곡군" },
{ id: 47110, name: "포항시" },
// ...경상북도의 시군구 추가
],
48: [
{ id: 0, name: '선택' },
{ id: 48310, name: "거제시" },
{ id: 48250, name: "김해시" },
{ id: 48270, name: "밀양시" },
{ id: 48220, name: "통영시" },
// ...경상남도의 시군구 추가
],
// ...나머지 시도별 시군구 추가
};
export function SidoSigunguSelectBox({
sido,
sigungu,
onSidoChange,
onSigunguChange,
className = "",
}: SidoSigunguSelectBoxProps) {
// 선택된 시도에 따라 시군구 리스트를 가져옴
const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '선택' }], [sido]);
return (
<div className={`flex items-center ${className}`}>
<SelectBox2
className="w-50 mr-8"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidoList.map(opt => ({ val: opt.id, text: opt.name }))}
value={sido}
onSelectValue={val => onSidoChange(Number(val))}
/>
<SelectBox2
className="w-50 mr-8"
selectClass="rounded-lg bg-white"
label="시군구"
selectArray={sidoList.map(opt => ({ val: opt.id, text: opt.name }))}
value={sigungu}
onSelectValue={val => onSigunguChange(Number(val))}
/>
</div>
);
}
@@ -0,0 +1,69 @@
import React from "react";
import { SelectBox2 } from "@common/business_components/ui/select/index-2";
interface YearRangeSelectBoxProps {
start: string;
end: string;
onChange: (range: { start: string; end: string }) => void;
startLabel?: string;
endLabel?: string;
minYear?: number;
maxYear?: number;
className?: string;
selectClass?: string;
}
export function YearRangeSelectBox({
start,
end,
onChange,
startLabel = "시작년도",
endLabel = "",
minYear = 1900,
maxYear = new Date().getFullYear(),
className = "",
selectClass = "rounded-lg bg-white"
}: YearRangeSelectBoxProps) {
// 시작년도 배열
const getStartYearArray = () => {
const years = Array.from({ length: maxYear - minYear + 1 }, (_, i) =>
(maxYear - i).toString()
).filter(y => !end || parseInt(y) <= parseInt(end));
return [
{ val: "0", text: "년" },
...years.map(y => ({ val: y, text: y })),
];
};
// 종료년도 배열
const getEndYearArray = () => {
const years = Array.from({ length: maxYear - minYear + 1 }, (_, i) =>
(maxYear - i).toString()
).filter(y => !start || parseInt(y) >= parseInt(start));
return [
{ val: "9999", text: "년" },
...years.map(y => ({ val: y, text: y })),
];
};
return (
<div className={`flex items-center ${className}`}>
<SelectBox2
className="w-50 mr-3"
selectClass={selectClass}
label={startLabel}
selectArray={getStartYearArray()}
onSelectValue={val => onChange({ start: String(val), end })}
value={start}
/>
<span className="font-semibold text-xl mx-2">~</span>
<SelectBox2
className="w-50 mr-8"
selectClass={selectClass}
label={endLabel}
selectArray={getEndYearArray()}
onSelectValue={val => onChange({ start, end: String(val) })}
value={end}
/>
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
/**
* @remarks
* 이 버전은 SearchModal에서 사용하기 위해 `SelectContent`의 z-index를 `z-[101]`로 수정하였습니다.
*/
import { Label } from "@common/components";
import React from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox";
/** 차트 기본 스타일 */
interface SelectBox1 {
text:string,
val:string|number
}
const SelectBox2 = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
label:string,
selectClass:string|undefined,
className:string,
selectArray:SelectBox1[],
onSelectValue:(val:string|number)=>void,
value?: string|number
}
>(({ label, className, selectClass, selectArray, onSelectValue, value }, ref)=>{
const placeHolder = selectArray?.[0]?.text ?? "";
return (
<div className={className+" flex items-center flex-nowrap"} ref={ref}>
{label && <Label className="inline-block w-auto whitespace-nowrap text-xl text-gray-600 mr-2">{label}</Label>}
<Select onValueChange={onSelectValue} value={value !== undefined ? String(value) : undefined}>
<SelectTrigger className={selectClass?selectClass:"flex-1 p-2 border rounded"}>
<SelectValue placeholder={placeHolder} />
</SelectTrigger>
<SelectContent className="z-[101]">
{selectArray.map((selectArray) => (
<SelectItem key={selectArray.val} value={String(selectArray.val)}>{selectArray.text}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
})
export { SelectBox2 };