- Backend: FastAPI + MongoDB + Redis (카테고리, 할일 CRUD, 파일 첨부, 검색, 대시보드) - Frontend: Next.js 15 + Tailwind + React Query + Zustand - 통합 TodoModal: 생성/수정 모달 통합, 탭 구조 (기본/태그와 첨부) - 간트차트: 카테고리별 할일 타임라인 시각화 - TodoCard: 제목/카테고리/우선순위/태그/첨부 한줄 표시 - Docker Compose 배포 (Frontend:3010, Backend:8010, MongoDB:27021, Redis:6391) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
2.6 KiB
Python
99 lines
2.6 KiB
Python
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
import redis.asyncio as aioredis
|
|
from app.config import get_settings
|
|
|
|
|
|
class Database:
|
|
client: AsyncIOMotorClient | None = None
|
|
db: AsyncIOMotorDatabase | None = None
|
|
redis: aioredis.Redis | None = None
|
|
|
|
|
|
db = Database()
|
|
|
|
|
|
def get_database() -> AsyncIOMotorDatabase:
|
|
"""MongoDB 데이터베이스 인스턴스를 반환한다.
|
|
주의: `if not db.db:` 사용 금지 (pymongo 4.9.x NotImplementedError)
|
|
"""
|
|
if db.db is None:
|
|
raise RuntimeError("Database not initialized")
|
|
return db.db
|
|
|
|
|
|
def get_redis() -> aioredis.Redis:
|
|
"""Redis 클라이언트 인스턴스를 반환한다."""
|
|
if db.redis is None:
|
|
raise RuntimeError("Redis not initialized")
|
|
return db.redis
|
|
|
|
|
|
async def connect_db():
|
|
"""MongoDB + Redis 연결 초기화"""
|
|
settings = get_settings()
|
|
|
|
# MongoDB
|
|
db.client = AsyncIOMotorClient(settings.mongodb_url)
|
|
db.db = db.client[settings.mongodb_database]
|
|
|
|
# 인덱스 생성
|
|
await create_indexes(db.db)
|
|
|
|
# Redis
|
|
db.redis = aioredis.from_url(
|
|
settings.redis_url,
|
|
encoding="utf-8",
|
|
decode_responses=True,
|
|
)
|
|
|
|
|
|
async def disconnect_db():
|
|
"""연결 종료"""
|
|
if db.client is not None:
|
|
db.client.close()
|
|
if db.redis is not None:
|
|
await db.redis.close()
|
|
|
|
|
|
async def create_indexes(database: AsyncIOMotorDatabase):
|
|
"""컬렉션 인덱스 생성"""
|
|
todos = database["todos"]
|
|
categories = database["categories"]
|
|
|
|
# todos 텍스트 검색 인덱스
|
|
await todos.create_index(
|
|
[("title", "text"), ("content", "text"), ("tags", "text")],
|
|
name="text_search_index",
|
|
weights={"title": 10, "content": 5, "tags": 3},
|
|
)
|
|
|
|
# todos 복합 인덱스
|
|
await todos.create_index(
|
|
[("category_id", 1), ("created_at", -1)],
|
|
name="category_created",
|
|
)
|
|
await todos.create_index(
|
|
[("completed", 1), ("created_at", -1)],
|
|
name="completed_created",
|
|
)
|
|
await todos.create_index(
|
|
[("priority", 1), ("created_at", -1)],
|
|
name="priority_created",
|
|
)
|
|
await todos.create_index(
|
|
[("tags", 1)],
|
|
name="tags",
|
|
)
|
|
await todos.create_index(
|
|
[("due_date", 1)],
|
|
name="due_date",
|
|
)
|
|
await todos.create_index(
|
|
[("completed", 1), ("due_date", 1)],
|
|
name="completed_due_date",
|
|
)
|
|
|
|
# categories 인덱스
|
|
await categories.create_index("name", unique=True, name="category_name_unique")
|
|
await categories.create_index("order", name="category_order")
|