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