feat: 사이드바 3단계 재귀 트리 + 너비 확장 (320px)
- tree API: domain 경로를 파싱하여 계층 구조로 반환 (Industrial_Safety → Practice → Patrol_Inspection) - Sidebar: 재귀 snippet으로 N단계 트리 렌더링 - domain 필터: prefix 매칭 (상위 클릭 시 하위 전부 포함) - 사이드바 너비: 260px → 320px Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,47 +78,59 @@ class DocumentUpdate(BaseModel):
|
||||
# ─── 스키마 (트리) ───
|
||||
|
||||
|
||||
class SubGroupNode(BaseModel):
|
||||
sub_group: str
|
||||
class TreeNode(BaseModel):
|
||||
name: str
|
||||
path: str
|
||||
count: int
|
||||
|
||||
|
||||
class DomainNode(BaseModel):
|
||||
domain: str
|
||||
count: int
|
||||
children: list[SubGroupNode]
|
||||
children: list["TreeNode"]
|
||||
|
||||
|
||||
# ─── 엔드포인트 ───
|
||||
|
||||
|
||||
@router.get("/tree", response_model=list[DomainNode])
|
||||
@router.get("/tree")
|
||||
async def get_document_tree(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
"""도메인/sub_group 트리 (사이드바용)"""
|
||||
"""도메인 트리 (3단계 경로 파싱, 사이드바용)"""
|
||||
from sqlalchemy import text as sql_text
|
||||
|
||||
result = await session.execute(
|
||||
sql_text("""
|
||||
SELECT ai_domain, ai_sub_group, COUNT(*)
|
||||
SELECT ai_domain, COUNT(*)
|
||||
FROM documents
|
||||
WHERE ai_domain IS NOT NULL
|
||||
GROUP BY ai_domain, ai_sub_group
|
||||
ORDER BY ai_domain, ai_sub_group
|
||||
WHERE ai_domain IS NOT NULL AND ai_domain != ''
|
||||
GROUP BY ai_domain
|
||||
ORDER BY ai_domain
|
||||
""")
|
||||
)
|
||||
|
||||
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))
|
||||
# 경로를 트리로 파싱
|
||||
root: dict = {}
|
||||
for domain_path, count in result:
|
||||
parts = domain_path.split("/")
|
||||
node = root
|
||||
for part in parts:
|
||||
if part not in node:
|
||||
node[part] = {"_count": 0, "_children": {}}
|
||||
node[part]["_count"] += count
|
||||
node = node[part]["_children"]
|
||||
|
||||
return list(tree.values())
|
||||
def build_tree(d: dict, prefix: str = "") -> list[dict]:
|
||||
nodes = []
|
||||
for name, data in sorted(d.items()):
|
||||
path = f"{prefix}/{name}" if prefix else name
|
||||
children = build_tree(data["_children"], path)
|
||||
nodes.append({
|
||||
"name": name,
|
||||
"path": path,
|
||||
"count": data["_count"],
|
||||
"children": children,
|
||||
})
|
||||
return nodes
|
||||
|
||||
return build_tree(root)
|
||||
|
||||
|
||||
@router.get("/", response_model=DocumentListResponse)
|
||||
@@ -136,9 +148,8 @@ async def list_documents(
|
||||
query = select(Document)
|
||||
|
||||
if domain:
|
||||
query = query.where(Document.ai_domain == domain)
|
||||
if sub_group:
|
||||
query = query.where(Document.ai_sub_group == sub_group)
|
||||
# prefix 매칭: Industrial_Safety 클릭 시 하위 전부 포함
|
||||
query = query.where(Document.ai_domain.startswith(domain))
|
||||
if source:
|
||||
query = query.where(Document.source_channel == source)
|
||||
if format:
|
||||
|
||||
Reference in New Issue
Block a user