From 0f01452c8a09ea8720eda08b0d27b480dedbd48c Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Tue, 12 May 2026 10:16:16 +0900 Subject: [PATCH 1/6] Bind refactory --- App/routes/image.py | 3 ++- App/routes/video.py | 3 ++- App/services/image_svc.py | 8 +++----- App/services/video_svc.py | 5 ++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/App/routes/image.py b/App/routes/image.py index 94c63d2..08605ce 100644 --- a/App/routes/image.py +++ b/App/routes/image.py @@ -51,8 +51,9 @@ async def delete_image_history( if not history: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="해당 이미지 기록을 찾을 수 없습니다.") + # DB 레코드 삭제 await image_svc.delete_image_db(conn, image_id) - + # 실제 이미지 삭제 await image_svc.delete_image(history.image_loc) return { diff --git a/App/routes/video.py b/App/routes/video.py index 5ccb134..239b925 100644 --- a/App/routes/video.py +++ b/App/routes/video.py @@ -56,8 +56,9 @@ async def delete_video_history( if not history: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="해당 비디오 기록을 찾을 수 없습니다.") + # DB삭제 await video_svc.delete_video_db(conn, video_id) - + #실제 비디오 삭제 await video_svc.delete_video(history.video_loc) return { diff --git a/App/services/image_svc.py b/App/services/image_svc.py index 9a92d68..93e8f89 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -96,8 +96,7 @@ async def get_user_histories(conn: Connection, user_id: int): ORDER BY created_at DESC; """) stmt = text(query) - bind_stmt = stmt.bindparams(user_id = user_id) - result = await conn.execute(bind_stmt) + result = await conn.execute(stmt, {"user_id": user_id}) user_histories = [UserHistory( image_id = row.id, @@ -133,8 +132,7 @@ async def get_user_history(conn: Connection, image_id: int): WHERE id = :image_id; """ stmt = text(query) - bind_stmt = stmt.bindparams(image_id=image_id) - result = await conn.execute(bind_stmt) + result = await conn.execute(stmt, {"image_id": image_id}) row = result.fetchone() if row is None: @@ -193,7 +191,7 @@ async def _delete(): async def delete_image_db(conn: Connection, image_id: int): try: delete_query = text("DELETE FROM image_result WHERE id = :image_id") - result = await conn.execute(delete_query.bindparams(image_id=image_id)) + result = await conn.execute(delete_query, {"image_id": image_id}) if result.rowcount == 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 이미지 id {id}는(은) 존재하지 않아 삭제할 수 없습니다.") diff --git a/App/services/video_svc.py b/App/services/video_svc.py index cdf5ac4..59ca068 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -95,8 +95,7 @@ async def get_user_histories(conn: Connection, user_id: int): ORDER BY created_at DESC; """ stmt = text(query) - bind_stmt = stmt.bindparams(user_id=user_id) - result = await conn.execute(bind_stmt) + result = await conn.execute(stmt, {"user_id": user_id}) video_histories = [VideoData( id = row.id, @@ -185,7 +184,7 @@ async def _delete(): async def delete_video_db(conn: Connection, video_id: int): try: delete_query = text("DELETE FROM video_result WHERE id = :video_id") - result = await conn.execute(delete_query.bindparams(video_id=video_id)) + result = await conn.execute(delete_query, {"video_id": video_id}) if result.rowcount == 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 비디오 id {id}는(은) 존재하지 않아 삭제할 수 없습니다.") From 31be8c14fb5169d0aa0fd2afc37de167e5a31148 Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Thu, 14 May 2026 18:30:22 +0900 Subject: [PATCH 2/6] Annotation --- App/routes/image.py | 6 +++--- App/routes/video.py | 6 +++--- App/services/video_svc.py | 10 ++++++++- App/sql/initial_video_frame_result.sql | 14 ++++++------- App/sql/initial_video_meta_result.sql | 14 ++++++------- App/sql/initial_video_result.sql | 28 +++++++++++++++++--------- 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/App/routes/image.py b/App/routes/image.py index 08605ce..df7214b 100644 --- a/App/routes/image.py +++ b/App/routes/image.py @@ -6,7 +6,7 @@ router = APIRouter(prefix="/image", tags=["image"]) # 사용자 전체 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK) +@router.get("/history", status_code=status.HTTP_200_OK, summary="이미지 히스토리 전체 조회") async def get_user_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -22,7 +22,7 @@ async def get_user_histories( } # 특정 이미지의 상세 내역 조회 (개별 히스토리) -@router.get("/history/{image_id}", status_code=status.HTTP_200_OK) +@router.get("/history/{image_id}", status_code=status.HTTP_200_OK, summary="이미지 개별 상세 조회") async def get_user_history( image_id: int, conn: Connection = Depends(context_get_conn), @@ -40,7 +40,7 @@ async def get_user_history( } # 이미지 히스토리 삭제 -@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK, summary="버튼 삭제") +@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK, summary="이미지 버튼 히스토리 삭제") async def delete_image_history( image_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/routes/video.py b/App/routes/video.py index 239b925..d8b48c1 100644 --- a/App/routes/video.py +++ b/App/routes/video.py @@ -6,7 +6,7 @@ router = APIRouter(prefix="/video", tags=["video"]) # 사용자 전체 비디오 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK) +@router.get("/history", status_code=status.HTTP_200_OK, summary="비디오 히스토리 전체 조회") async def get_video_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -22,7 +22,7 @@ async def get_video_histories( } # 특정 비디오의 상세 내역 조회 -@router.get("/history/{video_id}", status_code=status.HTTP_200_OK) +@router.get("/history/{video_id}", status_code=status.HTTP_200_OK, summary="비디오 개별 상세 조회") async def get_video_history( video_id: int, conn: Connection = Depends(context_get_conn), @@ -43,7 +43,7 @@ async def get_video_history( } # 비디오 히스토리 삭제 -@router.delete("/history/{video_id}", status_code=status.HTTP_200_OK, summary="버튼 삭제") +@router.delete("/history/{video_id}", status_code=status.HTTP_200_OK, summary="비디오 버튼 히스토리 삭제") async def delete_video_history( video_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/services/video_svc.py b/App/services/video_svc.py index 59ca068..e193a35 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -324,6 +324,8 @@ async def get_video_result(conn: Connection, detail="알수없는 이유로 문제가 발생하였습니다.") # 비디오 메타 데이터 정보 저장하기 +# 비디오 1개에 대한 프레임 요약 통계를 video_meta_result 테이블에 삽입 +# 호출 위치: services/inference_svc.py - process_video_task() → run_inference() async def save_video_meta_result(conn: Connection, video_id: int, analysis: dict): try: query = """ @@ -346,6 +348,8 @@ async def save_video_meta_result(conn: Connection, video_id: int, analysis: dict # 비디오 상세 결과 값 저장하기 # 비디오 프레임 별 딥페이크 점수, 얼굴 신뢰도, 얼굴 비율, 얼굴 조도 반환 +# 비디오 1개에 속한 여러 프레임의 분석 결과를 한 번에 INSERT. +# 호출 위치: services/inference_svc.py - process_video_task() → run_inference() async def save_video_frame_results(conn: Connection, video_id: int, frame_results: list): try: query = """ @@ -374,6 +378,8 @@ async def save_video_frame_results(conn: Connection, video_id: int, frame_result raise e # 비디오 메타 데이터 정보 가져오기 +# 호출 위치: routers/inference.py - get_video_detail() +# 초당프레임수, 영상 전체 프레임 수, 샘플링한 프레임 수, 얼굴 추출에 성공한 프레임 수, score산출 성공 프레임 수 async def get_video_meta_result(conn: Connection, video_id: int): try: query = text(""" @@ -405,7 +411,9 @@ async def get_video_meta_result(conn: Connection, video_id: int): print(e) raise HTTPException(status_code=500, detail="알수없는 이유로 문제가 발생하였습니다.") -# 비디오 상세 결과 값 가져오기 +# 비디오 프레임별 상세 결과 조회 +# 호출 위치: routers/inference.py - get_video_detail() +# video_id에 속한 모든 프레임 결과를 frame_index 반환 하여, 프레임별 점수 그래프, 의심 구간 표시 등 시각화에 활용. async def get_video_frame_results(conn: Connection, video_id: int): try: query = text(""" diff --git a/App/sql/initial_video_frame_result.sql b/App/sql/initial_video_frame_result.sql index c946b5d..d280613 100644 --- a/App/sql/initial_video_frame_result.sql +++ b/App/sql/initial_video_frame_result.sql @@ -1,14 +1,14 @@ drop table if exists video_frame_result; create table deepfake_db.video_frame_result ( - id integer auto_increment primary key, - video_id integer not null, - frame_index integer not null, + id integer auto_increment primary key, -- 행 자체의 고유 ID (대리키) + video_id integer not null, + frame_index integer not null, -- 영상 내 프레임 순번. frame_time float not null, -- 영상 내 타임스탬프(초) - score float not null, - face_conf float not null, - face_ratio float not null, - face_brightness float not null, + score float not null,-- 해당 프레임의 딥페이크 의심 점수 (0.0~1.0) + face_conf float not null,-- 얼굴 검출 신뢰도. 낮으면 score 신뢰도도 낮음. + face_ratio float not null,-- 프레임 대비 얼굴 면적 비율 (0.0~1.0) + face_brightness float not null,-- 얼굴 영역 평균 밝기 (조도 품질 지표) index video_id_idx(video_id) ); \ No newline at end of file diff --git a/App/sql/initial_video_meta_result.sql b/App/sql/initial_video_meta_result.sql index 80e95da..289f4d6 100644 --- a/App/sql/initial_video_meta_result.sql +++ b/App/sql/initial_video_meta_result.sql @@ -1,13 +1,13 @@ drop table if exists video_meta_result; create table deepfake_db.video_meta_result ( - id integer auto_increment primary key, + id integer auto_increment primary key,-- 행 자체의 고유 ID (대리키). video_id integer not null unique, - fps float not null, - total_frames integer not null, - num_sampled integer not null, - num_extracted integer not null, - num_detected integer not null, + fps float not null, -- 초당 프레임수 + total_frames integer not null,-- 영상 전체 프레임 수 + num_sampled integer not null,-- [1단계] 분석을 위해 샘플링한 프레임 수 + num_extracted integer not null,-- [2단계] 샘플 중 얼굴 추출에 성공한 프레임 수 + num_detected integer not null,-- [3단계] 추출된 얼굴 중 딥페이크 score 산출에 성공한 프레임 수 - index video_id_idx(video_id) + index video_id_idx(video_id)-- video_id로 메타 조회 시 사용 ); \ No newline at end of file diff --git a/App/sql/initial_video_result.sql b/App/sql/initial_video_result.sql index cc683a9..9daa890 100644 --- a/App/sql/initial_video_result.sql +++ b/App/sql/initial_video_result.sql @@ -1,20 +1,30 @@ drop table if exists video_result; +-- 딥페이크 분석 결과를 저장하는 테이블 생성 create table deepfake_db.video_result ( - id integer auto_increment primary key, - user_id integer null, -- null 허용 - video_loc varchar(300) not null unique, - status varchar(20) not null default "PENDING", - label varchar(10) not null, - score float not null, + id integer auto_increment primary key, -- 시스템에서 부여하는 데이터 고유 식별 번호 + user_id integer null, -- 영상을 업로드한 사용자의 ID (비회원 허용을 위해 null 가능) + video_loc varchar(300) not null unique, -- 영상 파일이 저장된 경로/URL (중복 등록 방지) + + -- 상태 및 결과 정보 + status varchar(20) not null default "PENDING", -- 분석 진행 상태 (대기중, 분석중, 완료, 실패 등) + label varchar(10) not null, -- 최종 판정 결과 (예: 'Fake', 'Real') + score float not null, + + -- 분석 품질 및 메타데이터 face_conf float not null, face_ratio float not null, face_brightness float not null, + + -- 분석 환경 정보 version_type varchar(10) not null, model_type varchar(10) not null, domain_type varchar(20) not null, - result_msg varchar(200) not null, - created_at timestamp default current_timestamp, - index user_id_idx(user_id) + -- 기타 기록 + result_msg varchar(200) not null, -- 분석 결과에 대한 상세 메시지나 오류 내용 + created_at timestamp default current_timestamp, -- 데이터가 생성된(분석 요청된) 시간 + + -- 성능 최적화 + index user_id_idx(user_id) -- 특정 사용자의 분석 이력을 빠르게 조회하기 위한 인덱스 ); \ No newline at end of file From 05a894dd4f4b3305350a52a6f1b0d5dee425d97c Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Thu, 14 May 2026 18:44:22 +0900 Subject: [PATCH 3/6] Add Summary info in routes folder --- App/routes/auth.py | 12 +++++------- App/routes/home.py | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/App/routes/auth.py b/App/routes/auth.py index b640953..80f3872 100644 --- a/App/routes/auth.py +++ b/App/routes/auth.py @@ -7,14 +7,13 @@ router = APIRouter(prefix="/auth", tags=["auth"]) -# --- 로그인 상태 확인 API --- # 프론트엔드 새로고침 시 세션 유지 여부를 확인하고 유저 정보를 반환합니다 -@router.get("/check", status_code=status.HTTP_200_OK) +@router.get("/check", status_code=status.HTTP_200_OK, summary= "로그인 상태 확인 API") async def check_session(session_user = Depends(session_svc.get_session_user_opt)): return {"user": session_user} -# --- 회원가입 API --- -@router.post("/register", status_code=status.HTTP_201_CREATED) + +@router.post("/register", status_code=status.HTTP_201_CREATED, summary="회원가입 API") async def register_user(request: Request, user_info: RegisterRequest, conn: Connection = Depends(context_get_conn)): @@ -40,8 +39,7 @@ async def register_user(request: Request, return {"message": "회원가입이 성공적으로 완료되었습니다.", "status": "success"} -# --- 로그인 API --- -@router.post("/login", status_code=status.HTTP_200_OK) +@router.post("/login", status_code=status.HTTP_200_OK,summary="로그인 API") async def login_user(request: Request, login_info: LoginRequest, conn: Connection = Depends(context_get_conn)): @@ -73,7 +71,7 @@ async def login_user(request: Request, "status": "success" } -@router.get("/logout", status_code=status.HTTP_200_OK) +@router.get("/logout", status_code=status.HTTP_200_OK, summary="로그아웃") async def logout_user(request: Request): request.state.session.clear() return { diff --git a/App/routes/home.py b/App/routes/home.py index 6a2a87f..17b1e3a 100644 --- a/App/routes/home.py +++ b/App/routes/home.py @@ -1,9 +1,9 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, status from services import session_svc router = APIRouter(prefix="/home", tags=["home"]) -@router.get("/") +@router.get("/", status_code=status.HTTP_200_OK, summary="세션 유저 정보 가져오기") async def home_ui(session_user = Depends(session_svc.get_session_user_opt)): return { "session_user": session_user From 40308b90b9e3c53e83cc38ee83c211bf6e933796 Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Thu, 14 May 2026 20:31:07 +0900 Subject: [PATCH 4/6] Add delete_video_frame_result and delete_video_meta_result --- App/routes/video.py | 13 +++-- App/services/inference_svc.py | 5 +- App/services/video_svc.py | 99 +++++++++++++++++++++++++++++++---- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/App/routes/video.py b/App/routes/video.py index d8b48c1..e303616 100644 --- a/App/routes/video.py +++ b/App/routes/video.py @@ -1,4 +1,6 @@ -from fastapi import APIRouter, Depends, status, HTTPException +from fastapi import APIRouter, Depends, status +from fastapi.exceptions import HTTPException +from fastapi.responses import JSONResponse from services import session_svc, video_svc from sqlalchemy import Connection from db.database import context_get_conn @@ -43,7 +45,8 @@ async def get_video_history( } # 비디오 히스토리 삭제 -@router.delete("/history/{video_id}", status_code=status.HTTP_200_OK, summary="비디오 버튼 히스토리 삭제") +@router.delete("/history/{video_id}", status_code=status.HTTP_200_OK, + response_class=JSONResponse, summary="비디오 버튼 히스토리 삭제") async def delete_video_history( video_id: int, conn: Connection = Depends(context_get_conn), @@ -56,9 +59,11 @@ async def delete_video_history( if not history: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="해당 비디오 기록을 찾을 수 없습니다.") - # DB삭제 + if history.status == "SUCCESS": + await video_svc.delete_video_meta_result(conn, video_id) + await video_svc.delete_video_frame_result(conn, video_id) + await video_svc.delete_video_db(conn, video_id) - #실제 비디오 삭제 await video_svc.delete_video(history.video_loc) return { diff --git a/App/services/inference_svc.py b/App/services/inference_svc.py index 3ca92d6..7c514a8 100644 --- a/App/services/inference_svc.py +++ b/App/services/inference_svc.py @@ -207,7 +207,7 @@ async def run_inference(): if result["analysis"].get("frame_results"): try: async with celery_db_conn() as conn: - await video_svc.save_video_frame_results( + await video_svc.save_video_frame_result( conn, video_id, result["analysis"]["frame_results"] ) except Exception as e: @@ -232,7 +232,8 @@ async def run_inference(): finally: if not user_id: - video_svc.cleanup_anonymous_video.apply_async(args=[video_id, video_loc], countdown=60) + is_success = result.get("status") == "success" + video_svc.cleanup_anonymous_video.apply_async(args=[video_id, video_loc, is_success], countdown=60) diff --git a/App/services/video_svc.py b/App/services/video_svc.py index e193a35..00d4c44 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -163,20 +163,42 @@ async def get_user_history(conn: Connection, user_id: int, video_id: int): print(e) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") -# 비회원 데이터 5분 후 자동 삭제 태스크 +# 비회원 데이터 1분 후 자동 삭제 태스크 @celery_app.task(name="cleanup_anonymous_video") -def cleanup_anonymous_video(video_id: int, video_loc: str): +def cleanup_anonymous_video(video_id: int, video_loc: str, is_success: bool): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: async def _delete(): + success = True async with celery_db_conn() as conn: - await delete_video_db(conn, video_id) - await delete_video(video_loc) - print(f"[Cleanup] 비회원 비디오 삭제 완료 - video_id: {video_id}, video_loc: {video_loc}") + if is_success: + try: + await delete_video_meta_result(conn, video_id) + except Exception as e : + print(f"[Cleanup] frame 삭제 실패 - video_id: {video_id}, error: {e}") + success=False + + try: + await delete_video_frame_result(conn, video_id) + except Exception as e : + print (f"[Cleanup] meta 삭제 실패 - video_id: {video_id}, error: {e}") + success=False + try: + await delete_video_db(conn, video_id) + except Exception as e: + print(f"[Cleanup] video_result 삭제 실패 - video_id: {video_id}, error:{e}") + success=False + try: + await delete_video(video_loc) + except Exception as e: + print(f"[Cleanup] 파일 삭제 실패 - video_loc: {video_loc}, error: {e}") + success=False + + if success: + print(f"[Cleanup] 비회원 비디오 삭제 완료 - video_id: {video_id}, video_loc: {video_loc}") + loop.run_until_complete(_delete()) - except Exception as e: - print(f"[Cleanup Error] 비회원 비디오 삭제 실패 - video_id: {video_id}, error: {e}") finally: loop.close() @@ -187,7 +209,7 @@ async def delete_video_db(conn: Connection, video_id: int): result = await conn.execute(delete_query, {"video_id": video_id}) if result.rowcount == 0: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 비디오 id {id}는(은) 존재하지 않아 삭제할 수 없습니다.") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 비디오 id {video_id}는(은) 존재하지 않아 삭제할 수 없습니다.") await conn.commit() @@ -350,7 +372,7 @@ async def save_video_meta_result(conn: Connection, video_id: int, analysis: dict # 비디오 프레임 별 딥페이크 점수, 얼굴 신뢰도, 얼굴 비율, 얼굴 조도 반환 # 비디오 1개에 속한 여러 프레임의 분석 결과를 한 번에 INSERT. # 호출 위치: services/inference_svc.py - process_video_task() → run_inference() -async def save_video_frame_results(conn: Connection, video_id: int, frame_results: list): +async def save_video_frame_result(conn: Connection, video_id: int, frame_results: list): try: query = """ INSERT INTO video_frame_result @@ -377,6 +399,60 @@ async def save_video_frame_results(conn: Connection, video_id: int, frame_result await conn.rollback() raise e +async def delete_video_meta_result(conn: Connection, video_id: int): + try: + delete_query = text(""" + DELETE FROM video_meta_result + WHERE video_id = :video_id + """) + result = await conn.execute(delete_query, {"video_id": video_id}) + if result.rowcount == 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 비디오 id {video_id}는(은) 존재하지 않아 삭제할 수 없습니다.") + + await conn.commit() + + except SQLAlchemyError as e: + print(e) + await conn.rollback() + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.") + + except HTTPException: + raise + + except Exception as e: + print(e) + await conn.rollback() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") + + +async def delete_video_frame_result(conn: Connection, video_id: int): + try: + delete_query = text(""" + DELETE FROM video_frame_result + WHERE video_id = :video_id + """) + result = await conn.execute(delete_query, {"video_id": video_id}) + if result.rowcount == 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"해당 비디오 id {video_id}는(은) 존재하지 않아 삭제할 수 없습니다.") + + await conn.commit() + + except SQLAlchemyError as e: + print(e) + await conn.rollback() + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.") + + except HTTPException: + raise + + except Exception as e: + print(e) + await conn.rollback() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") + + + + # 비디오 메타 데이터 정보 가져오기 # 호출 위치: routers/inference.py - get_video_detail() # 초당프레임수, 영상 전체 프레임 수, 샘플링한 프레임 수, 얼굴 추출에 성공한 프레임 수, score산출 성공 프레임 수 @@ -443,4 +519,7 @@ async def get_video_frame_results(conn: Connection, video_id: int): raise HTTPException(status_code=503, detail="데이터베이스 조회 중 문제가 발생했습니다.") except Exception as e: print(e) - raise HTTPException(status_code=500, detail="알수없는 이유로 문제가 발생하였습니다.") \ No newline at end of file + raise HTTPException(status_code=500, detail="알수없는 이유로 문제가 발생하였습니다.") + + + \ No newline at end of file From f25176b841abc310cef5b36ef8462a3e891a256c Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Tue, 19 May 2026 12:26:06 +0900 Subject: [PATCH 5/6] refactor: improve anonymous cleanup logic and standardize service comments --- App/routes/auth.py | 10 ++++++---- App/routes/home.py | 3 ++- App/routes/image.py | 7 ++++--- App/routes/inference.py | 2 +- App/routes/video.py | 5 +++-- App/schemas/auth_schema.py | 2 +- App/services/image_svc.py | 37 +++++++++++++++++++++++++++++++------ App/services/video_svc.py | 17 +++++++++++++++-- 8 files changed, 63 insertions(+), 20 deletions(-) diff --git a/App/routes/auth.py b/App/routes/auth.py index 80f3872..4a8154e 100644 --- a/App/routes/auth.py +++ b/App/routes/auth.py @@ -1,19 +1,21 @@ +from pydantic import BaseModel from fastapi import APIRouter, Depends, status, Request from services import auth_svc, session_svc from sqlalchemy import Connection from db.database import context_get_conn from fastapi.exceptions import HTTPException +from fastapi.responses import JSONResponse from schemas.auth_schema import RegisterRequest, LoginRequest router = APIRouter(prefix="/auth", tags=["auth"]) # 프론트엔드 새로고침 시 세션 유지 여부를 확인하고 유저 정보를 반환합니다 -@router.get("/check", status_code=status.HTTP_200_OK, summary= "로그인 상태 확인 API") +@router.get("/check", status_code=status.HTTP_200_OK, response_class=JSONResponse, summary= "로그인 상태 확인 API") async def check_session(session_user = Depends(session_svc.get_session_user_opt)): return {"user": session_user} -@router.post("/register", status_code=status.HTTP_201_CREATED, summary="회원가입 API") +@router.post("/register", status_code=status.HTTP_201_CREATED,response_class=JSONResponse, summary="회원가입 API") async def register_user(request: Request, user_info: RegisterRequest, conn: Connection = Depends(context_get_conn)): @@ -39,7 +41,7 @@ async def register_user(request: Request, return {"message": "회원가입이 성공적으로 완료되었습니다.", "status": "success"} -@router.post("/login", status_code=status.HTTP_200_OK,summary="로그인 API") +@router.post("/login", status_code=status.HTTP_200_OK,response_class=JSONResponse ,summary="로그인 API") async def login_user(request: Request, login_info: LoginRequest, conn: Connection = Depends(context_get_conn)): @@ -71,7 +73,7 @@ async def login_user(request: Request, "status": "success" } -@router.get("/logout", status_code=status.HTTP_200_OK, summary="로그아웃") +@router.get("/logout", status_code=status.HTTP_200_OK, response_class=JSONResponse, summary="로그아웃") async def logout_user(request: Request): request.state.session.clear() return { diff --git a/App/routes/home.py b/App/routes/home.py index 17b1e3a..112b09f 100644 --- a/App/routes/home.py +++ b/App/routes/home.py @@ -1,9 +1,10 @@ from fastapi import APIRouter, Depends, status from services import session_svc +from fastapi.responses import JSONResponse router = APIRouter(prefix="/home", tags=["home"]) -@router.get("/", status_code=status.HTTP_200_OK, summary="세션 유저 정보 가져오기") +@router.get("/", status_code=status.HTTP_200_OK,response_class=JSONResponse, summary="세션 유저 정보 가져오기") async def home_ui(session_user = Depends(session_svc.get_session_user_opt)): return { "session_user": session_user diff --git a/App/routes/image.py b/App/routes/image.py index df7214b..e603ffb 100644 --- a/App/routes/image.py +++ b/App/routes/image.py @@ -2,11 +2,12 @@ from services import session_svc, image_svc from sqlalchemy import Connection from db.database import context_get_conn +from fastapi.responses import JSONResponse router = APIRouter(prefix="/image", tags=["image"]) # 사용자 전체 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK, summary="이미지 히스토리 전체 조회") +@router.get("/history", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="이미지 히스토리 전체 조회") async def get_user_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -22,7 +23,7 @@ async def get_user_histories( } # 특정 이미지의 상세 내역 조회 (개별 히스토리) -@router.get("/history/{image_id}", status_code=status.HTTP_200_OK, summary="이미지 개별 상세 조회") +@router.get("/history/{image_id}", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="이미지 개별 상세 조회") async def get_user_history( image_id: int, conn: Connection = Depends(context_get_conn), @@ -40,7 +41,7 @@ async def get_user_history( } # 이미지 히스토리 삭제 -@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK, summary="이미지 버튼 히스토리 삭제") +@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK,responses_class=JSONResponse, summary="이미지 버튼 히스토리 삭제") async def delete_image_history( image_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/routes/inference.py b/App/routes/inference.py index ba856af..9df2e94 100644 --- a/App/routes/inference.py +++ b/App/routes/inference.py @@ -124,6 +124,6 @@ async def get_video_detail( session_user = Depends(session_svc.get_session_user_prt) # 로그인 유저만 가능 ): meta = await video_svc.get_video_meta_result(conn, video_id) - frames = await video_svc.get_video_frame_results(conn, video_id) + frames = await video_svc.get_video_frame_result(conn, video_id) return VideoDetailResponse(meta=meta, frames=frames) diff --git a/App/routes/video.py b/App/routes/video.py index e303616..526e7ce 100644 --- a/App/routes/video.py +++ b/App/routes/video.py @@ -4,11 +4,12 @@ from services import session_svc, video_svc from sqlalchemy import Connection from db.database import context_get_conn +from fastapi.responses import JSONResponse router = APIRouter(prefix="/video", tags=["video"]) # 사용자 전체 비디오 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK, summary="비디오 히스토리 전체 조회") +@router.get("/history", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="비디오 히스토리 전체 조회") async def get_video_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -24,7 +25,7 @@ async def get_video_histories( } # 특정 비디오의 상세 내역 조회 -@router.get("/history/{video_id}", status_code=status.HTTP_200_OK, summary="비디오 개별 상세 조회") +@router.get("/history/{video_id}", status_code=status.HTTP_200_OK,responses_class=JSONResponse, summary="비디오 개별 상세 조회") async def get_video_history( video_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/schemas/auth_schema.py b/App/schemas/auth_schema.py index b844abd..66c65a6 100644 --- a/App/schemas/auth_schema.py +++ b/App/schemas/auth_schema.py @@ -17,4 +17,4 @@ class LoginRequest(BaseModel): # [요청용] 회원가입 API (POST /auth/register) 수신 JSON 데이터 검증 class RegisterRequest(LoginRequest): - name: str = Field(min_length=2, max_length=100) + name: str = Field(min_length=2, max_length=100) \ No newline at end of file diff --git a/App/services/image_svc.py b/App/services/image_svc.py index 93e8f89..1d4dcf4 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -17,6 +17,8 @@ UPLOAD_DIR = os.getenv("UPLOAD_DIR") # 사용자 업로드 이미지 서버 내 저장 (회원/비회원 공통) +# 호출 : inference.py +# image_loc = await image_svc.upload_image(user_email, imagefile) -> 이미지 업로드 이후, 이미지 저장 경로 반환 async def upload_image(user_email: str | None, imagefile: UploadFile) -> str: try: # 1. 사용자별 하위 디렉토리 결정 @@ -68,6 +70,8 @@ async def upload_image(user_email: str | None, imagefile: UploadFile) -> str: detail="이미지 업로드 과정에서 예상치 못한 오류가 발생했습니다.") # 사용자 업로드 이미지 서버 내 삭제 +# 호출 : image.py : history 삭제 할 때 db 와 실제 파일 삭제 +# 호출 : inference.py : 추론 FAIL일 때 delete_video and delete_video_db 실행 async def delete_image(image_loc: str): try: @@ -85,7 +89,10 @@ async def delete_image(image_loc: str): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다." ) + + # 사용자 전체 히스토리 조회 (image_result 테이블 반영) +# 호출 : image.py / video.py async def get_user_histories(conn: Connection, user_id: int): try: # SQL에 맞춰 테이블명과 컬럼 변경 @@ -124,6 +131,7 @@ async def get_user_histories(conn: Connection, user_id: int): raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") # 사용자 개별 히스토리 조회 +# 호출 : image.py / video.py async def get_user_history(conn: Connection, image_id: int): try: query = """ @@ -169,7 +177,8 @@ async def get_user_history(conn: Connection, image_id: int): print(e) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") -# 비회원 데이터 5분 후 자동 삭제 태스크 +# 비회원 데이터 1분 후 자동 삭제 태스크 +# inference_svc.py : def process_image_task에서 추론이 성공 했을 때, 비회원일 경우 1분 후 자동 삭제 @celery_app.task(name="cleanup_anonymous_image") def cleanup_anonymous_image(image_id: int, image_loc: str): loop = asyncio.new_event_loop() @@ -177,17 +186,28 @@ def cleanup_anonymous_image(image_id: int, image_loc: str): try: async def _delete(): async with celery_db_conn() as conn: - await delete_image_db(conn, image_id) - await delete_image(image_loc) - print(f"[Cleanup] 비회원 이미지 삭제 완료 - image_id: {image_id}, image_loc: {image_loc}") + try: + await delete_image_db(conn, image_id) + except Exception as e: + print(f"[Cleanup] 이미지 DB 삭제 실패 - image_id: {image_id}, error: {e}") + + try: + await delete_image(image_loc) + except Exception as e: + print(f"[Cleanup] 이미지 파일 삭제 실패 - image_loc: {image_loc}, error: {e}") + + print(f"[Cleanup] 비회원 이미지 삭제 프로세스 완료 - image_id: {image_id}, image_loc: {image_loc}") + + loop.run_until_complete(_delete()) - except Exception as e: - print(f"[Cleanup Error] 비회원 이미지 삭제 실패 - image_id: {image_id}, error: {e}") + finally: loop.close() # 이미지 DB 레코드 및 물리 파일 완전 삭제 +# 호출 : image.py : history 삭제 시 +# 호출 : inference.py : get_image_result 에서 FALIED일 시 async def delete_image_db(conn: Connection, image_id: int): try: delete_query = text("DELETE FROM image_result WHERE id = :image_id") @@ -213,6 +233,8 @@ async def delete_image_db(conn: Connection, image_id: int): raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") # 빈 이미지 DB 생성 이후, image_id 반환(접수 완료) +# 호출 : inference.py : 빈 이미지 DB 생성 후, image_id 받기 +# image_id = (conn, user_id, image_loc, version_type, model_type, domain_type) async def register_image_result(conn: Connection, user_id: int | None, image_loc: str, version_type: str, model_type: str, domain_type: str): try: @@ -247,6 +269,7 @@ async def register_image_result(conn: Connection, user_id: int | None, image_loc detail="요청데이터가 제대로 전달되지 않았습니다") # 이미지 메타데이터 + 추론 결과값 DB에 저장 +# 호출 : inference_svc.py : process_image_task 추론 종료 시점 async def update_image_result(conn: Connection, image_id: int, analysis: dict, result_msg: str, status: str): @@ -292,6 +315,8 @@ async def update_image_result(conn: Connection, image_id: int, analysis: dict, raise e # 이미지 결과 값 가져오기 +# 프론트엔드가 특정 이미지의 분석 진행 상태 및 최종 결과를 확인하고자 할 때 데이터 반환 +# 호출 위치: routers/inference.py - get_image_result() API async def get_image_result(conn: Connection, image_id: int): try: diff --git a/App/services/video_svc.py b/App/services/video_svc.py index 00d4c44..b9f9f13 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -16,6 +16,8 @@ UPLOAD_DIR = os.getenv("UPLOAD_DIR") # 사용자 업로드 동영상 서버 내 저장 (회원/비회원 공통) +# 호출 : inference.py : video_loc = await video_svc.upload_video(user_email, videofile) +# -> 비디오 업로드 이후, 비디오 저장 경로 반환 async def upload_video(user_email: str | None, videofile: UploadFile) -> str: try: # 1. 사용자별 하위 디렉토리 결정 @@ -67,6 +69,7 @@ async def upload_video(user_email: str | None, videofile: UploadFile) -> str: detail="비디오 업로드 과정에서 예상치 못한 오류가 발생했습니다.") # 사용자 업로드 비디오 서버 내 삭제 +# 호출 : inference.py : 추론 FAIL일 때 delete_video and delete_video_db 실행 async def delete_video(video_loc: str): try: file_path = "." + video_loc @@ -85,6 +88,7 @@ async def delete_video(video_loc: str): ) # 사용자 전체 비디오 히스토리 조회 +# 호출 : image.py / video.py async def get_user_histories(conn: Connection, user_id: int): try: query = """ @@ -122,6 +126,7 @@ async def get_user_histories(conn: Connection, user_id: int): # 사용자 개별 비디오 히스토리 조회 +# 호출 : image.py / video.py async def get_user_history(conn: Connection, user_id: int, video_id: int): try: stmt = text(""" @@ -164,6 +169,7 @@ async def get_user_history(conn: Connection, user_id: int, video_id: int): raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.") # 비회원 데이터 1분 후 자동 삭제 태스크 +# inference_svc.py : def process_video_task에서 추론이 성공 했을 때, 비회원일 경우 1분 후 자동 삭제 @celery_app.task(name="cleanup_anonymous_video") def cleanup_anonymous_video(video_id: int, video_loc: str, is_success: bool): loop = asyncio.new_event_loop() @@ -195,7 +201,7 @@ async def _delete(): print(f"[Cleanup] 파일 삭제 실패 - video_loc: {video_loc}, error: {e}") success=False - if success: + if is_success: print(f"[Cleanup] 비회원 비디오 삭제 완료 - video_id: {video_id}, video_loc: {video_loc}") loop.run_until_complete(_delete()) @@ -203,6 +209,7 @@ async def _delete(): loop.close() # 비디오 DB 레코드 및 물리 파일 완전 삭제 +# 호출 : inference.py : 추론 FAIL일 때 delete_video and delete_video_db 실행 async def delete_video_db(conn: Connection, video_id: int): try: delete_query = text("DELETE FROM video_result WHERE id = :video_id") @@ -229,6 +236,7 @@ async def delete_video_db(conn: Connection, video_id: int): # 빈 비디오 DB 생성 이후, video_id 반환(접수 완료) +# 호출 : inference.py : video_id = await video_svc.register_video_result(conn, user_id, video_loc, version_type, model_type, domain_type) async def register_video_result(conn: Connection, user_id: int | None, video_loc: str, version_type: str, model_type: str, domain_type: str): try: @@ -263,6 +271,7 @@ async def register_video_result(conn: Connection, user_id: int | None, video_loc detail="요청데이터가 제대로 전달되지 않았습니다") # 비디오 메타데이터 + 추론 결과값 DB에 저장 +# 호출 : inference_svc.py : 비디오 메타데이터 + 추론 결과값 DB에 저장 / 더미값으로 오류처리 (무한로딩 방지) async def update_video_result(conn: Connection, video_id: int, analysis: dict, result_msg: str, status: str): @@ -304,6 +313,9 @@ async def update_video_result(conn: Connection, video_id: int, analysis: dict, raise e # 비디오 결과 값 가져오기 +# 프론트엔드가 특정 비디오의 분석 상태 및 최종 결과를 요청할 때 사용 +# 호출 : inference.py - get_video_result() API +# video_data = await video_svc.get_video_result(conn, video_id) video_data에 저장 async def get_video_result(conn: Connection, video_id: int): try: @@ -399,6 +411,7 @@ async def save_video_frame_result(conn: Connection, video_id: int, frame_results await conn.rollback() raise e + async def delete_video_meta_result(conn: Connection, video_id: int): try: delete_query = text(""" @@ -490,7 +503,7 @@ async def get_video_meta_result(conn: Connection, video_id: int): # 비디오 프레임별 상세 결과 조회 # 호출 위치: routers/inference.py - get_video_detail() # video_id에 속한 모든 프레임 결과를 frame_index 반환 하여, 프레임별 점수 그래프, 의심 구간 표시 등 시각화에 활용. -async def get_video_frame_results(conn: Connection, video_id: int): +async def get_video_frame_result(conn: Connection, video_id: int): try: query = text(""" SELECT frame_index, frame_time, score, face_conf, face_ratio, face_brightness From 80003711129069dc8cf8a20965f231e075199cc1 Mon Sep 17 00:00:00 2001 From: HanMoonSub Date: Thu, 21 May 2026 18:02:18 +0900 Subject: [PATCH 6/6] Delay Function Refectoring --- App/routes/image.py | 6 +++--- App/routes/video.py | 4 ++-- App/services/image_svc.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/App/routes/image.py b/App/routes/image.py index e603ffb..d8fea0e 100644 --- a/App/routes/image.py +++ b/App/routes/image.py @@ -7,7 +7,7 @@ router = APIRouter(prefix="/image", tags=["image"]) # 사용자 전체 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="이미지 히스토리 전체 조회") +@router.get("/history", status_code=status.HTTP_200_OK, response_class=JSONResponse,summary="이미지 히스토리 전체 조회") async def get_user_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -23,7 +23,7 @@ async def get_user_histories( } # 특정 이미지의 상세 내역 조회 (개별 히스토리) -@router.get("/history/{image_id}", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="이미지 개별 상세 조회") +@router.get("/history/{image_id}", status_code=status.HTTP_200_OK, response_class=JSONResponse,summary="이미지 개별 상세 조회") async def get_user_history( image_id: int, conn: Connection = Depends(context_get_conn), @@ -41,7 +41,7 @@ async def get_user_history( } # 이미지 히스토리 삭제 -@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK,responses_class=JSONResponse, summary="이미지 버튼 히스토리 삭제") +@router.delete("/history/{image_id}", status_code=status.HTTP_200_OK,response_class=JSONResponse, summary="이미지 버튼 히스토리 삭제") async def delete_image_history( image_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/routes/video.py b/App/routes/video.py index 526e7ce..99d84a6 100644 --- a/App/routes/video.py +++ b/App/routes/video.py @@ -9,7 +9,7 @@ router = APIRouter(prefix="/video", tags=["video"]) # 사용자 전체 비디오 업로드 히스토리 조회 -@router.get("/history", status_code=status.HTTP_200_OK, responses_class=JSONResponse,summary="비디오 히스토리 전체 조회") +@router.get("/history", status_code=status.HTTP_200_OK, response_class=JSONResponse,summary="비디오 히스토리 전체 조회") async def get_video_histories( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 @@ -25,7 +25,7 @@ async def get_video_histories( } # 특정 비디오의 상세 내역 조회 -@router.get("/history/{video_id}", status_code=status.HTTP_200_OK,responses_class=JSONResponse, summary="비디오 개별 상세 조회") +@router.get("/history/{video_id}", status_code=status.HTTP_200_OK,response_class=JSONResponse, summary="비디오 개별 상세 조회") async def get_video_history( video_id: int, conn: Connection = Depends(context_get_conn), diff --git a/App/services/image_svc.py b/App/services/image_svc.py index 1d4dcf4..509ec9b 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -186,18 +186,20 @@ def cleanup_anonymous_image(image_id: int, image_loc: str): try: async def _delete(): async with celery_db_conn() as conn: + success = True try: await delete_image_db(conn, image_id) except Exception as e: print(f"[Cleanup] 이미지 DB 삭제 실패 - image_id: {image_id}, error: {e}") - + success=False try: await delete_image(image_loc) except Exception as e: print(f"[Cleanup] 이미지 파일 삭제 실패 - image_loc: {image_loc}, error: {e}") - - print(f"[Cleanup] 비회원 이미지 삭제 프로세스 완료 - image_id: {image_id}, image_loc: {image_loc}") + success=False + if success: + print(f"[Cleanup] 비회원 이미지 삭제 프로세스 완료 - image_id: {image_id}, image_loc: {image_loc}") loop.run_until_complete(_delete()) @@ -220,7 +222,6 @@ async def delete_image_db(conn: Connection, image_id: int): except SQLAlchemyError as e: - print(e) await conn.rollback() raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.") @@ -228,7 +229,6 @@ async def delete_image_db(conn: Connection, image_id: int): raise except Exception as e: - print(e) await conn.rollback() raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다.")