diff --git a/README.md b/README.md index ac1a493..e95eb65 100644 --- a/README.md +++ b/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" \ No newline at end of file +- "강남역 근처 아파트 전세 2억 이하 투룸" +- "서초동 30평대 오피스텔 매매 10억 이하" +- "판교 빌라 방 3개 월세 100/50" +- "잠실 주상복합 전세 5억" +- "분당 단독주택 매매 20억" \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 5ea3d7e..d61f6b3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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)) diff --git a/backend/models.py b/backend/models.py index 99d825b..9121378 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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 \ No newline at end of file diff --git a/backend/openai_parser.py b/backend/openai_parser.py index 0af9c15..9c8028e 100644 --- a/backend/openai_parser.py +++ b/backend/openai_parser.py @@ -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 ) \ No newline at end of file diff --git a/backend/region_converter.py b/backend/region_converter.py new file mode 100644 index 0000000..52dd912 --- /dev/null +++ b/backend/region_converter.py @@ -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 \ No newline at end of file diff --git a/docs/project_logs.txt b/docs/project_logs.txt index 850dd0c..b05fe78 100644 --- a/docs/project_logs.txt +++ b/docs/project_logs.txt @@ -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 생성 (전체 시군구 코드) diff --git a/frontend/index.html b/frontend/index.html index 6c96eec..169fa16 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,18 +15,21 @@
- +

입력 예시:

- 강남 전세 3억 방 2개 - 판교 30평 아파트 매매 - 서초동 월세 200/100 + 강남 아파트 전세 3억 방 2개 + 판교 30평 오피스텔 매매 + 서초동 빌라 월세 200/100 + 잠실 주상복합 전세 5억 + 분당 단독주택 매매 20억
diff --git a/frontend/script.js b/frontend/script.js index 9476df6..b6488bf 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -48,6 +48,12 @@ function displayResults(data) { let html = ''; + if (data.property_type) { + html += `
+ 매물 형태: ${data.property_type} +
`; + } + if (data.transaction_type) { html += `
거래 유형: ${data.transaction_type} @@ -64,6 +70,14 @@ function displayResults(data) { html += `
위치: ${data.location}
`; + + // 지역 코드가 있으면 표시 + if (data.region_code) { + html += `
+ 지역 코드: ${data.region_code} + ${data.region_name ? ` (${data.region_name})` : ''} +
`; + } } if (data.area) { @@ -74,7 +88,7 @@ function displayResults(data) { if (data.rooms) { html += `
- 방 개수: ${data.rooms} + 방 개수: ${data.rooms}개
`; } diff --git a/frontend/style.css b/frontend/style.css index 855c4ca..ff797c0 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -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;