diff --git a/app/__init__.py b/app/__init__.py index a627129..e69de29 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,30 +0,0 @@ -# app/__init__.py -from flask import Flask -from flask_restx import Api -from app.router.recommendation_endpoint import api as recommendation_ns - -def create_app(): - app = Flask(__name__) - - # 앱 설정, 예: config.py에서 설정값 로드 (필요시) - # app.config.from_object('app.config') - - # Swagger UI는 기본적으로 /swagger에서 제공 (doc 인자를 조정 가능) - api = Api( - app, - version="1.0", - title="Recommendation API", - description="API for restaurant recommendation", - doc="/swagger" # Swagger UI 경로 - ) - - # 추천 API 네임스페이스 등록 - api.add_namespace(recommendation_ns, path="/recommend") - - # 전역 예외 핸들러 (선택 사항) - @app.errorhandler(Exception) - def handle_exception(e): - app.logger.error(f"Unhandled Exception: {e}", exc_info=True) - return {"error": "Internal Server Error", "message": str(e)}, 500 - - return app diff --git a/app/router/recommendation_api.py b/app/router/recommendation_api.py new file mode 100644 index 0000000..0258cf4 --- /dev/null +++ b/app/router/recommendation_api.py @@ -0,0 +1,100 @@ +import os +import time +import json +import logging +from fastapi import APIRouter, HTTPException +from app.config import UPLOAD_DIR, FEEDBACK_DIR +from app.schema.recommendation_schema import UserData, RecommendationItem, CATEGORY_MAPPING +from app.services.preprocess.data_loader import load_and_merge_json_files +from app.services.preprocess.preprocessor import preprocess_data +from app.services.model_trainer import train_model +from app.services.model_trainer.recommendation import generate_recommendations +from typing import List, Dict, Any + +# 라우터 설정 +logger = logging.getLogger("recommendation_api") +router = APIRouter() + +# 글로벌 변수 초기화 +globals_dict = {} + +# 초기 데이터 로딩 및 모델 학습 +def initialize_model(): + global globals_dict + json_directory = str(UPLOAD_DIR) + + try: + df_raw = load_and_merge_json_files(json_directory) + df_final = preprocess_data(df_raw) # 병합된 DataFrame 전달 + globals_dict = train_model(df_final) + logger.info("모델 초기화 성공") + except Exception as e: + logger.error(f"Error during initialization: {e}", exc_info=True) + globals_dict = {} + +# 서버 시작 시 모델 초기화 +initialize_model() + +@router.post("", + response_model=Dict[str, Any], # 구체적인 응답 모델이 필요하다면 별도로 정의 + responses={ + 200: {"description": "Success"}, + 400: {"description": "Bad Request"}, + 500: {"description": "Internal Server Error"} + }) + +async def recommend(user_data: UserData): + """사용자 데이터를 받아 추천 결과를 생성하고, 결과를 파일로 저장합니다.""" + try: + user_id = user_data.user_id + preferred_categories = user_data.preferred_categories + + if not user_id or not preferred_categories: + raise HTTPException(status_code=400, detail="user_id와 preferred_categories를 입력해주세요.") + + # 선호 카테고리 이름을 해당 번호로 변환 + preferred_ids = [CATEGORY_MAPPING.get(cat) for cat in preferred_categories if cat in CATEGORY_MAPPING] + + if not preferred_ids: + raise HTTPException(status_code=400, detail="유효한 선호 카테고리를 입력해주세요.") + + df_model = globals_dict.get("df_model") + if df_model is None: + raise HTTPException(status_code=500, detail="모델 데이터가 초기화되지 않았습니다.") + + filtered_df = df_model[df_model["category_id"].isin(preferred_ids)].copy() + if filtered_df.empty: + raise HTTPException(status_code=400, detail="해당 선호 카테고리에 해당하는 식당 데이터가 없습니다.") + + # 추천 결과 생성 (generate_recommendations는 JSON 문자열을 반환) + result_json = generate_recommendations( + filtered_df, + globals_dict["stacking_reg"], + globals_dict["model_features"], + user_id, + globals_dict["scaler"] + ) + + # 추천 결과를 FEEDBACK_DIR에 파일로 저장 + timestamp = int(time.time()) + feedback_filename = f"recommendation_{user_id}_{timestamp}.json" + feedback_filepath = os.path.join(str(FEEDBACK_DIR), feedback_filename) + + try: + os.makedirs(str(FEEDBACK_DIR), exist_ok=True) + with open(feedback_filepath, "w", encoding="utf-8") as f: + f.write(result_json) + logger.info(f"추천 결과가 {feedback_filepath}에 저장되었습니다.") + except Exception as file_err: + logger.error(f"추천 결과 저장 실패: {file_err}", exc_info=True) + + # JSON 문자열을 Python 객체로 변환하여 반환 + result_data = json.loads(result_json) + return result_data + + except HTTPException: + # 이미 생성된 HTTPException은 그대로 다시 발생시킴 + raise + except Exception as e: + logger.error(f"Error in recommendation endpoint: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/app/router/recommendation_endpoint.py b/app/router/recommendation_endpoint.py deleted file mode 100644 index 59f8e5b..0000000 --- a/app/router/recommendation_endpoint.py +++ /dev/null @@ -1,120 +0,0 @@ - -from flask_restx import Namespace, Resource, fields, abort -from flask import request, jsonify, Response -import json -from app.config import UPLOAD_DIR -from app.services.preprocess.data_loader import load_and_merge_json_files -from app.services.preprocess.preprocessor import preprocess_data -from app.services.model_trainer import train_model -from app.services.model_trainer.recommendation import generate_recommendations -import logging - -# 추천 결과를 FEEDBACK_DIR에 파일로 저장하는 코드 추가 -import os, time -from app.config import FEEDBACK_DIR - -# 라우터 설정 -logger = logging.getLogger(__name__) - -api = Namespace('recommendation', description="상위 15개 식당 추천 API") - -# 요청 모델 정의 (Swagger 문서용) -user_data_model = api.model('UserData', { - 'user_id': fields.String(required=True, description="사용자 ID"), - 'preferred_categories': fields.List(fields.String, required=True, min_items=1, max_items=3, description="선호 카테고리 목록") -}) - -# 응답 모델 정의 (참고용) -recommendation_item = api.model('RecommendationItem', { - 'restaurant_id': fields.Integer(description="식당 고유 식별자"), - 'category_id': fields.Integer(description="카테고리 ID"), - 'score': fields.Float(description="원래 평점"), - 'predicted_score': fields.Float(description="예측 평점"), - 'composite_score': fields.Float(description="최종 추천 점수") -}) -response_model = api.model('RecommendationResponse', { - 'user_id': fields.String(description="사용자 ID"), - 'recommendations': fields.List(fields.Nested(recommendation_item)) -}) - -json_directory = str(UPLOAD_DIR) - -try: - df_raw = load_and_merge_json_files(json_directory) - df_final = preprocess_data(df_raw) # 병합된 DataFrame 전달 - globals_dict = train_model(df_final) -except Exception as e: - logger.error(f"Error during initialization: {e}", exc_info=True) - # 초기화 실패 시 적절히 처리 (예: 프로그램 종료 등) - globals_dict = {} - -@api.route('', methods=['POST']) -class RecommendationResource(Resource): - @api.expect(user_data_model) - @api.response(200, 'Success', response_model) - @api.response(400, 'Bad Request') - @api.response(500, 'Internal Server Error') - def post(self): - """사용자 데이터를 받아 추천 결과를 생성하고, 결과를 파일로 저장합니다.""" - try: - - req_data = request.get_json() - user_id = req_data.get("user_id") - preferred_categories = req_data.get("preferred_categories") - - if not user_id or not preferred_categories: - abort(400, "user_id와 preferred_categories를 입력해주세요.") - - # 사용자 선호 카테고리를 기반으로 df_model 필터링 - category_mapping = { - "중식": 1, - "일식집": 2, - "브런치카페": 3, - "파스타": 4, - "이탈리안": 5, - "이자카야": 6, - "한식집": 7, - "치킨": 8, - "스테이크": 9, - "고깃집": 10, - "다이닝바": 11, - "오마카세": 12 - } - - # 선호 카테고리 이름을 해당 번호로 변환 - preferred_ids = [category_mapping.get(cat) for cat in preferred_categories if cat in category_mapping] - - df_model = globals_dict.get("df_model") - if df_model is None: - abort(500, "모델 데이터가 초기화되지 않았습니다.") - - filtered_df = df_model[df_model["category_id"].isin(preferred_ids)].copy() - if filtered_df.empty: - abort(400, "해당 선호 카테고리에 해당하는 식당 데이터가 없습니다.") - - # 추천 결과 생성 (generate_recommendations는 JSON 문자열을 반환) - result_json = generate_recommendations( - filtered_df, - globals_dict["stacking_reg"], - globals_dict["model_features"], - user_id, - globals_dict["scaler"] - ) - - # 추천 결과를 FEEDBACK_DIR에 파일로 저장 - timestamp = int(time.time()) - feedback_filename = f"recommendation_{user_id}_{timestamp}.json" - feedback_filepath = os.path.join(str(FEEDBACK_DIR), feedback_filename) - - try: - with open(feedback_filepath, "w", encoding="utf-8") as f: - f.write(result_json) - logger.info(f"추천 결과가 {feedback_filepath}에 저장되었습니다.") - except Exception as file_err: - logger.error(f"추천 결과 저장 실패: {file_err}", exc_info=True) - - return Response(result_json, mimetype='application/json') - - except Exception as e: - logger.error(f"Error in recommendation endpoint: {e}", exc_info=True) - abort(500, str(e)) \ No newline at end of file diff --git a/app/schema/recommendation_schema.py b/app/schema/recommendation_schema.py index c16f542..4983d21 100644 --- a/app/schema/recommendation_schema.py +++ b/app/schema/recommendation_schema.py @@ -1,7 +1,7 @@ # app/models/recommendation_schema.py -from pydantic import BaseModel, conlist -from typing import List +from pydantic import BaseModel, Field +from typing import List, Annotated class RecommendationItem(BaseModel): category_id: int @@ -28,5 +28,5 @@ class RecommendationItem(BaseModel): class UserData(BaseModel): user_id: str - # 선호 카테고리는 최소 1개, 최대 3개 - preferred_categories: conlist(str, min_items=1, max_items=3) + # 선호 카테고리는 최소 1개, 최대 3개 (Pydantic v2 방식) + preferred_categories: Annotated[List[str], Field(min_length=1, max_length=3)] diff --git a/app/services/model_trainer/train_model.py b/app/services/model_trainer/train_model.py index f022d4e..e8601ae 100644 --- a/app/services/model_trainer/train_model.py +++ b/app/services/model_trainer/train_model.py @@ -5,6 +5,16 @@ from .model_evaluation import evaluate_model import numpy as np import logging +import warnings +import atexit +import shutil +import os +import tempfile +from joblib import parallel_backend + +# joblib 경고 필터링 +warnings.filterwarnings("ignore", message="resource_tracker") +warnings.filterwarnings("ignore", message="There appear to be") logger = logging.getLogger(__name__) diff --git a/catboost_info/catboost_training.json b/catboost_info/catboost_training.json index dfeaf9e..a2ab02e 100644 --- a/catboost_info/catboost_training.json +++ b/catboost_info/catboost_training.json @@ -1,54 +1,57 @@ { "meta":{"test_sets":[],"test_metrics":[],"learn_metrics":[{"best_value":"Min","name":"RMSE"}],"launch_mode":"Train","parameters":"","iteration_count":50,"learn_sets":["learn"],"name":"experiment"}, "iterations":[ -{"learn":[1.041660961],"iteration":0,"passed_time":0.000387125,"remaining_time":0.018969125}, -{"learn":[1.03649279],"iteration":1,"passed_time":0.0007355416667,"remaining_time":0.017653}, -{"learn":[1.031012619],"iteration":2,"passed_time":0.00106775,"remaining_time":0.01672808333}, -{"learn":[1.026074549],"iteration":3,"passed_time":0.001428166667,"remaining_time":0.01642391667}, -{"learn":[1.022593925],"iteration":4,"passed_time":0.001692333333,"remaining_time":0.015231}, -{"learn":[1.019823421],"iteration":5,"passed_time":0.002072916667,"remaining_time":0.01520138889}, -{"learn":[1.016656153],"iteration":6,"passed_time":0.002374,"remaining_time":0.01458314286}, -{"learn":[1.014398135],"iteration":7,"passed_time":0.002720083333,"remaining_time":0.0142804375}, -{"learn":[1.011919826],"iteration":8,"passed_time":0.003048708333,"remaining_time":0.01388856019}, -{"learn":[1.009753542],"iteration":9,"passed_time":0.003359833333,"remaining_time":0.01343933333}, -{"learn":[1.007913933],"iteration":10,"passed_time":0.003663083333,"remaining_time":0.01298729545}, -{"learn":[1.006154154],"iteration":11,"passed_time":0.00402775,"remaining_time":0.01275454167}, -{"learn":[1.004688765],"iteration":12,"passed_time":0.004396083333,"remaining_time":0.01251192949}, -{"learn":[1.003690828],"iteration":13,"passed_time":0.004655958333,"remaining_time":0.01197246429}, -{"learn":[1.002848742],"iteration":14,"passed_time":0.004935083333,"remaining_time":0.01151519444}, -{"learn":[1.001808465],"iteration":15,"passed_time":0.005217208333,"remaining_time":0.01108656771}, -{"learn":[1.000337879],"iteration":16,"passed_time":0.00569825,"remaining_time":0.01106130882}, -{"learn":[0.9992846751],"iteration":17,"passed_time":0.006031,"remaining_time":0.01072177778}, -{"learn":[0.9980431336],"iteration":18,"passed_time":0.006510583333,"remaining_time":0.0106225307}, -{"learn":[0.9975941019],"iteration":19,"passed_time":0.006822583333,"remaining_time":0.010233875}, -{"learn":[0.9965668154],"iteration":20,"passed_time":0.007110708333,"remaining_time":0.009819549603}, -{"learn":[0.9959085662],"iteration":21,"passed_time":0.007417708333,"remaining_time":0.009440719697}, -{"learn":[0.9952216697],"iteration":22,"passed_time":0.007755666667,"remaining_time":0.009104478261}, -{"learn":[0.9935697235],"iteration":23,"passed_time":0.008235333333,"remaining_time":0.008921611111}, -{"learn":[0.9929913454],"iteration":24,"passed_time":0.008553458333,"remaining_time":0.008553458333}, -{"learn":[0.9920865001],"iteration":25,"passed_time":0.008893541667,"remaining_time":0.008209423077}, -{"learn":[0.9915112501],"iteration":26,"passed_time":0.009263875,"remaining_time":0.007891449074}, -{"learn":[0.9908434903],"iteration":27,"passed_time":0.009654083333,"remaining_time":0.00758535119}, -{"learn":[0.9901493036],"iteration":28,"passed_time":0.01008608333,"remaining_time":0.007303715517}, -{"learn":[0.9895396013],"iteration":29,"passed_time":0.01038954167,"remaining_time":0.006926361111}, -{"learn":[0.988461672],"iteration":30,"passed_time":0.01073883333,"remaining_time":0.006581865591}, -{"learn":[0.9864972226],"iteration":31,"passed_time":0.01105220833,"remaining_time":0.006216867187}, -{"learn":[0.9860557219],"iteration":32,"passed_time":0.01140370833,"remaining_time":0.005874637626}, -{"learn":[0.9849702579],"iteration":33,"passed_time":0.01171516667,"remaining_time":0.005513019608}, -{"learn":[0.9842232531],"iteration":34,"passed_time":0.01201945833,"remaining_time":0.005151196429}, -{"learn":[0.9834667705],"iteration":35,"passed_time":0.012320125,"remaining_time":0.004791159722}, -{"learn":[0.9830739831],"iteration":36,"passed_time":0.012686375,"remaining_time":0.004457375}, -{"learn":[0.9827276656],"iteration":37,"passed_time":0.01300920833,"remaining_time":0.004108171053}, -{"learn":[0.982392468],"iteration":38,"passed_time":0.01331341667,"remaining_time":0.003755066239}, -{"learn":[0.9816912276],"iteration":39,"passed_time":0.01370358333,"remaining_time":0.003425895833}, -{"learn":[0.9813869019],"iteration":40,"passed_time":0.014166625,"remaining_time":0.003109746951}, -{"learn":[0.9806031813],"iteration":41,"passed_time":0.01451766667,"remaining_time":0.002765269841}, -{"learn":[0.9797940078],"iteration":42,"passed_time":0.014996875,"remaining_time":0.002441351744}, -{"learn":[0.9787968043],"iteration":43,"passed_time":0.01526708333,"remaining_time":0.002081875}, -{"learn":[0.9785656214],"iteration":44,"passed_time":0.01554870833,"remaining_time":0.001727634259}, -{"learn":[0.9777627847],"iteration":45,"passed_time":0.01580079167,"remaining_time":0.001373981884}, -{"learn":[0.9772527321],"iteration":46,"passed_time":0.01614045833,"remaining_time":0.001030242021}, -{"learn":[0.9768006779],"iteration":47,"passed_time":0.016488625,"remaining_time":0.0006870260417}, -{"learn":[0.9760519369],"iteration":48,"passed_time":0.01681666667,"remaining_time":0.0003431972789}, -{"learn":[0.9757471956],"iteration":49,"passed_time":0.01774025,"remaining_time":0} +{"learn":[1.017646642],"iteration":0,"passed_time":0.000633679318,"remaining_time":0.03105028658}, +{"learn":[1.011603734],"iteration":1,"passed_time":0.001250399964,"remaining_time":0.03000959914}, +{"learn":[1.005283192],"iteration":2,"passed_time":0.001700450616,"remaining_time":0.02664039298}, +{"learn":[0.9996215147],"iteration":3,"passed_time":0.002098375227,"remaining_time":0.02413131511}, +{"learn":[0.9948680365],"iteration":4,"passed_time":0.002394172799,"remaining_time":0.021547555, +{, +{"learn":[0.9904453509],"iteration":5,"passed_time":0.002707095713,"remaining_time":0.0198520, +{"l, +{"learn":[0.9865144367],"iteration":6,"passed_time":0.003245773135,"remaining_time":0.0199383, +{"learn":[0.9895817305],"iteration":7,"passed_time":0.005525068641,"remaining_time":0.02900661036}, +{"learn":[0.9862797578],"iteration":8,"passed_time":0.006072954579,"remaining_time":0.02766568197}, +{"learn":[0.9826827108],"iteration":9,"passed_time":0.006616840438,"remaining_time":0.02646736175}, +{"learn":[0.9789153618],"iteration":10,"passed_time":0.007144100964,"remaining_time":0.02532908524}, +{"learn":[0.9766187678],"iteration":11,"passed_time":0.007760196598,"remaining_time":0.02457395589}, +{"learn":[0.9736712951],"iteration":12,"passed_time":0.008301374069,"remaining_time":0.02362698774}, +{"learn":[0.9715954757],"iteration":13,"passed_time":0.008856301815,"remaining_time":0.02277334752}, +{"learn":[0.9704534733],"iteration":14,"passed_time":0.009308019167,"remaining_time":0.02171871139}, +{"learn":[0.9695653121],"iteration":15,"passed_time":0.009502939725,"remaining_time":0.02019374692}, +{"learn":[0.9683020895],"iteration":16,"passed_time":0.01000378306,"remaining_time":0.01941910829}, +{"learn":[0.9669194499],"iteration":17,"passed_time":0.01040229101,"remaining_time":0.0184929618}, +{"learn":[0.9654271114],"iteration":18,"passed_time":0.01087780051,"remaining_time":0.0177479903}, +{"learn":[0.9633307278],"iteration":19,"passed_time":0.01136547691,"remaining_time":0.01704821537}, +{"learn":[0.9618691706],"iteration":20,"passed_time":0.01203319857,"remaining_time":0.01661727422}, +{"learn":[0.961005842],"iteration":21,"passed_time":0.01276683822,"remaining_time":0.01624870319}, +{"learn":[0.9591235279],"iteration":22,"passed_time":0.01357614605,"remaining_time":0.01593721492}, +{"learn":[0.9585316553],"iteration":23,"passed_time":0.01403844694,"remaining_time":0.01520831752}, +{"learn":[0.9573148648],"iteration":24,"passed_time":0.01443616322,"remaining_time":0.01443616322}, +{"learn":[0.9560791715],"iteration":25,"passed_time":0.01533268111,"remaining_time":0.01415324411}, +{"learn":[0.955590793],"iteration":26,"passed_time":0.01583994124,"remaining_time":0.01349328328}, +{"learn":[0.9539357914],"iteration":27,"passed_time":0.01632707597,"remaining_time":0.01282841683}, +{"learn":[0.9519096132],"iteration":28,"passed_time":0.01706475736,"remaining_time":0.01235723809}, +{"learn":[0.9509147509],"iteration":29,"passed_time":0.01764839401,"remaining_time":0.01176559601}, +{"learn":[0.9496869175],"iteration":30,"passed_time":0.01824911434,"remaining_time":0.01118494105}, +{"learn":[0.9492689716],"iteration":31,"passed_time":0.01871104023,"remaining_time":0.01052496013}, +{"learn":[0.9486499926],"iteration":32,"passed_time":0.019133507,"remaining_time":0.00985665512}, +{"learn":[0.9482030018],"iteration":33,"passed_time":0.01954801527,"remaining_time":0.009199066011}, +{"learn":[0.9468544997],"iteration":34,"passed_time":0.02002510813,"remaining_time":0.008582189199}, +{"learn":[0.9459224696],"iteration":35,"passed_time":0.02056311887,"remaining_time":0.007996768451}, +{"learn":[0.9444169549],"iteration":36,"passed_time":0.0210956295,"remaining_time":0.007411977934}, +{"learn":[0.9434100444],"iteration":37,"passed_time":0.02160843141,"remaining_time":0.006823715182}, +{"learn":[0.9423569439],"iteration":38,"passed_time":0.02352021958,"remaining_time":0.006633908086}, +{"learn":[0.9419094165],"iteration":39,"passed_time":0.02373264049,"remaining_time":0.005933160121}, +{"learn":[0.9411915517],"iteration":40,"passed_time":0.02402764638,"remaining_time":0.005274361399}, +{"learn":[0.940149036],"iteration":41,"passed_time":0.02487457995,"remaining_time":0.004738015229}, +{"learn":[0.9389662544],"iteration":42,"passed_time":0.02521000331,"remaining_time":0.004103954028}, +{"learn":[0.9381826109],"iteration":43,"passed_time":0.02563388678,"remaining_time":0.003495530015}, +{"learn":[0.9374298656],"iteration":44,"passed_time":0.02603764484,"remaining_time":0.002893071649}, +{"learn":[0.9351864641],"iteration":45,"passed_time":0.026546405,"remaining_time":0.002308383043}, +{"learn":[0.9342881765],"iteration":46,"passed_time":0.02706620704,"remaining_time":0.001727630237}, +{"learn":[0.9337759418],"iteration":47,"passed_time":0.02762625989,"remaining_time":0.001151094162}, +{"learn":[0.9316415182],"iteration":48,"passed_time":0.02817864592,"remaining_time":0.0005750744065}, +{"learn":[0.9294663575],"iteration":49,"passed_time":0.0286419885,"remaining_time":0} +]}ing_time":0} ]} \ No newline at end of file diff --git a/catboost_info/learn/events.out.tfevents b/catboost_info/learn/events.out.tfevents index bae3881..c8e4d21 100644 Binary files a/catboost_info/learn/events.out.tfevents and b/catboost_info/learn/events.out.tfevents differ diff --git a/catboost_info/learn_error.tsv b/catboost_info/learn_error.tsv index e6f2e4b..395b905 100644 --- a/catboost_info/learn_error.tsv +++ b/catboost_info/learn_error.tsv @@ -1,51 +1,49 @@ iter RMSE -0 1.041660961 -1 1.03649279 -2 1.031012619 -3 1.026074549 -4 1.022593925 -5 1.019823421 -6 1.016656153 -7 1.014398135 -8 1.011919826 -9 1.009753542 -10 1.007913933 -11 1.006154154 -12 1.004688765 -13 1.003690828 -14 1.002848742 -15 1.001808465 -16 1.000337879 -17 0.9992846751 -18 0.9980431336 -19 0.9975941019 -20 0.9965668154 -21 0.9959085662 -22 0.9952216697 -23 0.9935697235 -24 0.9929913454 -25 0.9920865001 -26 0.9915112501 -27 0.9908434903 -28 0.9901493036 -29 0.9895396013 -30 0.988461672 -31 0.9864972226 -32 0.9860557219 -33 0.9849702579 -34 0.9842232531 -35 0.9834667705 -36 0.9830739831 -37 0.9827276656 -38 0.982392468 -39 0.9816912276 -40 0.9813869019 -41 0.9806031813 -42 0.9797940078 -43 0.9787968043 -44 0.9785656214 -45 0.9777627847 -46 0.9772527321 -47 0.9768006779 -48 0.9760519369 -49 0.9757471956 +0 1.017646642 +1 1.011603734 +2 1.005283192 +3 0.9996215147 +4 0.9948680365 5 0.99044536 0.6 0.98651447 0.9895817305 +8 0.9862797578 +9 0.9826827108 +10 0.9789153618 +11 0.9766187678 +12 0.9736712951 +13 0.9715954757 +14 0.9704534733 +15 0.9695653121 +16 0.9683020895 +17 0.9669194499 +18 0.9654271114 +19 0.9633307278 +20 0.9618691706 +21 0.961005842 +22 0.9591235279 +23 0.9585316553 +24 0.9573148648 +25 0.9560791715 +26 0.955590793 +27 0.9539357914 +28 0.9519096132 +29 0.9509147509 +30 0.9496869175 +31 0.9492689716 +32 0.9486499926 +33 0.9482030018 +34 0.9468544997 +35 0.9459224696 +36 0.9444169549 +37 0.9434100444 +38 0.9423569439 +39 0.9419094165 +40 0.9411915517 +41 0.940149036 +42 0.9389662544 +43 0.9381826109 +44 0.9374298656 +45 0.9351864641 +46 0.9342881765 +47 0.9337759418 +48 0.9316415182 +49 0.9294663575 +168 diff --git a/catboost_info/time_left.tsv b/catboost_info/time_left.tsv index f7ccb18..359722f 100644 --- a/catboost_info/time_left.tsv +++ b/catboost_info/time_left.tsv @@ -1,51 +1,47 @@ iter Passed Remaining -0 0 18 -1 0 17 -2 1 16 -3 1 16 -4 1 15 -5 2 15 -6 2 14 -7 2 14 -8 3 13 -9 3 13 -10 3 12 -11 4 12 -12 4 12 -13 4 11 -14 4 11 -15 5 11 -16 5 11 -17 6 10 -18 6 10 -19 6 10 -20 7 9 -21 7 9 -22 7 9 -23 8 8 -24 8 8 -25 8 8 -26 9 7 -27 9 7 -28 10 7 -29 10 6 -30 10 6 -31 11 6 -32 11 5 -33 11 5 -34 12 5 -35 12 4 -36 12 4 -37 13 4 -38 13 3 -39 13 3 -40 14 3 -41 14 2 -42 14 2 -43 15 2 -44 15 1 -45 15 1 -46 16 1 -47 16 0 -48 16 0 -49 17 0 +0 0 31 +1 1 30 +2 1 26 +3 2 24 +4 2 21 +5 2 19 +6 3 19 +7 5 29 +8 6 27 +9 6 26 +10 7 25 +11 7 24 +12 8 23 +13 8 22 +14 9 21 +15 9 20 +16 10 19117 10 11818 10 19 19 1120 920 12 16 +21 12 16 +22 13 15 +23 14 15 +24 14 14 +25 15 14 +26 15 13 +27 16 12 +28 17 12 +29 17 11 +30 18 11 +31 18 10 +32 19 9 +33 19 9 +34 20 8 +35 20 7 +36 21 7 +37 21 6 +38 23 6 +39 23 5 +40 24 5 +41 24 4 +42 25 4 +43 25 3 +44 26 2 +45 26 2 +46 27 1 +47 27 1 +48 28 0 +49 28 0 diff --git a/logging_config.json b/logging_config.json index a91c7a7..eaf9d34 100644 --- a/logging_config.json +++ b/logging_config.json @@ -1,72 +1,86 @@ { - "version": 1, - "disable_existing_loggers": false, - "formatters": { - "default": { - "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" - }, - "access": { - "format": "%(asctime)s - %(levelname)s - %(message)s" - } + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "default": { + "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "formatter": "default", - "stream": "ext://sys.stdout" - }, - "file": { - "class": "logging.FileHandler", - "formatter": "default", - "filename": "logs/app.log", - "encoding": "utf8" - }, - "error_file": { - "class": "logging.FileHandler", - "formatter": "default", - "filename": "logs/error.log", - "encoding": "utf8", - "level": "ERROR" - }, - "access_file": { - "class": "logging.FileHandler", - "formatter": "access", - "filename": "logs/access.log", - "encoding": "utf8" - } + "access": { + "format": "%(asctime)s - %(levelname)s - %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + "stream": "ext://sys.stdout" + }, + "file": { + "class": "logging.FileHandler", + "formatter": "default", + "filename": "logs/app.log", + "encoding": "utf8" + }, + "error_file": { + "class": "logging.FileHandler", + "formatter": "default", + "filename": "logs/error.log", + "encoding": "utf8", + "level": "ERROR" + }, + "access_file": { + "class": "logging.FileHandler", + "formatter": "access", + "filename": "logs/access.log", + "encoding": "utf8" + } + }, + "loggers": { + "recommendation_api": { + "handlers": ["console", "file", "error_file"], + "level": "DEBUG", + "propagate": false + }, + "app": { + "handlers": ["console", "file", "error_file"], + "level": "DEBUG", + "propagate": false + }, + "app.router": { + "level": "DEBUG", + "propagate": true }, - "loggers": { - "flask": { - "handlers": ["console", "file", "error_file"], - "level": "DEBUG", - "propagate": false - }, - "app": { - "handlers": ["console", "file", "error_file"], - "level": "DEBUG", - "propagate": false - }, - "app.router": { - "level": "DEBUG", - "propagate": true - }, - "app.service": { - "level": "DEBUG", - "propagate": true - }, - "app.config": { - "level": "DEBUG", - "propagate": true - }, - "werkzeug": { - "handlers": ["access_file"], - "level": "INFO", - "propagate": false - } + "app.services": { + "level": "DEBUG", + "propagate": true }, - "root": { + "app.config": { + "level": "DEBUG", + "propagate": true + }, + "uvicorn": { + "handlers": ["access_file", "console"], + "level": "INFO", + "propagate": false + }, + "uvicorn.access": { + "handlers": ["access_file"], "level": "INFO", - "handlers": ["console", "file"] + "propagate": false + }, + "uvicorn.error": { + "handlers": ["error_file", "console"], + "level": "INFO", + "propagate": false + }, + "fastapi": { + "handlers": ["console", "file", "error_file"], + "level": "DEBUG", + "propagate": false } + }, + "root": { + "level": "INFO", + "handlers": ["console", "file"] } - \ No newline at end of file +} \ No newline at end of file diff --git a/main.py b/main.py index 6a97c54..fc44e64 100644 --- a/main.py +++ b/main.py @@ -1,43 +1,99 @@ -# main.py +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, HTMLResponse +from fastapi.openapi.docs import get_swagger_ui_html -import os +from app.config import FEEDBACK_DIR +from app.router.recommendation_api import router as recommendation_api + +from pathlib import Path import json +import os +import uvicorn +import logging import logging.config -from flask import Flask, request -from flask_restx import Api -from app.router.recommendation_endpoint import api as recommendation_ns # flask_restx Namespace - -def create_app(): - app = Flask(__name__) - - # logging_config.json 파일의 경로 (프로젝트 루트에 위치) - logging_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logging_config.json") - with open(logging_config_path, "r", encoding="utf-8") as f: - logging_config = json.load(f) - logging.config.dictConfig(logging_config) - logger = logging.getLogger(__name__) - logger.info("Logging is configured.") - - # Flask-RESTX API 객체 생성 및 Swagger UI 설정 (Swagger UI는 /swagger에서 제공) - api = Api( - app, - version="1.0", - title="Recommendation API", - description="식당 추천 API", - doc="/swagger" + +# 로깅 설정 +# JSON 기반 로깅 설정 적용 +logging_config_path = Path(__file__).resolve().parent / "logging_config.json" # 프로젝트 루트에 위치한 파일 경로 +with open(logging_config_path, "r", encoding="utf-8") as f: + logging_config = json.load(f) + +logging.config.dictConfig(logging_config) +logger = logging.getLogger("recommendation_api") + +app = FastAPI() + +# 모든 출처를 허용하는 CORS 설정 (자격 증명 포함 불가) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 모든 출처 허용 + allow_methods=["*"], + allow_headers=["*"], + allow_credentials=False, # credentials를 반드시 False로 설정 +) + +# 라우터 포함 +app.include_router(recommendation_api, prefix="/recommend", tags=["Restaurant Recommendation"]) + +# 피드백 파일을 저장하는 디렉토리를 정적 파일로 제공 (필요한 경우) +# 디렉토리가 존재하는지 확인 +feedback_dir = Path(str(FEEDBACK_DIR)) +if feedback_dir.exists(): + app.mount("/feedback", StaticFiles(directory=str(feedback_dir)), name="feedback") + +# 루트 엔드포인트 (선택 사항) +@app.get("/") +def read_root(): + logger.info("Root endpoint accessed") + return {"message": "Hello, Toby!"} + +# 요청 로깅 미들웨어 +@app.middleware("http") +async def log_requests(request: Request, call_next): + logger.info(f"Request: {request.method} {request.url}") + try: + response = await call_next(request) + logger.info(f"Response status: {response.status_code}") + return response + except Exception as e: + logger.error(f"Error processing request: {e}") + raise e + +# 예외 처리기 +class RecommendationProcessingError(Exception): + def __init__(self, message: str): + self.message = message + super().__init__(self.message) + +@app.exception_handler(RecommendationProcessingError) +async def recommendation_processing_exception_handler(request: Request, exc: RecommendationProcessingError): + return JSONResponse( + status_code=400, + content={"detail": exc.message}, + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + logging.error(f"Unhandled error: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={"detail": "서버 내부 오류가 발생했습니다."}, ) - - # 추천 API 네임스페이스 등록 - api.add_namespace(recommendation_ns, path="/recommend") - - # 요청 로깅 미들웨어 (모든 요청 정보를 로깅) - @app.before_request - def log_request_info(): - logger.info(f"Request: {request.method} {request.url}") - - return app - -app = create_app() - -if __name__ == '__main__': - app.run(debug=True, host="0.0.0.0", port=5000) + +# 서버 실행 +if __name__ == "__main__": + # FEEDBACK_DIR이 존재하는지 확인하고 없으면 생성 + os.makedirs(str(FEEDBACK_DIR), exist_ok=True) + + uvicorn.run( + "main:app", + host="0.0.0.0", + port=5000, + reload=True, + log_config=str(logging_config_path) # 로깅 설정 파일 지정 + ) + +# 실행 명령어 (터미널에서 실행 시): +# uvicorn main:app --host 0.0.0.0 --port 5000 --reload --log-config logging_config.json \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 23a50d2..52dcc01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Flask +fastapi +uvicorn pydantic pandas numpy @@ -8,6 +9,4 @@ lightgbm python-decouple python-dotenv pillow -jsonify -flask_restx catboost==1.2.7 \ No newline at end of file diff --git a/storage/output_feedback_json/recommendation_toby_1740987614.json b/storage/output_feedback_json/recommendation_toby_1740987614.json new file mode 100644 index 0000000..ac6de3b --- /dev/null +++ b/storage/output_feedback_json/recommendation_toby_1740987614.json @@ -0,0 +1,110 @@ +{ + "user": "toby", + "recommendations": [ + { + "category_id": 4, + "restaurant_id": 194, + "score": 5.0, + "predicted_score": 3.736, + "composite_score": 4.92 + }, + { + "category_id": 4, + "restaurant_id": 102, + "score": 5.0, + "predicted_score": 4.162, + "composite_score": 4.92 + }, + { + "category_id": 4, + "restaurant_id": 82, + "score": 5.0, + "predicted_score": 4.172, + "composite_score": 4.918 + }, + { + "category_id": 4, + "restaurant_id": 172, + "score": 4.9, + "predicted_score": 4.02, + "composite_score": 4.916 + }, + { + "category_id": 4, + "restaurant_id": 21, + "score": 4.9, + "predicted_score": 4.345, + "composite_score": 4.914 + }, + { + "category_id": 6, + "restaurant_id": 49, + "score": 5.0, + "predicted_score": 4.139, + "composite_score": 4.9 + }, + { + "category_id": 4, + "restaurant_id": 132, + "score": 5.0, + "predicted_score": 4.109, + "composite_score": 4.898 + }, + { + "category_id": 4, + "restaurant_id": 27, + "score": 4.8, + "predicted_score": 3.789, + "composite_score": 4.889 + }, + { + "category_id": 4, + "restaurant_id": 111, + "score": 5.0, + "predicted_score": 3.382, + "composite_score": 4.889 + }, + { + "category_id": 12, + "restaurant_id": 21, + "score": 5.0, + "predicted_score": 4.133, + "composite_score": 4.88 + }, + { + "category_id": 4, + "restaurant_id": 108, + "score": 5.0, + "predicted_score": 3.858, + "composite_score": 4.871 + }, + { + "category_id": 4, + "restaurant_id": 96, + "score": 5.0, + "predicted_score": 3.885, + "composite_score": 4.868 + }, + { + "category_id": 6, + "restaurant_id": 32, + "score": 4.7, + "predicted_score": 4.156, + "composite_score": 4.867 + }, + { + "category_id": 4, + "restaurant_id": 17, + "score": 5.0, + "predicted_score": 3.964, + "composite_score": 4.864 + }, + { + "category_id": 6, + "restaurant_id": 63, + "score": 4.9, + "predicted_score": 4.078, + "composite_score": 4.863 + } + ] +} \ No newline at end of file