import calendar from datetime import date from fastapi import APIRouter, Depends, File, Form, Request, Response, UploadFile from fastapi.responses import HTMLResponse, RedirectResponse from pydantic import ValidationError from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.database import get_db from app.models.journal import JournalMood from app.routers.pages import _current_user_or_redirect, templates from app.schemas.journal import JournalCategoryCreate, JournalEntryCreate, JournalEntryUpdate from app.services import journal_service router = APIRouter() # (enum 값, 이모지, 한글 라벨). 기분 선택 pill(작성/수정 폼)과 표시(day-detail)에서 공유해서 쓴다. # 여러 개를 동시에 고를 수 있어서(다중 선택) 라벨은 전부 명사형으로 통일한다. JOURNAL_MOOD_OPTIONS = [ (JournalMood.PAIN, "🤕", "아픔"), (JournalMood.ACHIEVEMENT, "🏆", "성취"), (JournalMood.ANGER, "😠", "분노"), (JournalMood.EXCITED, "🤩", "신남"), (JournalMood.CALM, "😌", "평온"), (JournalMood.HAPPY, "😊", "행복"), (JournalMood.WORRY, "😟", "걱정"), (JournalMood.TIRED, "😪", "피곤"), (JournalMood.SAD, "😢", "슬픔"), ] templates.env.globals["journal_mood_options"] = JOURNAL_MOOD_OPTIONS templates.env.globals["journal_mood_emoji"] = {m.value: emoji for m, emoji, _ in JOURNAL_MOOD_OPTIONS} def _parse_moods(raw: str) -> list[JournalMood]: moods = [] for v in raw.split(","): v = v.strip() if not v: continue try: moods.append(JournalMood(v)) except ValueError: continue return moods def _parse_tags(raw: str) -> list[str]: return [t for t in raw.split(",")] def _month_context(db: Session, user_id: int, year: int, month: int, category_id: int | None) -> dict: summary_map = journal_service.get_monthly_journal_summary(db, user_id, year, month, category_id) weeks = calendar.Calendar(firstweekday=6).monthdatescalendar(year, month) prev_year, prev_month = (year - 1, 12) if month == 1 else (year, month - 1) next_year, next_month = (year + 1, 1) if month == 12 else (year, month + 1) return { "year": year, "month": month, "weeks": weeks, "summary_map": summary_map, "prev_year": prev_year, "prev_month": prev_month, "next_year": next_year, "next_month": next_month, } def _render_day_detail(request: Request, db: Session, user_id: int, entry_date: date, **extra): return templates.TemplateResponse( request, "partials/journal_day_detail.html", { "entry_date": entry_date, "items": journal_service.get_day_entries(db, user_id, entry_date), "categories": journal_service.list_categories(db, user_id), **extra, }, ) @router.get("/journal") def journal_page( request: Request, tab: str = "calendar", year: int | None = None, month: int | None = None, category_id: int | None = None, db: Session = Depends(get_db), ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current journal_service.ensure_default_category(db, current.id) categories = journal_service.list_categories(db, current.id) today = date.today() year = year or today.year month = month or today.month tab = "manage" if tab == "manage" else "calendar" return templates.TemplateResponse( request, "journal.html", { "logged_in": True, "current_user": current, "tab": tab, "categories": categories, "category_id": category_id, "entry_counts": journal_service.count_entries_by_category(db, current.id) if tab == "manage" else {}, "on_this_day": journal_service.get_on_this_day(db, current.id, today), "prompt": journal_service.get_random_prompt(db), "today_iso": today.isoformat(), **_month_context(db, current.id, year, month, category_id), }, ) @router.get("/journal/day/{entry_date}") def journal_day_detail(request: Request, entry_date: date, db: Session = Depends(get_db)): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current return _render_day_detail(request, db, current.id, entry_date) @router.post("/journal/new") def create_entry_page( request: Request, category_id: int = Form(...), entry_date: str = Form(...), title: str | None = Form(None), content: str = Form(""), moods: str = Form(""), tags: str = Form(""), files: list[UploadFile] = File(default=[]), db: Session = Depends(get_db), ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current try: parsed_date = date.fromisoformat(entry_date) data = JournalEntryCreate( category_id=category_id, entry_date=parsed_date, title=title, content=content, moods=_parse_moods(moods), tags=_parse_tags(tags), ) except ValidationError as exc: message = exc.errors()[0]["msg"].removeprefix("Value error, ") return HTMLResponse(message) except ValueError: return HTMLResponse("입력값을 확인해주세요") entry = journal_service.create_entry(db, current.id, data) for upload in files: if not upload.filename: continue try: journal_service.save_attachment(db, entry, upload) except ValueError as exc: return HTMLResponse(str(exc)) response = Response(status_code=200) response.headers["HX-Redirect"] = f"/journal?year={parsed_date.year}&month={parsed_date.month}" return response @router.post("/journal/{entry_id}/edit") def edit_entry_page( request: Request, entry_id: int, category_id: int = Form(...), entry_date: str = Form(...), title: str | None = Form(None), content: str = Form(""), moods: str = Form(""), tags: str = Form(""), files: list[UploadFile] = File(default=[]), db: Session = Depends(get_db), ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current entry = journal_service.get_entry(db, entry_id, current.id) if entry is None: return Response(status_code=404) original_date = entry.entry_date try: parsed_date = date.fromisoformat(entry_date) data = JournalEntryUpdate( category_id=category_id, entry_date=parsed_date, title=title, content=content, moods=_parse_moods(moods), tags=_parse_tags(tags), ) except ValidationError as exc: message = exc.errors()[0]["msg"].removeprefix("Value error, ") return _render_day_detail( request, db, current.id, original_date, edit_error=message, editing_entry_id=entry_id ) except ValueError: return _render_day_detail( request, db, current.id, original_date, edit_error="입력값을 확인해주세요", editing_entry_id=entry_id, ) journal_service.update_entry(db, entry, data) for upload in files: if not upload.filename: continue try: journal_service.save_attachment(db, entry, upload) except ValueError: pass # 첨부 실패는 조용히 넘어간다 — 본문 수정은 이미 반영됐다 return _render_day_detail(request, db, current.id, parsed_date) @router.post("/journal/{entry_id}/delete") def delete_entry_page(request: Request, entry_id: int, db: Session = Depends(get_db)): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current entry = journal_service.get_entry(db, entry_id, current.id) if entry is None: return Response(status_code=404) entry_date_value = entry.entry_date journal_service.delete_entry(db, entry) return _render_day_detail(request, db, current.id, entry_date_value) @router.post("/journal/{entry_id}/attachments/{attachment_id}/delete") def delete_attachment_page( request: Request, entry_id: int, attachment_id: int, db: Session = Depends(get_db) ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current entry = journal_service.get_entry(db, entry_id, current.id) if entry is None: return Response(status_code=404) attachment = journal_service.get_attachment(db, attachment_id, current.id) if attachment is None or attachment.entry_id != entry.id: return Response(status_code=404) entry_date_value = entry.entry_date journal_service.delete_attachment(db, attachment) return _render_day_detail(request, db, current.id, entry_date_value) @router.post("/journal/categories/new") def create_category_page( request: Request, name: str = Form(""), color: str | None = Form(None), db: Session = Depends(get_db) ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current try: data = JournalCategoryCreate(name=name, color=color) except ValidationError as exc: message = exc.errors()[0]["msg"].removeprefix("Value error, ") return HTMLResponse(message) try: journal_service.create_category(db, current.id, data) except IntegrityError: db.rollback() return HTMLResponse("이미 같은 이름의 카테고리가 있어요") response = Response(status_code=200) response.headers["HX-Redirect"] = "/journal?tab=manage" return response @router.post("/journal/categories/{category_id}/edit") def edit_category_page( request: Request, category_id: int, name: str = Form(""), color: str | None = Form(None), db: Session = Depends(get_db), ): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current category = journal_service.get_category(db, category_id, current.id) if category is None: return Response(status_code=404) try: data = JournalCategoryCreate(name=name, color=color) except ValidationError as exc: message = exc.errors()[0]["msg"].removeprefix("Value error, ") return HTMLResponse(message) try: journal_service.update_category(db, category, data) except IntegrityError: db.rollback() return HTMLResponse("이미 같은 이름의 카테고리가 있어요") response = Response(status_code=200) response.headers["HX-Redirect"] = "/journal?tab=manage" return response @router.post("/journal/categories/{category_id}/delete") def delete_category_page(request: Request, category_id: int, db: Session = Depends(get_db)): current = _current_user_or_redirect(request, db) if isinstance(current, RedirectResponse): return current category = journal_service.get_category(db, category_id, current.id) if category is None: return Response(status_code=404) journal_service.delete_category(db, category) response = Response(status_code=200) response.headers["HX-Redirect"] = "/journal?tab=manage" return response