From a4c686038d0c11a22210acbfbd89c327630c7bc2 Mon Sep 17 00:00:00 2001 From: eunhak Date: Tue, 3 Sep 2024 18:55:25 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=8B=9C=EC=A7=80=EA=B0=80?= =?UTF-8?q?=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/product/detail/Map.tsx | 16 +- .../product/detail/OfficialPriceChart.tsx | 242 ++++++++++++++++++ .../product/detail/ProductDetail.tsx | 4 + .../product/detail/PublicTransport.tsx | 27 +- 4 files changed, 278 insertions(+), 11 deletions(-) create mode 100644 src/components/product/detail/OfficialPriceChart.tsx diff --git a/src/components/product/detail/Map.tsx b/src/components/product/detail/Map.tsx index 421d381..3d8e62a 100644 --- a/src/components/product/detail/Map.tsx +++ b/src/components/product/detail/Map.tsx @@ -42,14 +42,14 @@ const KakaoMap = () => { marker.setMap(map); const circle = new window.kakao.maps.Circle({ - center: markerPosition, // Set the center to the marker's position - radius: 400, // Radius of the circle in meters - strokeWeight: 2, // Border thickness - strokeColor: '#ff0000', // Border color - strokeOpacity: 0.8, // Border transparency - strokeStyle: 'solid', // Border style - fillColor: '#ff0000', // Fill color - fillOpacity: 0.4 // Fill transparency + center: markerPosition, + radius: 400, + strokeWeight: 2, + strokeColor: '#ff0000', + strokeOpacity: 0.8, + strokeStyle: 'solid', + fillColor: '#ff0000', + fillOpacity: 0.4 }); circle.setMap(map); circle.setZIndex(10); diff --git a/src/components/product/detail/OfficialPriceChart.tsx b/src/components/product/detail/OfficialPriceChart.tsx new file mode 100644 index 0000000..c9cccb1 --- /dev/null +++ b/src/components/product/detail/OfficialPriceChart.tsx @@ -0,0 +1,242 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Line } from 'react-chartjs-2'; +import axios from 'axios'; +import { usePathname } from 'next/navigation'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ChartOptions +} from 'chart.js'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ChartDataLabels +); + +// API 데이터 타입 정의 +interface LandData { + landPrice: number; + baseYear: string; + baseDay: string; +} + +interface ApiData { + lands: LandData[]; +} + +// 차트 데이터 타입 정의 +interface ChartData { + labels: string[]; + datasets: { + label: string; + data: number[]; + borderColor: string; + backgroundColor: string; + pointBackgroundColor: string; + pointBorderColor: string; + pointRadius: number; + pointHoverRadius: number; + fill: boolean; + }[]; +} + +const OfficialPriceChart = () => { + const pathname = usePathname(); + const lastSegment = pathname.split('/').pop(); // 경로의 마지막 부분 추출 + + // API에서 데이터를 가져오는 함수 + const fetchData = async (): Promise => { + const response = await axios.get( + `https://api.moaguide.com/detail/building/land/${lastSegment}` + ); + return response.data; + }; + + const chartRef = useRef(null); + + // 선택된 연도와 분기를 관리하는 상태 + const [startYear, setStartYear] = useState(2015); + const [endYear, setEndYear] = useState(2024); + + // 변환된 데이터를 관리할 상태 + const [filteredData, setFilteredData] = useState({ + labels: [], + datasets: [] + }); + + // 모바일 화면 여부를 확인하는 상태 + const [isMobile, setIsMobile] = useState(false); + + // useQuery로 데이터 패칭 + const { data, error, isLoading } = useQuery({ + queryKey: ['landPriceData'], + queryFn: fetchData + }); + + // 데이터를 필터링하고 변환하는 함수 + const transformAndFilterData = (): ChartData => { + if (!data || !data.lands) return { labels: [], datasets: [] }; + + // 사용자가 선택한 기간에 맞게 데이터를 필터링 + const filteredLandPrices = data.lands.filter((item) => { + const year = parseInt(item.baseYear); + return year >= startYear && year <= endYear; + }); + + // 레이블 생성 및 역순으로 정렬 + const labels = filteredLandPrices.map((item) => `${item.baseYear}.1Q`).reverse(); + + // 데이터셋 생성 및 역순으로 정렬 + const datasets = [ + { + label: '공시지가', + data: filteredLandPrices.map((item) => item.landPrice).reverse(), + borderColor: '#8A4AF3', // 선의 색상 + backgroundColor: '#8A4AF3', + pointBackgroundColor: '#8A4AF3', + pointBorderColor: '#8A4AF3', + pointRadius: 5, + pointHoverRadius: 7, + fill: false + } + ]; + + return { labels, datasets }; + }; + + // 검색 기간을 검증하는 함수 + const validateSearchPeriod = () => { + if (startYear > endYear) { + alert('잘못된 검색 기간입니다. 시작 날짜가 종료 날짜보다 앞서야 합니다.'); + return false; + } + return true; + }; + + // 화면 크기에 따라 데이터 라벨 표시 여부를 결정하는 함수 + const updateLabelVisibility = () => { + setIsMobile(window.innerWidth <= 768); + }; + + useEffect(() => { + updateLabelVisibility(); // 초기 화면 크기 체크 + window.addEventListener('resize', updateLabelVisibility); // 창 크기 변경 시 업데이트 + + return () => { + window.removeEventListener('resize', updateLabelVisibility); + }; + }, []); + + /* eslint-disable react-hooks/exhaustive-deps */ + // 검색 기간 변경 시 자동 필터링 + useEffect(() => { + if (validateSearchPeriod()) { + const transformedData = transformAndFilterData(); + setFilteredData(transformedData); + } + }, [startYear, endYear, data]); + + // 로딩 상태 처리 + if (isLoading) return
데이터를 불러오는 중...
; + if (error) + return ( +
데이터를 가져오는 데 오류가 발생했습니다: {(error as Error).message}
+ ); + + // 차트 옵션 + const options: ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'bottom' + }, + tooltip: { + enabled: true, + intersect: false + }, + datalabels: { + display: !isMobile, // 모바일일 때는 데이터 라벨 숨김 + anchor: 'end', + align: 'end', + formatter: (value: number) => { + return value !== null ? value.toLocaleString('ko-KR') : ''; + }, + color: '#000', + font: { + weight: 'bold' + } + } + }, + scales: { + x: { + display: true, + grid: { + display: false + } + }, + y: { + display: true, + beginAtZero: false, + grid: { + display: true + }, + ticks: { + callback: (value) => value.toLocaleString('ko-KR') + } + } + } + }; + + return ( + <> +
+
+ + ~ + +
+
+
+
+ +
+
+ + ); +}; + +export default OfficialPriceChart; diff --git a/src/components/product/detail/ProductDetail.tsx b/src/components/product/detail/ProductDetail.tsx index bd1d636..a67158c 100644 --- a/src/components/product/detail/ProductDetail.tsx +++ b/src/components/product/detail/ProductDetail.tsx @@ -3,6 +3,7 @@ import CommercialVacancyRateChart from './CommercialVacancyRateChart'; import FloatingPopulationChart from './FloatingPopulationChart'; import PopulationInformationChart from './PopulationInformationChart'; import PublicTransport from './PublicTransport'; +import OfficialPriceChart from './OfficialPriceChart'; const ProductDetail = ({ url }: { url: string }) => { console.log(url); return ( @@ -12,6 +13,9 @@ const ProductDetail = ({ url }: { url: string }) => {
상권 공실률
+ +
공시지가
+
접근성
diff --git a/src/components/product/detail/PublicTransport.tsx b/src/components/product/detail/PublicTransport.tsx index 5ea4de3..369162a 100644 --- a/src/components/product/detail/PublicTransport.tsx +++ b/src/components/product/detail/PublicTransport.tsx @@ -2,7 +2,7 @@ import React from 'react'; import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; import { usePathname } from 'next/navigation'; - +import Image from 'next/image'; interface NearSubway { station: string; route: string; @@ -29,6 +29,20 @@ interface PublicTransportData { busNode: number; } +const stationImageMap: { [key: string]: string } = { + '1호선': '/images/product/detail/subway1.svg', + '2호선': '/images/product/detail/subway2.svg', + '3호선': '/images/product/detail/subway3.svg', + '4호선': '/images/product/detail/subway4.svg', + '5호선': '/images/product/detail/subway5.svg', + '6호선': '/images/product/detail/subway6.svg', + '7호선': '/images/product/detail/subway7.svg', + '8호선': '/images/product/detail/subway8.svg', + '9호선': '/images/product/detail/subway9.svg', + 분당선: '/images/product/detail/subwaybundang.svg', + 경의중앙선: '/images/product/detail/subwaygyeonghye.svg' +}; + const PublicTransport = () => { const pathname = usePathname(); const lastSegment = pathname.split('/').pop(); // 경로의 마지막 부분 추출 @@ -89,8 +103,15 @@ const PublicTransport = () => {
주변 지하철
{data?.nearSubway.map((subway, index) => (
-
-
{subway.station}
+
+
+ {`${subway.station} +
{subway.route}