AI 실거래가 추천 10개 프로토타입

master
jaehwang 2025-08-19 15:45:22 +09:00
parent cb5dd5f25f
commit b1f8061811
8 changed files with 243 additions and 18 deletions

View File

@ -8,11 +8,13 @@ FastAPI와 OpenAI를 활용한 부동산 실거래가 검색 웹 애플리케이
- OpenAI API를 통한 자동 정보 추출 - OpenAI API를 통한 자동 정보 추출
- 국토교통부 공공데이터 API 연동 - 국토교통부 공공데이터 API 연동
- 실제 실거래가 데이터 조회 및 표시 - 실제 실거래가 데이터 조회 및 표시
- **AI 기반 매물 추천**: 사용자 요구사항과 가장 일치하는 TOP 10 매물 선별
- 지원 정보: - 지원 정보:
- 매물 형태 (아파트, 오피스텔, 빌라, 주택 등) - 매물 형태 (아파트, 오피스텔, 빌라, 주택 등)
- 거래 유형 (전세, 월세, 매매) - 거래 유형 (전세, 월세, 매매)
- 지역 자동 인식 및 코드 변환 - 지역 자동 인식 및 코드 변환
- 실거래가 목록 표시 - 실거래가 목록 표시
- AI 필터링을 통한 최적 매물 추천
## 설치 ## 설치

View File

@ -39,7 +39,7 @@ async def serve_index():
return FileResponse("../frontend/index.html") return FileResponse("../frontend/index.html")
@app.post("/api/search") @app.post("/api/search")
async def search_real_estate(query: RealEstateQuery): async def search_real_estate(query: RealEstateQuery, filter_results: bool = True):
"""자연어 검색 후 실거래가 데이터 조회""" """자연어 검색 후 실거래가 데이터 조회"""
if not parser: if not parser:
raise HTTPException( raise HTTPException(
@ -60,10 +60,55 @@ async def search_real_estate(query: RealEstateQuery):
region_code=parsed.region_code region_code=parsed.region_code
) )
# 3. OpenAI로 필터링 (옵션)
if filter_results and listings:
filtered_listings = await parser.filter_listings(
user_query=query.text,
listings=listings,
top_k=10
)
return {
"parsed": parsed,
"listings": filtered_listings,
"count": len(filtered_listings),
"total_count": len(listings),
"filtered": True
}
return { return {
"parsed": parsed, "parsed": parsed,
"listings": listings, "listings": listings[:20], # 필터링 안 할 경우 상위 20개만
"count": len(listings) "count": min(len(listings), 20),
"total_count": len(listings),
"filtered": False
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/filter")
async def filter_real_estate_listings(
user_query: str,
listings: list,
top_k: int = 10
):
"""공공데이터 결과를 OpenAI로 필터링하여 최적 매물 선별"""
if not parser:
raise HTTPException(
status_code=500,
detail="OpenAI API key not configured"
)
try:
filtered = await parser.filter_listings(
user_query=user_query,
listings=listings,
top_k=top_k
)
return {
"filtered_listings": filtered,
"count": len(filtered),
"original_count": len(listings)
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@ -136,4 +136,97 @@ class OpenAIParser:
property_type=None, property_type=None,
region_code=None, region_code=None,
region_name=None region_name=None
) )
async def filter_listings(self, user_query: str, listings: list, top_k: int = 10) -> list:
"""
OpenAI를 사용하여 실거래가 목록에서 사용자 요구사항과 가장 일치하는 매물 선별
Args:
user_query: 사용자의 원본 검색 쿼리
listings: 공공데이터 API에서 받은 실거래가 목록
top_k: 선별할 매물 개수 (기본값: 10)
Returns:
필터링된 매물 목록
"""
if not listings:
return []
# 매물 정보를 간략하게 정리
simplified_listings = []
for idx, item in enumerate(listings[:50]): # 최대 50개만 처리 (토큰 제한)
simplified = {
"index": idx,
"거래유형": item.get("거래유형", ""),
"매물형태": item.get("매물형태", ""),
"거래금액": item.get("거래금액", ""),
"보증금액": item.get("보증금액", ""),
"월세금액": item.get("월세금액", ""),
"전용면적": item.get("전용면적", ""),
"": item.get("", ""),
"건축년도": item.get("건축년도", ""),
"법정동": item.get("법정동", ""),
"거래일": f"{item.get('', '')}.{item.get('', '')}.{item.get('', '')}"
}
# 빈 값 제거
simplified = {k: v for k, v in simplified.items() if v}
simplified_listings.append(simplified)
system_prompt = """
당신은 부동산 전문가입니다.
사용자의 요구사항과 실거래가 데이터를 비교하여 가장 적합한 매물을 선별해주세요.
평가 기준:
1. 가격 조건 일치도 (사용자가 언급한 가격 범위)
2. 면적 조건 일치도 (평수, 개수 )
3. 층수 선호도
4. 건축년도 (신축/구축 선호도)
5. 위치 적합성
JSON 형식으로 응답하세요:
{
"selected_indices": [가장 적합한 매물의 index 10],
"reason": "선별 이유 간단 설명"
}
"""
user_prompt = f"""
사용자 요구사항: {user_query}
실거래가 데이터:
{json.dumps(simplified_listings, ensure_ascii=False, indent=2)}
데이터에서 사용자 요구사항과 가장 맞는 매물 {top_k}개를 선별해주세요.
"""
try:
response = self.client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1,
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
selected_indices = result.get("selected_indices", [])
# 선별된 매물 반환
filtered_listings = []
for idx in selected_indices:
if 0 <= idx < len(listings):
listing = listings[idx].copy()
listing["match_reason"] = result.get("reason", "")
listing["rank"] = len(filtered_listings) + 1
filtered_listings.append(listing)
return filtered_listings[:top_k]
except Exception as e:
print(f"Error filtering listings: {e}")
# 에러 시 원본 데이터의 상위 10개 반환
return listings[:top_k]

View File

@ -47,6 +47,37 @@
- docs/project_plan.md 업데이트 (API 가이드 문서 추가) - docs/project_plan.md 업데이트 (API 가이드 문서 추가)
- 웹 검색 및 브라우저를 통한 실제 API 정보 수집 - 웹 검색 및 브라우저를 통한 실제 API 정보 수집
[2025-08-19 15:22:10] OpenAI 기반 매물 필터링 기능 추가 완료
- backend/openai_parser.py 수정
- filter_listings 메서드 추가
- 공공데이터 결과를 OpenAI로 분석
- 사용자 요구사항과 일치도 평가
- 가격, 면적, 층수, 건축년도 등 종합 평가
- 상위 10개 매물 선별
- backend/main.py 수정
- /api/search에 filter_results 파라미터 추가
- 필터링 옵션 적용 (기본값: True)
- /api/filter 엔드포인트 추가 (별도 필터링 API)
- frontend/script.js 수정
- 필터링 결과 표시 UI 개선
- AI 추천 매물 TOP 10 표시
- 순위 배지 추가
- frontend/style.css 수정
- 필터링 정보 스타일 추가
- 순위 배지 디자인
- 추천 매물 강조 효과
- README.md 및 문서 업데이트
- 필터링 평가 기준:
- 가격 조건 일치도
- 면적 조건 일치도
- 층수 선호도
- 건축년도 (신축/구축)
- 위치 적합성
[2025-08-19 15:17:59] OpenAI 기반 매물 필터링 기능 추가 시작
- 공공데이터 결과를 OpenAI로 분석
- 사용자 요구사항과 가장 일치하는 10개 선별
[2025-08-19 15:12:44] 사용하지 않는 API 엔드포인트 제거 작업 완료 [2025-08-19 15:12:44] 사용하지 않는 API 엔드포인트 제거 작업 완료
- backend/main.py 정리 - backend/main.py 정리
- /api/parse 엔드포인트 제거 (사용 안함) - /api/parse 엔드포인트 제거 (사용 안함)

View File

@ -52,8 +52,14 @@ C:\o2o\RealEstateSearch\
### 2. POST `/api/search` ### 2. POST `/api/search`
- 자연어 검색 및 실거래가 조회 - 자연어 검색 및 실거래가 조회
- 요청: `{"text": "강남 아파트 전세"}` - AI 필터링 옵션 (filter_results 파라미터)
- 응답: 파싱 결과 + 실거래가 목록 - 요청: `{"text": "강남 아파트 전세 3억"}`
- 응답: 파싱 결과 + 실거래가 목록 (필터링 포함)
### 3. GET `/static/*` ### 3. POST `/api/filter`
- 공공데이터 결과를 OpenAI로 필터링
- 사용자 요구사항과 가장 일치하는 매물 선별
- 파라미터: user_query, listings, top_k
### 4. GET `/static/*`
- CSS, JS 등 정적 파일 제공 - CSS, JS 등 정적 파일 제공

View File

@ -75,18 +75,25 @@ function displayResults(data) {
// 실거래가 목록 // 실거래가 목록
if (data.listings && data.listings.length > 0) { if (data.listings && data.listings.length > 0) {
html += '<div class="listings-section">'; html += '<div class="listings-section">';
html += `<h3>📊 실거래가 정보 (${data.count}건)</h3>`;
html += '<div class="listings-container">';
// 최대 20개만 표시 // 필터링 정보 표시
const displayCount = Math.min(data.listings.length, 20); if (data.filtered) {
for (let i = 0; i < displayCount; i++) { html += `<h3>🎯 AI 추천 매물 TOP ${data.count} (전체 ${data.total_count}건 중)</h3>`;
const item = data.listings[i]; html += '<p class="filter-info">AI가 요구사항과 가장 일치하는 매물을 선별했습니다.</p>';
html += createListingCard(item); } else {
html += `<h3>📊 실거래가 정보 (${data.total_count || data.count}건)</h3>`;
} }
if (data.listings.length > 20) { html += '<div class="listings-container">';
html += `<p class="more-info">... 외 ${data.listings.length - 20}건</p>`;
// 매물 카드 표시
for (let i = 0; i < data.listings.length; i++) {
const item = data.listings[i];
html += createListingCard(item, data.filtered);
}
if (!data.filtered && data.total_count > data.count) {
html += `<p class="more-info">... 외 ${data.total_count - data.count}건</p>`;
} }
html += '</div>'; html += '</div>';
@ -108,9 +115,14 @@ function displayResults(data) {
} }
// 실거래가 카드 생성 // 실거래가 카드 생성
function createListingCard(item) { function createListingCard(item, isFiltered = false) {
let html = '<div class="listing-card">'; let html = '<div class="listing-card">';
// 순위 표시 (필터링된 경우)
if (isFiltered && item.rank) {
html += `<div class="rank-badge">TOP ${item.rank}</div>`;
}
// 제목 (아파트명 또는 주소) // 제목 (아파트명 또는 주소)
const title = item['아파트'] || item['법정동'] || '정보 없음'; const title = item['아파트'] || item['법정동'] || '정보 없음';
html += `<h4>${title}</h4>`; html += `<h4>${title}</h4>`;

View File

@ -272,4 +272,39 @@ header p {
color: #888; color: #888;
margin-top: 20px; margin-top: 20px;
font-style: italic; font-style: italic;
}
/* 필터링 정보 */
.filter-info {
color: #667eea;
font-size: 0.95em;
margin-bottom: 15px;
padding: 10px;
background: #f0f3ff;
border-radius: 8px;
text-align: center;
}
/* 순위 배지 */
.rank-badge {
position: absolute;
top: -10px;
right: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: bold;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
.listing-card {
position: relative;
background: white;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 15px;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
} }

View File

@ -3,4 +3,5 @@ uvicorn[standard]
openai openai
python-dotenv python-dotenv
requests requests
pydantic pydantic
python-multipart