diff --git a/src/entities/chart/index.ts b/src/entities/chart/index.ts
new file mode 100644
index 0000000..fcb073f
--- /dev/null
+++ b/src/entities/chart/index.ts
@@ -0,0 +1 @@
+export * from './types';
diff --git a/src/entities/chart/types/chart.type.ts b/src/entities/chart/types/chart.type.ts
new file mode 100644
index 0000000..98cee81
--- /dev/null
+++ b/src/entities/chart/types/chart.type.ts
@@ -0,0 +1,21 @@
+/**
+ * 차트 관련 entities
+ */
+
+// 차트 라인 데이터
+export interface ChartLine {
+ key: string;
+ name: string;
+}
+
+// 차트 데이터 포인트
+export interface ChartDataPoint {
+ [key: string]: string | number;
+}
+
+// 차트 데이터
+export interface ChartData {
+ title: string;
+ lines: ChartLine[];
+ data: ChartDataPoint[];
+}
diff --git a/src/entities/chart/types/index.ts b/src/entities/chart/types/index.ts
new file mode 100644
index 0000000..45df87b
--- /dev/null
+++ b/src/entities/chart/types/index.ts
@@ -0,0 +1,2 @@
+export * from './chart.type';
+export * from './risk-analysis.type';
diff --git a/src/entities/chart/types/risk-analysis.type.ts b/src/entities/chart/types/risk-analysis.type.ts
new file mode 100644
index 0000000..f0fd2ce
--- /dev/null
+++ b/src/entities/chart/types/risk-analysis.type.ts
@@ -0,0 +1,58 @@
+/**
+ * 위험도 분석 관련 entities
+ */
+
+// 위험 요인
+export type RiskFactor = {
+ name: string;
+ percent: number;
+};
+
+// 위험도 요약
+export type RiskSummary = {
+ score: number;
+ factors: RiskFactor[];
+};
+
+// 임대인 정보
+export type Landlord = {
+ landlordId: number;
+ name: string;
+ normalizedKey: string;
+ ownedCount: number;
+ grade: string;
+ createdAt: string;
+ updatedAt: string;
+};
+
+// 임대인 신뢰도
+export type LandlordTrust = {
+ trustScore: number;
+ subrogationCount: number;
+ arrearsCount: number;
+ litigationCount: number;
+ ownedUnsoldCount: number;
+ grade: string;
+};
+
+// 임대인 소유 매물
+export type LandlordPlace = {
+ placeId: number;
+ label: string;
+ address: string;
+ addressDetail: string;
+};
+
+// 위험도 분석 응답 데이터
+export type RiskAnalysisResponse = {
+ data: {
+ riskSummary: RiskSummary;
+ landlord: Landlord;
+ landlordTrust: LandlordTrust;
+ landlordPlaces: LandlordPlace[];
+ };
+ status: string;
+ serverDateTime: string;
+ errorCode: string | null;
+ errorMessage: string | null;
+};
diff --git a/src/entities/index.ts b/src/entities/index.ts
index e69de29..5a0bd7b 100644
--- a/src/entities/index.ts
+++ b/src/entities/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './chart';
+export * from './risk-analysis';
diff --git a/src/features/main/components/common/ChartAxis.tsx b/src/features/main/components/common/ChartAxis.tsx
new file mode 100644
index 0000000..b7dd6e3
--- /dev/null
+++ b/src/features/main/components/common/ChartAxis.tsx
@@ -0,0 +1,18 @@
+import { XAxis, YAxis } from 'recharts';
+
+import { useChartConfig } from '../../hooks';
+
+type Props = {
+ dataKey: string;
+};
+
+export const ChartAxis = ({ dataKey }: Props) => {
+ const { xAxisConfig, yAxisConfig } = useChartConfig();
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/src/features/main/components/common/ChartGrid.tsx b/src/features/main/components/common/ChartGrid.tsx
new file mode 100644
index 0000000..0b021b7
--- /dev/null
+++ b/src/features/main/components/common/ChartGrid.tsx
@@ -0,0 +1,16 @@
+import { CartesianGrid } from 'recharts';
+
+import { useChartConfig } from '../../hooks';
+
+export const ChartGrid = () => {
+ const { gridConfig } = useChartConfig();
+
+ return (
+ <>
+ {/* 가로선 - 실선 */}
+
+ {/* 세로선 - 점선 */}
+
+ >
+ );
+};
diff --git a/src/features/main/components/common/ChartLineItem.tsx b/src/features/main/components/common/ChartLineItem.tsx
new file mode 100644
index 0000000..a082bfb
--- /dev/null
+++ b/src/features/main/components/common/ChartLineItem.tsx
@@ -0,0 +1,17 @@
+import { Line } from 'recharts';
+
+import type { ChartLine } from '@/entities';
+
+import { useChartConfig } from '../../hooks';
+
+type Props = {
+ line: ChartLine;
+ index: number;
+};
+
+export const ChartLineItem = ({ line, index }: Props) => {
+ const { getLineConfig } = useChartConfig();
+ const config = getLineConfig(line, index);
+
+ return ;
+};
diff --git a/src/features/main/components/common/LegendLine.tsx b/src/features/main/components/common/LegendLine.tsx
new file mode 100644
index 0000000..c0dde2f
--- /dev/null
+++ b/src/features/main/components/common/LegendLine.tsx
@@ -0,0 +1,25 @@
+import type { ChartLine } from '@/entities';
+
+import { useChartConfig } from '../../hooks';
+
+type Props = {
+ lines: ChartLine[];
+};
+
+export const LegendLine = ({ lines }: Props) => {
+ const { getLegendItemConfig } = useChartConfig();
+
+ return (
+
- {title && (
-
{title}
- )}
+
+
{title}
-
- {/* 가로선 - 실선 */}
-
- {/* 세로선 - 점선 */}
-
-
-
-
+
+
+
+
{
if (!payload || payload.length === 0) return null;
- // lines 배열의 순서대로 범례를 생성
- return (
-
- {lines.map((line, index) => (
-
-
- {line.name}
-
- ))}
-
- );
+ return ;
}}
/>
- {lines.map((line, index) => {
- const color = getChartColor(index);
- return (
-
- );
- })}
+ {lines.map((line, index) => (
+
+ ))}
diff --git a/src/features/main/components/features/chart/RiskChartBox.tsx b/src/features/main/components/features/chart/RiskChartBox.tsx
new file mode 100644
index 0000000..559e0bf
--- /dev/null
+++ b/src/features/main/components/features/chart/RiskChartBox.tsx
@@ -0,0 +1,65 @@
+import { Pie, PieChart, ResponsiveContainer } from 'recharts';
+
+import {
+ type GaugeData,
+ getGaugeAngle,
+ getGaugeData,
+ getRiskBoxShadowClass,
+ getRiskColorClass,
+ getRiskScoreColorClass,
+} from '@/shared';
+
+import { Needle } from '../../common';
+
+type Props = {
+ riskScore: number;
+};
+
+export const RiskChartBox = ({ riskScore }: Props) => {
+ const gaugeData: GaugeData = getGaugeData(riskScore);
+
+ const scoreColorClass = getRiskScoreColorClass(riskScore);
+
+ const needleAngle = getGaugeAngle(riskScore);
+
+ return (
+
+
전세 계약 최종 위험도
+
+ 위험 점수 :
+ {riskScore}
+ 점
+
+
+
+
+
+
+
+
+
+ {/* 게이지 바늘 */}
+
+
+ {/* 위험도 레이블 */}
+
+ {gaugeData.label}
+
+
+
+ );
+};
diff --git a/src/features/main/components/features/chart/RiskFactorsBox.tsx b/src/features/main/components/features/chart/RiskFactorsBox.tsx
new file mode 100644
index 0000000..3788324
--- /dev/null
+++ b/src/features/main/components/features/chart/RiskFactorsBox.tsx
@@ -0,0 +1,27 @@
+import type { RiskFactor } from '@/entities';
+
+type Props = {
+ riskFactors: RiskFactor[];
+};
+
+export const RiskFactorsBox = ({ riskFactors }: Props) => {
+ return (
+
+
주요 위험 요인
+
+ {riskFactors.map((factor) => (
+
+
+
+ {factor.name}
+
+ {factor.percent}
+ %
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/features/main/components/features/chart/index.ts b/src/features/main/components/features/chart/index.ts
index a08fef3..a51f5bc 100644
--- a/src/features/main/components/features/chart/index.ts
+++ b/src/features/main/components/features/chart/index.ts
@@ -1 +1,3 @@
-export * from './ChartBox';
+export { ChartBox } from './ChartBox';
+export { RiskChartBox } from './RiskChartBox';
+export { RiskFactorsBox } from './RiskFactorsBox';
diff --git a/src/features/main/hooks/index.ts b/src/features/main/hooks/index.ts
index e69de29..6eb5c2e 100644
--- a/src/features/main/hooks/index.ts
+++ b/src/features/main/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useChartConfig';
diff --git a/src/features/main/hooks/useChartConfig.ts b/src/features/main/hooks/useChartConfig.ts
new file mode 100644
index 0000000..0bcb7ad
--- /dev/null
+++ b/src/features/main/hooks/useChartConfig.ts
@@ -0,0 +1,94 @@
+/**
+ * 차트 설정 관련 hook
+ */
+import { getChartColor, getChartColorClass } from '@/shared/utils/chart/chart-colors';
+
+import type { ChartLine } from '@/entities';
+
+export const useChartConfig = () => {
+ // 차트 기본 설정
+ const chartConfig = {
+ margin: { top: 5, right: 30, left: 0, bottom: 5 },
+ height: 300,
+ };
+
+ // X축 설정
+ const xAxisConfig = {
+ stroke: '#BDBDBF',
+ fontSize: 14,
+ tickLine: false,
+ axisLine: true,
+ tickMargin: 10,
+ };
+
+ // Y축 설정
+ const yAxisConfig = {
+ stroke: '#BDBDBF',
+ fontSize: 12,
+ tickLine: false,
+ axisLine: false,
+ };
+
+ // 그리드 설정
+ const gridConfig = {
+ horizontal: {
+ horizontal: true,
+ vertical: false,
+ strokeDasharray: '0',
+ stroke: '#BDBDBF',
+ },
+ vertical: {
+ horizontal: false,
+ vertical: true,
+ strokeDasharray: '3 3',
+ stroke: '#f0f0f0',
+ },
+ };
+
+ // 범례 설정
+ const legendConfig = {
+ verticalAlign: 'top' as const,
+ align: 'left' as const,
+ height: 36,
+ wrapperStyle: {
+ paddingBottom: '10px',
+ marginLeft: '30px',
+ marginTop: '-20px',
+ },
+ };
+
+ // 라인 설정 생성 함수
+ const getLineConfig = (line: ChartLine, index: number) => ({
+ type: 'monotone' as const,
+ dataKey: line.key,
+ stroke: getChartColor(index),
+ strokeWidth: 2,
+ dot: {
+ fill: getChartColor(index),
+ strokeWidth: 2,
+ r: 4,
+ },
+ activeDot: {
+ r: 6,
+ stroke: getChartColor(index),
+ strokeWidth: 2,
+ },
+ });
+
+ // 범례 아이템 설정 생성 함수
+ const getLegendItemConfig = (line: ChartLine, index: number) => ({
+ key: line.key,
+ colorClass: getChartColorClass(index),
+ name: line.name,
+ });
+
+ return {
+ chartConfig,
+ xAxisConfig,
+ yAxisConfig,
+ gridConfig,
+ legendConfig,
+ getLineConfig,
+ getLegendItemConfig,
+ };
+};
diff --git a/src/features/main/mock/chart-data.mock.ts b/src/features/main/mock/chart-data.mock.ts
index f42fc2d..024ef33 100644
--- a/src/features/main/mock/chart-data.mock.ts
+++ b/src/features/main/mock/chart-data.mock.ts
@@ -1,84 +1,79 @@
-// 임의의 데이터 생성 (2020-2025년)
+/**
+ * 차트 임시 데이터
+ */
+import type { ChartData } from '@/entities';
-// 최근 6개월 가격 지수 변동률
-
-const CHART_DATA = {
- // 최근 6개월 가격 지수 변동률
- RECENT_PRICE_FLUCTUATIONS: [
- { year: '20', 기준치: 0, 변동률: 2.5 },
- { year: '21', 기준치: 0, 변동률: 1.8 },
- { year: '22', 기준치: 0, 변동률: 0.9 },
- { year: '23', 기준치: 0, 변동률: 3.2 },
- { year: '24', 기준치: 0, 변동률: 1.5 },
- { year: '25', 기준치: 0, 변동률: 2.1 },
- ],
- // LTV 전세금, 매매가 그래프
- REAL_ESTATE_PRICE_DATA: [
- { year: '20', 전세금: 2.8, 매매가: 3.2, 임대료: 1.5 },
- { year: '21', 전세금: 3.1, 매매가: 3.5, 임대료: 1.8 },
- { year: '22', 전세금: 2.5, 매매가: 2.9, 임대료: 1.2 },
- { year: '23', 전세금: 3.8, 매매가: 4.1, 임대료: 2.1 },
- { year: '24', 전세금: 3.0, 매매가: 3.3, 임대료: 1.6 },
- { year: '25', 전세금: 3.4, 매매가: 3.7, 임대료: 1.9 },
- ],
- // 해당 지역 대위변제 발생 빈도 및 증가율
- REGIONAL_INCREASE_RATE: [
- { year: '20', 증가율: 1.2 },
- { year: '21', 증가율: 1.8 },
- { year: '22', 증가율: 0.9 },
- { year: '23', 증가율: 2.5 },
- { year: '24', 증가율: 1.6 },
- { year: '25', 증가율: 2.1 },
- ],
-};
-
-export const CHART_DATA_CONFIGS = [
+// 차트 데이터 배열
+export const CHART_DATA_LIST: ChartData[] = [
{
- title: '최근 6개월 가격 지수 변동률',
- data: CHART_DATA.RECENT_PRICE_FLUCTUATIONS,
+ title: '부동산 가격 변동 추이',
lines: [
- {
- key: '기준치',
- name: '기준치',
- showDot: false,
- },
- {
- key: '변동률',
- name: '변동률',
- showDot: true,
- },
+ { key: '전세금', name: '전세금' },
+ { key: '매매가', name: '매매가' },
+ { key: '임대료', name: '임대료' },
+ ],
+ data: [
+ { month: '1월', 전세금: 400, 매매가: 800, 임대료: 50 },
+ { month: '2월', 전세금: 420, 매매가: 820, 임대료: 52 },
+ { month: '3월', 전세금: 450, 매매가: 850, 임대료: 55 },
+ { month: '4월', 전세금: 480, 매매가: 880, 임대료: 58 },
+ { month: '5월', 전세금: 500, 매매가: 900, 임대료: 60 },
+ { month: '6월', 전세금: 520, 매매가: 920, 임대료: 62 },
+ { month: '7월', 전세금: 550, 매매가: 950, 임대료: 65 },
+ { month: '8월', 전세금: 580, 매매가: 980, 임대료: 68 },
+ { month: '9월', 전세금: 600, 매매가: 1000, 임대료: 70 },
+ { month: '10월', 전세금: 620, 매매가: 1020, 임대료: 72 },
+ { month: '11월', 전세금: 650, 매매가: 1050, 임대료: 75 },
+ { month: '12월', 전세금: 680, 매매가: 1080, 임대료: 78 },
],
},
{
- title: 'LTV 전세금, 매매가 그래프',
- data: CHART_DATA.REAL_ESTATE_PRICE_DATA,
+ title: '지역별 가격 변동률',
lines: [
- {
- key: '전세금',
- name: '전세금',
- showDot: true,
- },
- {
- key: '매매가',
- name: '매매가',
- showDot: true,
- },
- {
- key: '임대료',
- name: '임대료',
- showDot: true,
- },
+ { key: '강남구', name: '강남구' },
+ { key: '서초구', name: '서초구' },
+ { key: '마포구', name: '마포구' },
+ { key: '송파구', name: '송파구' },
+ ],
+ data: [
+ { month: '1월', 강남구: 2.1, 서초구: 1.8, 마포구: 1.5, 송파구: 1.9 },
+ { month: '2월', 강남구: 2.3, 서초구: 2.0, 마포구: 1.7, 송파구: 2.1 },
+ { month: '3월', 강남구: 2.5, 서초구: 2.2, 마포구: 1.9, 송파구: 2.3 },
+ { month: '4월', 강남구: 2.8, 서초구: 2.5, 마포구: 2.1, 송파구: 2.6 },
+ { month: '5월', 강남구: 3.0, 서초구: 2.7, 마포구: 2.3, 송파구: 2.8 },
+ { month: '6월', 강남구: 3.2, 서초구: 2.9, 마포구: 2.5, 송파구: 3.0 },
+ { month: '7월', 강남구: 3.5, 서초구: 3.1, 마포구: 2.7, 송파구: 3.2 },
+ { month: '8월', 강남구: 3.7, 서초구: 3.3, 마포구: 2.9, 송파구: 3.4 },
+ { month: '9월', 강남구: 3.9, 서초구: 3.5, 마포구: 3.1, 송파구: 3.6 },
+ { month: '10월', 강남구: 4.1, 서초구: 3.7, 마포구: 3.3, 송파구: 3.8 },
+ { month: '11월', 강남구: 4.3, 서초구: 3.9, 마포구: 3.5, 송파구: 4.0 },
+ { month: '12월', 강남구: 4.5, 서초구: 4.1, 마포구: 3.7, 송파구: 4.2 },
],
},
{
- title: '해당 지역 대위변제 발생 빈도 및 증가율',
- data: CHART_DATA.REGIONAL_INCREASE_RATE,
+ title: '대위변제 발생률 추이',
lines: [
- {
- key: '증가율',
- name: '증가율',
- showDot: true,
- },
+ { key: '발생률', name: '발생률' },
+ { key: '평균', name: '평균' },
+ ],
+ data: [
+ { month: '1월', 발생률: 1.2, 평균: 1.5 },
+ { month: '2월', 발생률: 1.4, 평균: 1.5 },
+ { month: '3월', 발생률: 1.1, 평균: 1.5 },
+ { month: '4월', 발생률: 1.6, 평균: 1.5 },
+ { month: '5월', 발생률: 1.3, 평균: 1.5 },
+ { month: '6월', 발생률: 1.8, 평균: 1.5 },
+ { month: '7월', 발생률: 1.5, 평균: 1.5 },
+ { month: '8월', 발생률: 1.7, 평균: 1.5 },
+ { month: '9월', 발생률: 1.4, 평균: 1.5 },
+ { month: '10월', 발생률: 1.9, 평균: 1.5 },
+ { month: '11월', 발생률: 1.6, 평균: 1.5 },
+ { month: '12월', 발생률: 1.3, 평균: 1.5 },
],
},
];
+
+// 개별 차트 데이터 (기존 코드와의 호환성을 위해 유지)
+export const REAL_ESTATE_PRICE_DATA = CHART_DATA_LIST[0];
+export const REGIONAL_PRICE_CHANGE_DATA = CHART_DATA_LIST[1];
+export const SUBROGATION_RATE_DATA = CHART_DATA_LIST[2];
diff --git a/src/features/main/mock/index.ts b/src/features/main/mock/index.ts
index 67bf2e5..5627d49 100644
--- a/src/features/main/mock/index.ts
+++ b/src/features/main/mock/index.ts
@@ -1 +1,2 @@
export * from './chart-data.mock';
+export * from './risk-analysis.mock';
diff --git a/src/features/main/mock/risk-analysis.mock.ts b/src/features/main/mock/risk-analysis.mock.ts
new file mode 100644
index 0000000..b3ff28f
--- /dev/null
+++ b/src/features/main/mock/risk-analysis.mock.ts
@@ -0,0 +1,90 @@
+/**
+ * 위험도 분석 임시 데이터
+ */
+import type { RiskAnalysisResponse } from '@/entities';
+
+// 서버 응답 형태의 임시 데이터들
+export const TEMP_RISK_ANALYSIS_DATA: RiskAnalysisResponse[] = [
+ {
+ data: {
+ riskSummary: {
+ score: 50,
+ factors: [
+ { name: '전세가율', percent: 12 },
+ { name: '가격하락', percent: 10 },
+ { name: '미분양(재고)', percent: 7 },
+ { name: '정책/규제', percent: 18 },
+ { name: '법적 리스크', percent: 16 },
+ ],
+ },
+ landlord: {
+ landlordId: 1,
+ name: '홍길동',
+ normalizedKey: '부산-사업자번호-해시',
+ ownedCount: 1,
+ grade: 'B',
+ createdAt: '2025-08-21T02:26:22.055226',
+ updatedAt: '2025-08-21T02:26:22.055226',
+ },
+ landlordTrust: {
+ trustScore: 95,
+ subrogationCount: 0,
+ arrearsCount: 0,
+ litigationCount: 0,
+ ownedUnsoldCount: 0,
+ grade: 'A',
+ },
+ landlordPlaces: [
+ {
+ placeId: 1,
+ label: '부산광역시 해운대구 센텀동로 45',
+ address: '부산광역시 해운대구 센텀동로 45',
+ addressDetail: '101동 1001호',
+ },
+ ],
+ },
+ status: 'SUCCESS',
+ serverDateTime: '2025-08-21T02:28:11.398139',
+ errorCode: null,
+ errorMessage: null,
+ },
+];
+
+// 기본 위험도 데이터 (사진과 일치)
+export const DEFAULT_RISK_ANALYSIS_DATA: RiskAnalysisResponse = TEMP_RISK_ANALYSIS_DATA[0];
+
+// 위험도 구간별 설명
+export const RISK_LEVEL_DESCRIPTIONS = {
+ SAFE: '매우 안전한 전세 계약입니다.',
+ MODERATE_SAFE: '안전한 전세 계약입니다.',
+ GOOD: '양호한 전세 계약입니다.',
+ MODERATE_RISK: '주의가 필요한 전세 계약입니다.',
+ RISK: '위험한 전세 계약입니다.',
+} as const;
+
+// 위험도 점수별 등급 매핑
+export const getRiskGrade = (score: number): string => {
+ if (score <= 20) return '매우 위험';
+ if (score <= 40) return '위험';
+ if (score <= 60) return '양호';
+ if (score <= 80) return '다소 안전';
+ return '안전';
+};
+
+// 위험도 등급별 색상
+export const getRiskGradeColor = (grade: string): string => {
+ switch (grade) {
+ case '매우 위험':
+ case '위험':
+ return 'text-red-600';
+ case '양호':
+ return 'text-green-600';
+ case '다소 위험':
+ return 'text-yellow-600';
+ case '다소 안전':
+ case '안전':
+ return 'text-red-600';
+ default:
+ return 'text-gray-600';
+ }
+};
diff --git a/src/features/main/model/data/index.ts b/src/features/main/model/data/index.ts
new file mode 100644
index 0000000..7c78729
--- /dev/null
+++ b/src/features/main/model/data/index.ts
@@ -0,0 +1 @@
+export * from './risk-segment';
diff --git a/src/features/main/model/data/risk-segment.ts b/src/features/main/model/data/risk-segment.ts
new file mode 100644
index 0000000..86345ac
--- /dev/null
+++ b/src/features/main/model/data/risk-segment.ts
@@ -0,0 +1,8 @@
+// 5개 구간 정의 (각각 20점씩) - 왼쪽부터 안전/다소안전/양호/다소위험/위험
+export const RISK_SEGMENTS = [
+ { name: '위험', value: 20, fill: '#ef4444' },
+ { name: '다소 위험', value: 20, fill: '#f97316' },
+ { name: '양호', value: 20, fill: '#fbbf24' },
+ { name: '다소안전', value: 20, fill: '#34d399' },
+ { name: '안전', value: 20, fill: '#10b981' },
+];
diff --git a/src/features/main/model/index.ts b/src/features/main/model/index.ts
new file mode 100644
index 0000000..3707679
--- /dev/null
+++ b/src/features/main/model/index.ts
@@ -0,0 +1 @@
+export * from './data';
diff --git a/src/features/main/ui/ChartSection.tsx b/src/features/main/ui/ChartSection.tsx
index d9698b2..a9c8737 100644
--- a/src/features/main/ui/ChartSection.tsx
+++ b/src/features/main/ui/ChartSection.tsx
@@ -1,13 +1,13 @@
import { ChartBox } from '../components';
-import { CHART_DATA_CONFIGS } from '../mock';
+import { CHART_DATA_LIST } from '../mock';
export const ChartSection = () => {
return (
-
- {CHART_DATA_CONFIGS.map((config) => (
-
-
+
+ {CHART_DATA_LIST.map((chartData, index) => (
+
+
))}
diff --git a/src/features/main/ui/InputSection.tsx b/src/features/main/ui/InputSection.tsx
index 193c5ec..bcd4819 100644
--- a/src/features/main/ui/InputSection.tsx
+++ b/src/features/main/ui/InputSection.tsx
@@ -5,8 +5,8 @@ export const InputSection = () => {
-
- 계약
+
+ 계약
하려고 하는 그 집
안심할 수 있는지 확인해봐요!
diff --git a/src/features/main/ui/RiskAnalysisSummarySection.tsx b/src/features/main/ui/RiskAnalysisSummarySection.tsx
new file mode 100644
index 0000000..0a134cb
--- /dev/null
+++ b/src/features/main/ui/RiskAnalysisSummarySection.tsx
@@ -0,0 +1,19 @@
+import { RiskChartBox, RiskFactorsBox } from '../components';
+import { DEFAULT_RISK_ANALYSIS_DATA } from '../mock';
+
+export const RiskAnalysisSummarySection = () => {
+ // 현재 사용할 임시 데이터
+ const currentData = DEFAULT_RISK_ANALYSIS_DATA; // 사진과 일치하는 기본 데이터
+
+ return (
+
+ {/* 메인 위험도 분석 섹션 */}
+
+ {/* 왼쪽: 위험도 게이지 */}
+
+ {/* 오른쪽: 핵심 위험 요인 */}
+
+
+
+ );
+};
diff --git a/src/features/main/ui/index.ts b/src/features/main/ui/index.ts
index 724d6dc..d8dd51d 100644
--- a/src/features/main/ui/index.ts
+++ b/src/features/main/ui/index.ts
@@ -1,3 +1,4 @@
export * from './ChartSection';
export * from './InputSection';
export * from './MapSection';
+export * from './RiskAnalysisSummarySection';
diff --git a/src/index.css b/src/index.css
index 15854c6..e06becf 100644
--- a/src/index.css
+++ b/src/index.css
@@ -69,6 +69,11 @@
--color-lighthouse-button-secondary: var(--lighthouse-button-secondary);
--color-lighthouse-button-secondary-text: var(--lighthouse-button-secondary-text);
--color-lighthouse-button-secondary-hover: var(--lighthouse-button-secondary-hover);
+ --color-risk-very-danger: var(--risk-very-danger);
+ --color-risk-danger: var(--risk-danger);
+ --color-risk-good: var(--risk-good);
+ --color-risk-safe: var(--risk-safe);
+ --color-risk-very-safe: var(--risk-very-safe);
}
:root {
@@ -122,6 +127,11 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
+ --risk-very-danger: #ff6f6f;
+ --risk-danger: #ffba6f;
+ --risk-good: #ffe93f;
+ --risk-safe: #d9ff41;
+ --risk-very-safe: #2cdf44;
}
.dark {
diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx
index c56903f..3be8d11 100644
--- a/src/pages/main/MainPage.tsx
+++ b/src/pages/main/MainPage.tsx
@@ -1,4 +1,4 @@
-import { ChartSection, InputSection, MapSection } from '@/features';
+import { ChartSection, InputSection, MapSection, RiskAnalysisSummarySection } from '@/features';
export default function MainPage() {
return (
@@ -7,9 +7,8 @@ export default function MainPage() {
-
-
-
+
+
);
}
diff --git a/src/shared/components/common/icons/NeedleIcon.tsx b/src/shared/components/common/icons/NeedleIcon.tsx
new file mode 100644
index 0000000..ff52d89
--- /dev/null
+++ b/src/shared/components/common/icons/NeedleIcon.tsx
@@ -0,0 +1,27 @@
+import { type SVGProps } from 'react';
+
+type NeedleIconProps = {
+ className?: string;
+} & SVGProps
;
+
+export const NeedleIcon = ({ className = '', ...props }: NeedleIconProps) => {
+ return (
+
+ {/* 바늘 몸체 (삼각형 모양) */}
+
+ {/* 바늘 베이스 (원형) */}
+
+
+ );
+};
diff --git a/src/shared/components/common/icons/index.ts b/src/shared/components/common/icons/index.ts
new file mode 100644
index 0000000..5964166
--- /dev/null
+++ b/src/shared/components/common/icons/index.ts
@@ -0,0 +1 @@
+export { NeedleIcon } from './NeedleIcon';
diff --git a/src/shared/components/common/index.ts b/src/shared/components/common/index.ts
index 4790b7f..f26d5b1 100644
--- a/src/shared/components/common/index.ts
+++ b/src/shared/components/common/index.ts
@@ -1,2 +1,3 @@
export * from './logo-section';
export * from './image-preloader';
+export * from './icons';
diff --git a/src/shared/constants/chart-color.ts b/src/shared/constants/chart-color.ts
new file mode 100644
index 0000000..d6bf3de
--- /dev/null
+++ b/src/shared/constants/chart-color.ts
@@ -0,0 +1,23 @@
+// CSS 변수 기반 색상 배열
+export const CHART_COLORS = [
+ 'var(--chart-orange)', // 첫 번째 (1개일 때)
+ 'var(--chart-blue)', // 두 번째
+ 'var(--chart-green)', // 세 번째
+ 'var(--chart-purple)', // 네 번째
+ 'var(--chart-red)', // 다섯 번째
+ 'var(--chart-yellow)', // 여섯 번째
+ 'var(--chart-pink)', // 일곱 번째
+ 'var(--chart-indigo)', // 여덟 번째
+] as const;
+
+// Tailwind CSS 클래스 배열
+export const CHART_COLOR_CLASSES = [
+ 'bg-chart-orange', // 첫 번째 (1개일 때)
+ 'bg-chart-blue', // 두 번째
+ 'bg-chart-green', // 세 번째
+ 'bg-chart-purple', // 네 번째
+ 'bg-chart-red', // 다섯 번째
+ 'bg-chart-yellow', // 여섯 번째
+ 'bg-chart-pink', // 일곱 번째
+ 'bg-chart-indigo', // 여덟 번째
+] as const;
diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts
index 194fd2d..0899ce9 100644
--- a/src/shared/constants/index.ts
+++ b/src/shared/constants/index.ts
@@ -1 +1,3 @@
export * from './router-path';
+export * from './risk-chart-segments';
+export * from './chart-color';
diff --git a/src/shared/constants/risk-chart-segments.ts b/src/shared/constants/risk-chart-segments.ts
new file mode 100644
index 0000000..1d20f7e
--- /dev/null
+++ b/src/shared/constants/risk-chart-segments.ts
@@ -0,0 +1,63 @@
+/**
+ * 위험도 차트 구간 정의
+ */
+
+// 타입 정의
+export type RiskLevel = '매우 위험' | '위험' | '양호' | '안전' | '매우 안전';
+
+export type RiskSegment = {
+ name: RiskLevel;
+ value: number;
+ fill: string;
+};
+
+// 색상 상수
+const COLORS = {
+ RED: '#ff6f6f',
+ ORANGE: '#ffba6f',
+ YELLOW: '#ffe93f',
+ YELLOW_GREEN: '#d9ff41',
+ GREEN: '#2cdf44',
+} as const;
+
+// 위험도 점수별 색상 클래스
+export const RISK_SCORE_COLORS = {
+ VERY_DANGER: 'text-risk-very-danger', // 매우 위험 (0-20점)
+ DANGER: 'text-risk-danger', // 위험 (21-40점)
+ CAUTION: 'text-risk-good', // 양호 (41-60점)
+ SAFE: 'text-risk-safe', // 안전 (61-80점)
+ VERY_SAFE: 'text-risk-very-safe', // 매우 안전 (81-100점)
+} as const;
+
+// 값 상수
+const VALUES = {
+ VERY_RISK: 20,
+ RISK: 20,
+ GOOD: 20,
+ SAFE: 20,
+ VERY_SAFE: 20,
+} as const;
+
+// 이름 상수
+const NAMES = {
+ VERY_RISK: '매우 위험' as const,
+ RISK: '위험' as const,
+ GOOD: '양호' as const,
+ SAFE: '안전' as const,
+ VERY_SAFE: '매우 안전' as const,
+} as const;
+
+// 위험도 구간 정의 (왼쪽이 위험, 오른쪽이 안전)
+export const RISK_CHART_SEGMENTS: readonly RiskSegment[] = [
+ { name: NAMES.VERY_SAFE, value: VALUES.VERY_SAFE, fill: COLORS.GREEN },
+ { name: NAMES.SAFE, value: VALUES.SAFE, fill: COLORS.YELLOW_GREEN },
+ { name: NAMES.GOOD, value: VALUES.GOOD, fill: COLORS.YELLOW },
+ { name: NAMES.RISK, value: VALUES.RISK, fill: COLORS.ORANGE },
+ { name: NAMES.VERY_RISK, value: VALUES.VERY_RISK, fill: COLORS.RED },
+];
+
+// 구간 인덱스 타입
+export type RiskSegmentIndex = 0 | 1 | 2 | 3 | 4;
+
+// 점수 범위 타입
+export type RiskScore = number & { __brand: 'RiskScore' };
diff --git a/src/shared/utils/chart/chart-colors.ts b/src/shared/utils/chart/chart-colors.ts
index dcc5dcd..39e2b65 100644
--- a/src/shared/utils/chart/chart-colors.ts
+++ b/src/shared/utils/chart/chart-colors.ts
@@ -1,30 +1,7 @@
/**
* 차트 색상 관련 유틸리티
*/
-
-// CSS 변수 기반 색상 배열
-const CHART_COLORS = [
- 'var(--chart-orange)', // 첫 번째 (1개일 때)
- 'var(--chart-blue)', // 두 번째
- 'var(--chart-green)', // 세 번째
- 'var(--chart-purple)', // 네 번째
- 'var(--chart-red)', // 다섯 번째
- 'var(--chart-yellow)', // 여섯 번째
- 'var(--chart-pink)', // 일곱 번째
- 'var(--chart-indigo)', // 여덟 번째
-] as const;
-
-// Tailwind CSS 클래스 배열
-const CHART_COLOR_CLASSES = [
- 'bg-chart-orange', // 첫 번째 (1개일 때)
- 'bg-chart-blue', // 두 번째
- 'bg-chart-green', // 세 번째
- 'bg-chart-purple', // 네 번째
- 'bg-chart-red', // 다섯 번째
- 'bg-chart-yellow', // 여섯 번째
- 'bg-chart-pink', // 일곱 번째
- 'bg-chart-indigo', // 여덟 번째
-] as const;
+import { CHART_COLORS, CHART_COLOR_CLASSES, RISK_SCORE_COLORS } from '../../constants';
/**
* 인덱스에 따른 차트 색상 CSS 변수를 반환
@@ -44,6 +21,47 @@ export const getChartColorClass = (index: number): string => {
return CHART_COLOR_CLASSES[index % CHART_COLOR_CLASSES.length];
};
+/**
+ * 위험도 점수에 따른 색상 클래스를 반환
+ * @param score - 위험도 점수 (0-100, 낮을수록 위험)
+ * @returns Tailwind CSS 색상 클래스명
+ */
+export const getRiskScoreColorClass = (score: number): string => {
+ if (score <= 20) return RISK_SCORE_COLORS.VERY_DANGER; // 매우 위험
+ if (score <= 40) return RISK_SCORE_COLORS.DANGER; // 위험
+ if (score <= 60) return RISK_SCORE_COLORS.CAUTION; // 양호
+ if (score <= 80) return RISK_SCORE_COLORS.SAFE; // 안전
+ return RISK_SCORE_COLORS.VERY_SAFE; // 매우 안전
+};
+
+/**
+ * 위험도 색상 HEX 값을 Tailwind CSS 클래스로 변환
+ * @param hexColor - HEX 색상 값
+ * @returns Tailwind CSS box-shadow 클래스
+ */
+export const getRiskColorClass = (hexColor: string): string => {
+ const colorMap: Record = {
+ '#ff6f6f': 'bg-risk-very-danger', // 매우 위험
+ '#ffba6f': 'bg-risk-danger', // 위험
+ '#ffe93f': 'bg-risk-good', // 양호
+ '#d9ff41': 'bg-risk-safe', // 안전
+ '#2cdf44': 'bg-risk-very-safe', // 매우 안전
+ };
+
+ return colorMap[hexColor] || 'bg-gray-400';
+};
+
+export const getRiskBoxShadowClass = (hexColor: string): string => {
+ const colorMap: Record = {
+ '#ff6f6f': 'shadow-[0_0_12px_var(--color-risk-very-danger)]', // 매우 위험
+ '#ffba6f': 'shadow-[0_0_12px_var(--color-risk-danger)]', // 위험
+ '#ffe93f': 'shadow-[0_0_12px_var(--color-risk-good)]', // 양호
+ '#d9ff41': 'shadow-[0_0_12px_var(--color-risk-safe)]', // 안전
+ '#2cdf44': 'shadow-[0_0_12px_var(--color-risk-very-safe)]', // 매우 안전
+ };
+ return colorMap[hexColor] || 'shadow-[0_0_12px_rgba(156,163,175,0.5)]';
+};
+
/**
* 차트 색상 개수
*/
diff --git a/src/shared/utils/chart/index.ts b/src/shared/utils/chart/index.ts
index e616679..951d09b 100644
--- a/src/shared/utils/chart/index.ts
+++ b/src/shared/utils/chart/index.ts
@@ -1 +1,2 @@
export * from './chart-colors';
+export * from './risk-chart';
diff --git a/src/shared/utils/chart/risk-chart.ts b/src/shared/utils/chart/risk-chart.ts
new file mode 100644
index 0000000..c7ef34f
--- /dev/null
+++ b/src/shared/utils/chart/risk-chart.ts
@@ -0,0 +1,121 @@
+/**
+ * 위험도 게이지 차트 관련 유틸리티
+ */
+import {
+ RISK_CHART_SEGMENTS,
+ type RiskScore,
+ type RiskSegment,
+ type RiskSegmentIndex,
+} from '../../constants';
+
+// 게이지 데이터 타입 정의
+export type GaugeData = {
+ data: RiskSegment[];
+ color: string;
+ label: string;
+ angle: number;
+};
+
+// 상수 정의
+const SEGMENT_SIZE = 20;
+
+// 점수 범위 검증 함수
+export const isValidRiskScore = (score: number): score is RiskScore => {
+ return score >= 0 && score <= 100;
+};
+
+// 안전한 점수 변환 함수
+export const toRiskScore = (score: number): RiskScore => {
+ return Math.max(0, Math.min(100, score)) as RiskScore;
+};
+
+/**
+ * 위험도 점수에 따른 구간 인덱스를 계산
+ * 점수가 낮을수록 위험하므로 인덱스를 반대로 계산
+ * @param score - 위험도 점수 (0-100, 낮을수록 위험)
+ * @returns 구간 인덱스 (0-4, 0=매우 안전, 4=매우 위험)
+ */
+export const getSegmentIndex = (score: RiskScore): RiskSegmentIndex => {
+ // 점수가 낮을수록 위험하므로 인덱스를 반대로 계산
+ // 0-20점: 4번 인덱스 (매우 위험)
+ // 21-40점: 3번 인덱스 (위험)
+ // 41-60점: 2번 인덱스 (양호)
+ // 61-80점: 1번 인덱스 (안전)
+ // 81-100점: 0번 인덱스 (매우 안전)
+ const segmentIndex = Math.floor((score - 1) / SEGMENT_SIZE);
+ const reversedIndex = 4 - Math.max(0, Math.min(segmentIndex, 4));
+ return reversedIndex as RiskSegmentIndex;
+};
+
+/**
+ * 위험도 점수에 따른 구간 정보를 반환
+ * @param score - 위험도 점수 (0-100, 낮을수록 위험)
+ * @returns 현재 구간 정보
+ */
+export const getRiskSegment = (score: number): RiskSegment => {
+ const normalizedScore = toRiskScore(score);
+ const segmentIndex = getSegmentIndex(normalizedScore);
+ const result = RISK_CHART_SEGMENTS[segmentIndex];
+ return result;
+};
+
+/**
+ * 위험도 점수를 5구간으로 나눠서 각도로 변환 (이산형)
+ * 5구간을 36도씩 나누어 표현 (0-36, 36-72, 72-108, 108-144, 144-180)
+ * @param score - 위험도 점수 (0-100, 낮을수록 위험)
+ * @returns 바늘 각도 (0-180도)
+ */
+export const getGaugeAngle = (score: number): number => {
+ const normalized = Math.max(0, Math.min(100, score)); // 0~100 보정
+
+ // 5구간을 36도씩 나누어 계산
+ let result: number;
+ if (normalized <= 20) {
+ // 0-20점: 매우 위험 (0-36도, 왼쪽)
+ result = (normalized / 20) * 36;
+ } else if (normalized <= 40) {
+ // 21-40점: 위험 (36-72도)
+ result = 36 + ((normalized - 20) / 20) * 36;
+ } else if (normalized <= 60) {
+ // 41-60점: 양호 (72-108도, 중앙)
+ result = 72 + ((normalized - 40) / 20) * 36;
+ } else if (normalized <= 80) {
+ // 61-80점: 안전 (108-144도)
+ result = 108 + ((normalized - 60) / 20) * 36;
+ } else {
+ // 81-100점: 매우 안전 (144-180도, 오른쪽)
+ result = 144 + ((normalized - 80) / 20) * 36;
+ }
+
+ console.log(`보정점수 : ${normalized} 바늘 위치: ${score}점 → ${result}도`);
+ return result;
+};
+
+/**
+ * 위험도 점수에 따른 게이지 데이터를 생성
+ * @param score - 위험도 점수 (0-100, 낮을수록 위험)
+ * @returns 게이지 차트에 필요한 데이터
+ */
+export const getGaugeData = (score: number): GaugeData => {
+ const normalizedScore = toRiskScore(score);
+ const currentSegment = getRiskSegment(normalizedScore);
+ const angle = getGaugeAngle(normalizedScore);
+
+ const result = {
+ data: RISK_CHART_SEGMENTS as RiskSegment[],
+ color: currentSegment.fill,
+ label: currentSegment.name,
+ angle,
+ };
+
+ return result;
+};
+
+/**
+ * 위험도 점수가 유효한지 검증
+ * @param score - 검증할 점수
+ * @returns 유효성 여부
+ */
+export const validateRiskScore = (score: number): boolean => {
+ return isValidRiskScore(score);
+};