Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 공시지가 그래프 구현 #16

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/components/product/detail/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
242 changes: 242 additions & 0 deletions src/components/product/detail/OfficialPriceChart.tsx
Original file line number Diff line number Diff line change
@@ -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<ApiData> => {
const response = await axios.get(
`https://api.moaguide.com/detail/building/land/${lastSegment}`
);
return response.data;
};

const chartRef = useRef(null);

// 선택된 연도와 분기를 관리하는 상태
const [startYear, setStartYear] = useState<number>(2015);
const [endYear, setEndYear] = useState<number>(2024);

// 변환된 데이터를 관리할 상태
const [filteredData, setFilteredData] = useState<ChartData>({
labels: [],
datasets: []
});

// 모바일 화면 여부를 확인하는 상태
const [isMobile, setIsMobile] = useState<boolean>(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 <div>데이터를 불러오는 중...</div>;
if (error)
return (
<div>데이터를 가져오는 데 오류가 발생했습니다: {(error as Error).message}</div>
);

// 차트 옵션
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 (
<>
<div className="mb-4 flex justify-end space-x-2">
<div>
<select
id="startYear"
value={startYear}
onChange={(e) => setStartYear(parseInt(e.target.value))}>
{[...Array(2025 - 2000).keys()].map((year) => (
<option key={year + 2000} value={year + 2000}>
{year + 2000}년
</option>
))}
</select>
~
<select
id="endYear"
value={endYear}
onChange={(e) => setEndYear(parseInt(e.target.value))}>
{[...Array(2025 - 2000).keys()].map((year) => (
<option key={year + 2000} value={year + 2000}>
{year + 2000}년
</option>
))}
</select>
</div>
</div>
<div className="flex flex-col items-center justify-center h-full bg-gray-50 mb-[100px]">
<div className="w-full max-w-4xl h-[400px]">
<Line ref={chartRef} data={filteredData} options={options} />
</div>
</div>
</>
);
};

export default OfficialPriceChart;
4 changes: 4 additions & 0 deletions src/components/product/detail/ProductDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -12,6 +13,9 @@ const ProductDetail = ({ url }: { url: string }) => {

<div className=" text-lg font-bold mb-[20px]">상권 공실률</div>
<CommercialVacancyRateChart />

<div className=" text-lg font-bold mb-[20px]">공시지가</div>
<OfficialPriceChart />
<div className=" text-lg font-bold mb-[20px]">접근성</div>
<PublicTransport />

Expand Down
27 changes: 24 additions & 3 deletions src/components/product/detail/PublicTransport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(); // 경로의 마지막 부분 추출
Expand Down Expand Up @@ -89,8 +103,15 @@ const PublicTransport = () => {
<div className="text-base font-bold">주변 지하철</div>
{data?.nearSubway.map((subway, index) => (
<div key={index} className="flex justify-between mt-2">
<div className="flex">
<div className="mr-2">{subway.station}</div>
<div className="flex items-center">
<div className="mr-1">
<Image
src={stationImageMap[subway.station]}
alt={`${subway.station} 이미지`}
width={20}
height={20}
/>
</div>
<div>{subway.route}</div>
</div>
<div className="flex">
Expand Down
Loading