diff --git a/public/images/product/detail/subway1.svg b/public/images/product/detail/subway1.svg new file mode 100644 index 0000000..ec53b85 --- /dev/null +++ b/public/images/product/detail/subway1.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway2.svg b/public/images/product/detail/subway2.svg new file mode 100644 index 0000000..6affe26 --- /dev/null +++ b/public/images/product/detail/subway2.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway3.svg b/public/images/product/detail/subway3.svg new file mode 100644 index 0000000..83e6f4c --- /dev/null +++ b/public/images/product/detail/subway3.svg @@ -0,0 +1,17 @@ +ㄱ + + + + + + + + diff --git a/public/images/product/detail/subway4.svg b/public/images/product/detail/subway4.svg new file mode 100644 index 0000000..4daf66c --- /dev/null +++ b/public/images/product/detail/subway4.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway5.svg b/public/images/product/detail/subway5.svg new file mode 100644 index 0000000..3e68eb8 --- /dev/null +++ b/public/images/product/detail/subway5.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway6.svg b/public/images/product/detail/subway6.svg new file mode 100644 index 0000000..191f251 --- /dev/null +++ b/public/images/product/detail/subway6.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway7.svg b/public/images/product/detail/subway7.svg new file mode 100644 index 0000000..8968178 --- /dev/null +++ b/public/images/product/detail/subway7.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway8.svg b/public/images/product/detail/subway8.svg new file mode 100644 index 0000000..6f82a60 --- /dev/null +++ b/public/images/product/detail/subway8.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/public/images/product/detail/subway9.svg b/public/images/product/detail/subway9.svg new file mode 100644 index 0000000..f257115 --- /dev/null +++ b/public/images/product/detail/subway9.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/public/images/product/detail/subwaybundang.svg b/public/images/product/detail/subwaybundang.svg new file mode 100644 index 0000000..17de6c4 --- /dev/null +++ b/public/images/product/detail/subwaybundang.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/product/detail/subwaygyeonghye.svg b/public/images/product/detail/subwaygyeonghye.svg new file mode 100644 index 0000000..c749a66 --- /dev/null +++ b/public/images/product/detail/subwaygyeonghye.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/product/detail/[id]/page.tsx b/src/app/product/detail/[id]/page.tsx index bd1dd46..ec27861 100644 --- a/src/app/product/detail/[id]/page.tsx +++ b/src/app/product/detail/[id]/page.tsx @@ -12,15 +12,15 @@ import { useState } from 'react'; import { getProductDetail } from '@/factory/ProductDetail'; const Detailpage = (props: any) => { const [sort, setSort] = useState('profit'); - - console.log(props.params.id); + const url = props.params.id; + console.log(url); const { data, isLoading, isError } = getProductDetail(props.params.id); console.log(data); return (
-
-
+
+
{ alt="Profile Image" /> -
+
{data?.category === 'building' ? '부동산' : '오류'} @@ -40,7 +40,7 @@ const Detailpage = (props: any) => { {data?.name}
-
+
해당 플랫폼으로 이동
{ />
-
+
관심 종목
{
-
-
+
+
+
+
+ {data?.category === 'building' ? '부동산' : '오류'} +
+
{data?.platform}
+
+ +
+ {data?.name} +
+
+
+ +
+
현재가
-
{data?.price}
+
{data?.price.toLocaleString()}원
({data?.priceRate}%)
-
+
시가총액
-
{data?.totalPrice}
+
{data?.totalPrice.toLocaleString()}원
-
+
최근 배당금
{data?.lastDivide}원
-
+
배당 수익률
{data?.lastDivideRate}%
-
+
배당 주기
{data?.divideCycle}개월
+ +
+
+
해당 플랫폼으로 이동
+ Right Arrow +
+
@@ -104,9 +131,9 @@ const Detailpage = (props: any) => { ) : sort === 'report' ? ( ) : sort === 'profit' ? ( - + ) : sort === 'detail' ? ( - + ) : undefined}
); diff --git a/src/components/product/Dividend.tsx b/src/components/product/Dividend.tsx index 3fd2efd..53eba5d 100644 --- a/src/components/product/Dividend.tsx +++ b/src/components/product/Dividend.tsx @@ -74,7 +74,7 @@ const Dividend = memo(({ dividend }: DividendProps) => {
-
+
{
부동산
{item.name}
+
+ (1주당 {item.dividend}원) +
-
+
(1주당 {item.dividend}원)
diff --git a/src/components/product/Product.tsx b/src/components/product/Product.tsx index 177b435..d742a6b 100644 --- a/src/components/product/Product.tsx +++ b/src/components/product/Product.tsx @@ -59,7 +59,9 @@ const Product = ({
-
관련 리포트
+
+ 관련 리포트 +
diff --git a/src/components/product/ProductList.tsx b/src/components/product/ProductList.tsx index 47c8aec..331ba1c 100644 --- a/src/components/product/ProductList.tsx +++ b/src/components/product/ProductList.tsx @@ -3,6 +3,7 @@ import Container from '../common/Container'; import { IProductDetailData } from '@/types/Diviend'; import ProductPagenation from './ProductPagenation'; import Link from 'next/link'; +import ProductMobilePagenation from './ProductMobilePagenation'; interface IProductContentListProps { content: IProductDetailData['content']; totalPages: IProductDetailData['totalPages']; @@ -18,52 +19,80 @@ const ProductContentList = ({ return (
-
+
이미지
카테고리
운영 플랫폼
종목 명
-
현재가
-
시가총액
-
배당 수익률
+
현재가
+
시가총액
+
배당 수익률
#
-
+
{content?.map((item) => ( -
+
-
- image - -
+
+
+ image +
+
{item.category === 'building' ? '부동산' : '부동산'}
-
+
{item.platform}
-
+
{item.name}
+ {/* ///// 반응형 //////////// */} +
+
+
+ {item.category === 'building' ? '부동산' : '부동산'} +
+
+ {item.platform} +
+
+ +
+ {item.name} +
+ +
+
+ {item.price.toLocaleString()}원 +
+
({item.priceRate}%)
+
+
+ + {/* ///// */} +
-
{item.price}
-
+
+ {item.price.toLocaleString()} 원 +
+
({item.priceRate}%)
-
- {item.totalPrice} +
+ {item.totalPrice.toLocaleString()}원
-
+
{item.lastDivide_rate}%
@@ -72,15 +101,20 @@ const ProductContentList = ({ width={24} height={24} alt="Bookmark" + className=" desk:hidden md:flex" />
-
+
))}
- - +
+ +
+
+ +
); diff --git a/src/components/product/ProductMobilePagenation.tsx b/src/components/product/ProductMobilePagenation.tsx new file mode 100644 index 0000000..91d0153 --- /dev/null +++ b/src/components/product/ProductMobilePagenation.tsx @@ -0,0 +1,85 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import React from 'react'; + +interface PaginationProps { + totalPages: number; +} + +const ProductMobilePagenation = ({ totalPages }: PaginationProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const [currentPage, setCurrentPage] = useState(1); + const [pageNumbers, setPageNumbers] = useState([]); + const [currentRange, setCurrentRange] = useState(0); + + useEffect(() => { + const page = parseInt(searchParams.get('page') || '1', 10); + setCurrentPage(page); + const range = Math.floor((page - 1) / 5); + setCurrentRange(range); + }, [searchParams]); + + useEffect(() => { + const start = currentRange * 5 + 1; + const end = Math.min(start + 4, totalPages); + const pages: number[] = []; + for (let i = start; i <= end; i++) { + pages.push(i); + } + setPageNumbers(pages); + }, [currentRange, totalPages]); + + const handlePageClick = (page: number) => { + setCurrentPage(page); + const params = new URLSearchParams(searchParams.toString()); + params.set('page', page.toString()); + router.replace(`?${params.toString()}`); + }; + + const handleNextRange = () => { + if ((currentRange + 1) * 5 < totalPages) { + const newRange = currentRange + 1; + setCurrentRange(newRange); + const firstPageOfNextRange = newRange * 5 + 1; + handlePageClick(firstPageOfNextRange); + } + }; + + const handlePrevRange = () => { + if (currentRange > 0) { + const newRange = currentRange - 1; + setCurrentRange(newRange); + const firstPageOfPrevRange = newRange * 5 + 1; + handlePageClick(firstPageOfPrevRange); + } + }; + + return ( +
+ + {pageNumbers.map((page) => ( + + ))} + +
+ ); +}; + +export default ProductMobilePagenation; diff --git a/src/components/product/ProductPagenation.tsx b/src/components/product/ProductPagenation.tsx index f0ccf50..fb01514 100644 --- a/src/components/product/ProductPagenation.tsx +++ b/src/components/product/ProductPagenation.tsx @@ -61,7 +61,7 @@ const ProductPagenation = ({ totalPages }: PaginationProps) => { {pageNumbers.map((page) => ( diff --git a/src/components/product/ProductSort.tsx b/src/components/product/ProductSort.tsx index 07f9e6b..150c85e 100644 --- a/src/components/product/ProductSort.tsx +++ b/src/components/product/ProductSort.tsx @@ -22,7 +22,7 @@ const ProductSort = ({ sort, setSort }: ProductSortProps) => { }; return (
-
+
정렬
|
diff --git a/src/components/product/Report.tsx b/src/components/product/Report.tsx index 340bd4b..a8adbbf 100644 --- a/src/components/product/Report.tsx +++ b/src/components/product/Report.tsx @@ -74,25 +74,27 @@ const Report = ({ report }: IReportProps) => { return (
-
-
-
- {item.category === 'building' ? <>부동산 : <>미술품} -
+
+
+
+
+ {item.category === 'building' ? <>부동산 : <>미술품} +
-
{item.title}
-
- {format(new Date(item.date), 'yyyy.MM.dd')} +
{item.title}
+
+ {format(new Date(item.date), 'yyyy.MM.dd')} +
+
+
+ Report
-
-
- Report
diff --git a/src/components/product/TopProduct.tsx b/src/components/product/TopProduct.tsx index 5a4338a..fd1b941 100644 --- a/src/components/product/TopProduct.tsx +++ b/src/components/product/TopProduct.tsx @@ -80,56 +80,62 @@ const TopProduct = memo(({ summary }: TopProductProps) => { return (
- -
-
-
-
- Building -
-
-
+
+ +
+
+
+
Building -
- {idx + 1} + width={396} + height={237} + /> +
+
+
+ Building +
+ {idx + 1} +
-
-
-
-
- 부동산 +
+
+
+ 부동산 +
+
+ {item.platform} +
-
{item.platform}
-
-
-
{item.name}
-
- {item.priceRate}% +
+
+ {item.name} +
+
+ {item.priceRate}% +
-
-
-
{item.price}
-
- (+ {item.lastDivide_rate}%) +
+
{item.price}
+
+ (+ {item.lastDivide_rate}%) +
-
- + +
); diff --git a/src/components/product/detail/CommercialRentChart.tsx b/src/components/product/detail/CommercialRentChart.tsx index d6959b3..7f3fbf3 100644 --- a/src/components/product/detail/CommercialRentChart.tsx +++ b/src/components/product/detail/CommercialRentChart.tsx @@ -1,5 +1,7 @@ -import React, { useRef, useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Line } from 'react-chartjs-2'; +import axios from 'axios'; import { Chart as ChartJS, CategoryScale, @@ -8,9 +10,12 @@ import { LineElement, Title, Tooltip, - Legend + Legend, + ChartOptions } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; +import { usePathname } from 'next/navigation'; + ChartJS.register( CategoryScale, LinearScale, @@ -22,71 +27,189 @@ ChartJS.register( ChartDataLabels ); -const CommercialRentChart = () => { +// API 데이터 타입 정의 +interface RentData { + year: number; + quarter: number; + region: string; + rent: number; +} + +interface VacancyRateData { + year: number; + quarter: number; + region: string; + vacancyRate: number; +} + +interface ApiData { + rent: RentData[]; + vacancyrate: VacancyRateData[]; +} + +// 차트 데이터 타입 정의 +interface ChartData { + labels: string[]; + datasets: { + label: string; + data: (number | null)[]; + borderColor: string; + backgroundColor: string; + pointBackgroundColor: string; + pointBorderColor: string; + pointRadius: number; + pointHoverRadius: number; + fill: boolean; + }[]; +} + +// 랜덤 색상 생성 함수 +const generateRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; + +const CommercialRentChart: React.FC = () => { + const pathname = usePathname(); + const lastSegment = pathname.split('/').pop(); // 경로의 마지막 부분 추출 + console.log(lastSegment); + // API에서 데이터를 가져오는 함수 + const fetchData = async (): Promise => { + const response = await axios.get( + `https://api.moaguide.com/detail/building/rate/${lastSegment}?type=오피스` + ); + return response.data; + }; const chartRef = useRef(null); - const [chartData, setChartData] = useState('1년'); - - const data = { - labels: [ - '2022.1Q', - '2022.2Q', - '2022.3Q', - '2022.4Q', - '2023.1Q', - '2023.2Q', - '2023.3Q', - '2023.4Q', - '2024.1Q' - ], - datasets: [ - { - label: '서울', - data: [23.4, 23.6, 23.8, 24.0, 24.1, 24.2, 24.3, 24.5, 24.8], - borderColor: '#1E90FF', - backgroundColor: '#1E90FF', - pointBackgroundColor: '#1E90FF', - pointBorderColor: '#1E90FF', - pointRadius: 5, - pointHoverRadius: 7, - fill: false - }, - { - label: '강남', - data: [22.7, 22.8, 23.0, 23.1, 23.3, 23.4, 23.5, 23.7, 23.9], - borderColor: '#8A4AF3', - backgroundColor: '#8A4AF3', - pointBackgroundColor: '#8A4AF3', - pointBorderColor: '#8A4AF3', - pointRadius: 5, - pointHoverRadius: 7, - fill: false - }, - { - label: '고대역', - data: [21.3, 21.5, 21.6, 21.7, 21.8, 21.8, 21.9, 21.9, 21.7], - borderColor: '#228B22', - backgroundColor: '#228B22', - pointBackgroundColor: '#228B22', - pointBorderColor: '#228B22', + // 선택된 연도와 분기를 관리하는 상태 + const [startYear, setStartYear] = useState(2022); + const [startQuarter, setStartQuarter] = useState(1); + const [endYear, setEndYear] = useState(2024); + const [endQuarter, setEndQuarter] = useState(2); + + // 지역별 색상을 저장하는 상태 + const [regionColors, setRegionColors] = useState<{ [key: string]: string }>({}); + + // 변환된 데이터를 관리할 상태 + const [filteredData, setFilteredData] = useState({ + labels: [], + datasets: [] + }); + + // useQuery로 데이터 패칭 + const { data, error, isLoading } = useQuery({ + queryKey: ['rentData'], + queryFn: fetchData + }); + + // 데이터를 필터링하고 변환하는 함수 + const transformAndFilterData = (): ChartData => { + if (!data || !data.rent) return { labels: [], datasets: [] }; + + // 사용자가 선택한 기간에 맞게 데이터를 필터링 + const filteredRentRates = data.rent.filter((item) => { + const startCondition = + item.year > startYear || + (item.year === startYear && item.quarter >= startQuarter); + const endCondition = + item.year < endYear || (item.year === endYear && item.quarter <= endQuarter); + return startCondition && endCondition; + }); + + // 레이블 생성 + const labels = Array.from( + new Set(filteredRentRates.map((item) => `${item.year}.${item.quarter}Q`)) + ); + + // 지역별로 데이터셋 생성 + const regions = Array.from(new Set(filteredRentRates.map((item) => item.region))); + const newRegionColors = { ...regionColors }; + + const datasets = regions.map((region) => { + // 해당 지역의 색상이 아직 없다면 랜덤 색상을 생성 + if (!newRegionColors[region]) { + newRegionColors[region] = generateRandomColor(); + } + const regionData = filteredRentRates.filter((item) => item.region === region); + return { + label: region, + data: labels.map((label) => { + const [year, quarter] = label.split('.'); + const quarterData = regionData.find( + (item) => + item.year === parseInt(year) && item.quarter === parseInt(quarter[0]) + ); + return quarterData ? quarterData.rent : null; // 데이터가 없을 경우 null 반환 + }), + borderColor: newRegionColors[region], + backgroundColor: newRegionColors[region], + pointBackgroundColor: newRegionColors[region], + pointBorderColor: newRegionColors[region], pointRadius: 5, pointHoverRadius: 7, fill: false - } - ] + }; + }); + + // 지역별 색상을 상태에 저장 + setRegionColors(newRegionColors); + + return { labels, datasets }; }; - const options = { + // 검색 기간을 검증하는 함수 + const validateSearchPeriod = () => { + if (startYear > endYear || (startYear === endYear && startQuarter > endQuarter)) { + alert('잘못된 검색 기간입니다. 시작 날짜가 종료 날짜보다 앞서야 합니다.'); + return false; + } + return true; + }; + + /* eslint-disable react-hooks/exhaustive-deps */ + // 검색 기간 변경 시 자동 필터링 + useEffect(() => { + if (validateSearchPeriod()) { + const transformedData = transformAndFilterData(); + setFilteredData(transformedData); + } + }, [startYear, startQuarter, endYear, endQuarter, 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' as const + position: 'bottom' }, tooltip: { enabled: true, intersect: false + }, + datalabels: { + anchor: 'end', + align: 'top', + formatter: (value: number | null) => { + return value !== null ? value.toString() : ''; + }, + color: '#000', + font: { + weight: 'bold' + } } }, scales: { @@ -113,7 +236,7 @@ const CommercialRentChart = () => { }, elements: { line: { - tension: 0 + tension: 0.3 // 이 값을 낮추면 선이 직선에 가까워지고, 공백이 줄어듭니다. }, point: { pointStyle: 'circle' @@ -123,21 +246,55 @@ const CommercialRentChart = () => { return ( <> -
- - +
+
+ {/* */} + + + ~ + + +
- +
diff --git a/src/components/product/detail/CommercialVacancyRateChart.tsx b/src/components/product/detail/CommercialVacancyRateChart.tsx index 1b9ac89..48e0873 100644 --- a/src/components/product/detail/CommercialVacancyRateChart.tsx +++ b/src/components/product/detail/CommercialVacancyRateChart.tsx @@ -1,5 +1,8 @@ -import React, { useRef, useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Bar } from 'react-chartjs-2'; +import axios from 'axios'; +import { usePathname } from 'next/navigation'; import { Chart as ChartJS, CategoryScale, @@ -7,58 +10,203 @@ import { BarElement, Title, Tooltip, - Legend + Legend, + ChartOptions } from 'chart.js'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; -ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + ChartDataLabels +); + +// API 데이터 타입 정의 +interface RentData { + year: number; + quarter: number; + region: string; + rent: number; +} + +interface VacancyRateData { + year: number; + quarter: number; + region: string; + vacancyRate: number; +} + +interface ApiData { + rent: RentData[]; + vacancyrate: VacancyRateData[]; +} + +// 차트 데이터 타입 정의 +interface ChartData { + labels: string[]; + datasets: { + label: string; + data: (number | null)[]; + borderColor: string; + backgroundColor: string; + pointBackgroundColor: string; + pointBorderColor: string; + pointRadius: number; + pointHoverRadius: number; + fill: boolean; + }[]; +} -const CommercialVacancyRateChart = () => { +// 랜덤 색상 생성 함수 +const generateRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; + +const CommercialVacancyRateChart: React.FC = () => { + const pathname = usePathname(); + const lastSegment = pathname.split('/').pop(); // 경로의 마지막 부분 추출 + console.log(lastSegment); + // API에서 데이터를 가져오는 함수 + const fetchData = async (): Promise => { + const response = await axios.get( + `https://api.moaguide.com/detail/building/rate/${lastSegment}?type=오피스` + ); + return response.data; + }; const chartRef = useRef(null); - const [chartData, setChartData] = useState('1년'); - - const data = { - labels: [ - '2022.1Q', - '2022.2Q', - '2022.3Q', - '2022.4Q', - '2023.1Q', - '2023.2Q', - '2023.3Q', - '2023.4Q', - '2024.1Q' - ], - datasets: [ - { - label: '서울', - data: [7.1, 6.4, 6.9, 6.4, 6.1, 6.2, 6.5, 6.2, 5.9], - backgroundColor: '#8A4AF3' - }, - { - label: '강남', - data: [4.8, 4.5, 4.9, 4.4, 4.2, 4.3, 4.4, 4.3, 4.1], - backgroundColor: '#1E90FF' - }, - { - label: '고대역', - data: [6.2, 5.8, 6.1, 5.7, 5.6, 5.7, 5.8, 5.6, 5.8], - backgroundColor: '#228B22' + // 선택된 연도와 분기를 관리하는 상태 + const [startYear, setStartYear] = useState(2022); + const [startQuarter, setStartQuarter] = useState(1); + const [endYear, setEndYear] = useState(2024); + const [endQuarter, setEndQuarter] = useState(2); + + // 지역별 색상을 저장하는 상태 + const [regionColors, setRegionColors] = useState<{ [key: string]: string }>({}); + + // 변환된 데이터를 관리할 상태 + const [filteredData, setFilteredData] = useState({ + labels: [], + datasets: [] + }); + + // useQuery로 데이터 패칭 + const { data, error, isLoading } = useQuery({ + queryKey: ['vacancyRateData'], + queryFn: fetchData + }); + + // 데이터를 필터링하고 변환하는 함수 + const transformAndFilterData = (): ChartData => { + if (!data || !data.vacancyrate) return { labels: [], datasets: [] }; + + // 사용자가 선택한 기간에 맞게 데이터를 필터링 + const filteredVacancyRates = data.vacancyrate.filter((item) => { + const startCondition = + item.year > startYear || + (item.year === startYear && item.quarter >= startQuarter); + const endCondition = + item.year < endYear || (item.year === endYear && item.quarter <= endQuarter); + return startCondition && endCondition; + }); + + // 레이블 생성 + const labels = Array.from( + new Set(filteredVacancyRates.map((item) => `${item.year}.${item.quarter}Q`)) + ); + + // 지역별로 데이터셋 생성 + const regions = Array.from(new Set(filteredVacancyRates.map((item) => item.region))); + const newRegionColors = { ...regionColors }; + + const datasets = regions.map((region) => { + // 해당 지역의 색상이 아직 없다면 랜덤 색상을 생성 + if (!newRegionColors[region]) { + newRegionColors[region] = generateRandomColor(); } - ] + const regionData = filteredVacancyRates.filter((item) => item.region === region); + return { + label: region, + data: labels.map((label) => { + const [year, quarter] = label.split('.'); + const quarterData = regionData.find( + (item) => + item.year === parseInt(year) && item.quarter === parseInt(quarter[0]) + ); + return quarterData ? quarterData.vacancyRate : null; // 데이터가 없을 경우 null 반환 + }), + borderColor: newRegionColors[region], + backgroundColor: newRegionColors[region], + pointBackgroundColor: newRegionColors[region], + pointBorderColor: newRegionColors[region], + pointRadius: 5, + pointHoverRadius: 7, + fill: false + }; + }); + + // 지역별 색상을 상태에 저장 + setRegionColors(newRegionColors); + + return { labels, datasets }; + }; + + // 검색 기간을 검증하는 함수 + const validateSearchPeriod = () => { + if (startYear > endYear || (startYear === endYear && startQuarter > endQuarter)) { + alert('잘못된 검색 기간입니다. 시작 날짜가 종료 날짜보다 앞서야 합니다.'); + return false; + } + return true; }; + /* eslint-disable react-hooks/exhaustive-deps */ + // 검색 기간 변경 시 자동 필터링 + useEffect(() => { + if (validateSearchPeriod()) { + const transformedData = transformAndFilterData(); + setFilteredData(transformedData); + } + }, [startYear, startQuarter, endYear, endQuarter, data]); + + // 로딩 상태 처리 + if (isLoading) return
데이터를 불러오는 중...
; + if (error) + return ( +
데이터를 가져오는 데 오류가 발생했습니다: {(error as Error).message}
+ ); - const options = { + // 차트 옵션 + const options: ChartOptions<'bar'> = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, - position: 'bottom' as const + position: 'bottom' }, tooltip: { enabled: true, intersect: false + }, + datalabels: { + anchor: 'end', + align: 'end', + formatter: (value: number | null) => { + return value !== null ? value.toString() : ''; + }, + color: '#000', + font: { + weight: 'bold' + } } }, scales: { @@ -83,21 +231,55 @@ const CommercialVacancyRateChart = () => { return ( <> -
- - +
+
+ {/* */} + + + ~ + + +
- +
diff --git a/src/components/product/detail/Map.tsx b/src/components/product/detail/Map.tsx index 073577e..421d381 100644 --- a/src/components/product/detail/Map.tsx +++ b/src/components/product/detail/Map.tsx @@ -1,58 +1,91 @@ -// kakaoMap.tsx - import React, { useEffect, useRef } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; declare global { interface Window { kakao: any; } } -const KakaoMap = ({ props }: any) => { + +const fetchBuildingData = async () => { + const response = await axios.get( + 'https://api.moaguide.com/detail/building/area/sou.10' + ); + return response.data; +}; + +const KakaoMap = () => { const mapRef = useRef(null); + + const { data, isLoading, error } = useQuery({ + queryKey: ['buildingData'], + queryFn: fetchBuildingData + }); + console.log(data); useEffect(() => { - if (window.kakao) { - console.log('KakaoMap is loaded'); + if (window.kakao && data) { window.kakao.maps.load(() => { const mapOption = { - center: new window.kakao.maps.LatLng(37.566826, 126.9786567), - level: 3 + center: new window.kakao.maps.LatLng(data.latitude, data.longitude), + level: 6 }; const map = new window.kakao.maps.Map(mapRef.current, mapOption); - // 마커를 생성하고 지도에 표시 - const markerPosition = new window.kakao.maps.LatLng(37.566826, 126.9786567); + const markerPosition = new window.kakao.maps.LatLng( + data.latitude, + data.longitude + ); const marker = new window.kakao.maps.Marker({ position: markerPosition }); 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 + }); + circle.setMap(map); + circle.setZIndex(10); window.kakao.maps.event.addListener(map, 'zoom_changed', () => { map.setCenter(markerPosition); }); - // 폴리곤을 그릴 좌표 배열 - const polygonPath = [ - new window.kakao.maps.LatLng(37.566826, 126.9786567), - new window.kakao.maps.LatLng(37.565826, 126.9786567), - new window.kakao.maps.LatLng(37.565826, 126.9796567), - new window.kakao.maps.LatLng(37.566826, 126.9796567) - ]; - - // 폴리곤을 생성하고 지도에 표시 - const polygon = new window.kakao.maps.Polygon({ - path: polygonPath, - strokeWeight: 3, - strokeColor: 'green', - strokeOpacity: 0.8, - fillOpacity: 0 + // API에서 가져온 데이터를 사용해 폴리곤을 그리기 + data.areas.forEach((area: any) => { + const coordinates = area.polygon + .replace('POLYGON ((', '') + .replace('))', '') + .split(', ') + .map((coord: string) => { + const [lng, lat] = coord.split(' ').map(Number); + return new window.kakao.maps.LatLng(lat, lng); + }); + + const polygon = new window.kakao.maps.Polygon({ + path: coordinates, + strokeWeight: 3, + strokeColor: area.color.toLowerCase(), + strokeOpacity: 0.8, + fillOpacity: 0.5, + fillColor: area.color.toLowerCase() + }); + polygon.setMap(map); }); - polygon.setMap(map); }); } - }, []); + }, [data]); + + if (isLoading) return
Loading...
; + if (error) return
Error fetching data
; - return
; + return
; }; export default KakaoMap; diff --git a/src/components/product/detail/News.tsx b/src/components/product/detail/News.tsx index 8e15d15..99db11b 100644 --- a/src/components/product/detail/News.tsx +++ b/src/components/product/detail/News.tsx @@ -48,8 +48,11 @@ const NewsItem = ({ title, date, id, category, link }: INewsItem) => { key={id} className=" flex justify-between border-b-[1px] border-gray-200 py-[20px] px-[20px] rounded-lg">
- News -
+
+ News +
+ +
{title}
{category === 'building' ? <>부동산 : undefined} diff --git a/src/components/product/detail/ProductDetail.tsx b/src/components/product/detail/ProductDetail.tsx index 07685ba..bd1d636 100644 --- a/src/components/product/detail/ProductDetail.tsx +++ b/src/components/product/detail/ProductDetail.tsx @@ -2,51 +2,9 @@ import CommercialRentChart from './CommercialRentChart'; import CommercialVacancyRateChart from './CommercialVacancyRateChart'; import FloatingPopulationChart from './FloatingPopulationChart'; import PopulationInformationChart from './PopulationInformationChart'; - -const ProductDetail = () => { - // const MOCK = { - // product_Id: 'sou.8', - // name: '신도림 핀포인트 타워 1호', - // publisher: '신영부동산신탁', - // piece: 196000, - // basePrice: 5000, - // totalPrice: '8.42억원', - // subscription: '23.12.14 ~ 23.12.29', - // listingDate: '2024-01-09', - // address: '서울특별시구로구신도림동 337 ', - // useArea: '일반상업지역', - // mainUse: '업무시설', - // completionDate: '2007년 08월 06일', - // landArea: '19.51㎡ / 전체 16,529㎡', - // floorAreaRate: 659.98, - // dryRatio: 55.43, - // height: 102.8, - // scale: '지하 5층, 지상 30층', - // mainStructure: '철근콘크리트구조', - // parking: 1701, - // lift: 24, - // landElevation: '평지', - // landShape: '가장형', - // zoningNational: - // '도시지역,일반상업지역,지구단위계획구역,도로(접합),중로2류(폭 80m~20m)(2016-12-01)(접합)', - // zoningOther: - // '교육환경보호구역(남부교육청에 반드시 확인요망)<교육환경 보호에 관한 법률>,대공방어협조구역(위탁고도:해발165m(지반+건축+옥탑 등), 육군수도방위사령부(02-524-3146)관할)<군사기지 및 군사시설 보호법>,과밀억제권역<수도권정비계획법>', - // latitude: 37.509421, - // longitude: 126.887744, - // leases: [ - // { - // tenant: '㈜삼성화재금융서비스', - // tenantIntroduction: '', - // leasePeriod: '2022.12.26 ~ 2024.12.25', - // leaseArea: 219.88, - // deposit: 32589000, - // rent: '3,258,990원', - // administrationCost: '1,995,300원', - // detailedConditions: '연 2.5%의 인상률 적용' - // } - // ] - // }; - +import PublicTransport from './PublicTransport'; +const ProductDetail = ({ url }: { url: string }) => { + console.log(url); return (
상권 임대료
@@ -55,45 +13,7 @@ const ProductDetail = () => {
상권 공실률
접근성
-
주요 업무 지구
- -
-
-
-
가까운 역까지
-
차량으로
-
대중교통으로
-
- -
-
CBD(도심권역)
-
약 80km
-
평균 30분 소요
-
평균 10분 소요
-
-
-
CBD(도심권역)
-
약 80km
-
평균 30분 소요
-
평균 10분 소요
-
-
-
CBD(도심권역)
-
약 80km
-
평균 30분 소요
-
평균 10분 소요
-
-
-
0.5km 이내 대중교통
- -
-
-
주변 지하철
-
-
-
주변 버스정류장
-
-
+
유동인구
diff --git a/src/components/product/detail/Profit.tsx b/src/components/product/detail/Profit.tsx index 8c2d16f..9489121 100644 --- a/src/components/product/detail/Profit.tsx +++ b/src/components/product/detail/Profit.tsx @@ -8,7 +8,10 @@ const Profit = ({ url }: { url: string }) => { return (
위치
- +
+ +
+
주가
@@ -226,14 +229,18 @@ const Profit = ({ url }: { url: string }) => {
-
-
지역지구 등 지정여부
-
{data?.landRegistry.zoningNational}
+
+
+ 지역지구 등 지정여부 (국토의 계획 및 이용에 관한 법률) +
+
{data?.landRegistry.zoningNational}
-
-
지역지구 등 지정여부
+
+
+ 지역지구 등 지정여부 (기타법률) +
{data?.landRegistry.zoningOther}
diff --git a/src/components/product/detail/PublicTransport.tsx b/src/components/product/detail/PublicTransport.tsx new file mode 100644 index 0000000..5ea4de3 --- /dev/null +++ b/src/components/product/detail/PublicTransport.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import axios from 'axios'; +import { useQuery } from '@tanstack/react-query'; +import { usePathname } from 'next/navigation'; + +interface NearSubway { + station: string; + route: string; + distance: number; + time: number; +} + +interface PublicTransportData { + type: { type: string }[]; + cbd: string; + cbdDistance: string; + cbdCar: string; + cbdSubway: string; + gbd: string; + gbdDistance: string; + gbdCar: string; + gbdSubway: string; + ybd: string; + ybdDistance: string; + ybdCar: string; + ybdSubway: string; + nearSubway: NearSubway[]; + busLine: number; + busNode: number; +} + +const PublicTransport = () => { + const pathname = usePathname(); + const lastSegment = pathname.split('/').pop(); // 경로의 마지막 부분 추출 + console.log(lastSegment); + const fetchData = async () => { + const response = await axios.get( + `https://api.moaguide.com/detail/building/sub/${lastSegment}` + ); + return response.data; + }; + + const { data, isLoading, error } = useQuery({ + queryKey: ['PublicTransport'], + queryFn: fetchData + }); + + if (isLoading) return
Loading...
; + if (error) return
Error fetching data
; + + return ( +
+
주요 업무 지구
+ +
+
+
+
가까운 역까지
+
차량으로
+
대중교통으로
+
+ +
+
{data?.cbd}(도심권역)
+
{data?.cbdDistance}
+
{data?.cbdCar}
+
{data?.cbdSubway}
+
+ +
+
{data?.gbd}(강남권역)
+
{data?.gbdDistance}
+
{data?.gbdCar}
+
{data?.gbdSubway}
+
+ +
+
{data?.ybd}(여의도권역)
+
{data?.ybdDistance}
+
{data?.ybdCar}
+
{data?.ybdSubway}
+
+
+ +
0.5km 이내 대중교통
+ +
+
+
주변 지하철
+ {data?.nearSubway.map((subway, index) => ( +
+
+
{subway.station}
+
{subway.route}
+
+
+
{subway.distance}m
+
{subway.time}분
+
+
+ ))} +
+ +
+
주변 버스정류장
+
+
정류장이름
+
+
+
+ {data?.busLine} +
+
+ {data?.busNode} +
+
+
+
+
+ ); +}; + +export default PublicTransport; diff --git a/src/components/product/detail/Report.tsx b/src/components/product/detail/Report.tsx index 47739ca..dcd55d1 100644 --- a/src/components/product/detail/Report.tsx +++ b/src/components/product/detail/Report.tsx @@ -93,7 +93,9 @@ const ReportItem = ({ content, id, category, title, date }: IreportItem) => {
{title}
{formatDate(date)}
- News +
+ News +
); }; diff --git a/tsconfig.json b/tsconfig.json index 7ea3d71..257ec8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es6", "lib": [ "dom", "dom.iterable", @@ -7,8 +8,8 @@ ], "allowJs": true, "skipLibCheck": true, - "strict": true, "noEmit": true, + "strict": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node",