diff --git a/.env b/.env index b37d85b..9dd8133 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ SERVER_TYPE=local ROOT_PATH= DB_URL=localhost -HOUSE_REC_URL=https://sarabwayu5.hackathon.sparcs.net/ \ No newline at end of file +HOUSE_REC_URL=https://sarabwayu6.hackathon.sparcs.net/ \ No newline at end of file diff --git a/.env-prod b/.env-prod index c69fa93..984ee5f 100644 --- a/.env-prod +++ b/.env-prod @@ -1,4 +1,4 @@ SERVER_TYPE=prod ROOT_PATH=/api DB_URL=mysql-container -HOUSE_REC_URL=https://sarabwayu5.hackathon.sparcs.net/ \ No newline at end of file +HOUSE_REC_URL=https://sarabwayu6.hackathon.sparcs.net/ \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 35f0f33..a243013 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -8,5 +8,12 @@ jdbc:mysql://localhost:3306 $ProjectFileDir$ + + redis + true + jdbc.RedisDriver + jdbc:redis://localhost:6379/0 + $ProjectFileDir$ + \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index fa3b045..3baea53 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,5 @@ from pydantic import BaseSettings - class Settings(BaseSettings): SERVER_TYPE: str ROOT_PATH: str diff --git a/app/db/database.py b/app/db/database.py index 29d442b..d31962c 100644 --- a/app/db/database.py +++ b/app/db/database.py @@ -1,15 +1,14 @@ +import json + import jwt from fastapi import HTTPException, status, Depends from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session - from app.core.config import settings from app.db.models import get_Base, User import aioredis -from fastapi.security import OAuth2PasswordBearer from fastapi.security.api_key import APIKeyHeader -API_KEY_NAME = "Authorization" -api_key_header_auth = APIKeyHeader(name=API_KEY_NAME, auto_error=False) +api_key_header_auth = APIKeyHeader(name="Authorization", auto_error=False) DB_URL = f'mysql+pymysql://root:0000@{settings.DB_URL}/sarabwayu' @@ -25,12 +24,12 @@ def get_Base(): def get_SessionLocal(): return SessionLocal -# async def get_redis_client() -> aioredis.Redis: -# redis = aioredis.from_url(f"redis://localhost:6379/0", encoding="utf-8", decode_responses=True) -# try: -# yield redis -# finally: -# await redis.close() +async def get_redis_client() -> aioredis.Redis: + redis = aioredis.from_url(f"redis://{settings.DB_URL}:6379/0", encoding="utf-8", decode_responses=True) + try: + yield redis + finally: + await redis.close() def get_db() -> Session: db = SessionLocal() @@ -51,9 +50,22 @@ def save_db(data, db): detail="데이터베이스에 오류가 발생했습니다." ) + +async def user_to_json(user): + return json.dumps( + { + "id": user.id, + "nickname": user.nickname, + "phone": user.phone, + "is_deleted": user.is_deleted, + } + ) + + async def get_current_user( api_key: str = Depends(api_key_header_auth), db: Session = Depends(get_db), + redis: aioredis.Redis = Depends(get_redis_client) ) -> User: if api_key is None: @@ -79,7 +91,15 @@ async def get_current_user( headers={"WWW-Authenticate": "Bearer"}, ) + + nickname: str = payload.get("sub") + user_info = await redis.get(f"user:{nickname}") + if user_info: + return User(**json.loads(user_info)) + user = db.query(User).filter(User.nickname == nickname).first() + await redis.set(f"user:{nickname}", await user_to_json(user), ex=3600) + return user \ No newline at end of file diff --git a/app/db/models.py b/app/db/models.py index 53b12af..69ea61b 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,5 +1,4 @@ from sqlalchemy import Column, Integer, Text, ForeignKey, String, Boolean, DateTime, func, JSON, Date, FLOAT -from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base from datetime import datetime import pytz diff --git a/app/router/auth.py b/app/router/auth.py index c254f1d..227caae 100644 --- a/app/router/auth.py +++ b/app/router/auth.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends +from app.db.database import get_current_user from app.schemas.request import Auth from app.schemas.response import ApiResponse from app.service.auth import AuthService @@ -15,4 +16,11 @@ async def get_auth_login( ): return ApiResponse( data=await auth_service.login(auth_data) - ) \ No newline at end of file + ) + +@router.get("/info", response_model=ApiResponse, tags=["Auth"]) +async def get_auth_info( + user: Annotated[get_current_user, Depends()] +): + user.hashed_password = None + return ApiResponse(data=user) \ No newline at end of file diff --git a/app/router/chat.py b/app/router/chat.py index cd55a96..72ff524 100644 --- a/app/router/chat.py +++ b/app/router/chat.py @@ -6,7 +6,7 @@ router = APIRouter(prefix="/chat") -@router.post("/", response_model=ApiResponse, tags=["Chat"]) +@router.post("", response_model=ApiResponse, tags=["Chat"]) async def post_chat( chat_data: Chat, chat_service: Annotated[ChatService, Depends()] diff --git a/app/router/house.py b/app/router/house.py index a353b4e..6098693 100644 --- a/app/router/house.py +++ b/app/router/house.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, BackgroundTasks from app.schemas.request import House from app.schemas.response import ApiResponse @@ -20,8 +20,7 @@ async def post_house_create( house_data: House, house_service: Annotated[HouseService, Depends()] ): - print(house_data.house_info) - return ApiResponse() + return ApiResponse(data=await house_service.create(house_data)) @router.patch("/like/{house_id}", response_model=ApiResponse, tags=["House"]) async def patch_house_like( house_id: int, @@ -29,16 +28,25 @@ async def patch_house_like( ): return ApiResponse(data=await house_service.like(house_id)) +@router.get("/detail/{house_id}", response_model=ApiResponse, tags=["House"]) +async def patch_house_detail( + house_id: int, + house_service: Annotated[HouseService, Depends()] +): + return ApiResponse(data=await house_service.detail(house_id)) + @router.get("/recommendation/list/{page}", response_model=ApiResponse, tags=["House"]) async def get_house_recommendation( page: int, + background_tasks: BackgroundTasks, house_service: Annotated[HouseService, Depends()] ): - return ApiResponse(data=await house_service.recommendation_list(page)) + return ApiResponse(data=await house_service.recommendation_list(background_tasks, page)) @router.get("/list/{page}", response_model=ApiResponse, tags=["House"]) async def get_house_list( page: int, + background_tasks: BackgroundTasks, house_service: Annotated[HouseService, Depends()] ): - return ApiResponse(data=await house_service.list(page)) + return ApiResponse(data=await house_service.list(background_tasks, page)) diff --git a/app/service/auth.py b/app/service/auth.py index 24f20e9..7a51253 100644 --- a/app/service/auth.py +++ b/app/service/auth.py @@ -1,4 +1,3 @@ -import aioredis from fastapi import Depends, HTTPException, status from sqlalchemy.orm import Session from app.db.database import get_db, save_db diff --git a/app/service/chat.py b/app/service/chat.py index f2900b5..0e42d4c 100644 --- a/app/service/chat.py +++ b/app/service/chat.py @@ -127,4 +127,4 @@ async def check_format(data): ) save_db(recommendation, self.db) - return return_data \ No newline at end of file + return {"rank": rank_data, "reason": return_data} \ No newline at end of file diff --git a/app/service/house.py b/app/service/house.py index 4549319..37770fc 100644 --- a/app/service/house.py +++ b/app/service/house.py @@ -1,15 +1,12 @@ import json -import requests - -from fastapi import Depends, HTTPException, status -from sqlalchemy import select, and_ +import aioredis +from fastapi import Depends, BackgroundTasks +from sqlalchemy import select from sqlalchemy.orm import Session from sklearn.metrics.pairwise import cosine_similarity from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np - -from app.core.config import settings -from app.db.database import get_db, get_current_user, save_db +from app.db.database import get_db, get_current_user, save_db, get_redis_client from app.db.models import User, House, Recommendation, LikedHouse @@ -101,11 +98,12 @@ def recommend(self, persona, top_n=3): class HouseService: - def __init__(self, db: Session = Depends(get_db), user: User = Depends(get_current_user)): + def __init__(self, db: Session = Depends(get_db), user: User = Depends(get_current_user), redis: aioredis.Redis = Depends(get_redis_client)): self.db = db self.user = user + self.redis = redis - async def initailize(self): + async def initailize(self) -> None: with open('app/service/apartment_info.jsonl', 'r') as f: data = f.readlines() for line in data: @@ -137,7 +135,7 @@ async def initailize(self): save_db(house_info, self.db) - async def create(self, house_data): + async def create(self, house_data: dict) -> House: house = House( aptName=house_data['aptName'], tradeBuildingTypeCode=house_data['tradeBuildingTypeCode'], @@ -163,7 +161,7 @@ async def create(self, house_data): return house_data - async def like(self, house_id): + async def like(self, house_id: int) -> None: # db에서 좋아요를 누른 이력이 있는지 확인합니다. like = self.db.query(LikedHouse).filter( @@ -188,16 +186,28 @@ async def like(self, house_id): ) save_db(liked_house, self.db) - async def recommendation_list(self, page): + await self.redis.delete(f"house:{self.user.id}:{house_id}") + + async def detail(self, house_id: int) -> dict: - # Recommendation 테이블에서 삭제되지 않은 데이터를 페이지네이션 해서 가져옵니다. - # 이 때 house_id를 이용하여 House 테이블에서 데이터를 가져옵니다. - houses = self.db.execute( + house = await self.redis.get(f"house:{self.user.id}:{house_id}") + if house: + return json.loads(house) + + house = self.db.execute( select( - Recommendation.house_id, + House.id, House.aptName, - House.image_url, House.exposureAddress, + Recommendation.reason, + House.tagList, + House.aptHeatMethodTypeName, + House.aptHeatFuelTypeName, + House.aptHouseholdCount, + House.schoolName, + House.organizationType, + House.walkTime, + House.studentCountPerTeacher ).join( House, Recommendation.house_id == House.id @@ -205,27 +215,97 @@ async def recommendation_list(self, page): Recommendation.user_id == self.user.id, Recommendation.is_deleted == False, House.is_deleted == False - ).limit(5).offset((page - 1) * 5) - ).all() + ) + ).first() + + # 좋아요 한 기록이 있는지 확인 + + liked_house = self.db.query(LikedHouse).filter( + LikedHouse.user_id == self.user.id, + LikedHouse.house_id == house_id + ).first() + + is_like = False + if liked_house: + is_like = True + + house = { + "id": house[0], + "aptName": house[1], + "exposureAddress": house[2], + "reason": house[3], + "tagList": house[4], + "aptHeatMethodTypeName": house[5], + "aptHeatFuelTypeName": house[6], + "aptHouseholdCount": house[7], + "schoolName": house[8], + "organizationType": house[9], + "walkTime": house[10], + "studentCountPerTeacher": house[11], + "is_like": is_like + } + + await self.redis.set(f"house:{self.user.id}:{house_id}", json.dumps(house, ensure_ascii=False) , ex=3600) + + return house + + async def fetch_rec_houses_data(self, page) -> list: + houses_query = select( + Recommendation.house_id, + House.aptName, + House.image_url, + House.exposureAddress, + ).join( + House, + Recommendation.house_id == House.id + ).filter( + Recommendation.user_id == self.user.id, + Recommendation.is_deleted == False, + House.is_deleted == False + ).limit(5).offset((page - 1) * 5) + houses = self.db.execute(houses_query).all() - # 사용자가 '좋아요'한 집의 ID를 세트로 생성 liked_houses_set = {liked_house.house_id for liked_house in self.db.query(LikedHouse).filter( LikedHouse.user_id == self.user.id, LikedHouse.is_deleted == False )} - # 가져온 집 정보에 '좋아요' 정보를 추가하여 반환 - return_houses = [{ + return [{ "house_id": house[0], "aptName": house[1], "image_url": house[2], "exposureAddress": house[3], - "is_like": house[0] in liked_houses_set # set를 사용하여 빠르게 확인 + "is_like": house[0] in liked_houses_set } for house in houses] + async def cache_recommendation_list(self, page) -> None: + redis_key = f"rec:list:{self.user.id}:{page}" + return_houses = await self.fetch_rec_houses_data(page) + await self.redis.set(redis_key, json.dumps(return_houses, ensure_ascii=False), ex=1800) + + + async def recommendation_list(self, background_tasks: BackgroundTasks, page: int) -> list: + + # backgroud task를 사용하여 다음 페이지의 데이터를 미리 캐싱합니다. + background_tasks.add_task(self.cache_recommendation_list, page + 1) + + redis_key = f"rec:list:{self.user.id}:{page}" + cached_data = await self.redis.get(redis_key) + + if cached_data: + return json.loads(cached_data) + + return_houses = await self.fetch_rec_houses_data(page) + + # redis에 데이터를 저장합니다. + await self.redis.set(redis_key, json.dumps(return_houses, ensure_ascii=False), ex=1800) + + + return return_houses - async def list(self, page): + async def fatch_house_list(self, page: int) -> list: + # House 테이블과 Recommendation 테이블을 left join하고, # Recommendation 테이블의 house_id가 NULL인 경우만 필터링합니다. houses_query = select( @@ -233,33 +313,47 @@ async def list(self, page): House.aptName, House.image_url, House.exposureAddress - ).outerjoin( - Recommendation, and_( - Recommendation.house_id == House.id, - Recommendation.user_id == self.user.id, - Recommendation.is_deleted == False - ) ).filter( - Recommendation.house_id == None, # Recommendation에 없는 House House.is_deleted == False ).limit(5).offset((page - 1) * 5) - houses = self.db.execute(houses_query).all() # 사용자가 '좋아요'한 집 목록을 가져옵니다. - liked_houses_query = select(LikedHouse.house_id).filter( - LikedHouse.user_id == self.user.id - ) - liked_houses = {house_id for (house_id,) in self.db.execute(liked_houses_query).all()} + liked_houses_set = {liked_house.house_id for liked_house in self.db.query(LikedHouse).filter( + LikedHouse.user_id == self.user.id, + LikedHouse.is_deleted == False + )} # 가져온 집 정보에 '좋아요' 정보를 추가하여 반환합니다. - return_houses = [{ + return [{ "house_id": house[0], "aptName": house[1], "image_url": house[2], "exposureAddress": house[3], - "is_like": house[0] in liked_houses + "is_like": house[0] in liked_houses_set } for house in houses] + async def cache_house_list(self, page: int) -> None: + redis_key = f"house:list:{self.user.id}:{page}" + return_houses = await self.fatch_house_list(page) + await self.redis.set(redis_key, json.dumps(return_houses, ensure_ascii=False), ex=1800) + + async def list(self, background_tasks: BackgroundTasks, page: int) -> list: + + # backgroud task를 사용하여 다음 페이지의 데이터를 미리 캐싱합니다. + background_tasks.add_task(self.cache_house_list, page + 1) + + # redis에 저장된 데이터를 가져옵니다. + redis_key = f"house:list:{self.user.id}:{page}" + redis_data = await self.redis.get(redis_key) + + if redis_data: + return json.loads(redis_data) + + return_houses = await self.fatch_house_list(page) + + # redis에 데이터를 저장합니다. + await self.redis.set(redis_key, json.dumps(return_houses, ensure_ascii=False), ex=1800) + return return_houses diff --git a/main.py b/main.py index ebf94ca..9c0c138 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,9 @@ from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware - from app.core.config import settings from app.router import auth, chat, house - app = FastAPI( root_path=settings.ROOT_PATH, ) @@ -21,5 +19,4 @@ allow_credentials=True, allow_methods=["*"], allow_headers=["*"], -) - +) \ No newline at end of file diff --git a/start.sh b/start.sh index 3724db6..a4fd374 100644 --- a/start.sh +++ b/start.sh @@ -1,10 +1,3 @@ - -# mysql 실행 스크립트 -# - - -#!/bin/bash - # Redis 데이터 플러시 redis-cli flushall