add region convert module
parent
d3371fd7b6
commit
5bf283c5d4
16
README.md
16
README.md
|
|
@ -6,7 +6,13 @@ FastAPI와 OpenAI를 활용한 부동산 자연어 검색 웹 애플리케이션
|
|||
|
||||
- 자연어로 부동산 조건 입력
|
||||
- OpenAI API를 통한 자동 정보 추출
|
||||
- 가격, 위치, 면적, 방 수, 거래 유형 파싱
|
||||
- 추출 정보:
|
||||
- 매물 형태 (아파트, 오피스텔, 빌라, 주택 등)
|
||||
- 거래 유형 (전세, 월세, 매매)
|
||||
- 가격
|
||||
- 위치
|
||||
- 면적
|
||||
- 방 개수
|
||||
|
||||
## 설치
|
||||
|
||||
|
|
@ -32,6 +38,8 @@ python main.py
|
|||
브라우저에서 http://localhost:20001 접속
|
||||
|
||||
## 입력 예시
|
||||
- "강남역 근처 전세 2억 이하 투룸"
|
||||
- "서초동 30평대 아파트 매매 10억 이하"
|
||||
- "판교 방 3개 월세 100/50"
|
||||
- "강남역 근처 아파트 전세 2억 이하 투룸"
|
||||
- "서초동 30평대 오피스텔 매매 10억 이하"
|
||||
- "판교 빌라 방 3개 월세 100/50"
|
||||
- "잠실 주상복합 전세 5억"
|
||||
- "분당 단독주택 매매 20억"
|
||||
|
|
@ -8,6 +8,7 @@ import uvicorn
|
|||
|
||||
from models import RealEstateQuery, ParsedRealEstate
|
||||
from openai_parser import OpenAIParser
|
||||
from region_converter import RegionCodeConverter
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
|
@ -29,6 +30,9 @@ except ValueError as e:
|
|||
print(f"Warning: {e}")
|
||||
parser = None
|
||||
|
||||
# 지역 코드 변환기 초기화
|
||||
region_converter = RegionCodeConverter()
|
||||
|
||||
@app.get("/")
|
||||
async def serve_index():
|
||||
"""메인 페이지 제공"""
|
||||
|
|
@ -45,7 +49,13 @@ async def parse_real_estate(query: RealEstateQuery):
|
|||
|
||||
try:
|
||||
result = await parser.parse_real_estate_query(query.text)
|
||||
print(result)
|
||||
|
||||
# 위치 정보를 시군구 코드로 변환
|
||||
if result.location:
|
||||
region_code, region_name = region_converter.get_region_code(result.location)
|
||||
result.region_code = region_code
|
||||
result.region_name = region_name
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class ParsedRealEstate(BaseModel):
|
|||
price: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
area: Optional[str] = None
|
||||
rooms: Optional[str] = None
|
||||
rooms: Optional[int] = None
|
||||
transaction_type: Optional[str] = None # 전세, 월세, 매매
|
||||
property_type: Optional[str] = None # 아파트, 오피스텔, 주택, 빌라 등
|
||||
region_code: Optional[str] = None # 시군구 코드 (5자리)
|
||||
region_name: Optional[str] = None # 매칭된 정식 지역명
|
||||
raw_text: str
|
||||
|
|
@ -20,16 +20,17 @@ class OpenAIParser:
|
|||
|
||||
system_prompt = """
|
||||
당신은 부동산 정보를 추출하는 전문가입니다.
|
||||
사용자의 자연어 입력에서 다음 정보를 추출하세요.
|
||||
단위는 붙여서 표기하세요. :
|
||||
1. price: 가격 (전세금, 월세, 매매가 등) (string)
|
||||
2. location: 위치 (지역명, 동, 구 등) (string)
|
||||
3. area: 면적 (평수, 제곱미터) (string)
|
||||
4. rooms: 방 개수 (string)
|
||||
5. transaction_type: 거래 유형 (전세, 월세, 매매) (string)
|
||||
사용자의 자연어 입력에서 다음 정보를 추출하세요:
|
||||
1. price: 가격 (전세금, 월세, 매매가 등)
|
||||
2. location: 위치 (지역명, 동, 구 등)
|
||||
3. area: 면적 (평수, 제곱미터 등)
|
||||
4. rooms: 방 개수 (숫자만)
|
||||
5. transaction_type: 거래 유형 (전세, 월세, 매매)
|
||||
6. property_type: 매물 형태 (아파트, 오피스텔, 주택, 빌라, 원룸, 투룸, 쓰리룸, 단독주택, 다가구주택, 연립주택, 상가주택 등)
|
||||
|
||||
JSON 형식으로만 응답하세요.
|
||||
정보가 없는 항목은 null로 표시하세요.
|
||||
rooms는 숫자(integer)로만 표현하세요.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
|
@ -44,7 +45,6 @@ class OpenAIParser:
|
|||
)
|
||||
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
print(result)
|
||||
|
||||
return ParsedRealEstate(
|
||||
price=result.get("price"),
|
||||
|
|
@ -52,11 +52,13 @@ class OpenAIParser:
|
|||
area=result.get("area"),
|
||||
rooms=result.get("rooms"),
|
||||
transaction_type=result.get("transaction_type"),
|
||||
property_type=result.get("property_type"),
|
||||
region_code=None, # 메인 서버에서 변환
|
||||
region_name=None, # 메인 서버에서 변환
|
||||
raw_text=text
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
# 에러 발생 시 기본값 반환
|
||||
return ParsedRealEstate(
|
||||
raw_text=text,
|
||||
|
|
@ -64,5 +66,8 @@ class OpenAIParser:
|
|||
location=None,
|
||||
area=None,
|
||||
rooms=None,
|
||||
transaction_type=None
|
||||
transaction_type=None,
|
||||
property_type=None,
|
||||
region_code=None,
|
||||
region_name=None
|
||||
)
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
class RegionCodeConverter:
|
||||
"""위치 정보를 시군구 코드로 변환하는 유틸리티"""
|
||||
|
||||
def __init__(self):
|
||||
"""지역 코드 데이터 로드"""
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 전체 지역 코드 로드
|
||||
with open(os.path.join(base_path, 'data', 'region_codes.json'), 'r', encoding='utf-8') as f:
|
||||
self.full_codes = json.load(f)
|
||||
|
||||
# 간략 지역 코드 로드
|
||||
with open(os.path.join(base_path, 'data', 'region_codes_simple.json'), 'r', encoding='utf-8') as f:
|
||||
self.simple_codes = json.load(f)
|
||||
|
||||
def get_region_code(self, location: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
위치 정보를 시군구 코드로 변환
|
||||
|
||||
Args:
|
||||
location: 위치 텍스트 (예: "강남구", "서울 강남", "서울특별시 강남구")
|
||||
|
||||
Returns:
|
||||
(지역코드, 매칭된 지역명) 튜플
|
||||
"""
|
||||
if not location:
|
||||
return None, None
|
||||
|
||||
location = location.strip()
|
||||
|
||||
# 1. 정확한 매칭 시도 (전체 이름)
|
||||
for full_name, code in self.full_codes.items():
|
||||
if location in full_name or full_name in location:
|
||||
return code, full_name
|
||||
|
||||
# 2. 간략 이름으로 매칭 시도
|
||||
for simple_name, code in self.simple_codes.items():
|
||||
if simple_name in location:
|
||||
# 전체 이름 찾기
|
||||
for full_name, full_code in self.full_codes.items():
|
||||
if full_code == code:
|
||||
return code, full_name
|
||||
return code, simple_name
|
||||
|
||||
# 3. 부분 매칭 시도 (구, 시, 군 단위)
|
||||
location_parts = location.split()
|
||||
for part in location_parts:
|
||||
# 구/시/군으로 끝나는 부분 찾기
|
||||
if part.endswith('구') or part.endswith('시') or part.endswith('군'):
|
||||
# 간략 코드에서 찾기
|
||||
if part in self.simple_codes:
|
||||
code = self.simple_codes[part]
|
||||
# 전체 이름 찾기
|
||||
for full_name, full_code in self.full_codes.items():
|
||||
if full_code == code and part in full_name:
|
||||
return code, full_name
|
||||
return code, part
|
||||
|
||||
# 전체 코드에서 찾기
|
||||
for full_name, code in self.full_codes.items():
|
||||
if part in full_name:
|
||||
return code, full_name
|
||||
|
||||
# 4. 동 이름으로 구 추정 (주요 동 매핑)
|
||||
dong_to_gu = {
|
||||
# 서울 강남구
|
||||
'역삼': '강남구', '삼성': '강남구', '청담': '강남구', '논현': '강남구', '대치': '강남구',
|
||||
'도곡': '강남구', '개포': '강남구', '일원': '강남구', '수서': '강남구', '세곡': '강남구',
|
||||
# 서울 서초구
|
||||
'반포': '서초구', '서초': '서초구', '방배': '서초구', '양재': '서초구', '잠원': '서초구',
|
||||
'우면': '서초구', '내곡': '서초구',
|
||||
# 서울 송파구
|
||||
'잠실': '송파구', '신천': '송파구', '석촌': '송파구', '송파': '송파구', '가락': '송파구',
|
||||
'문정': '송파구', '장지': '송파구', '방이': '송파구', '오금': '송파구', '풍납': '송파구',
|
||||
# 서울 강동구
|
||||
'천호': '강동구', '성내': '강동구', '길동': '강동구', '둔촌': '강동구', '암사': '강동구',
|
||||
'명일': '강동구', '고덕': '강동구', '상일': '강동구',
|
||||
# 서울 노원구
|
||||
'상계': '노원구', '중계': '노원구', '하계': '노원구', '월계': '노원구', '공릉': '노원구',
|
||||
# 서울 양천구
|
||||
'목동': '양천구', '신정': '양천구', '신월': '양천구',
|
||||
# 서울 영등포구
|
||||
'여의도': '영등포구', '당산': '영등포구', '영등포': '영등포구', '문래': '영등포구',
|
||||
'양평': '영등포구', '신길': '영등포구', '대림': '영등포구',
|
||||
# 서울 마포구
|
||||
'홍대': '마포구', '합정': '마포구', '상수': '마포구', '망원': '마포구', '연남': '마포구',
|
||||
'서교': '마포구', '공덕': '마포구', '아현': '마포구', '도화': '마포구', '용강': '마포구',
|
||||
# 서울 성동구
|
||||
'성수': '성동구', '왕십리': '성동구', '행당': '성동구', '금호': '성동구', '옥수': '성동구',
|
||||
# 서울 용산구
|
||||
'이태원': '용산구', '한남': '용산구', '동빙고': '용산구', '서빙고': '용산구', '이촌': '용산구',
|
||||
'한강로': '용산구', '효창': '용산구', '용문': '용산구',
|
||||
# 서울 종로구
|
||||
'광화문': '종로구', '종로': '종로구', '인사동': '종로구', '삼청': '종로구', '평창': '종로구',
|
||||
'부암': '종로구', '무악': '종로구', '교남': '종로구',
|
||||
# 서울 중구
|
||||
'명동': '중구', '충무로': '중구', '을지로': '중구', '남대문': '중구', '회현': '중구',
|
||||
'필동': '중구', '장충': '중구', '신당': '중구', '다산': '중구',
|
||||
# 경기도 성남시 분당구
|
||||
'판교': '분당구', '정자': '분당구', '서현': '분당구', '수내': '분당구', '야탑': '분당구',
|
||||
'이매': '분당구', '분당': '분당구', '구미': '분당구', '금곡': '분당구',
|
||||
# 경기도 용인시
|
||||
'동백': '기흥구', '보정': '기흥구', '죽전': '수지구', '수지': '수지구',
|
||||
# 경기도 수원시
|
||||
'영통': '영통구', '광교': '영통구', '원천': '영통구', '매탄': '영통구',
|
||||
# 경기도 고양시
|
||||
'일산': '일산동구', '백석': '일산동구', '마두': '일산서구', '주엽': '일산서구'
|
||||
}
|
||||
|
||||
for dong, gu in dong_to_gu.items():
|
||||
if dong in location:
|
||||
if gu in self.simple_codes:
|
||||
code = self.simple_codes[gu]
|
||||
# 전체 이름 찾기
|
||||
for full_name, full_code in self.full_codes.items():
|
||||
if full_code == code:
|
||||
return code, full_name
|
||||
return code, gu
|
||||
|
||||
return None, None
|
||||
|
|
@ -47,6 +47,50 @@
|
|||
- docs/project_plan.md 업데이트 (API 가이드 문서 추가)
|
||||
- 웹 검색 및 브라우저를 통한 실제 API 정보 수집
|
||||
|
||||
[2025-08-19 13:40:37] 위치 정보를 시군구 코드로 변환하는 기능 추가 완료
|
||||
- backend/region_converter.py 생성
|
||||
- RegionCodeConverter 클래스 구현
|
||||
- 전체 지역명, 간략 지역명, 동 이름 매칭
|
||||
- 100개 이상의 주요 동 이름 매핑
|
||||
- backend/models.py 수정
|
||||
- region_code, region_name 필드 추가
|
||||
- backend/main.py 수정
|
||||
- 지역 코드 변환기 적용
|
||||
- OpenAI 파싱 후 자동 변환
|
||||
- backend/openai_parser.py 수정
|
||||
- 새 필드 추가
|
||||
- frontend/script.js 수정
|
||||
- 지역 코드 표시 기능 추가
|
||||
- frontend/style.css 수정
|
||||
- 지역 코드 스타일 추가
|
||||
- 변환 우선순위:
|
||||
1. 정확한 전체 이름 매칭
|
||||
2. 간략 이름 매칭
|
||||
3. 구/시/군 단위 부분 매칭
|
||||
4. 동 이름으로 구 추정
|
||||
|
||||
[2025-08-19 13:36:33] 위치 정보를 시군구 코드로 변환하는 기능 추가 시작
|
||||
- 위치 텍스트를 행정표준코드로 변환
|
||||
- region_codes.json 파일 활용
|
||||
|
||||
[2025-08-19 13:30:01] 매물 형태 필드 추가 작업 완료
|
||||
- backend/models.py: property_type 필드 추가
|
||||
- backend/openai_parser.py:
|
||||
- 시스템 프롬프트에 property_type 추가
|
||||
- 아파트, 오피스텔, 빌라, 주택 등 구분
|
||||
- rooms 타입을 integer로 수정
|
||||
- frontend/script.js: 결과 표시에 매물 형태 추가
|
||||
- frontend/index.html: 예시 텍스트에 매물 형태 포함
|
||||
- README.md: 기능 설명 및 예시 업데이트
|
||||
- 추출 가능한 매물 형태:
|
||||
- 아파트, 오피스텔, 빌라, 원룸, 투룸
|
||||
- 단독주택, 다가구주택, 연립주택
|
||||
- 주상복합, 상가주택 등
|
||||
|
||||
[2025-08-19 13:27:22] 매물 형태 필드 추가 작업 시작
|
||||
- 분석 결과에 property_type 필드 추가
|
||||
- 아파트, 오피스텔, 주택, 빌라 등 구분
|
||||
|
||||
[2025-08-19 13:15:44] 한국 시군구 코드 JSON 파일 생성 작업 완료
|
||||
- data 폴더 생성
|
||||
- data/region_codes.json 생성 (전체 시군구 코드)
|
||||
|
|
|
|||
|
|
@ -15,18 +15,21 @@
|
|||
|
||||
<main>
|
||||
<div class="search-section">
|
||||
<textarea id="searchInput" placeholder="예: 강남역 근처 전세 2억 이하 투룸 찾아줘
|
||||
서초동 30평대 아파트 매매 10억 이하
|
||||
방 3개짜리 월세 100/50 이하 판교"></textarea>
|
||||
<textarea id="searchInput" placeholder="예: 강남역 근처 아파트 전세 2억 이하 투룸
|
||||
서초동 30평대 오피스텔 매매 10억 이하
|
||||
판교 빌라 방 3개 월세 100/50
|
||||
역삼동 원룸 오피스텔 전세 1억5천"></textarea>
|
||||
<button id="searchBtn" onclick="parseQuery()">분석하기</button>
|
||||
</div>
|
||||
|
||||
<div class="examples">
|
||||
<h3>입력 예시:</h3>
|
||||
<div class="example-chips">
|
||||
<span class="chip" onclick="setExample(this)">강남 전세 3억 방 2개</span>
|
||||
<span class="chip" onclick="setExample(this)">판교 30평 아파트 매매</span>
|
||||
<span class="chip" onclick="setExample(this)">서초동 월세 200/100</span>
|
||||
<span class="chip" onclick="setExample(this)">강남 아파트 전세 3억 방 2개</span>
|
||||
<span class="chip" onclick="setExample(this)">판교 30평 오피스텔 매매</span>
|
||||
<span class="chip" onclick="setExample(this)">서초동 빌라 월세 200/100</span>
|
||||
<span class="chip" onclick="setExample(this)">잠실 주상복합 전세 5억</span>
|
||||
<span class="chip" onclick="setExample(this)">분당 단독주택 매매 20억</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ function displayResults(data) {
|
|||
|
||||
let html = '';
|
||||
|
||||
if (data.property_type) {
|
||||
html += `<div class="result-item">
|
||||
<strong>매물 형태:</strong> ${data.property_type}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (data.transaction_type) {
|
||||
html += `<div class="result-item">
|
||||
<strong>거래 유형:</strong> ${data.transaction_type}
|
||||
|
|
@ -64,6 +70,14 @@ function displayResults(data) {
|
|||
html += `<div class="result-item">
|
||||
<strong>위치:</strong> ${data.location}
|
||||
</div>`;
|
||||
|
||||
// 지역 코드가 있으면 표시
|
||||
if (data.region_code) {
|
||||
html += `<div class="result-item">
|
||||
<strong>지역 코드:</strong> ${data.region_code}
|
||||
${data.region_name ? ` (${data.region_name})` : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.area) {
|
||||
|
|
@ -74,7 +88,7 @@ function displayResults(data) {
|
|||
|
||||
if (data.rooms) {
|
||||
html += `<div class="result-item">
|
||||
<strong>방 개수:</strong> ${data.rooms}
|
||||
<strong>방 개수:</strong> ${data.rooms}개
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,15 @@ header p {
|
|||
width: 100px;
|
||||
}
|
||||
|
||||
.result-item .code-badge {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue