diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml
index a76b8ab..2b67a13 100644
--- a/.github/workflows/auto-pr.yml
+++ b/.github/workflows/auto-pr.yml
@@ -157,7 +157,7 @@ jobs:
// 429 에러 (Rate Limit)인 경우 재시도 대기
if (response.status === 429 && attempt < maxRetries) {
- const waitTime = Math.pow(2, attempt) * 1000; // 지수 백오프
+ const waitTime = Math.pow(2, attempt) * 2000;
console.log(`Rate limit 초과. ${waitTime}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
lastError = error;
@@ -199,9 +199,20 @@ jobs:
throw new Error(`Invalid or empty text response from Gemini.\nParts: ${JSON.stringify(candidate.content.parts)}`);
}
+ // 응답 검증 - 최소 길이 확인
+ if (aiResponse.length < 100) {
+ throw new Error(`Generated response is too short (${aiResponse.length} characters). This might indicate an incomplete response.`);
+ }
+
aiResponse = aiResponse.replace(/^```markdown\n/, '').replace(/\n```$/, '');
- console.log('Generated PR body:', aiResponse);
+ console.log('Generated PR body length:', aiResponse.length);
+ console.log('Generated PR body preview:', aiResponse.substring(0, 200) + '...');
+
+ // 출력 설정 전에 잠시 대기
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
core.setOutput('body', aiResponse);
+ console.log('PR body successfully set to output');
return; // 성공 시 함수 종료
} catch (error) {
@@ -213,7 +224,7 @@ jobs:
}
// 마지막 시도가 아니면 잠시 대기
- const waitTime = 1000 * attempt;
+ const waitTime = 2000 * attempt; // 대기 시간 증가
console.log(`${waitTime}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
@@ -224,6 +235,7 @@ jobs:
return;
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ timeout-minutes: 10 # 타임아웃 시간 증가
- name: Create Pull Request
if: steps.check_pr.outputs.exists == 'false'
@@ -231,9 +243,23 @@ jobs:
GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
PR_BODY: ${{ steps.generate-body.outputs.body }}
run: |
+ # PR body가 비어있는지 확인
+ if [ -z "$PR_BODY" ]; then
+ echo "Error: PR body is empty. Skipping PR creation."
+ exit 1
+ fi
+
+ echo "PR body length: ${#PR_BODY}"
+ echo "PR body preview: ${PR_BODY:0:200}..."
+
+ # 잠시 대기 후 PR 생성
+ sleep 2
+
gh pr create \
--base main \
--head ${{ github.ref_name }} \
--title "${{ github.ref_name }}" \
--body "$PR_BODY" \
- --assignee ${{ github.actor }}
\ No newline at end of file
+ --assignee ${{ github.actor }}
+
+ echo "Pull Request created successfully!"
\ No newline at end of file
diff --git a/src/features/main/_assets/Home.webp b/src/features/main/_assets/Home.webp
new file mode 100644
index 0000000..bce6dcf
Binary files /dev/null and b/src/features/main/_assets/Home.webp differ
diff --git a/src/features/main/components/common/Pagination.tsx b/src/features/main/components/common/Pagination.tsx
new file mode 100644
index 0000000..5fc6f1a
--- /dev/null
+++ b/src/features/main/components/common/Pagination.tsx
@@ -0,0 +1,50 @@
+import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+
+import { Button } from '@/shared';
+
+type Props = {
+ currentPage: number;
+ setCurrentPage: (page: number) => void;
+ totalPages: number;
+};
+
+export const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
+ return (
+
-
전세 계약 최종 위험도
+
{title}
위험 점수 :
{riskScore}
diff --git a/src/features/main/components/features/index.ts b/src/features/main/components/features/index.ts
index 13bd4e4..2528bb2 100644
--- a/src/features/main/components/features/index.ts
+++ b/src/features/main/components/features/index.ts
@@ -1,2 +1,4 @@
export * from './form';
export * from './chart';
+export * from './reliability';
+export * from './property';
diff --git a/src/features/main/components/features/property/PropertyListBox.tsx b/src/features/main/components/features/property/PropertyListBox.tsx
new file mode 100644
index 0000000..a2561c9
--- /dev/null
+++ b/src/features/main/components/features/property/PropertyListBox.tsx
@@ -0,0 +1,60 @@
+import { useState } from 'react';
+
+import HomeIcon from '../../../_assets/Home.webp';
+import type { Property } from '../../../types';
+import { getPropertyTypeColor } from '../../../utils';
+import { Pagination } from '../../common';
+
+type Props = {
+ properties: Property[];
+ title: string;
+};
+
+export const PropertyListBox = ({ properties, title }: Props) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 5;
+ const totalPages = Math.ceil(properties.length / itemsPerPage);
+
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const currentProperties = properties.slice(startIndex, endIndex);
+
+ return (
+
+
{title}
+
+
+ {currentProperties.map((property) => (
+
+

+
+
+ {/* 매물 타입 라벨 */}
+
+
+ {property.type}
+
+ {property.buildingName}
+
+
+ {/* 상세 주소 */}
+
{property.address}
+
+
+ ))}
+
+
+ {/* 페이지네이션 */}
+
+
+ );
+};
diff --git a/src/features/main/components/features/property/index.ts b/src/features/main/components/features/property/index.ts
new file mode 100644
index 0000000..f341ae7
--- /dev/null
+++ b/src/features/main/components/features/property/index.ts
@@ -0,0 +1 @@
+export * from './PropertyListBox';
diff --git a/src/features/main/components/features/reliability/GradeBox.tsx b/src/features/main/components/features/reliability/GradeBox.tsx
new file mode 100644
index 0000000..6a8aea1
--- /dev/null
+++ b/src/features/main/components/features/reliability/GradeBox.tsx
@@ -0,0 +1,23 @@
+import { getTrustGradeColorClass } from '@/shared';
+
+import { LANDLORD_TRUST_DATA } from '../../../mock';
+
+export const GradeBox = () => {
+ return (
+
+
임대인 신뢰도 등급
+
+ 위험점수 :
+ {LANDLORD_TRUST_DATA.trustScore}
+ 점
+
+
+
+ {LANDLORD_TRUST_DATA.grade}
+
+
+
+ );
+};
diff --git a/src/features/main/components/features/reliability/MultiHouseBox.tsx b/src/features/main/components/features/reliability/MultiHouseBox.tsx
new file mode 100644
index 0000000..2a34b74
--- /dev/null
+++ b/src/features/main/components/features/reliability/MultiHouseBox.tsx
@@ -0,0 +1,15 @@
+import { LANDLORD_TRUST_DATA } from '../../../mock';
+
+export const MultiHouseBox = () => {
+ return (
+
+
다주택자
+
+
+ {LANDLORD_TRUST_DATA.ownedUnsoldCount}
+
+ 채
+
+
+ );
+};
diff --git a/src/features/main/components/features/reliability/ReasonBox.tsx b/src/features/main/components/features/reliability/ReasonBox.tsx
new file mode 100644
index 0000000..63adbdd
--- /dev/null
+++ b/src/features/main/components/features/reliability/ReasonBox.tsx
@@ -0,0 +1,24 @@
+import { RELIABILITY_REASONS } from '../../../mock';
+
+export const ReasonBox = () => {
+ return (
+
+
임대인 신뢰도 등급 사유
+ {RELIABILITY_REASONS.map((reason, index) => (
+
+
+
+ {reason.name}
+
+ {reason.percent}
+ %
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/features/main/components/features/reliability/SubrogationPaymentBox.tsx b/src/features/main/components/features/reliability/SubrogationPaymentBox.tsx
new file mode 100644
index 0000000..c9977e8
--- /dev/null
+++ b/src/features/main/components/features/reliability/SubrogationPaymentBox.tsx
@@ -0,0 +1,19 @@
+import { getSubrogationColorClass } from '@/shared';
+
+import { LANDLORD_TRUST_DATA } from '../../../mock';
+
+export const SubrogationPaymentBox = () => {
+ return (
+
+
대위변제 이력
+
+
+ {LANDLORD_TRUST_DATA.subrogationCount}
+
+ 회
+
+
+ );
+};
diff --git a/src/features/main/components/features/reliability/index.ts b/src/features/main/components/features/reliability/index.ts
new file mode 100644
index 0000000..7b4eb68
--- /dev/null
+++ b/src/features/main/components/features/reliability/index.ts
@@ -0,0 +1,4 @@
+export * from './GradeBox';
+export * from './ReasonBox';
+export * from './SubrogationPaymentBox';
+export * from './MultiHouseBox';
diff --git a/src/features/main/constants/index.ts b/src/features/main/constants/index.ts
index 5636141..ba50c60 100644
--- a/src/features/main/constants/index.ts
+++ b/src/features/main/constants/index.ts
@@ -1,2 +1,3 @@
export * from './form-type';
export * from './house-type';
+export * from './property-type';
diff --git a/src/features/main/constants/property-type.ts b/src/features/main/constants/property-type.ts
new file mode 100644
index 0000000..d96f334
--- /dev/null
+++ b/src/features/main/constants/property-type.ts
@@ -0,0 +1,24 @@
+// 기본 매물 타입별 색상 정의
+export const PROPERTY_TYPE_COLORS = {
+ 오피스텔: '#B27373', // 적갈색
+ 원룸: '#73B286', // 연한 초록색
+ 투룸: '#7573B2', // 보라색
+ 아파트: '#B273AE', // 연한 분홍색
+ 복층: '#739EB2', // 청록색
+ 빌라: '#FFA500', // 주황색
+ 단독주택: '#32CD32', // 라임색
+} as const;
+
+// 동적 색상 생성을 위한 fallback 색상 배열
+export const FALLBACK_COLORS = [
+ '#FF6B6B', // 빨간색
+ '#4ECDC4', // 청록색
+ '#45B7D1', // 파란색
+ '#96CEB4', // 연한 초록색
+ '#FFEAA7', // 연한 노란색
+ '#DDA0DD', // 연한 보라색
+ '#98D8C8', // 민트색
+ '#F7DC6F', // 노란색
+ '#BB8FCE', // 연한 보라색
+ '#85C1E9', // 하늘색
+] as const;
diff --git a/src/features/main/mock/index.ts b/src/features/main/mock/index.ts
index 5627d49..0d17208 100644
--- a/src/features/main/mock/index.ts
+++ b/src/features/main/mock/index.ts
@@ -1,2 +1,4 @@
export * from './chart-data.mock';
+export * from './landlord-reliability.mock';
+export * from './property-data.mock';
export * from './risk-analysis.mock';
diff --git a/src/features/main/mock/landlord-reliability.mock.ts b/src/features/main/mock/landlord-reliability.mock.ts
new file mode 100644
index 0000000..2ae0920
--- /dev/null
+++ b/src/features/main/mock/landlord-reliability.mock.ts
@@ -0,0 +1,33 @@
+// 임대인 신뢰도 등급, 대위변제 이력, 다주택자
+export const LANDLORD_TRUST_DATA = {
+ trustScore: 85,
+ subrogationCount: 0,
+ arrearsCount: 0,
+ litigationCount: 0,
+ ownedUnsoldCount: 2,
+ grade: 'A',
+};
+
+// 임대인 신뢰도 등급 사유
+export const RELIABILITY_REASONS = [
+ {
+ name: '임대인 신뢰도 등급 사유',
+ percent: 25,
+ },
+ {
+ name: '임대인 신뢰도 등급 사유',
+ percent: 20,
+ },
+ {
+ name: '임대인 신뢰도 등급 사유',
+ percent: 18,
+ },
+ {
+ name: '임대인 신뢰도 등급 사유',
+ percent: 15,
+ },
+ {
+ name: '임대인 신뢰도 등급 사유',
+ percent: 12,
+ },
+];
diff --git a/src/features/main/mock/property-data.mock.ts b/src/features/main/mock/property-data.mock.ts
new file mode 100644
index 0000000..29cb693
--- /dev/null
+++ b/src/features/main/mock/property-data.mock.ts
@@ -0,0 +1,46 @@
+import type { Property } from '../types';
+
+export const SERVER_PROPERTIES: Property[] = [
+ {
+ id: 1,
+ type: '오피스텔',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 2,
+ type: '원룸',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 3,
+ type: '투룸',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 4,
+ type: '아파트',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 5,
+ type: '복층',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 6,
+ type: '빌라',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+ {
+ id: 7,
+ type: '단독주택',
+ buildingName: '센텀시티 파크빌',
+ address: '부산 해운대구 센텀시티 대로 123',
+ },
+];
diff --git a/src/features/main/types/index.ts b/src/features/main/types/index.ts
index 24ae138..8aefa50 100644
--- a/src/features/main/types/index.ts
+++ b/src/features/main/types/index.ts
@@ -1 +1,2 @@
export * from './house-types.type';
+export * from './properties-type.type';
diff --git a/src/features/main/types/properties-type.type.ts b/src/features/main/types/properties-type.type.ts
new file mode 100644
index 0000000..b406212
--- /dev/null
+++ b/src/features/main/types/properties-type.type.ts
@@ -0,0 +1,6 @@
+export type Property = {
+ id: number;
+ type: string;
+ buildingName: string;
+ address: string;
+};
diff --git a/src/features/main/ui/AlternativeSection.tsx b/src/features/main/ui/AlternativeSection.tsx
new file mode 100644
index 0000000..891bfe8
--- /dev/null
+++ b/src/features/main/ui/AlternativeSection.tsx
@@ -0,0 +1,22 @@
+import { PropertyListBox, RiskChartBox } from '../components';
+import { SERVER_PROPERTIES } from '../mock';
+import { addColorsToProperties } from '../utils';
+
+export const AlternativeSection = () => {
+ const PROPERTIES_WITH_COLORS = addColorsToProperties(SERVER_PROPERTIES);
+
+ // TODO: 위험 점수 신뢰도 비교 차트 데이터 추가
+ const riskScore = 70;
+
+ return (
+
+
+ {/* 왼쪽: 임대인 소유 매물 조회 */}
+
+
+ {/* 오른쪽: 임대인 매물별 위험 점수 신뢰도 비교 */}
+
+
+
+ );
+};
diff --git a/src/features/main/ui/LandlordPropertySection.tsx b/src/features/main/ui/LandlordPropertySection.tsx
new file mode 100644
index 0000000..e038893
--- /dev/null
+++ b/src/features/main/ui/LandlordPropertySection.tsx
@@ -0,0 +1,19 @@
+import { PropertyListBox, RiskChartBox } from '../components';
+import { SERVER_PROPERTIES } from '../mock';
+
+export const LandlordPropertySection = () => {
+ // TODO: 위험 점수 신뢰도 비교 차트 데이터 추가
+ const riskScore = 70;
+
+ return (
+
+
+ {/* 왼쪽: 임대인 소유 매물 조회 */}
+
+
+ {/* 오른쪽: 임대인 매물별 위험 점수 신뢰도 비교 */}
+
+
+
+ );
+};
diff --git a/src/features/main/ui/LandlordReliabilitySection.tsx b/src/features/main/ui/LandlordReliabilitySection.tsx
new file mode 100644
index 0000000..5e4c80e
--- /dev/null
+++ b/src/features/main/ui/LandlordReliabilitySection.tsx
@@ -0,0 +1,14 @@
+import { GradeBox, MultiHouseBox, ReasonBox, SubrogationPaymentBox } from '../components';
+
+export const LandlordReliabilitySection = () => {
+ return (
+
+ );
+};
diff --git a/src/features/main/ui/RiskAnalysisSummarySection.tsx b/src/features/main/ui/RiskAnalysisSummarySection.tsx
index 0a134cb..67a8987 100644
--- a/src/features/main/ui/RiskAnalysisSummarySection.tsx
+++ b/src/features/main/ui/RiskAnalysisSummarySection.tsx
@@ -10,7 +10,10 @@ export const RiskAnalysisSummarySection = () => {
{/* 메인 위험도 분석 섹션 */}
{/* 왼쪽: 위험도 게이지 */}
-
+
{/* 오른쪽: 핵심 위험 요인 */}
diff --git a/src/features/main/ui/index.ts b/src/features/main/ui/index.ts
index d8dd51d..eba819b 100644
--- a/src/features/main/ui/index.ts
+++ b/src/features/main/ui/index.ts
@@ -2,3 +2,6 @@ export * from './ChartSection';
export * from './InputSection';
export * from './MapSection';
export * from './RiskAnalysisSummarySection';
+export * from './LandlordReliabilitySection';
+export * from './LandlordPropertySection';
+export * from './AlternativeSection';
diff --git a/src/features/main/utils/index.ts b/src/features/main/utils/index.ts
new file mode 100644
index 0000000..6d2fd2e
--- /dev/null
+++ b/src/features/main/utils/index.ts
@@ -0,0 +1 @@
+export * from './property-colors';
diff --git a/src/features/main/utils/property-colors.ts b/src/features/main/utils/property-colors.ts
new file mode 100644
index 0000000..3ee1b6e
--- /dev/null
+++ b/src/features/main/utils/property-colors.ts
@@ -0,0 +1,33 @@
+import { FALLBACK_COLORS, PROPERTY_TYPE_COLORS } from '../constants';
+
+/**
+ * 매물 타입에 따른 색상을 반환하는 함수
+ * @param type - 매물 타입
+ * @returns 색상 코드
+ */
+export const getPropertyTypeColor = (type: string): string => {
+ // 이미 정의된 타입이면 해당 색상 반환
+ if (type in PROPERTY_TYPE_COLORS) {
+ return PROPERTY_TYPE_COLORS[type as keyof typeof PROPERTY_TYPE_COLORS];
+ }
+
+ // 정의되지 않은 타입이면 해시를 기반으로 동적 색상 할당
+ let hash = 0;
+ for (let i = 0; i < type.length; i++) {
+ hash = type.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ const index = Math.abs(hash) % FALLBACK_COLORS.length;
+ return FALLBACK_COLORS[index];
+};
+
+/**
+ * 매물 목록에 색상 정보를 추가하는 함수
+ * @param properties - 매물 목록
+ * @returns 색상 정보가 추가된 매물 목록
+ */
+export const addColorsToProperties =
(properties: T[]) => {
+ return properties.map((property) => ({
+ ...property,
+ typeColor: getPropertyTypeColor(property.type),
+ }));
+};
diff --git a/src/index.css b/src/index.css
index e06becf..d0b87c2 100644
--- a/src/index.css
+++ b/src/index.css
@@ -74,9 +74,11 @@
--color-risk-good: var(--risk-good);
--color-risk-safe: var(--risk-safe);
--color-risk-very-safe: var(--risk-very-safe);
+ --text-landlord-reliability: var(--landlord-reliability-font-size);
}
:root {
+ --landlord-reliability-font-size: 250px;
--chart-blue: #0c3165;
--chart-orange: #f57a0c;
--chart-green: #10b981;
diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx
index 3be8d11..8d07d37 100644
--- a/src/pages/main/MainPage.tsx
+++ b/src/pages/main/MainPage.tsx
@@ -1,4 +1,12 @@
-import { ChartSection, InputSection, MapSection, RiskAnalysisSummarySection } from '@/features';
+import {
+ AlternativeSection,
+ ChartSection,
+ InputSection,
+ LandlordPropertySection,
+ LandlordReliabilitySection,
+ MapSection,
+ RiskAnalysisSummarySection,
+} from '@/features';
export default function MainPage() {
return (
@@ -9,6 +17,9 @@ export default function MainPage() {