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 ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} + + +
+ ); +}; diff --git a/src/features/main/components/common/index.ts b/src/features/main/components/common/index.ts index e70a782..b4699d7 100644 --- a/src/features/main/components/common/index.ts +++ b/src/features/main/components/common/index.ts @@ -3,3 +3,4 @@ export * from './ChartGrid'; export * from './ChartLineItem'; export * from './LegendLine'; export * from './Needle'; +export * from './Pagination'; diff --git a/src/features/main/components/features/chart/RiskChartBox.tsx b/src/features/main/components/features/chart/RiskChartBox.tsx index 559e0bf..3ba5f26 100644 --- a/src/features/main/components/features/chart/RiskChartBox.tsx +++ b/src/features/main/components/features/chart/RiskChartBox.tsx @@ -13,9 +13,10 @@ import { Needle } from '../../common'; type Props = { riskScore: number; + title: string; }; -export const RiskChartBox = ({ riskScore }: Props) => { +export const RiskChartBox = ({ riskScore, title }: Props) => { const gaugeData: GaugeData = getGaugeData(riskScore); const scoreColorClass = getRiskScoreColorClass(riskScore); @@ -24,7 +25,7 @@ export const RiskChartBox = ({ riskScore }: 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) => ( +
+ home + +
+ {/* 매물 타입 라벨 */} +
+ + {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() {
+ + +
); } diff --git a/src/shared/utils/chart/chart-colors.ts b/src/shared/utils/chart/chart-colors.ts index 39e2b65..61cd998 100644 --- a/src/shared/utils/chart/chart-colors.ts +++ b/src/shared/utils/chart/chart-colors.ts @@ -66,3 +66,54 @@ export const getRiskBoxShadowClass = (hexColor: string): string => { * 차트 색상 개수 */ export const CHART_COLOR_COUNT = CHART_COLORS.length; + +/** + * 대위변제 이력에 따른 색상 클래스 반환 + * @param count - 대위변제 횟수 + * @returns Tailwind CSS 텍스트 색상 클래스 + */ +export const getSubrogationColorClass = (count: number): string => { + if (count === 0) { + return 'text-risk-very-safe'; // 초록색 #2cdf44 + } else if (count === 1) { + return 'text-risk-danger'; // 주황색 #ffba6f + } else { + return 'text-risk-very-danger'; // 빨간색 #ff6f6f + } +}; + +/** + * 다주택자 수에 따른 색상 클래스 반환 (임의 색상) + * @param count - 다주택자 수 + * @returns Tailwind CSS 텍스트 색상 클래스 + */ + +// TODO: 적절한 기준에 따른 색상 클래스 반환 +export const getMultiHouseColorClass = (count: number): string => { + if (count === 0) { + return 'text-risk-very-safe'; // 초록색 + } else if (count <= 2) { + return 'text-risk-good'; // 노란색 + } else if (count <= 5) { + return 'text-risk-danger'; // 주황색 + } else { + return 'text-risk-very-danger'; // 빨간색 + } +}; + +/** + * 신뢰도 등급에 따른 색상 클래스 반환 + * @param grade - 신뢰도 등급 (A~E) + * @returns Tailwind CSS 텍스트 색상 클래스 + */ +export const getTrustGradeColorClass = (grade: string): string => { + const gradeMap: Record = { + A: 'text-risk-very-safe', // 초록색 + B: 'text-risk-safe', // 연두색 + C: 'text-risk-good', // 노란색 + D: 'text-risk-danger', // 주황색 + E: 'text-risk-very-danger', // 빨간색 + }; + + return gradeMap[grade] || 'text-gray-600'; +}; diff --git a/src/shared/utils/chart/risk-chart.ts b/src/shared/utils/chart/risk-chart.ts index c7ef34f..bcd5e80 100644 --- a/src/shared/utils/chart/risk-chart.ts +++ b/src/shared/utils/chart/risk-chart.ts @@ -87,7 +87,6 @@ export const getGaugeAngle = (score: number): number => { result = 144 + ((normalized - 80) / 20) * 36; } - console.log(`보정점수 : ${normalized} 바늘 위치: ${score}점 → ${result}도`); return result; };