AkJeond commited on
Commit
75ac4cf
·
1 Parent(s): 91b5793

feat: AI 통계 탭 조회 기능 구현

Browse files
Files changed (2) hide show
  1. app/routers/pages.py +65 -16
  2. app/schemas.py +12 -1
app/routers/pages.py CHANGED
@@ -3,11 +3,11 @@ from __future__ import annotations
3
  import asyncio
4
  import os
5
  from pathlib import Path
6
- from typing import List, Optional, Union
7
  from uuid import uuid4
8
 
9
  import cv2
10
- from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
11
  from loguru import logger
12
  from sqlalchemy import func
13
  from sqlalchemy.orm import Session
@@ -34,6 +34,7 @@ DEFAULT_PROJECT_NAME = "temp"
34
  DEFAULT_DOC_TYPE_ID = 1
35
  DEFAULT_ANALYSIS_MODE = AnalysisModeEnum.AUTO
36
  DEFAULT_USER_ID = 1
 
37
 
38
 
39
  def _page_to_response(page: Page) -> schemas.PageResponse:
@@ -209,25 +210,15 @@ async def upload_page(
209
  )
210
  def get_page_detail(
211
  page_id: int,
 
 
212
  db: Session = Depends(get_db),
213
- ) -> Union[schemas.PageResponse, schemas.PageWithElementsResponse]:
214
- if include_layout:
215
- page = crud.get_page_with_elements(db, page_id)
216
- else:
217
- page = crud.get_page(db, page_id)
218
 
219
  if not page:
220
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="페이지를 찾을 수 없습니다.")
221
 
222
- if include_layout:
223
- page_response = schemas.PageWithElementsResponse.model_validate(page)
224
-
225
- if include_text:
226
- version_data = get_current_page_text(db, page_id)
227
- page_response.text_content = version_data.content if version_data else None
228
-
229
- return page_response
230
-
231
  return _page_to_response(page)
232
 
233
 
@@ -299,3 +290,61 @@ def save_page_text(
299
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
300
  detail="페이지 텍스트 저장 중 오류가 발생했습니다.",
301
  ) from error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import asyncio
4
  import os
5
  from pathlib import Path
6
+ from typing import Dict, List, Optional, Union
7
  from uuid import uuid4
8
 
9
  import cv2
10
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
11
  from loguru import logger
12
  from sqlalchemy import func
13
  from sqlalchemy.orm import Session
 
34
  DEFAULT_DOC_TYPE_ID = 1
35
  DEFAULT_ANALYSIS_MODE = AnalysisModeEnum.AUTO
36
  DEFAULT_USER_ID = 1
37
+ ANCHOR_CLASS_NAMES = {"question number", "question type"}
38
 
39
 
40
  def _page_to_response(page: Page) -> schemas.PageResponse:
 
210
  )
211
  def get_page_detail(
212
  page_id: int,
213
+ include_layout: bool = Query(False, description="레이아웃 요소 포함 여부"), # noqa: ARG001
214
+ include_text: bool = Query(False, description="텍스트 콘텐츠 포함 여부"), # noqa: ARG001
215
  db: Session = Depends(get_db),
216
+ ) -> schemas.PageResponse:
217
+ page = crud.get_page(db, page_id)
 
 
 
218
 
219
  if not page:
220
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="페이지를 찾을 수 없습니다.")
221
 
 
 
 
 
 
 
 
 
 
222
  return _page_to_response(page)
223
 
224
 
 
290
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
291
  detail="페이지 텍스트 저장 중 오류가 발생했습니다.",
292
  ) from error
293
+
294
+
295
+ @router.get(
296
+ "/{page_id}/stats",
297
+ response_model=schemas.PageStatsResponse,
298
+ summary="페이지 통계 조회",
299
+ )
300
+ def get_page_stats(
301
+ page_id: int,
302
+ db: Session = Depends(get_db),
303
+ ) -> schemas.PageStatsResponse:
304
+ """
305
+ 페이지별 레이아웃 통계를 계산하여 반환합니다.
306
+
307
+ - 총 레이아웃 요소 수
308
+ - 앵커 요소(question_number, question_type) 수
309
+ - 클래스별 요소 분포
310
+ - 클래스별 평균 신뢰도
311
+ - 처리 시간
312
+ """
313
+ page = crud.get_page_with_elements(db, page_id)
314
+ if not page:
315
+ raise HTTPException(
316
+ status_code=status.HTTP_404_NOT_FOUND,
317
+ detail="페이지를 찾을 수 없습니다.",
318
+ )
319
+
320
+ distribution: Dict[str, int] = {}
321
+ confidence_sums: Dict[str, float] = {}
322
+ confidence_counts: Dict[str, int] = {}
323
+
324
+ for element in page.layout_elements:
325
+ class_name = element.class_name or "unknown"
326
+ distribution[class_name] = distribution.get(class_name, 0) + 1
327
+
328
+ if element.confidence is not None:
329
+ confidence_sums[class_name] = confidence_sums.get(class_name, 0.0) + float(element.confidence)
330
+ confidence_counts[class_name] = confidence_counts.get(class_name, 0) + 1
331
+
332
+ confidence_scores: Dict[str, float] = {}
333
+ for class_name, total in confidence_sums.items():
334
+ count = confidence_counts.get(class_name, 0)
335
+ if count:
336
+ confidence_scores[class_name] = total / count
337
+
338
+ anchor_count = sum(
339
+ 1 for element in page.layout_elements if element.class_name in ANCHOR_CLASS_NAMES
340
+ )
341
+
342
+ return schemas.PageStatsResponse(
343
+ page_id=page.page_id,
344
+ project_id=page.project_id,
345
+ total_elements=len(page.layout_elements),
346
+ anchor_element_count=anchor_count,
347
+ processing_time=page.processing_time,
348
+ class_distribution=distribution,
349
+ confidence_scores=confidence_scores,
350
+ )
app/schemas.py CHANGED
@@ -13,7 +13,7 @@ ERD v2 기준 API 요청/응답 검증을 위한 Pydantic 스키마 정의
13
  """
14
 
15
  from pydantic import BaseModel, EmailStr, Field, ConfigDict
16
- from typing import Optional, List
17
  from datetime import datetime
18
  from enum import Enum
19
 
@@ -564,6 +564,17 @@ class QuestionGroupWithElementsResponse(QuestionGroupResponse):
564
  question_elements: List[QuestionElementResponse] = []
565
 
566
 
 
 
 
 
 
 
 
 
 
 
 
567
  # ============================================================================
568
  # 유틸리티 스키마
569
  # ============================================================================
 
13
  """
14
 
15
  from pydantic import BaseModel, EmailStr, Field, ConfigDict
16
+ from typing import Optional, List, Dict
17
  from datetime import datetime
18
  from enum import Enum
19
 
 
564
  question_elements: List[QuestionElementResponse] = []
565
 
566
 
567
+ class PageStatsResponse(BaseModel):
568
+ """페이지 통계 응답"""
569
+ page_id: int = Field(..., description="페이지 ID")
570
+ project_id: int = Field(..., description="프로젝트 ID")
571
+ total_elements: int = Field(..., ge=0, description="총 레이아웃 요소 개수")
572
+ anchor_element_count: int = Field(..., ge=0, description="앵커 요소(문제 번호/유형) 개수")
573
+ processing_time: Optional[float] = Field(None, ge=0, description="처리 시간(초)")
574
+ class_distribution: Dict[str, int] = Field(default_factory=dict, description="클래스별 요소 개수 분포")
575
+ confidence_scores: Dict[str, float] = Field(default_factory=dict, description="클래스별 평균 신뢰도 (0~1)")
576
+
577
+
578
  # ============================================================================
579
  # 유틸리티 스키마
580
  # ============================================================================