Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions admin/admin_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from core.models import Config
from core.template import (
AdminTemplates, TEMPLATES, TemplateService, UserTemplates,
get_current_theme, get_theme_list, get_theme_info, register_theme_statics,
get_current_theme, get_theme_list, get_theme_info,
)
from lib.dependency.dependencies import validate_super_admin, validate_theme

Expand Down Expand Up @@ -110,7 +110,7 @@ async def theme_update(
os.unlink(file_path)

# 테마 관련 정적 파일을 등록합니다.
register_theme_statics(app)
TemplateService.register_statics(app)

# 이전 테마 경로를 제거 후 새로운 테마 경로를 추가합니다.
user_template = UserTemplates()
Expand Down
6 changes: 2 additions & 4 deletions admin/admin_visit.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,10 @@ async def visit_hour(
# 합계
total_count = db.scalar(query.add_columns(func.count(Visit.vi_id)))
# 시간별 접속자집계
# TODO: postgresql는 테스트가 안되어 있음
if dialect == 'mysql':
query = query.add_columns(func.hour(Visit.vi_time).label('hour'))
elif dialect == 'postgresql':
query = query.add_columns(func.to_char(Visit.vi_time, 'HH24').label('hour'))
query = query.add_columns(extract('hour', Visit.vi_time).label('hour'))
elif dialect == 'sqlite':
query = query.add_columns(func.strftime('%H', Visit.vi_time).label('hour'))
query_result = db.execute(
Expand Down Expand Up @@ -472,11 +471,10 @@ async def visit_weekday(
# 합계
total_count = db.scalar(query.add_columns(func.count(Visit.vi_id)))
# 요일별 접속자집계
# TODO: postgresql는 테스트가 안되어 있음
if dialect == 'mysql':
query = query.add_columns(func.dayofweek(Visit.vi_date).label('dow'))
elif dialect == 'postgresql':
query = query.add_columns(func.to_char(Visit.vi_date, 'D').label('dow'))
query = query.add_columns(extract('dow', Visit.vi_date).label('dow'))
elif dialect == 'sqlite':
query = query.add_columns(func.strftime('%w', Visit.vi_date).label('dow'))
query_result = db.execute(
Expand Down
26 changes: 23 additions & 3 deletions bbs/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing_extensions import Annotated, List

from fastapi import APIRouter, Depends, Request, Form, Path, Query, File, UploadFile
from typing import Union
from fastapi.responses import FileResponse, RedirectResponse

from core.database import db_session
Expand Down Expand Up @@ -183,7 +184,15 @@ async def write_form_add(
else:
service.validate_write_level()

# TODO: 포인트 검증
# 포인트 검증
required_point = (
board.bo_comment_point if parent_write else board.bo_write_point
)
service.point_service.validate_enough_point(
service.member.mb_id,
required_point,
"답변 작성" if parent_write else "게시글 작성",
)

# 게시판 제목 설정
board.subject = service.subject
Expand Down Expand Up @@ -298,7 +307,7 @@ async def create_post(
form_data: Annotated[WriteForm, Depends()],
service: Annotated[CreatePostService, Depends(CreatePostService.async_init)],
file_service: Annotated[BoardFileService, Depends()],
parent_id: int = Form(None),
parent_id: Union[int, None, str] = Form(None),
notice: bool = Form(False),
secret: str = Form(""),
html: str = Form(""),
Expand All @@ -310,6 +319,10 @@ async def create_post(
recaptcha_response: str = Form("", alias="g-recaptcha-response"),
):
"""게시글을 작성한다."""
if parent_id in ("", None):
parent_id = None
else:
parent_id = int(parent_id)
await service.validate_captcha(recaptcha_response)
service.validate_write_delay()
service.validate_write_level()
Expand Down Expand Up @@ -466,6 +479,12 @@ async def write_comment_update(
form: WriteCommentForm = Depends(),
recaptcha_response: str = Form("", alias="g-recaptcha-response"),
):
# 여기서 if 문을 사용해야 함!
if form.comment_id in ("", None):
comment_id = None
else:
comment_id = int(form.comment_id)

"""
댓글 등록/수정
"""
Expand All @@ -489,7 +508,8 @@ async def write_comment_update(
elif form.w == "cu":
# 댓글 수정
write_model = service.write_model
comment = service.db.get(write_model, form.comment_id)
# comment = service.db.get(write_model, form.comment_id)
comment = service.db.get(write_model, comment_id)
if not comment:
raise AlertException(f"{form.comment_id} : 존재하지 않는 댓글입니다.", 404)

Expand Down
10 changes: 3 additions & 7 deletions core/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from starlette.templating import _TemplateResponse

from slowapi.errors import RateLimitExceeded
Expand Down Expand Up @@ -135,13 +134,10 @@ def template_response(
Returns:
_TemplateResponse: 템플릿 응답 객체
"""
from core.template import TemplateService, theme_asset
from core.template import TemplateService

# 새로운 템플릿 응답 객체를 생성합니다.
# - UserTemplates, AdminTemplates 클래스는 기본 컨텍스트 설정 시 DB를 조회하는데,
# 처음 설치 시에는 DB가 없으므로 새로운 템플릿 응답 객체를 생성합니다.
template = Jinja2Templates(directory=TemplateService.get_templates_dir())
template.env.globals["theme_asset"] = theme_asset
# UserTemplates/AdminTemplates는 DB 조회가 필요하므로 사용하지 않는다.
template = TemplateService.get_templates()
return template.TemplateResponse(
name=template_html,
context=context,
Expand Down
6 changes: 4 additions & 2 deletions core/formclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

from typing import Union
from fastapi import Form

from core.exception import AlertException
Expand Down Expand Up @@ -456,7 +456,9 @@ class WriteCommentForm:
wr_name: str = Form(None)
wr_password: str = Form(None)
wr_secret: str = Form(None)
comment_id: int = Form(None)
# comment_id: int = Form(None)
comment_id: Union[int, str, None] = Form(None)



@dataclass
Expand Down
89 changes: 61 additions & 28 deletions core/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,75 @@ def get_admin_theme_path() -> str:
ADMIN_TEMPLATES_DIR = get_admin_theme_path() # 관리자 템플릿 경로

class TemplateService():
"""템플릿 서비스 클래스
- TODO: 이외의 다른 부분도 클래스화 해야한다.
"""템플릿 서비스 클래스.

템플릿 경로와 정적 파일, 렌더링 옵션을 관리한다.
"""
_templates_dir: str = None # 사용자 템플릿 경로

_templates_dir: typing.Optional[str] = None # 사용자 템플릿 경로
_templates: typing.Optional[Jinja2Templates] = None
_env_options: dict = {}

@classmethod
def get_templates_dir(cls) -> str:
"""현재 테마의 템플릿 디렉터리 경로를 반환한다."""
if cls._templates_dir is None:
cls.set_templates_dir()

return cls._templates_dir

@classmethod
def set_templates_dir(cls) -> None:
cls._templates_dir = get_theme_path()
def set_templates_dir(cls, template_dir: typing.Optional[str] = None) -> None:
"""템플릿 디렉터리를 설정한다."""
cls._templates_dir = template_dir or get_theme_path()
cls._templates = None

@classmethod
def set_env_options(cls, **env_options) -> None:
"""Jinja2 환경 설정을 갱신한다."""
cls._env_options.update(env_options)
cls._templates = None

@classmethod
def get_templates(cls, **env_options) -> Jinja2Templates:
"""Jinja2Templates 객체를 반환한다.

Args:
**env_options: Environment 옵션
"""
if env_options:
options = {**cls._env_options, **env_options}
templates = Jinja2Templates(
directory=cls.get_templates_dir(),
**options
)
templates.env.globals["theme_asset"] = theme_asset
return templates

if cls._templates is None:
cls._templates = Jinja2Templates(
directory=cls.get_templates_dir(),
**cls._env_options
)
cls._templates.env.globals["theme_asset"] = theme_asset

return cls._templates

@classmethod
def register_statics(cls, app: FastAPI) -> None:
"""현재 테마의 static 디렉터리를 FastAPI에 등록한다."""
theme = get_current_theme()
directories = ["/mobile", ""]
for directory in directories:
static_directory = f"{TEMPLATES}/{theme}{directory}/static"

if not os.path.isdir(static_directory):
continue

url = f"/theme_static/{theme}{directory}"
path = StaticFiles(directory=static_directory)
static_device = directory.replace("/", "_")
app.mount(url, path, name=f"static_{theme}{static_device}")


class UserTemplates(Jinja2Templates):
Expand Down Expand Up @@ -285,29 +339,8 @@ def theme_asset(request: Request, asset_path: str) -> str:


def register_theme_statics(app: FastAPI) -> None:
"""
현재 테마의 static 경로를 가상의 경로로 등록하는 함수
- ex) PC: /{theme}/basic/static/css -> /theme_static/basic/css
- ex) Mobile: /{theme}/basic/mobile/static/css -> /theme_static/basic/mobile/css

Args:
app (FastAPI): FastAPI 객체
"""
theme = get_current_theme()
directories = ["/mobile", ""]
for directory in directories:
static_directory = f"{TEMPLATES}/{theme}{directory}/static"

if not os.path.isdir(static_directory):
# logger = logging.getLogger("uvicorn.error")
# logger.warning("theme has not static directory : ",
# static_directory)
continue

url = f"/theme_static/{theme}{directory}"
path = StaticFiles(directory=static_directory)
static_device = directory.replace("/", "_")
app.mount(url, path, name=f"static_{theme}{static_device}") # tag
"""Backward compatible wrapper for :meth:`TemplateService.register_statics`."""
TemplateService.register_statics(app)


def get_theme_list():
Expand Down
59 changes: 51 additions & 8 deletions lib/board_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import bleach
from typing import List
from fastapi import Request
from fastapi.templating import Jinja2Templates
from sqlalchemy import and_, asc, desc, func, insert, or_, select
from sqlalchemy.sql.expression import Select
from sqlalchemy.orm import Session
from cachetools import TTLCache

from core.database import DBConnect
from core.exception import AlertException
Expand All @@ -24,6 +24,53 @@
from service.board_file_service import BoardFileService as FileService


# Caches for reducing DB and disk I/O
FILE_META_CACHE = TTLCache(maxsize=1024, ttl=300)
WRITE_CACHE = TTLCache(maxsize=1024, ttl=300)
FILE_EXIST_CACHE = TTLCache(maxsize=1024, ttl=300)


def _cache_key(bo_table: str, wr_id: int) -> str:
"""Create a unified cache key."""
return f"{bo_table}:{wr_id}"


def get_write_cached(bo_table: str, wr_id: int) -> WriteBaseModel | None:
"""Return write object from cache or database."""
key = _cache_key(bo_table, wr_id)
write = WRITE_CACHE.get(key)
if write is None:
with DBConnect().sessionLocal() as db:
write_model = dynamic_create_write_table(bo_table)
write = db.get(write_model, wr_id)
if write:
WRITE_CACHE[key] = write
return write


def get_board_files_cached(request: Request, bo_table: str, wr_id: int):
"""Return board files grouped by type using cache."""
key = _cache_key(bo_table, wr_id)
result = FILE_META_CACHE.get(key)
if result is None:
with DBConnect().sessionLocal() as db:
service = FileService(request, db)
result = service.get_board_files_by_type(bo_table, wr_id)
FILE_META_CACHE[key] = result
return result


def is_file_exist_cached(request: Request, db: Session, bo_table: str, wr_id: int) -> bool:
"""Check file existence using cache."""
key = _cache_key(bo_table, wr_id)
exist = FILE_EXIST_CACHE.get(key)
if exist is None:
service = FileService(request, db)
exist = service.is_exist(bo_table, wr_id)
FILE_EXIST_CACHE[key] = exist
return exist


class BoardConfig():
"""게시판 설정 정보를 담는 클래스."""

Expand Down Expand Up @@ -560,7 +607,6 @@ def get_list(request: Request, db: Session, write: WriteBaseModel, board_config:
Returns:
WriteBaseModel: 게시글 목록.
"""
file_service = FileService(request, db)
write.subject = board_config.cut_write_subject(write.wr_subject, subject_len)
write.name = cut_name(request, write.wr_name)
write.email = StringEncrypt().encrypt(write.wr_email)
Expand All @@ -570,7 +616,7 @@ def get_list(request: Request, db: Session, write: WriteBaseModel, board_config:
write.icon_secret = "secret" in write.wr_option
write.icon_hot = board_config.is_icon_hot(write.wr_hit)
write.icon_new = board_config.is_icon_new(write.wr_datetime)
write.icon_file = file_service.is_exist(board_config.board.bo_table, write.wr_id)
write.icon_file = is_file_exist_cached(request, db, board_config.board.bo_table, write.wr_id)
write.icon_link = write.wr_link1 or write.wr_link2
write.icon_reply = write.wr_reply

Expand Down Expand Up @@ -674,8 +720,7 @@ def send_write_mail(request: Request, board: Board, write: WriteBaseModel, origi
"""
with DBConnect().sessionLocal() as db:
config = request.state.config
templates = Jinja2Templates(
directory=TemplateService.get_templates_dir())
templates = TemplateService.get_templates()

def _add_admin_email(admin_id: str):
admin = db.scalar(select(Member).filter_by(mb_id=admin_id))
Expand Down Expand Up @@ -738,9 +783,7 @@ def get_list_thumbnail(request: Request, board: Board, write: WriteBaseModel, th
thumb_height (int, optional): _description_. Defaults to 0.
"""
config = request.state.config
with DBConnect().sessionLocal() as db:
service = FileService(request, db)
images, files = service.get_board_files_by_type(board.bo_table, write.wr_id)
images, files = get_board_files_cached(request, board.bo_table, write.wr_id)
source_file = None
result = {"src": "", "alt": "", "noimg":""}

Expand Down
Loading