Skip to content

Commit

Permalink
Merge pull request #16 from Moaguide-develop/feat/productpage
Browse files Browse the repository at this point in the history
feat : 공시지가 그래프 구현
  • Loading branch information
eun-hak authored Sep 3, 2024
2 parents ddd4271 + a4c6860 commit 2d39ebd
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 11 deletions.
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

0 comments on commit 2d39ebd

Please sign in to comment.