feat: implement Phase 4 SvelteKit frontend + backend enhancements
Backend: - Add dashboard API (today stats, inbox count, law alerts, pipeline status) - Add /api/documents/tree endpoint for sidebar domain/sub_group tree - Migrate auth to HttpOnly cookie for refresh token (XSS defense) - Add /api/auth/logout endpoint (cookie cleanup) - Register dashboard router in main.py Frontend (SvelteKit + Tailwind CSS v4): - api.ts: fetch wrapper with refresh queue pattern, 401 single retry, forced logout on refresh failure - Auth store: login/logout/refresh with memory-based access token - UI store: toast system, sidebar state - Login page with TOTP support - Dashboard with 4 stat widgets + recent documents - Document list with hybrid search (debounce, URL query state, mode select) - Document detail with format-aware viewer (markdown/PDF/HWP/Synology/fallback) - Metadata panel (AI summary, tags, processing history) - Inbox triage UI (batch select, confirm dialog, domain override) - Settings page (password change, TOTP status) Infrastructure: - Enable frontend service in docker-compose - Caddy path routing (/api/* → fastapi, / → frontend) + gzip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,9 +63,52 @@ class DocumentUpdate(BaseModel):
|
||||
data_origin: str | None = None
|
||||
|
||||
|
||||
# ─── 스키마 (트리) ───
|
||||
|
||||
|
||||
class SubGroupNode(BaseModel):
|
||||
sub_group: str
|
||||
count: int
|
||||
|
||||
|
||||
class DomainNode(BaseModel):
|
||||
domain: str
|
||||
count: int
|
||||
children: list[SubGroupNode]
|
||||
|
||||
|
||||
# ─── 엔드포인트 ───
|
||||
|
||||
|
||||
@router.get("/tree", response_model=list[DomainNode])
|
||||
async def get_document_tree(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
"""도메인/sub_group 트리 (사이드바용)"""
|
||||
from sqlalchemy import text as sql_text
|
||||
|
||||
result = await session.execute(
|
||||
sql_text("""
|
||||
SELECT ai_domain, ai_sub_group, COUNT(*)
|
||||
FROM documents
|
||||
WHERE ai_domain IS NOT NULL
|
||||
GROUP BY ai_domain, ai_sub_group
|
||||
ORDER BY ai_domain, ai_sub_group
|
||||
""")
|
||||
)
|
||||
|
||||
tree: dict[str, DomainNode] = {}
|
||||
for domain, sub_group, count in result:
|
||||
if domain not in tree:
|
||||
tree[domain] = DomainNode(domain=domain, count=0, children=[])
|
||||
tree[domain].count += count
|
||||
if sub_group:
|
||||
tree[domain].children.append(SubGroupNode(sub_group=sub_group, count=count))
|
||||
|
||||
return list(tree.values())
|
||||
|
||||
|
||||
@router.get("/", response_model=DocumentListResponse)
|
||||
async def list_documents(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
|
||||
Reference in New Issue
Block a user