feat: Add bulletin board system
- Add BoardCategory and BoardPost models with multi-language support - Add bulletin API endpoints (CRUD, notice toggle, pin toggle) - Add board_enabled setting to control menu visibility - Create frontend board pages (list, detail, write, edit) - Create admin board management and category management pages - Update Header.tsx with conditional Board menu between Inquiry and Contact Us - Update admin settings with board_enabled toggle - Add Board menu to admin sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
524
backend/app/api/bulletin.py
Normal file
524
backend/app/api/bulletin.py
Normal file
@@ -0,0 +1,524 @@
|
||||
"""
|
||||
Bulletin Board API - 게시판 API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc, or_
|
||||
from typing import Optional
|
||||
import math
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.bulletin import BoardCategory, BoardPost
|
||||
from ..models.user import User
|
||||
from ..schemas.bulletin import (
|
||||
BoardCategoryCreate, BoardCategoryUpdate, BoardCategoryResponse, BoardCategoryListResponse,
|
||||
BoardPostCreate, BoardPostUpdate, BoardPostResponse, BoardPostListItem, BoardPostListResponse
|
||||
)
|
||||
from .auth import get_current_user, get_current_user_optional
|
||||
|
||||
router = APIRouter(prefix="/board", tags=["board"])
|
||||
|
||||
|
||||
# ============ Category Endpoints ============
|
||||
|
||||
@router.get("/categories", response_model=BoardCategoryListResponse)
|
||||
def get_categories(
|
||||
include_inactive: bool = False,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""카테고리 목록 조회 (공개)"""
|
||||
query = db.query(BoardCategory)
|
||||
|
||||
if not include_inactive:
|
||||
query = query.filter(BoardCategory.is_active == True)
|
||||
|
||||
categories = query.order_by(BoardCategory.sort_order, BoardCategory.id).all()
|
||||
|
||||
# 각 카테고리별 게시물 수 계산
|
||||
result = []
|
||||
for cat in categories:
|
||||
post_count = db.query(func.count(BoardPost.id)).filter(
|
||||
BoardPost.category_id == cat.id,
|
||||
BoardPost.is_published == True
|
||||
).scalar()
|
||||
|
||||
cat_dict = {
|
||||
"id": cat.id,
|
||||
"name": cat.name,
|
||||
"name_en": cat.name_en,
|
||||
"name_mn": cat.name_mn,
|
||||
"name_ru": cat.name_ru,
|
||||
"slug": cat.slug,
|
||||
"description": cat.description,
|
||||
"sort_order": cat.sort_order,
|
||||
"is_active": cat.is_active,
|
||||
"created_at": cat.created_at,
|
||||
"updated_at": cat.updated_at,
|
||||
"post_count": post_count
|
||||
}
|
||||
result.append(cat_dict)
|
||||
|
||||
return BoardCategoryListResponse(categories=result, total=len(result))
|
||||
|
||||
|
||||
@router.post("/categories", response_model=BoardCategoryResponse)
|
||||
def create_category(
|
||||
category: BoardCategoryCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""카테고리 생성 (관리자만)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only administrators can create categories"
|
||||
)
|
||||
|
||||
# slug 중복 체크
|
||||
existing = db.query(BoardCategory).filter(BoardCategory.slug == category.slug).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Category slug already exists"
|
||||
)
|
||||
|
||||
db_category = BoardCategory(**category.model_dump())
|
||||
db.add(db_category)
|
||||
db.commit()
|
||||
db.refresh(db_category)
|
||||
|
||||
return {**db_category.__dict__, "post_count": 0}
|
||||
|
||||
|
||||
@router.put("/categories/{category_id}", response_model=BoardCategoryResponse)
|
||||
def update_category(
|
||||
category_id: int,
|
||||
category_update: BoardCategoryUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""카테고리 수정 (관리자만)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only administrators can update categories"
|
||||
)
|
||||
|
||||
db_category = db.query(BoardCategory).filter(BoardCategory.id == category_id).first()
|
||||
if not db_category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Category not found"
|
||||
)
|
||||
|
||||
# slug 중복 체크 (다른 카테고리)
|
||||
if category_update.slug:
|
||||
existing = db.query(BoardCategory).filter(
|
||||
BoardCategory.slug == category_update.slug,
|
||||
BoardCategory.id != category_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Category slug already exists"
|
||||
)
|
||||
|
||||
update_data = category_update.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_category, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_category)
|
||||
|
||||
post_count = db.query(func.count(BoardPost.id)).filter(
|
||||
BoardPost.category_id == category_id,
|
||||
BoardPost.is_published == True
|
||||
).scalar()
|
||||
|
||||
return {**db_category.__dict__, "post_count": post_count}
|
||||
|
||||
|
||||
@router.delete("/categories/{category_id}")
|
||||
def delete_category(
|
||||
category_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""카테고리 삭제 (관리자만)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only administrators can delete categories"
|
||||
)
|
||||
|
||||
db_category = db.query(BoardCategory).filter(BoardCategory.id == category_id).first()
|
||||
if not db_category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Category not found"
|
||||
)
|
||||
|
||||
# 해당 카테고리에 게시물이 있는지 확인
|
||||
post_count = db.query(func.count(BoardPost.id)).filter(
|
||||
BoardPost.category_id == category_id
|
||||
).scalar()
|
||||
|
||||
if post_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot delete category with {post_count} posts. Please delete or move posts first."
|
||||
)
|
||||
|
||||
db.delete(db_category)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Category deleted successfully"}
|
||||
|
||||
|
||||
# ============ Post Endpoints ============
|
||||
|
||||
@router.get("/posts", response_model=BoardPostListResponse)
|
||||
def get_posts(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
category_id: Optional[int] = None,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""게시물 목록 조회 (공개)"""
|
||||
# 공지사항 (항상 상단에)
|
||||
notices_query = db.query(BoardPost).filter(
|
||||
BoardPost.is_published == True,
|
||||
BoardPost.is_notice == True
|
||||
).order_by(desc(BoardPost.is_pinned), desc(BoardPost.created_at))
|
||||
|
||||
if category_id:
|
||||
notices_query = notices_query.filter(BoardPost.category_id == category_id)
|
||||
|
||||
notices = notices_query.all()
|
||||
|
||||
# 일반 게시물
|
||||
query = db.query(BoardPost).filter(
|
||||
BoardPost.is_published == True,
|
||||
BoardPost.is_notice == False
|
||||
)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(BoardPost.category_id == category_id)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
BoardPost.title.ilike(f"%{search}%"),
|
||||
BoardPost.content.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
total_pages = math.ceil(total / page_size) if total > 0 else 1
|
||||
|
||||
# Pagination
|
||||
posts = query.order_by(
|
||||
desc(BoardPost.is_pinned),
|
||||
desc(BoardPost.created_at)
|
||||
).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# Format response
|
||||
def format_post(post: BoardPost) -> dict:
|
||||
return {
|
||||
"id": post.id,
|
||||
"title": post.title,
|
||||
"category_id": post.category_id,
|
||||
"category_name": post.category.name if post.category else None,
|
||||
"author_id": post.author_id,
|
||||
"author_name": post.author.name if post.author else None,
|
||||
"is_notice": post.is_notice,
|
||||
"is_pinned": post.is_pinned,
|
||||
"view_count": post.view_count,
|
||||
"created_at": post.created_at
|
||||
}
|
||||
|
||||
return BoardPostListResponse(
|
||||
posts=[format_post(p) for p in posts],
|
||||
notices=[format_post(n) for n in notices],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=total_pages
|
||||
)
|
||||
|
||||
|
||||
@router.get("/posts/{post_id}", response_model=BoardPostResponse)
|
||||
def get_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""게시물 상세 조회 (공개)"""
|
||||
post = db.query(BoardPost).filter(
|
||||
BoardPost.id == post_id,
|
||||
BoardPost.is_published == True
|
||||
).first()
|
||||
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
# 조회수 증가
|
||||
post.view_count += 1
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
|
||||
return post
|
||||
|
||||
|
||||
@router.post("/posts", response_model=BoardPostResponse)
|
||||
def create_post(
|
||||
post_data: BoardPostCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""게시물 작성 (회원만)"""
|
||||
# 카테고리 존재 확인
|
||||
category = db.query(BoardCategory).filter(
|
||||
BoardCategory.id == post_data.category_id,
|
||||
BoardCategory.is_active == True
|
||||
).first()
|
||||
|
||||
if not category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid category"
|
||||
)
|
||||
|
||||
# 공지사항은 관리자만 작성 가능
|
||||
is_notice = post_data.is_notice
|
||||
if is_notice and not current_user.is_admin:
|
||||
is_notice = False # 관리자가 아니면 공지사항 불가
|
||||
|
||||
db_post = BoardPost(
|
||||
title=post_data.title,
|
||||
content=post_data.content,
|
||||
category_id=post_data.category_id,
|
||||
author_id=current_user.id,
|
||||
is_notice=is_notice,
|
||||
is_pinned=is_notice # 공지사항은 자동으로 고정
|
||||
)
|
||||
|
||||
db.add(db_post)
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
|
||||
return db_post
|
||||
|
||||
|
||||
@router.put("/posts/{post_id}", response_model=BoardPostResponse)
|
||||
def update_post(
|
||||
post_id: int,
|
||||
post_update: BoardPostUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""게시물 수정 (작성자 또는 관리자)"""
|
||||
db_post = db.query(BoardPost).filter(BoardPost.id == post_id).first()
|
||||
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
# 권한 체크: 작성자 또는 관리자만
|
||||
if db_post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You don't have permission to edit this post"
|
||||
)
|
||||
|
||||
# 카테고리 변경 시 존재 확인
|
||||
if post_update.category_id:
|
||||
category = db.query(BoardCategory).filter(
|
||||
BoardCategory.id == post_update.category_id,
|
||||
BoardCategory.is_active == True
|
||||
).first()
|
||||
if not category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid category"
|
||||
)
|
||||
|
||||
update_data = post_update.model_dump(exclude_unset=True)
|
||||
|
||||
# 공지사항/고정은 관리자만 변경 가능
|
||||
if not current_user.is_admin:
|
||||
update_data.pop("is_notice", None)
|
||||
update_data.pop("is_pinned", None)
|
||||
update_data.pop("is_published", None)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(db_post, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
|
||||
return db_post
|
||||
|
||||
|
||||
@router.delete("/posts/{post_id}")
|
||||
def delete_post(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""게시물 삭제 (작성자 또는 관리자)"""
|
||||
db_post = db.query(BoardPost).filter(BoardPost.id == post_id).first()
|
||||
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
# 권한 체크: 작성자 또는 관리자만
|
||||
if db_post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You don't have permission to delete this post"
|
||||
)
|
||||
|
||||
db.delete(db_post)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Post deleted successfully"}
|
||||
|
||||
|
||||
# ============ Admin Endpoints ============
|
||||
|
||||
@router.get("/admin/posts", response_model=BoardPostListResponse)
|
||||
def admin_get_posts(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
category_id: Optional[int] = None,
|
||||
is_notice: Optional[bool] = None,
|
||||
is_published: Optional[bool] = None,
|
||||
search: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""관리자용 게시물 목록 (모든 게시물 포함)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin access required"
|
||||
)
|
||||
|
||||
query = db.query(BoardPost)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(BoardPost.category_id == category_id)
|
||||
|
||||
if is_notice is not None:
|
||||
query = query.filter(BoardPost.is_notice == is_notice)
|
||||
|
||||
if is_published is not None:
|
||||
query = query.filter(BoardPost.is_published == is_published)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
BoardPost.title.ilike(f"%{search}%"),
|
||||
BoardPost.content.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
total_pages = math.ceil(total / page_size) if total > 0 else 1
|
||||
|
||||
posts = query.order_by(
|
||||
desc(BoardPost.is_notice),
|
||||
desc(BoardPost.is_pinned),
|
||||
desc(BoardPost.created_at)
|
||||
).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
def format_post(post: BoardPost) -> dict:
|
||||
return {
|
||||
"id": post.id,
|
||||
"title": post.title,
|
||||
"category_id": post.category_id,
|
||||
"category_name": post.category.name if post.category else None,
|
||||
"author_id": post.author_id,
|
||||
"author_name": post.author.name if post.author else None,
|
||||
"is_notice": post.is_notice,
|
||||
"is_pinned": post.is_pinned,
|
||||
"view_count": post.view_count,
|
||||
"created_at": post.created_at
|
||||
}
|
||||
|
||||
# 공지사항 분리
|
||||
notices = [p for p in posts if p.is_notice]
|
||||
regular = [p for p in posts if not p.is_notice]
|
||||
|
||||
return BoardPostListResponse(
|
||||
posts=[format_post(p) for p in regular],
|
||||
notices=[format_post(n) for n in notices],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=total_pages
|
||||
)
|
||||
|
||||
|
||||
@router.put("/admin/posts/{post_id}/toggle-notice")
|
||||
def toggle_notice(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""공지사항 토글 (관리자만)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin access required"
|
||||
)
|
||||
|
||||
db_post = db.query(BoardPost).filter(BoardPost.id == post_id).first()
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
db_post.is_notice = not db_post.is_notice
|
||||
db_post.is_pinned = db_post.is_notice # 공지사항이면 자동 고정
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
|
||||
return {"is_notice": db_post.is_notice, "is_pinned": db_post.is_pinned}
|
||||
|
||||
|
||||
@router.put("/admin/posts/{post_id}/toggle-pin")
|
||||
def toggle_pin(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""상단 고정 토글 (관리자만)"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin access required"
|
||||
)
|
||||
|
||||
db_post = db.query(BoardPost).filter(BoardPost.id == post_id).first()
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
db_post.is_pinned = not db_post.is_pinned
|
||||
db.commit()
|
||||
|
||||
return {"is_pinned": db_post.is_pinned}
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from .database import engine, Base, SessionLocal
|
||||
from .api import cars, auth, inquiries, hero_banners, carmodoo, translations, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor, sns_share
|
||||
from .api import cars, auth, inquiries, hero_banners, carmodoo, translations, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor, sns_share, bulletin
|
||||
from .config import get_settings
|
||||
from .services.exchange_rate_service import update_exchange_rates
|
||||
from .services.visitor_service import aggregate_daily_stats, cleanup_old_visitor_logs
|
||||
@@ -238,6 +238,7 @@ app.include_router(exchange_rate.router)
|
||||
app.include_router(verification.router, prefix="/api")
|
||||
app.include_router(visitor.router, prefix="/api")
|
||||
app.include_router(sns_share.router, prefix="/api")
|
||||
app.include_router(bulletin.router, prefix="/api")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
@@ -18,6 +18,7 @@ from .car_specification import CarSpecification
|
||||
from .exchange_rate import ExchangeRate, ExchangeRateHistory
|
||||
from .cc_package import CCPackage, DEFAULT_CC_PACKAGES
|
||||
from .visitor import VisitorLog, VisitorDailyStats, VisitorSession
|
||||
from .bulletin import BoardCategory, BoardPost
|
||||
|
||||
__all__ = [
|
||||
"CarMaker",
|
||||
@@ -63,4 +64,6 @@ __all__ = [
|
||||
"VisitorLog",
|
||||
"VisitorDailyStats",
|
||||
"VisitorSession",
|
||||
"BoardCategory",
|
||||
"BoardPost",
|
||||
]
|
||||
|
||||
59
backend/app/models/bulletin.py
Normal file
59
backend/app/models/bulletin.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Bulletin Board Models - 게시판 모델
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class BoardCategory(Base):
|
||||
"""게시판 카테고리"""
|
||||
__tablename__ = "board_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False) # 카테고리명 (한국어)
|
||||
name_en = Column(String(100), nullable=True) # 영어
|
||||
name_mn = Column(String(100), nullable=True) # 몽골어
|
||||
name_ru = Column(String(100), nullable=True) # 러시아어
|
||||
slug = Column(String(50), unique=True, nullable=False) # URL용 슬러그
|
||||
description = Column(String(255), nullable=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
posts = relationship("BoardPost", back_populates="category")
|
||||
|
||||
|
||||
class BoardPost(Base):
|
||||
"""게시판 글"""
|
||||
__tablename__ = "board_posts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
|
||||
# Category
|
||||
category_id = Column(Integer, ForeignKey("board_categories.id"), nullable=False)
|
||||
|
||||
# Author
|
||||
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Status
|
||||
is_notice = Column(Boolean, default=False) # 공지사항 여부 (관리자만 설정 가능)
|
||||
is_pinned = Column(Boolean, default=False) # 상단 고정 여부
|
||||
is_published = Column(Boolean, default=True) # 게시 여부
|
||||
|
||||
# Stats
|
||||
view_count = Column(Integer, default=0)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
category = relationship("BoardCategory", back_populates="posts")
|
||||
author = relationship("User", foreign_keys=[author_id])
|
||||
@@ -63,6 +63,9 @@ class SystemSettings(Base):
|
||||
withdrawal_enabled = Column(Boolean, default=True) # 출금 기능 활성화
|
||||
min_withdrawal_usd = Column(Float, default=10.0) # 최소 출금 금액 (USD)
|
||||
|
||||
# 게시판 설정
|
||||
board_enabled = Column(Boolean, default=True) # 게시판 상단 메뉴 표시 여부
|
||||
|
||||
# 차량 판매상태 검증 설정
|
||||
car_availability_check_enabled = Column(Boolean, default=True) # 자동 검증 활성화
|
||||
car_availability_check_hour = Column(Integer, default=6) # 검증 시간 (0-23, 기본 새벽 6시)
|
||||
|
||||
125
backend/app/schemas/bulletin.py
Normal file
125
backend/app/schemas/bulletin.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Bulletin Board Schemas - 게시판 스키마
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# ============ Category Schemas ============
|
||||
|
||||
class BoardCategoryBase(BaseModel):
|
||||
name: str
|
||||
name_en: Optional[str] = None
|
||||
name_mn: Optional[str] = None
|
||||
name_ru: Optional[str] = None
|
||||
slug: str
|
||||
description: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class BoardCategoryCreate(BoardCategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class BoardCategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
name_en: Optional[str] = None
|
||||
name_mn: Optional[str] = None
|
||||
name_ru: Optional[str] = None
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class BoardCategoryResponse(BoardCategoryBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
post_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ============ Post Schemas ============
|
||||
|
||||
class BoardPostBase(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
category_id: int
|
||||
|
||||
|
||||
class BoardPostCreate(BoardPostBase):
|
||||
is_notice: bool = False # 관리자만 true 가능
|
||||
|
||||
|
||||
class BoardPostUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
category_id: Optional[int] = None
|
||||
is_notice: Optional[bool] = None
|
||||
is_pinned: Optional[bool] = None
|
||||
is_published: Optional[bool] = None
|
||||
|
||||
|
||||
class AuthorResponse(BaseModel):
|
||||
id: int
|
||||
name: Optional[str] = None
|
||||
email: str
|
||||
is_admin: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BoardPostResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
content: str
|
||||
category_id: int
|
||||
category: Optional[BoardCategoryResponse] = None
|
||||
author_id: int
|
||||
author: Optional[AuthorResponse] = None
|
||||
is_notice: bool
|
||||
is_pinned: bool
|
||||
is_published: bool
|
||||
view_count: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BoardPostListItem(BaseModel):
|
||||
"""목록에서 보여줄 간략한 정보"""
|
||||
id: int
|
||||
title: str
|
||||
category_id: int
|
||||
category_name: Optional[str] = None
|
||||
author_id: int
|
||||
author_name: Optional[str] = None
|
||||
is_notice: bool
|
||||
is_pinned: bool
|
||||
view_count: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BoardPostListResponse(BaseModel):
|
||||
posts: List[BoardPostListItem]
|
||||
notices: List[BoardPostListItem] # 공지사항 (상단 고정)
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class BoardCategoryListResponse(BaseModel):
|
||||
categories: List[BoardCategoryResponse]
|
||||
total: int
|
||||
@@ -32,6 +32,9 @@ class SystemSettingsUpdate(BaseModel):
|
||||
withdrawal_enabled: Optional[bool] = None
|
||||
min_withdrawal_usd: Optional[float] = None
|
||||
|
||||
# 게시판 설정
|
||||
board_enabled: Optional[bool] = None
|
||||
|
||||
# 차량 판매상태 검증 설정
|
||||
car_availability_check_enabled: Optional[bool] = None
|
||||
car_availability_check_hour: Optional[int] = None
|
||||
@@ -67,6 +70,9 @@ class SystemSettingsResponse(BaseModel):
|
||||
withdrawal_enabled: bool = True
|
||||
min_withdrawal_usd: float = 10.0
|
||||
|
||||
# 게시판 설정
|
||||
board_enabled: bool = True
|
||||
|
||||
# 차량 판매상태 검증 설정
|
||||
car_availability_check_enabled: bool = True
|
||||
car_availability_check_hour: int = 6
|
||||
|
||||
Reference in New Issue
Block a user