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;

차량 상세 페이지에서도 해당 모델에 대한 가격 분석 차트를 제공하여 사용자가 현재 보고 있는 차량이 시장 가격과 비교하여 어느 위치에 있는지 파악할 수 있게 한다.