From 4d0d627f57fbabf321db7e552151227bb81783bb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 11:13:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(B03):=20WF1=20=EB=B0=B1=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=EC=9E=91=EC=97=85=20=EC=9C=A0=EC=8B=A4=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20+=20=ED=8C=8C=EC=9D=BC=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4=20=EC=8B=9C=20=EA=B0=9C=EC=88=98=20=EC=B4=88=EA=B3=BC?= =?UTF-8?q?=20=EC=98=A4=ED=83=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상 1 — 업로드는 성공하고 stage 0은 COMPLETE인데 stage 1(PREPROCESS)이 NOT_STARTED로 남아 분석이 시작되지 않고 다음 페이지로 넘어가지 않음. 원인: asyncio.create_task 결과를 아무도 붙잡지 않아 이벤트 루프가 약한 참조만 보유 -> GC가 대기 중인 작업을 회수하면 WF1 트리거가 조용히 사라짐. 조치: _BACKGROUND_TASKS 집합에 강한 참조를 보관하고 완료 시 해제, 취소 경로 로깅 및 작업 시작 로그 추가. 증상 2 — 5개 슬롯을 모두 고른 뒤 한 슬롯만 교체하면 개수 초과 오류. 원인: 교체 대상 슬롯을 기존 선택 개수에서 빼지 않고 더해 6개로 계산. 조치: 대상 슬롯을 제외하고 계산, 통과 시 이전 오류 문구 제거. Co-Authored-By: Claude Fable 5 --- B03_FileInput/B03_FileInput_Router.py | 11 +++++++++++ B03_FileInput/B03_FileInput_UI_Page.ts | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/B03_FileInput/B03_FileInput_Router.py b/B03_FileInput/B03_FileInput_Router.py index 94bb5b56..1ee99b67 100644 --- a/B03_FileInput/B03_FileInput_Router.py +++ b/B03_FileInput/B03_FileInput_Router.py @@ -132,16 +132,27 @@ def _write_stage_metadata( ) +# 실행 중인 백그라운드 작업의 강한 참조. 이벤트 루프는 작업을 약한 참조로만 들고 있어, +# 여기서 붙잡지 않으면 GC가 대기 중인 작업을 통째로 회수해 WF1 분석이 조용히 사라진다 +# (업로드는 성공·stage 0은 COMPLETE인데 stage 1은 NOT_STARTED로 남는 증상). +_BACKGROUND_TASKS: set[asyncio.Task] = set() + + def _schedule_background_task(coro: Any, *, task_name: str) -> None: task = asyncio.create_task(coro, name=task_name) + _BACKGROUND_TASKS.add(task) def _log_task_failure(completed: asyncio.Task) -> None: + _BACKGROUND_TASKS.discard(completed) try: completed.result() + except asyncio.CancelledError: + logger.warning("백그라운드 작업 취소됨: %s", task_name) except Exception: logger.exception("백그라운드 작업 실패: %s", task_name) task.add_done_callback(_log_task_failure) + logger.info("백그라운드 작업 시작: %s", task_name) async def _get_project_notification_info( diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index ff49b79d..85686106 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -244,10 +244,14 @@ export async function renderB03FileInput(root: HTMLElement): Promise { function onFileSelected(files: readonly File[], targetSlot?: FileSlot): void { if (files.length === 0) return; - if (selectedStates().length + files.length > UPLOAD_MAX_FILES) { + // 이미 고른 슬롯을 다시 고르는 것은 "교체"라 총 개수가 늘지 않는다 — 그 슬롯을 빼고 센다. + // (빼지 않으면 5개를 다 고른 뒤 하나만 바꾸려 할 때 개수 초과로 막힌다.) + const occupied = selectedStates().filter((state) => state.slot !== targetSlot).length; + if (occupied + files.length > UPLOAD_MAX_FILES) { pageError.textContent = L("B03_File_Error_Count"); return; } + pageError.textContent = ""; void (async () => { for (const file of files) await assignFileToSlot(file, targetSlot); await detectPausedUploads();