diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index a7319cb01..eee09fb65 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -109,6 +109,35 @@ export default function LineBarchart({ const lineMaxValue = getMaxValue(chartData, 'lineVal'); const barMaxValue = getMaxValue(chartData, 'barVal'); + /** + * 최대값과 구간 수에 따른 범례 만들기 + * maxValue : 최대값, segments : 구간 수 (구간수가 5면 눈금은 6) + */ + const makeNiceScale = (maxValue: number, segments = 5) => { + // 최대값이 이상할 경우 0, 0으로 반환 + if (!isFinite(maxValue) || maxValue <= 0) { + return { max: 0, ticks: [0] }; + } + + const exponent = Math.floor(Math.log10(maxValue)); + const base = Math.pow(10, exponent); + const fraction = maxValue / base; + + let niceFraction: number; + if (fraction <= 1) niceFraction = 1; + else if (fraction <= 2) niceFraction = 3; + else if (fraction <= 5) niceFraction = 5; + else niceFraction = 10; + + const niceMax = niceFraction * base; // round up to 1/2/5 * 10^k + const step = niceMax / segments; + const ticks = Array.from({ length: segments + 1 }, (_, i) => Number((i * step).toFixed(10))); + return { max: niceMax, ticks }; + }; + + const leftScale = makeNiceScale(lineMaxValue, 5); + const rightScale = makeNiceScale(barMaxValue, 5); + const getTicks = (data : any []) => { const monthSet = new Set(); const rtnData : string[] = []; @@ -182,23 +211,26 @@ export default function LineBarchart({ {yFieldText && typeof value === "number" ? value.toLocaleString() : value } + domain={[0, leftScale.max]} + ticks={leftScale.ticks} orientation="left" label={(props) => } + />} {/* 오른쪽 Y축: barVal */} {yFieldText2 && typeof value === "number" ? value.toLocaleString() : value } - label={(props) => } + domain={[0, rightScale.max]} + ticks={rightScale.ticks} + label={(props) => } reversed={true} />} } /> diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index c7e8442e9..bf8b1d128 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -52,7 +52,6 @@ function Gis({ isPlaying = false, setIsPlaying = () => { }, targetLayerTypes = [], - orderedLayers = [], checkInterval = 3000, height: height = "h-[calc(100vh-5rem)]", onOpenPopup = (data) => { }, @@ -290,7 +289,6 @@ function Gis({ isPlaying={isPlaying} setIsPlaying={setIsPlaying} targetLayerTypes={targetLayerTypes} - orderedLayers={orderedLayers} checkInterval={checkInterval} onProgressChange={handleProgressChange} /> diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 54baddba5..197827010 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -39,7 +39,6 @@ export default React.memo(function GisTree({ isPlaying = false, setIsPlaying = () => { }, targetLayerTypes = [], - orderedLayers = [], checkInterval = 3000, onProgressChange = () => { }, }) { @@ -313,14 +312,15 @@ export default React.memo(function GisTree({ // orderedLayers 순서를 우선 적용하면서, 타입 필터에 맞는 타겟 노드만 선별 const getTargetNodesInOrder = () => { - const orderedNodes = []; - orderedLayers.forEach((lyrSn) => { - const node = nodes.find( - (n) => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm) - ); - if (node) orderedNodes.push(node); + const target = nodes.filter((n) => n.data?.actlLyrYn === 'N' && layerTypes.includes(n.data?.lyrTyNm)); + + // drawOrdr 값을 기준으로 오름차순 정렬 + target.sort((a, b) => { + const aOrder = a.data?.drawOrdr || 0; + const bOrder = b.data?.drawOrdr || 0; + return aOrder - bOrder; }); - return orderedNodes; + return target; }; const targetNodes = getTargetNodesInOrder(); @@ -510,8 +510,8 @@ export default React.memo(function GisTree({ {/* 사이드 패널 */}
= { - tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설 - tb_wr_bdst_ruse_fclt: - "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post - tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get - tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get - tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get - tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get - tb_wr_tmpr_dmd_co: - "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get -}; - -interface SearchBarProps { - onFeatureSelect: (feature: any) => void; - gisMaps: React.RefObject; - facilityTypes: { text: string; val: string }[]; -} - -export const ComboSearchBar = ({ - onFeatureSelect, - gisMaps, - facilityTypes, -}: SearchBarProps) => { - const [sidoValue, setSidoValue] = useState("0"); - const [sigunValue, setSigunValue] = useState("0"); - const [sidoData, setSidoData] = useState([ - { text: "선택", val: "0" }, - ]); - const [sigunData, setSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - const [filteredSigunData, setFilteredSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - - const [facilityData, setFacilityData] = useState([ - { text: "선택", val: "0" }, - ]); - const [facilityValue, setFacilityValue] = useState("0"); - - const [facilityType, setFacilityType] = useState( - facilityTypes?.length === 1 && facilityTypes[0] - ? facilityTypes[0].val - : "0", - ); - - //시도, 시군구 조회 - const fetchSido = async () => { - const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`; - const res = await fetch(url).then(res => res.json()); - - const list = res.features - .map((f: any) => ({ - text: f.properties.mctpv_korn_nm, - val: f.properties.mctpv_cd, - })).sort((a, b) => { - // 커스텀 순서 정의 - const customOrder = [ - "서울특별시", - "부산광역시", - "대구광역시", - "인천광역시", - "광주광역시", - "대전광역시", - "울산광역시", - "세종특별자치시", - "경기도", - "강원특별자치도", // 경기도 다음에 배치 - "충청북도", - "충청남도", - "전북특별자치도", // 충청남도 다음에 배치 - "전라남도", - "경상북도", - "경상남도", - "제주특별자치도" - ]; - - const indexA = customOrder.indexOf(a.text); - const indexB = customOrder.indexOf(b.text); - - // 둘 다 커스텀 순서에 있으면 해당 순서로 - if (indexA !== -1 && indexB !== -1) { - return indexA - indexB; - } - - // 하나만 커스텀 순서에 있으면 그것을 앞으로 - if (indexA !== -1) return -1; - if (indexB !== -1) return 1; - - // 둘 다 커스텀 순서에 없으면 기본 알파벳 순서 - return a.text.localeCompare(b.text, 'ko'); - }); - setSidoData([{ text: "선택", val: "0" }, ...list]); - }; - - const fetchSigungu = async () => { - const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`; - const res = await fetch(url).then(res => res.json()); - - const list = res.features - .map((f: any) => ({ - text: f.properties.sgg_korn_nm, - val: f.properties.sgg_cd, - })) - .sort((a, b) => a.text.localeCompare(b.text, 'ko')); - setSigunData(list); - }; - - //시설명 콤보박스 조회 - const fetchFacilitiesBySggNm = async ( - sggCd: string, - sggNm: string, - sidoNm: string, - currentFacilityType?: string, - ) => { - const type = currentFacilityType ?? facilityType; - if (!type || type === "0") return; - - const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; - if (!baseUrlTemplate) return; - - const year = new Date().getFullYear().toString(); - let url = baseUrlTemplate - .replace("{year}", year) - .replace("{sggCd}", sggCd) - .replace("{sidoNm}", encodeURIComponent(sidoNm)) - .replace("{sggNm}", encodeURIComponent(sggNm)); - - let list: SelectDataType[] = []; - - if ( - type === "tb_il_swage_prcs_fclt" //하수처리시설 - ) { - const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`; - url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`; - const res = await fetch(url); - const json = await res.json(); - list = json.features.map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", - val: f.id, - })); - } else if ( - type === "tb_wr_bdst_ruse_fclt" // 재이용시설 - ) { - //재용에서 참조하는 하수처리시설 목록 - const paramsRes = await fetch("/reuse-service/reuseFc/pubFc"); - const paramsJson = await paramsRes.json(); - const params = paramsJson.data; - - const res = await fetch(url, { - method: "POST", - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", - val: item.pbnstRuseFcltId, - })); - } else if ( - type === "tb_wr_ruse_fclt_dmd_co" //수요처 - ) { - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.dmdInfoId, - })); - } else if ( - type === "tb_wr_tmpr_dmd_co" //잠재수요처 - ) { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.fctryMngSn, - })); - } else { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", - val: item.bdstInfoSn, - })); - } - - setFacilityData([{ text: "선택", val: "0" }, ...list]); - setFacilityValue("0"); - }; - - //시도, 시군구 초기 데이터 조회 - useEffect(() => { - fetchSido(); - fetchSigungu(); - }, []); - - //시군구 데이터 초기화 - const initializeSigunData = useCallback( - (sidoCd: string) => { - if (sidoCd === "0") { - setFilteredSigunData([{ text: "선택", val: "0" }]); - setSigunValue("0"); - return; - } - const prefix = sidoCd.substring(0, 2); - const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]); - setSigunValue("0"); - }, - [sigunData], - ); - - //콤보박스 선택시 연관 콤보박스 처리 - const onSelectValue = useCallback( - (val: string, type: string) => { - if (type === "ctpvCd") { - setSidoValue(val); - initializeSigunData(val); - setFacilityData([{ text: "선택", val: "0" }]); - setFacilityValue("0"); - } else if (type === "sggCd") { - setSigunValue(val); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = sigunData.find((item) => item.val === val)?.text || ""; - const sggCd = sigunData.find((item) => item.val === val)?.val || ""; - if (sggNm && sidoNm) - fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType); - } else if (type === "facilityType") { - setFacilityType(val); - setFacilityValue("0"); - setFacilityData([{ text: "선택", val: "0" }]); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = - sigunData.find((item) => item.val === sigunValue)?.text || ""; - const sggCd = - sigunData.find((item) => item.val === sigunValue)?.val || ""; - if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val); - } else if (type === "fcltCd") { - setFacilityValue(val); - } - }, - [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue], - ); - - //조회 버튼 클릭 - const handleSearch = () => { - if (!gisMaps.current || facilityValue === "0") return; - const layers = gisMaps.current.getLayers().getArray(); - for (const layer of layers) { - const source = layer.getSource?.(); - const feature = source?.getFeatureById?.(facilityValue); - if (feature) { - onFeatureSelect(feature); - return; - } - } - alert("해당 처리시설을 지도에서 찾을 수 없습니다."); - }; - - return ( -
-
-
-
- {facilityTypes.length > 1 && ( - - onSelectValue(val.toString(), "facilityType") - } - value={facilityType} - /> - )} - - onSelectValue(val.toString(), "ctpvCd") - } - value={sidoValue} - /> - - onSelectValue(val.toString(), "sggCd") - } - value={sigunValue} - /> - - onSelectValue(val.toString(), "fcltCd") - } - value={facilityValue} - /> -
-
- -
-
-
-
- ); -}; diff --git a/packages/common/business_components/src/ui/map/searchBar/index.tsx b/packages/common/business_components/src/ui/map/searchBar/index.tsx index 4ba24de3a..d578b2894 100644 --- a/packages/common/business_components/src/ui/map/searchBar/index.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/index.tsx @@ -1,3 +1,2 @@ -export * from "./comboSearchBar"; export * from "./procSearchBar"; export * from './searchBar' diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx index 465471715..79e195925 100644 --- a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx @@ -18,19 +18,10 @@ interface SearchBarProps { export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { const [sidoValue, setSidoValue] = useState("0"); const [sigunValue, setSigunValue] = useState("0"); - const [sidoData, setSidoData] = useState([ - { text: "시도", val: "0" }, - ]); - const [sigunData, setSigunData] = useState([ - { text: "시군구", val: "0" }, - ]); - const [filteredSigunData, setFilteredSigunData] = useState([ - { text: "시군구", val: "0" }, - ]); - - const [facilityData, setFacilityData] = useState([ - { text: "하수처리시설", val: "0" }, - ]); + const [sidoData, setSidoData] = useState([{ text: "시도", val: "0" }]); + const [sigunData, setSigunData] = useState([{ text: "시군구", val: "0" }]); + const [filteredSigunData, setFilteredSigunData] = useState([{ text: "시군구", val: "0" }]); + const [facilityData, setFacilityData] = useState([{ text: "하수처리시설", val: "0" }]); const [facilityValue, setFacilityValue] = useState("0"); const [facilityType, setFacilityType] = useState( @@ -48,7 +39,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { text: f.properties.mctpv_korn_nm, val: f.properties.mctpv_cd, })) - .sort((a, b) => { + .sort((a: any, b: any) => { // 커스텀 순서 정의 const customOrder = [ "서울특별시", @@ -97,7 +88,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { text: f.properties.sgg_korn_nm, val: f.properties.sgg_cd, })) - .sort((a, b) => a.text.localeCompare(b.text, 'ko')); + .sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko')); setSigunData(list); }; @@ -135,7 +126,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", val: f.id, })) - .sort((a, b) => a.text.localeCompare(b.text, 'ko')); + .sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko')); } else if ( type === "tb_wr_bdst_ruse_fclt" // 재이용시설 ) { diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index 305c0afe1..a1201d23a 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -61,7 +61,7 @@ export const SearchBar = ({ const sidoList = json .filter((item: any) => item.lwpoCd?.length === 2) .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })) - .sort((a, b) => { + .sort((a: any, b: any) => { // 커스텀 순서 정의 const customOrder = [ "서울특별시", @@ -101,7 +101,7 @@ export const SearchBar = ({ const sigunList = json .filter((item: any) => item.lwpoCd?.length > 2) .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })) - .sort((a, b) => a.text.localeCompare(b.text, 'ko')); + .sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko')); setSidoData([{ text: "시도", val: "0" }, ...sidoList]); setSigunData(sigunList); };