"""자료실 경로 유틸. 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