전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -8,7 +8,7 @@ import re
import uuid
from typing import Annotated, Any, Literal
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi import APIRouter, Depends, File, Form, HTTPException, Response, UploadFile, status
from ..db import acquire
from ..deps import CurrentPrincipal, Principal, Role, require_role
@ -52,6 +52,7 @@ from ..persona_read_model import (
from ..engine_client import EngineError, EngineMessage, GenerateRequest, engine_client
from ..services import rag
from ..services.guardrail import mask_pii
from ..services.tabular_ingest import TabularIngestError, extract_tabular_text
router = APIRouter(prefix="/personas", tags=["personas"])
TeacherOrAdmin = Annotated[Principal, Depends(require_role(Role.TEACHER, Role.ADMIN))]
@ -555,6 +556,42 @@ async def create_persona_source_route(
return await _register_persona_source_document(request, principal)
@router.post(
"/sources/upload",
response_model=PersonaSourceDocumentResponse,
status_code=status.HTTP_201_CREATED,
)
async def upload_persona_source_route(
principal: TeacherOrAdmin,
file: UploadFile = File(...),
source_kind: PersonaSourceKind = Form("mixed_notes"),
title: str | None = Form(None),
source_note: str = Form(""),
) -> PersonaSourceDocumentResponse:
"""자유 양식 엑셀/CSV 업로드 → 텍스트 변환 → 기존 source 등록 경로 재사용 (P4).
업로드 원본 바이트는 핸들러 메모리에서만 파싱하고 저장하지 않는다(원본 파기).
파생 텍스트만 기존 마스킹·hash-only 증거·sanitized chunk 경로로 등록된다.
"""
_ensure_teacher_or_admin(principal)
data = await file.read()
try:
text = extract_tabular_text(filename=file.filename or "", data=data)
except TabularIngestError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
finally:
# 원본 바이트 참조를 즉시 놓는다 — 파생 텍스트 외에는 남기지 않는다.
del data
request = PersonaSourceDocumentRequest(
filename=(file.filename or "uploaded-table.xlsx")[:240],
source_kind=source_kind,
text=text,
title=(title or None),
source_note=source_note[:800],
)
return await _register_persona_source_document(request, principal)
@router.post("/drafts/generate", response_model=PersonaDraftGenerateResponse)
async def generate_persona_draft_route(
request: PersonaDraftGenerateRequest,