Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a329f5a
✨feat: Elasticsearch 및 Kibana 서비스 추가
minseo0810 May 26, 2025
e85c324
🔄refactor: README.md 파일 구조 및 내용 업데이트
minseo0810 May 26, 2025
dc413d1
✨feat: Elasticsearch 카테고리 인덱스 생성 및 employee DB 검색 API 추가
minseo0810 May 26, 2025
1c21c68
✨feat: DB_search 엔드포인트 추가 및 검색 기능 설명 업데이트
minseo0810 May 26, 2025
365c6db
✨feat: Elasticsearch를 이용한 카테고리 검색 기능 추가 및 검색 결과 처리 개선
minseo0810 May 26, 2025
56df63b
✨feat: certifi 및 elasticsearch 패키지 추가
minseo0810 May 26, 2025
bd393fb
✨feat: Elasticsearch 연결을 환경 변수로 설정하고 카테고리 색인 생성 시 분석기 추가
minseo0810 May 27, 2025
709eeba
✨feat: DB_search 엔드포인트의 검색 파라미터 이름 수정 및 검색 결과 처리 개선
minseo0810 May 27, 2025
c8953cc
✨feat: 검색 결과 처리 로직 개선 및 기본 카테고리 설정 추가
minseo0810 May 27, 2025
295394d
✨feat: 검색 결과가 없을 경우 기본 카테고리 설정 추가
minseo0810 May 27, 2025
f7e1ee5
✨feat: 검색 기능에 'employee' 카테고리 필터 추가
minseo0810 May 27, 2025
174844e
✨fix: 검색 결과에서 'organization' 필드를 'institution'으로 수정
minseo0810 May 27, 2025
b61219b
✨refactor: 중복된 APIRouter 인스턴스 제거 및 DB_search 엔드포인트 경로 수정
minseo0810 May 27, 2025
57e927d
✨fix: 채용 공고 데이터 수정 및 검색 결과 수 조정
minseo0810 May 27, 2025
6afc5a5
✨refactor: 테스트 데이터 생성 로직 간소화 및 사용자 미존재 테스트 수정
minseo0810 May 28, 2025
167dc79
✨feat: Elasticsearch 카테고리 검색 기능 개선 및 인덱스 생성 로직 수정
minseo0810 May 28, 2025
109c50c
Add sentence-transformers dependency to pyproject.toml
minseo0810 May 28, 2025
b411a50
✨fix: Elasticsearch 검색 쿼리의 임베딩 필드 이름 수정 및 pyproject.toml에서 sentence-t…
minseo0810 May 28, 2025
a203e27
✨fix: Elasticsearch 및 Kibana 이미지 버전 업데이트 및 검색 쿼리의 임베딩 필드 이름 수정
minseo0810 May 28, 2025
3e2fe68
✨fix: Elasticsearch 검색 쿼리에서 벡터 차원 및 에러 로그 출력 추가, 인덱스 이름 수정
minseo0810 May 28, 2025
b6f850c
✨fix: Elasticsearch 검색 쿼리에서 필터 조건을 수정하여 카테고리 벡터 존재 여부 추가
minseo0810 May 28, 2025
41e4bed
✨fix: 환경변수 로딩 및 Hugging Face 로그인 추가, Elasticsearch 인덱스 존재 여부 체크 기능 구현
minseo0810 May 29, 2025
b18f39d
✨fix: 유사도 결과 필터링 기능 추가 및 검색 결과 정렬 개선
minseo0810 May 29, 2025
80f8e13
✨fix: Hugging Face 모델 로드 및 임베딩 차원 자동 추정 기능 추가, Elasticsearch 인덱스 생성 시…
minseo0810 May 29, 2025
3ae0e34
✨fix: 코드 정리 및 임포트 순서 수정, 테스트 모듈에서 불필요한 임포트 제거
minseo0810 May 29, 2025
3c412ab
✨fix: 환경변수 로딩 및 Hugging Face 로그인 로직 개선, Elasticsearch 연결 시 의존성 정의 수정
minseo0810 May 29, 2025
4702526
✨fix: 초기화 스크립트 주석 추가 및 사용 방법 설명 개선
minseo0810 May 29, 2025
8b40acc
✨fix: 인덱스 초기화 함수 추가 및 docker-compose 실행 시 자동 초기화 설정
minseo0810 May 29, 2025
d342fa6
✨fix: Elasticsearch 및 Kibana 버전 다운그레이드, 초기화 스크립트 및 API 코드 정리,
minseo0810 Jun 2, 2025
0ca45b6
Implement feature X to enhance user experience and optimize performance
minseo0810 Jun 2, 2025
822f3af
✨fix: Elasticsearch 및 Kibana 버전 업데이트 (7.17.0 → 8.12.0)
minseo0810 Jun 2, 2025
c7745e4
✨fix: 검색 API의 매개변수 및 예외 처리 개선, Elasticsearch 쿼리 로직 최적화 + BM25기법
minseo0810 Jun 2, 2025
f0eeb68
✨fix: Elasticsearch 임포트 순서 수정 및 코드 정리
minseo0810 Jun 2, 2025
6f6accd
✨fix: 불필요 주석 및 공백 제거 등
minseo0810 Jun 2, 2025
916a5e4
✨feat: Elasticsearch 카테고리 인덱스 생성 및 초기 데이터 색인 기능 추가
minseo0810 Jun 4, 2025
99e02a5
✨fix: Elasticsearch 데이터 볼륨 경로 수정
cux-maks Jun 7, 2025
60ee2b8
✨fix: Elasticsearch 호스트 주소를 환경변수에서 고정값으로 변경
cux-maks Jun 7, 2025
6fed410
✨fix: FastAPI 설정에 lifespan 인자 추가
cux-maks Jun 7, 2025
cf8cd1d
✨fix: 불필요한 import 문 제거 및 코드 정리
cux-maks Jun 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 60 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,50 @@ FastAPI와 MySQL을 사용한 카카오톡 챗봇 개발을 위한 템플릿 프

```
├── app/
│ ├── scripts/
│ │ └── core/
│ │ └── endpoints.py
│ ├── core/
│ │ └── __init__.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── category.py
│ │ ├── employee_category.py
│ │ ├── employeee_hire_type.py
│ │ ├── employee.py
│ │ ├── feature.py
│ │ ├── hire_type.py
│ │ ├── news.py
│ │ ├── user_category.py
│ │ └── users.py
│ ├── routers/
│ │ ├── employee.py
│ │ ├── feature.py
│ │ ├── news.py
│ │ └── user.py
│ ├── utils/
│ │ ├── response_builder.py
│ │ └── text_utils.py
│ └── main.py
│ │ ├── __init__.py
│ │ ├── db_manager.py
│ │ ├── init_default_data.py
│ │ ├── init_elasticsearch_index.py
│ │ ├── insert_employee_data.py
│ │ ├── news_client.py
│ │ ├── news_provider_mapping.py
│ │ └── verifier.py
│ ├── dev.Dockerfile
│ ├── Dockerfile
│ ├── main.py
│ └── test.Dockerfile
├── tests/
│ └── unit/
│ ├── routers/
│ │ └── test_kakao_router.py
│ ├── test_add_employee_data.py
│ ├── test_add_user_favorit.py
│ ├── test_employee_DB_search.py
│ ├── test_employee_recommendation.py
│ ├── test_get_categories_by_feature.py
│ ├── test_get_my_category_list.py
│ ├── test_get_news_recommendations.py
│ ├── test_news_client.py
│ │── test_user_delete_favorite.py
│ └── utils/
│ ├── response_builder/
│ │ └── test_response_builder.py
Expand Down Expand Up @@ -63,18 +96,21 @@ FastAPI와 MySQL을 사용한 카카오톡 챗봇 개발을 위한 템플릿 프
### 설치 및 실행

1. 저장소 클론

```bash
git clone <repository-url>
cd kakaotalk-chatbot-template
```

2. 환경 변수 설정

```bash
cp .env.example .env
# .env 파일을 열어 필요한 설정 수정
```

3. Docker 컨테이너 실행

```bash
docker-compose up --build
```
Expand All @@ -91,18 +127,21 @@ docker-compose up --build
### 의존성 관리

새로운 패키지 추가:

```bash
poetry add <package-name>
```

개발 의존성 추가:

```bash
poetry add --group dev <package-name>
```

### 테스트

테스트 실행:

```bash
# Docker 컨테이너 내부에서 실행
docker-compose exec app poetry run pytest
Expand All @@ -114,6 +153,7 @@ docker-compose exec app poetry run pytest --cov=app --cov-report=term-missing
### 코드 품질

린팅 및 포맷팅:

```bash
# Ruff를 사용한 린팅
poetry run ruff check .
Expand All @@ -134,19 +174,19 @@ poetry run black .

## 환경 변수

| 변수명 | 설명 | 기본값 |
|--------|------|---------|
| APP_NAME | 애플리케이션 이름 | kakaotalk-chatbot |
| ENVIRONMENT | 실행 환경 | development |
| DEBUG | 디버그 모드 | True |
| HOST | 서버 호스트 | 0.0.0.0 |
| PORT | 서버 포트 | 8000 |
| DB_HOST | 데이터베이스 호스트 | db |
| DB_PORT | 데이터베이스 포트 | 3306 |
| DB_NAME | 데이터베이스 이름 | chatbot |
| DB_USER | 데이터베이스 사용자 | user |
| DB_PASSWORD | 데이터베이스 비밀번호 | password |
| SECRET_KEY | 보안 키 | your-secret-key-here |
| 변수명 | 설명 | 기본값 |
| ----------- | --------------------- | -------------------- |
| APP_NAME | 애플리케이션 이름 | kakaotalk-chatbot |
| ENVIRONMENT | 실행 환경 | development |
| DEBUG | 디버그 모드 | True |
| HOST | 서버 호스트 | 0.0.0.0 |
| PORT | 서버 포트 | 8000 |
| DB_HOST | 데이터베이스 호스트 | db |
| DB_PORT | 데이터베이스 포트 | 3306 |
| DB_NAME | 데이터베이스 이름 | chatbot |
| DB_USER | 데이터베이스 사용자 | user |
| DB_PASSWORD | 데이터베이스 비밀번호 | password |
| SECRET_KEY | 보안 키 | your-secret-key-here |

## 기여하기

Expand Down
14 changes: 13 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,30 @@
- 기본 루트 엔드포인트 제공
"""

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.routers.employee import router as employee_router
from app.routers.feature import router as feature_router
from app.routers.news import router as news_router
from app.routers.user import router as user_router
from app.utils.init_elasticsearch_index import create_category_index


@asynccontextmanager
async def lifespan(app: FastAPI):
# 서버 시작 시 실행할 코드
create_category_index()
yield
# 서버 종료 시 실행할 코드 (필요 시 여기에 정리 작업 가능)

app = FastAPI(
title="KakaoTalk Chatbot API",
description="KakaoTalk Chatbot Template API",
version="0.1.0"
version="0.1.0",
lifespan=lifespan
)

# CORS 미들웨어 설정
Expand Down
134 changes: 132 additions & 2 deletions app/routers/employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
사용자의 구독 정보를 바탕으로 관련된 채용 공고를 필터링하여 반환합니다.
"""


from elasticsearch import Elasticsearch
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session

Expand All @@ -13,7 +13,7 @@

router = APIRouter()
db_dependency = Depends(db_manager.get_db) # 전역 변수로 설정

es = Elasticsearch("http://elasticsearch:9200")

@router.get("/recommend")
def get_recruit_recommendations(
Expand Down Expand Up @@ -80,3 +80,133 @@ def get_recruit_recommendations(
"results": jobs,
"message": message
}

@router.get("/DB_search")
def search_employees(
user_id: str = Query(..., description="사용자 ID"),
keyword: str = Query(..., description="검색할 카테고리 키워드 (예: '정보통신', '디자인')"),
limit: int = Query(10, ge=1, le=100, description="검색 결과 최대 개수 (기본값: 10, 최대: 100)"),
db: Session = db_dependency
):
"""
사용자가 입력한 키워드를 기반으로 가장 유사한 카테고리를 찾고, 해당 카테고리에 속한 채용 공고를 반환합니다.

Args:
user_id (str): 검색을 수행할 사용자 ID.
keyword (str): 검색 키워드 (카테고리명).
limit (int): 반환할 채용 공고 수 (기본값: 10, 최대 100).
db (Session): 데이터베이스 세션 객체.

Returns:
dict: 매칭된 카테고리명과 해당 카테고리에 속한 채용 공고 목록.
- matched_category (str): 검색 키워드와 가장 유사한 카테고리명.
- results (List[dict]): 채용공고 목록 (제목, 기관, 시작일, 종료일, 상세 URL 포함).

Raises:
HTTPException 404: 사용자가 존재하지 않는 경우.
HTTPException 500: Elasticsearch 연결 실패 또는 기타 오류 발생 시.
"""

# ✅ 1. 사용자 존재 여부 확인
user = db.query(Users).filter(Users.user_id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")

try:
# ✅ 2-1. match_phrase_prefix로 후보군 검색 (자동완성 역할)
prefix_result = es.search(
index="categories",
body={
"size": 10,
"query": {
"bool": {
"must": {
"match_phrase_prefix": {
"category_name": {
"query": keyword
}
}
},
"filter": {
"term": {
"feature": "employee"
}
}
}
}
}
)
prefix_hits = prefix_result.get("hits", {}).get("hits", [])
if not prefix_hits:
# 후보군 없으면 바로 기타 처리
matched_category = "기타"
category_id = 0
else:
candidate_names = [hit["_source"]["category_name"] for hit in prefix_hits]

# ✅ 2-2. BM25 기반 match 쿼리로 후보군 중 가장 유사한 카테고리 검색
bm25_result = es.search(
index="categories",
body={
"size": 1,
"query": {
"bool": {
"must": {
"match": {
"category_name": {
"query": keyword,
"operator": "and"
}
}
},
"filter": {
"terms": {
"category_name.keyword": candidate_names # 후보군 필터링
}
}
}
}
}
)
bm25_hits = bm25_result.get("hits", {}).get("hits", [])
if bm25_hits:
matched_source = bm25_hits[0]["_source"]
matched_category = matched_source["category_name"]
category_id = matched_source["category_id"]
else:
# BM25가 후보군 중 적합한 것을 못 찾으면 prefix 후보 중 1순위 반환
matched_category = prefix_hits[0]["_source"]["category_name"]
category_id = prefix_hits[0]["_source"]["category_id"]

except ConnectionError as e:
raise HTTPException(status_code=500, detail="Elasticsearch 연결 실패") from e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e

# ✅ 3. 해당 카테고리에 속한 채용 공고 최신순 조회
jobs = (
db.query(Employee)
.join(EmployeeCategory, Employee.recruit_id == EmployeeCategory.recruit_id)
.filter(EmployeeCategory.category_id == category_id)
.order_by(Employee.start_date.desc())
.limit(limit)
.all()
)

# ✅ 4. 결과를 JSON 형태로 정리
results = [
{
"title": job.title,
"institution": job.institution,
"start_date": job.start_date.isoformat(), # date → 문자열 (ISO 포맷)
"end_date": job.end_date.isoformat(),
"url": job.detail_url
}
for job in jobs
]

# ✅ 5. 최종 응답 반환
return {
"matched_category": matched_category,
"results": results
}
3 changes: 3 additions & 0 deletions app/utils/init_default_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ def add_default_categories(db):
Category(category_name="환경·에너지·안전", feature=feature_employee),
Category(category_name="농림어업", feature=feature_employee),
Category(category_name="연구", feature=feature_employee),


Category(category_id = 0, category_name="연구", feature=feature_employee),
]

for category in categories:
Expand Down
Loading