SIDE PROJECT/AUTOFINDER
데이터 분석 및 시각화 구현
sooindev
2025. 4. 18. 00:09
728x90
2025년 4월 3일에 작성됨
백엔드 : 데이터 분석 기능 구현
1. 분석 컨트롤러 (AnalyticsController)
AnalyticsController
는 차량 데이터를 분석하고 통계를 제공하는 REST API 엔드포인트를 정의한다.
@RestController
@RequestMapping("/api/analytics")
@RequiredArgsConstructor
public class AnalyticsController {
private final AnalyticsService analyticsService;
/**
* 특정 모델의 연식별 가격 통계 조회 API
* @param model 차량 모델명
* @return 연식별 가격 통계 데이터 (최저가, 평균가, 최고가)
*/
@GetMapping("/price-by-year/{model}")
public ResponseEntity<List<Map<String, Object>>> getPriceStatsByYear(@PathVariable String model) {
List<Map<String, Object>> priceStats = analyticsService.getPriceStatsByYear(model);
return ResponseEntity.ok(priceStats);
}
}
/api/analytics/price-by-year/{model}
: 특정 차량 모델의 연식별 가격 통계(최저가, 평균가, 최고가)를 제공
2. 분석 서비스 (AnalyticsService)
AnalyticsService
는 실제 데이터 분석 로직을 구현한다.
@Service
@RequiredArgsConstructor
public class AnalyticsService {
private final CarRepository carRepository;
/**
* 특정 모델의 연식별 가격 통계 조회
* @param model 차량 모델명
* @return 연식별 최저가, 평균가, 최고가 통계
*/
public List<Map<String, Object>> getPriceStatsByYear(String model) {
// 해당 모델의 모든 차량 조회
List<Car> cars = carRepository.findByModelContaining(model);
// 연식별로 그룹화하여 통계 계산
return cars.stream()
.collect(Collectors.groupingBy(Car::getYear))
.entrySet().stream()
.map(entry -> {
String year = entry.getKey();
List<Car> carsInYear = entry.getValue();
// 최저가, 평균가, 최고가 계산
long minPrice = carsInYear.stream()
.mapToLong(Car::getPrice)
.min()
.orElse(0);
double avgPrice = carsInYear.stream()
.mapToLong(Car::getPrice)
.average()
.orElse(0);
long maxPrice = carsInYear.stream()
.mapToLong(Car::getPrice)
.max()
.orElse(0);
long count = carsInYear.size();
// 결과 맵 생성
Map<String, Object> yearStats = Map.of(
"year", year,
"minPrice", minPrice,
"avgPrice", Math.round(avgPrice),
"maxPrice", maxPrice,
"count", count
);
return yearStats;
})
.sorted((a, b) -> {
// 연식 기준으로 정렬 (최신순)
String yearA = a.get("year").toString().replaceAll("[^0-9]", "");
String yearB = b.get("year").toString().replaceAll("[^0-9]", "");
return yearB.compareTo(yearA);
})
.collect(Collectors.toList());
}
}
- Java 8 Stream API를 활용한 데이터 처리 및 분석
- 차량 모델별 필터링 및 연식별 그룹화
- 각 연도별 최저가, 평균가, 최고가 계산
- 결과 데이터 정렬 및 포맷팅
프론트엔드 : 데이터 시각화 구현
1. 가격 분석 차트 컴포넌트 (PriceAnalysisChart)
// PriceAnalysisChart.js
import React, { useState, useEffect } from 'react';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
Legend, ResponsiveContainer, Area, ComposedChart
} from 'recharts';
import axios from 'axios';
const PriceAnalysisChart = ({ modelName }) => {
const [priceData, setPriceData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!modelName) return;
const loadPriceStats = async () => {
try {
setIsLoading(true);
setError(null);
const response = await axios.get(`/api/analytics/price-by-year/${encodeURIComponent(modelName)}`);
setPriceData(response.data);
} catch (err) {
console.error("가격 통계 데이터 로드 실패:", err);
setError("데이터를 불러오는 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
loadPriceStats();
}, [modelName]);
// 차트 렌더링 로직
return (
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-800 mb-4">{modelName} 연식별 가격 분석</h3>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={priceData}
margin={{ top: 10, right: 30, left: 20, bottom: 30 }}
>
<CartesianGrid strokeDasharray="3 3" opacity={0.1} />
<XAxis
dataKey="year"
label={{
value: '연식',
position: 'insideBottomRight',
offset: -10
}}
/>
<YAxis
tickFormatter={value => `${value}만`}
label={{
value: '가격 (만원)',
angle: -90,
position: 'insideLeft',
style: { textAnchor: 'middle' }
}}
/>
<Tooltip
formatter={formatPrice}
labelFormatter={value => `${value} 연식`}
/>
<Legend />
<Area
type="monotone"
dataKey="minPrice"
name="최저가"
fill="#e5f7f7"
stroke="#68c2c0"
fillOpacity={0.3}
/>
<Line
type="monotone"
dataKey="avgPrice"
name="평균가"
stroke="#0e7490"
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
<Area
type="monotone"
dataKey="maxPrice"
name="최고가"
fill="#e5f7f7"
stroke="#68c2c0"
fillOpacity={0.3}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
{/* 데이터 테이블 렌더링 로직 */}
</div>
);
};
export default PriceAnalysisChart;
- Recharts 라이브러리를 활용한 반응형 차트 구현
- ComposedChart를 이용하여 여러 유형의 차트(선 그래프, 영역 그래프) 조합
- 최저가와 최고가는 영역 그래프로, 평균가는 선 그래프로 표현
- 로딩 상태와 에러 처리를 통한 사용자 경험 개선
2. 모델 분석 페이지 (ModelAnalysisPage)
// ModelAnalysisPage.js
import React, { useState } from 'react';
import PriceAnalysisChart from '../components/PriceAnalysisChart';
import { useParams } from 'react-router-dom';
const ModelAnalysisPage = () => {
const { model } = useParams();
const [searchModel, setSearchModel] = useState(model || '');
const [currentModel, setCurrentModel] = useState(model || '');
const handleSubmit = (e) => {
e.preventDefault();
setCurrentModel(searchModel);
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 mb-2">중고차 시장 분석</h1>
<p className="text-gray-500">
차량 모델별 시장 가격 추이와 통계 데이터를 확인하세요.
</p>
</div>
{/* 검색 폼 */}
<div className="mb-8 bg-white p-4 rounded-lg shadow">
<form onSubmit={handleSubmit} className="flex flex-col md:flex-row gap-4">
<div className="flex-grow">
<label htmlFor="modelSearch" className="block text-sm font-medium text-gray-700 mb-1">
차량 모델 검색
</label>
<input
type="text"
id="modelSearch"
className="shadow-sm focus:ring-teal-500 focus:border-teal-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="예: 아반떼, 쏘나타, 그랜저 등"
value={searchModel}
onChange={(e) => setSearchModel(e.target.value)}
/>
</div>
<div className="flex items-end">
<button
type="submit"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-teal-600 hover:bg-teal-700"
>
분석하기
</button>
</div>
</form>
</div>
{/* 가격 분석 차트 */}
{currentModel && <PriceAnalysisChart modelName={currentModel} />}
{/* 분석 정보 */}
<div className="mt-8 bg-gray-50 p-6 rounded-lg">
<h2 className="text-lg font-medium text-gray-900 mb-2">분석 정보</h2>
<ul className="list-disc pl-5 text-sm text-gray-600 space-y-1">
<li>그래프는 입력한 모델과 일치하는 모든 차량의 연식별 가격 통계를 보여줍니다.</li>
<li>최저가와 최고가 사이의 영역은 해당 연식의 차량 가격 범위를 나타냅니다.</li>
<li>평균가는 해당 연식 차량들의 평균 가격입니다.</li>
<li>더 정확한 분석을 위해 모델명을 구체적으로 입력하세요. (예: "아반떼" 대신 "아반떼 AD")</li>
</ul>
</div>
</div>
);
};
export default ModelAnalysisPage;
- URL 파라미터를 통한 차량 모델 초기화
- 사용자 입력을 통한 모델 검색 기능
- 차량 모델이 지정된 경우만 차트 표시
- 분석 정보를 통한 사용자 가이드 제공
3. 차량 상세 페이지에서의 데이터 시각화
// CarDetailPage.js (일부)
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { fetchCarById } from '../api/services';
import CarInfo from '../components/CarInfo';
import PriceAnalysisChart from '../components/PriceAnalysisChart';
const CarDetailPage = ({ userId, favorites, setFavorites }) => {
const { id } = useParams();
const [car, setCar] = useState(null);
// ... 다른 코드 생략 ...
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* ... 다른 컴포넌트 생략 ... */}
{car && (
<>
<CarInfo car={car} />
{/* 가격 분석 차트 추가 */}
<div className="mt-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">시장 가격 분석</h2>
<PriceAnalysisChart modelName={car.model} />
</div>
</>
)}
</div>
);
};
export default CarDetailPage;
차량 상세 페이지에서도 해당 모델에 대한 가격 분석 차트를 제공하여 사용자가 현재 보고 있는 차량이 시장 가격과 비교하여 어느 위치에 있는지 파악할 수 있게 한다.