add region convert module
parent
d3371fd7b6
commit
5bf283c5d4
16
README.md
16
README.md
|
|
@ -6,7 +6,13 @@ FastAPI와 OpenAI를 활용한 부동산 자연어 검색 웹 애플리케이션
|
||||||
|
|
||||||
- 자연어로 부동산 조건 입력
|
- 자연어로 부동산 조건 입력
|
||||||
- OpenAI API를 통한 자동 정보 추출
|
- OpenAI API를 통한 자동 정보 추출
|
||||||
- 가격, 위치, 면적, 방 수, 거래 유형 파싱
|
- 추출 정보:
|
||||||
|
- 매물 형태 (아파트, 오피스텔, 빌라, 주택 등)
|
||||||
|
- 거래 유형 (전세, 월세, 매매)
|
||||||
|
- 가격
|
||||||
|
- 위치
|
||||||
|
- 면적
|
||||||
|
- 방 개수
|
||||||
|
|
||||||
## 설치
|
## 설치
|
||||||
|
|
||||||
|
|
@ -32,6 +38,8 @@ python main.py
|
||||||
브라우저에서 http://localhost:20001 접속
|
브라우저에서 http://localhost:20001 접속
|
||||||
|
|
||||||
## 입력 예시
|
## 입력 예시
|
||||||
- "강남역 근처 전세 2억 이하 투룸"
|
- "강남역 근처 아파트 전세 2억 이하 투룸"
|
||||||
- "서초동 30평대 아파트 매매 10억 이하"
|
- "서초동 30평대 오피스텔 매매 10억 이하"
|
||||||
- "판교 방 3개 월세 100/50"
|
- "판교 빌라 방 3개 월세 100/50"
|
||||||
|
- "잠실 주상복합 전세 5억"
|
||||||
|
- "분당 단독주택 매매 20억"
|
||||||
|
|
@ -8,6 +8,7 @@ import uvicorn
|
||||||
|
|
||||||
from models import RealEstateQuery, ParsedRealEstate
|
from models import RealEstateQuery, ParsedRealEstate
|
||||||
from openai_parser import OpenAIParser
|
from openai_parser import OpenAIParser
|
||||||
|
from region_converter import RegionCodeConverter
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
@ -29,6 +30,9 @@ except ValueError as e:
|
||||||
print(f"Warning: {e}")
|
print(f"Warning: {e}")
|
||||||
parser = None
|
parser = None
|
||||||
|
|
||||||
|
# 지역 코드 변환기 초기화
|
||||||
|
region_converter = RegionCodeConverter()
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def serve_index():
|
async def serve_index():
|
||||||
"""메인 페이지 제공"""
|
"""메인 페이지 제공"""
|
||||||
|
|
@ -45,7 +49,13 @@ async def parse_real_estate(query: RealEstateQuery):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await parser.parse_real_estate_query(query.text)
|
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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ class ParsedRealEstate(BaseModel):
|
||||||
price: Optional[str] = None
|
price: Optional[str] = None
|
||||||
location: Optional[str] = None
|
location: Optional[str] = None
|
||||||
area: Optional[str] = None
|
area: Optional[str] = None
|
||||||
rooms: Optional[str] = None
|
rooms: Optional[int] = None
|
||||||
transaction_type: Optional[str] = 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
|
raw_text: str
|
||||||
|
|
@ -20,16 +20,17 @@ class OpenAIParser:
|
||||||
|
|
||||||
system_prompt = """
|
system_prompt = """
|
||||||
당신은 부동산 정보를 추출하는 전문가입니다.
|
당신은 부동산 정보를 추출하는 전문가입니다.
|
||||||
사용자의 자연어 입력에서 다음 정보를 추출하세요.
|
사용자의 자연어 입력에서 다음 정보를 추출하세요:
|
||||||
단위는 붙여서 표기하세요. :
|
1. price: 가격 (전세금, 월세, 매매가 등)
|
||||||
1. price: 가격 (전세금, 월세, 매매가 등) (string)
|
2. location: 위치 (지역명, 동, 구 등)
|
||||||
2. location: 위치 (지역명, 동, 구 등) (string)
|
3. area: 면적 (평수, 제곱미터 등)
|
||||||
3. area: 면적 (평수, 제곱미터) (string)
|
4. rooms: 방 개수 (숫자만)
|
||||||
4. rooms: 방 개수 (string)
|
5. transaction_type: 거래 유형 (전세, 월세, 매매)
|
||||||
5. transaction_type: 거래 유형 (전세, 월세, 매매) (string)
|
6. property_type: 매물 형태 (아파트, 오피스텔, 주택, 빌라, 원룸, 투룸, 쓰리룸, 단독주택, 다가구주택, 연립주택, 상가주택 등)
|
||||||
|
|
||||||
JSON 형식으로만 응답하세요.
|
JSON 형식으로만 응답하세요.
|
||||||
정보가 없는 항목은 null로 표시하세요.
|
정보가 없는 항목은 null로 표시하세요.
|
||||||
|
rooms는 숫자(integer)로만 표현하세요.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -44,7 +45,6 @@ class OpenAIParser:
|
||||||
)
|
)
|
||||||
|
|
||||||
result = json.loads(response.choices[0].message.content)
|
result = json.loads(response.choices[0].message.content)
|
||||||
print(result)
|
|
||||||
|
|
||||||
return ParsedRealEstate(
|
return ParsedRealEstate(
|
||||||
price=result.get("price"),
|
price=result.get("price"),
|
||||||
|
|
@ -52,11 +52,13 @@ class OpenAIParser:
|
||||||
area=result.get("area"),
|
area=result.get("area"),
|
||||||
rooms=result.get("rooms"),
|
rooms=result.get("rooms"),
|
||||||
transaction_type=result.get("transaction_type"),
|
transaction_type=result.get("transaction_type"),
|
||||||
|
property_type=result.get("property_type"),
|
||||||
|
region_code=None, # 메인 서버에서 변환
|
||||||
|
region_name=None, # 메인 서버에서 변환
|
||||||
raw_text=text
|
raw_text=text
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
|
||||||
# 에러 발생 시 기본값 반환
|
# 에러 발생 시 기본값 반환
|
||||||
return ParsedRealEstate(
|
return ParsedRealEstate(
|
||||||
raw_text=text,
|
raw_text=text,
|
||||||
|
|
@ -64,5 +66,8 @@ class OpenAIParser:
|
||||||
location=None,
|
location=None,
|
||||||
area=None,
|
area=None,
|
||||||
rooms=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 가이드 문서 추가)
|
- docs/project_plan.md 업데이트 (API 가이드 문서 추가)
|
||||||
- 웹 검색 및 브라우저를 통한 실제 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 파일 생성 작업 완료
|
[2025-08-19 13:15:44] 한국 시군구 코드 JSON 파일 생성 작업 완료
|
||||||
- data 폴더 생성
|
- data 폴더 생성
|
||||||
- data/region_codes.json 생성 (전체 시군구 코드)
|
- data/region_codes.json 생성 (전체 시군구 코드)
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,21 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="search-section">
|
<div class="search-section">
|
||||||
<textarea id="searchInput" placeholder="예: 강남역 근처 전세 2억 이하 투룸 찾아줘
|
<textarea id="searchInput" placeholder="예: 강남역 근처 아파트 전세 2억 이하 투룸
|
||||||
서초동 30평대 아파트 매매 10억 이하
|
서초동 30평대 오피스텔 매매 10억 이하
|
||||||
방 3개짜리 월세 100/50 이하 판교"></textarea>
|
판교 빌라 방 3개 월세 100/50
|
||||||
|
역삼동 원룸 오피스텔 전세 1억5천"></textarea>
|
||||||
<button id="searchBtn" onclick="parseQuery()">분석하기</button>
|
<button id="searchBtn" onclick="parseQuery()">분석하기</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="examples">
|
<div class="examples">
|
||||||
<h3>입력 예시:</h3>
|
<h3>입력 예시:</h3>
|
||||||
<div class="example-chips">
|
<div class="example-chips">
|
||||||
<span class="chip" onclick="setExample(this)">강남 전세 3억 방 2개</span>
|
<span class="chip" onclick="setExample(this)">강남 아파트 전세 3억 방 2개</span>
|
||||||
<span class="chip" onclick="setExample(this)">판교 30평 아파트 매매</span>
|
<span class="chip" onclick="setExample(this)">판교 30평 오피스텔 매매</span>
|
||||||
<span class="chip" onclick="setExample(this)">서초동 월세 200/100</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ function displayResults(data) {
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
if (data.property_type) {
|
||||||
|
html += `<div class="result-item">
|
||||||
|
<strong>매물 형태:</strong> ${data.property_type}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.transaction_type) {
|
if (data.transaction_type) {
|
||||||
html += `<div class="result-item">
|
html += `<div class="result-item">
|
||||||
<strong>거래 유형:</strong> ${data.transaction_type}
|
<strong>거래 유형:</strong> ${data.transaction_type}
|
||||||
|
|
@ -64,6 +70,14 @@ function displayResults(data) {
|
||||||
html += `<div class="result-item">
|
html += `<div class="result-item">
|
||||||
<strong>위치:</strong> ${data.location}
|
<strong>위치:</strong> ${data.location}
|
||||||
</div>`;
|
</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) {
|
if (data.area) {
|
||||||
|
|
@ -74,7 +88,7 @@ function displayResults(data) {
|
||||||
|
|
||||||
if (data.rooms) {
|
if (data.rooms) {
|
||||||
html += `<div class="result-item">
|
html += `<div class="result-item">
|
||||||
<strong>방 개수:</strong> ${data.rooms}
|
<strong>방 개수:</strong> ${data.rooms}개
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,15 @@ header p {
|
||||||
width: 100px;
|
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 {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue