From 8622a97e7d93c9a4d4a98673c3f41fe556ecbf72 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Fri, 17 Apr 2026 08:02:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(upload):=20backend-owned=20upload=20size?= =?UTF-8?q?=20contract=20+=20public=20config=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 업로드 크기 한도를 프론트 하드코딩이 아닌 서버 config 의 단일 진실 공급원 으로 이동. 프론트는 Phase B 후속 커밋에서 이 값을 읽어 pre-check UX 에 사용. - config.yaml 에 `upload` 블록 추가: * max_bytes (authoritative policy) * content_length_slack_ratio (multipart 오버헤드 여유) * stream_chunk_bytes (스트리밍 IO 단위) - app/core/config.py 에 UploadConfig pydantic 모델 + Settings.upload 필드 - app/api/config.py 신규 — GET /api/config/public 엔드포인트 * 민감정보 없는 프론트 필수 설정만 노출 * 범용 서버 설정 공개 창구로 확대 금지 (docstring 명시) - /api/config 를 setup redirect bypass 에 추가 (초기 setup 전에도 조회 가능) 이 커밋 자체는 기존 upload 동작에 영향 없음. 후속 커밋에서 enforcement + 프론트 구독을 연결. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/config.py | 34 ++++++++++++++++++++++++++++++++++ app/core/config.py | 15 +++++++++++++++ app/main.py | 4 +++- config.yaml | 7 +++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 app/api/config.py diff --git a/app/api/config.py b/app/api/config.py new file mode 100644 index 0000000..f270160 --- /dev/null +++ b/app/api/config.py @@ -0,0 +1,34 @@ +"""공개 설정 엔드포인트 + +이 엔드포인트의 scope: +- 민감정보 없는, 프론트 동작에 필수인 최소 공개 설정만 제공. +- 임의의 서버 설정을 프론트에 노출하는 범용 창구가 아님. +- 필드 추가 시 "민감정보 여부 + 프론트 필수 여부" 2가지 기준 통과 필요. +""" + +from fastapi import APIRouter +from pydantic import BaseModel + +from core.config import settings + +router = APIRouter() + + +class UploadPublicConfig(BaseModel): + max_bytes: int + + +class PublicConfigResponse(BaseModel): + upload: UploadPublicConfig + + +@router.get("/public", response_model=PublicConfigResponse) +async def get_public_config() -> PublicConfigResponse: + """프론트가 초기 로드 시 조회하는 공개 설정. + + 현재 제공: upload.max_bytes (업로드 pre-check UX 용도). + slack_ratio, stream_chunk_bytes 등 서버 내부 정책은 노출하지 않음. + """ + return PublicConfigResponse( + upload=UploadPublicConfig(max_bytes=settings.upload.max_bytes), + ) diff --git a/app/core/config.py b/app/core/config.py index 49ba5b8..c9ce914 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -7,6 +7,12 @@ import yaml from pydantic import BaseModel +class UploadConfig(BaseModel): + max_bytes: int = 100_000_000 + content_length_slack_ratio: float = 1.05 + stream_chunk_bytes: int = 1_048_576 + + class AIModelConfig(BaseModel): endpoint: str model: str @@ -55,6 +61,9 @@ class Settings(BaseModel): taxonomy: dict = {} document_types: list[str] = [] + # 업로드 한도 (authoritative policy) + upload: UploadConfig = UploadConfig() + def load_settings() -> Settings: """config.yaml + 환경변수에서 설정 로딩""" @@ -105,6 +114,11 @@ def load_settings() -> Settings: taxonomy = raw.get("taxonomy", {}) if config_path.exists() and raw else {} document_types = raw.get("document_types", []) if config_path.exists() and raw else [] + upload_cfg = ( + UploadConfig(**raw["upload"]) + if config_path.exists() and raw and "upload" in raw + else UploadConfig() + ) return Settings( database_url=database_url, @@ -117,6 +131,7 @@ def load_settings() -> Settings: ocr_endpoint=ocr_endpoint, taxonomy=taxonomy, document_types=document_types, + upload=upload_cfg, ) diff --git a/app/main.py b/app/main.py index 4980042..7273826 100644 --- a/app/main.py +++ b/app/main.py @@ -7,6 +7,7 @@ from fastapi.responses import RedirectResponse from sqlalchemy import func, select, text from api.auth import router as auth_router +from api.config import router as config_router from api.dashboard import router as dashboard_router from api.digest import router as digest_router from api.documents import router as documents_router @@ -87,6 +88,7 @@ app = FastAPI( # ─── 라우터 등록 ─── app.include_router(setup_router, prefix="/api/setup", tags=["setup"]) +app.include_router(config_router, prefix="/api/config", tags=["config"]) app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) app.include_router(documents_router, prefix="/api/documents", tags=["documents"]) app.include_router(search_router, prefix="/api/search", tags=["search"]) @@ -104,7 +106,7 @@ app.include_router(digest_router, prefix="/api/digest", tags=["digest"]) # ─── 셋업 미들웨어: 유저 0명이면 /setup으로 리다이렉트 ─── SETUP_BYPASS_PREFIXES = ( - "/api/setup", "/setup", "/health", "/docs", "/openapi.json", "/redoc", + "/api/setup", "/api/config", "/setup", "/health", "/docs", "/openapi.json", "/redoc", ) diff --git a/config.yaml b/config.yaml index 9e64b24..cc20cbe 100644 --- a/config.yaml +++ b/config.yaml @@ -46,6 +46,13 @@ nas: mount_path: "/documents" pkm_root: "/documents/PKM" +# ─── 업로드 한도 정책 (authoritative) ─── +# 프록시(home-caddy 등) request_body 한도는 max_bytes * content_length_slack_ratio 이상 유지. +upload: + max_bytes: 100000000 # 100 MB (SI). 업로드 실제 제한의 단일 진실 공급원. + content_length_slack_ratio: 1.05 # multipart form 오버헤드(헤더/바운더리) 여유. + stream_chunk_bytes: 1048576 # 1 MiB 단위 스트리밍 read/write. + # ─── 문서 분류 체계 ─── taxonomy: Philosophy: