deb5c1b704
목적성 문서(양식, 템플릿, 연간보고서)를 @library/ 태그로 분류하고 트리 구조로 탐색하는 자료실 페이지 추가. 백엔드: 경로 정규화 유틸, library-tree/library 엔드포인트, 다운로드 Content-Disposition 개선(원본/PDF 분리, 한글 filename*) 프론트: /library 페이지, LibraryPathEditor, 상단 nav/사이드바 링크 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
"""자료실 경로 유틸.
|
|
|
|
user_tags 내 @library/ 접두사 태그를 정규화·검증·추출한다.
|
|
"""
|
|
|
|
LIBRARY_PREFIX = "@library/"
|
|
MAX_DEPTH = 5
|
|
MAX_SEGMENT_LEN = 30
|
|
|
|
|
|
def normalize_library_path(raw: str) -> str:
|
|
"""경로 정규화. 엄격 정책 — 규칙 위반 시 ValueError 즉시 raise.
|
|
|
|
규칙:
|
|
- 앞뒤 공백·슬래시 제거
|
|
- segment별 trim
|
|
- 빈 segment(// 또는 공백만) → ValueError
|
|
- segment 30자 초과 → ValueError
|
|
- 5단계 초과 → ValueError
|
|
|
|
GET /documents/library?path= 쿼리에도 동일하게 적용.
|
|
"""
|
|
stripped = raw.strip().strip("/")
|
|
if not stripped:
|
|
raise ValueError("빈 경로")
|
|
|
|
segments = stripped.split("/")
|
|
normalized: list[str] = []
|
|
for s in segments:
|
|
s = s.strip()
|
|
if not s:
|
|
raise ValueError("빈 세그먼트 (// 또는 공백만 있는 구간)")
|
|
if len(s) > MAX_SEGMENT_LEN:
|
|
raise ValueError(f"세그먼트 '{s}'가 {MAX_SEGMENT_LEN}자 초과")
|
|
normalized.append(s)
|
|
|
|
if len(normalized) > MAX_DEPTH:
|
|
raise ValueError(f"최대 {MAX_DEPTH}단계까지 가능")
|
|
|
|
return "/".join(normalized)
|
|
|
|
|
|
def extract_library_paths(user_tags: list[str] | None) -> list[str]:
|
|
"""user_tags에서 @library/ 경로만 추출 (prefix 포함)."""
|
|
if not user_tags:
|
|
return []
|
|
return [t for t in user_tags if t.startswith(LIBRARY_PREFIX)]
|
|
|
|
|
|
def validate_user_tags(tags: list) -> list[str]:
|
|
"""user_tags 전체 검증. 입력 순서 보존, 중복 제거.
|
|
|
|
- 문자열이 아닌 원소 → TypeError
|
|
- 빈 문자열 / 공백만 있는 태그 → 제거
|
|
- 일반 태그 → strip() 후 통과
|
|
- @library/ 태그 → normalize_library_path() 적용
|
|
- 중복 → 첫 출현만 유지 (입력 순서 보존)
|
|
"""
|
|
result: list[str] = []
|
|
for tag in tags:
|
|
if not isinstance(tag, str):
|
|
raise TypeError(f"태그는 문자열이어야 합니다: {tag!r}")
|
|
tag = tag.strip()
|
|
if not tag:
|
|
continue
|
|
if tag.startswith(LIBRARY_PREFIX):
|
|
path = tag[len(LIBRARY_PREFIX):]
|
|
normalized = normalize_library_path(path)
|
|
tag = f"{LIBRARY_PREFIX}{normalized}"
|
|
result.append(tag)
|
|
|
|
# 중복 제거 (입력 순서 보존)
|
|
seen: set[str] = set()
|
|
deduped: list[str] = []
|
|
for t in result:
|
|
if t not in seen:
|
|
seen.add(t)
|
|
deduped.append(t)
|
|
return deduped
|