feat: 풀스택 할일관리 앱 구현 (통합 모달 + 간트차트)

- 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>
This commit is contained in:
jungwoo choi
2026-02-12 15:45:03 +09:00
parent b54811ad8d
commit 074b5133bf
81 changed files with 17027 additions and 19 deletions

98
backend/app/database.py Normal file
View File

@ -0,0 +1,98 @@
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")