260710_0
This commit is contained in:
@@ -145,6 +145,9 @@ export interface WF1AnalysisStatus {
|
||||
project_id: string;
|
||||
status: "pending" | "in_progress" | "completed" | "failed";
|
||||
model_count: number;
|
||||
progress_percent: number;
|
||||
current_stage: string;
|
||||
message: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,20 @@ async def _get_project_notification_info(
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def _update_project_status(project_id: UUID, status: str) -> None:
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
UPDATE projects
|
||||
SET status = %s, updated_at = NOW()
|
||||
WHERE id = %s AND deleted_at IS NULL
|
||||
""",
|
||||
(status, str(project_id)),
|
||||
)
|
||||
await connection.commit()
|
||||
|
||||
|
||||
async def _send_upload_complete_notification(
|
||||
*,
|
||||
project_id: UUID,
|
||||
@@ -141,6 +155,7 @@ async def trigger_wf1_analysis_and_email(
|
||||
pool = get_db_pool()
|
||||
project_info: dict[str, Any] | None = None
|
||||
try:
|
||||
await _update_project_status(project_id, "WF1_ANALYZING")
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_info = await _get_project_notification_info(connection, project_id)
|
||||
@@ -192,13 +207,18 @@ async def trigger_wf1_analysis_and_email(
|
||||
source_filters=source_filters,
|
||||
)
|
||||
await connection.commit()
|
||||
await _update_project_status(project_id, "WF1_COMPLETE")
|
||||
logger.info("WF1 분석 결과 DB 저장 완료: project_id=%s", project_id)
|
||||
except Exception as e:
|
||||
logger.exception("WF1 분석 결과 DB 저장 실패: %s", e)
|
||||
await connection.rollback()
|
||||
raise
|
||||
|
||||
logger.info("SEND_ANALYSIS_COMPLETION_EMAIL=%s, project_info=%s", SEND_ANALYSIS_COMPLETION_EMAIL, bool(project_info))
|
||||
logger.info(
|
||||
"SEND_ANALYSIS_COMPLETION_EMAIL=%s, project_info=%s",
|
||||
SEND_ANALYSIS_COMPLETION_EMAIL,
|
||||
bool(project_info),
|
||||
)
|
||||
if SEND_ANALYSIS_COMPLETION_EMAIL and project_info and project_info.get("user_email"):
|
||||
logger.info("WF1 완료 이메일 발송 시작: to=%s", project_info["user_email"])
|
||||
await send_analysis_completion_email(
|
||||
@@ -209,11 +229,15 @@ async def trigger_wf1_analysis_and_email(
|
||||
)
|
||||
logger.info("WF1 완료 이메일 발송 완료: project_id=%s", project_id)
|
||||
else:
|
||||
logger.info("WF1 완료 이메일 발송 스킵: SEND_ANALYSIS_COMPLETION_EMAIL=%s, has_email=%s",
|
||||
SEND_ANALYSIS_COMPLETION_EMAIL, project_info and project_info.get("user_email") is not None)
|
||||
logger.info(
|
||||
"WF1 완료 이메일 발송 스킵: SEND_ANALYSIS_COMPLETION_EMAIL=%s, has_email=%s",
|
||||
SEND_ANALYSIS_COMPLETION_EMAIL,
|
||||
project_info and project_info.get("user_email") is not None,
|
||||
)
|
||||
logger.info("WF1 백그라운드 분석 완료: project_id=%s", project_id)
|
||||
except Exception as exc:
|
||||
logger.exception("WF1 백그라운드 분석 실패: project_id=%s", project_id)
|
||||
await _update_project_status(project_id, "WF1_FAILED")
|
||||
if project_info and project_info.get("user_email"):
|
||||
await send_analysis_error_email(
|
||||
project_id=project_id,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
export type B03AnalysisStatus = "pending" | "in_progress" | "completed" | "failed";
|
||||
|
||||
export interface B03StoredFileState {
|
||||
slot: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
status: "completed" | "failed";
|
||||
}
|
||||
|
||||
export interface B03ProjectState {
|
||||
projectId: string;
|
||||
uploadedAt: number;
|
||||
files: B03StoredFileState[];
|
||||
analysisStatus: B03AnalysisStatus;
|
||||
analysisProgress: number;
|
||||
lastCheckedAt: number;
|
||||
}
|
||||
|
||||
function key(projectId: string): string {
|
||||
return `b03_project_state_${projectId}`;
|
||||
}
|
||||
|
||||
export function loadB03ProjectState(projectId: string): B03ProjectState | null {
|
||||
const raw = localStorage.getItem(key(projectId));
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as B03ProjectState;
|
||||
} catch {
|
||||
localStorage.removeItem(key(projectId));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveB03UploadedFile(
|
||||
projectId: string,
|
||||
file: { slot: string; fileName: string; fileSize: number },
|
||||
): B03ProjectState {
|
||||
const current = loadB03ProjectState(projectId);
|
||||
const nextFiles = (current?.files ?? []).filter((item) => item.slot !== file.slot);
|
||||
nextFiles.push({ ...file, status: "completed" });
|
||||
const next: B03ProjectState = {
|
||||
projectId,
|
||||
uploadedAt: current?.uploadedAt ?? Date.now(),
|
||||
files: nextFiles,
|
||||
analysisStatus: "pending",
|
||||
analysisProgress: 0,
|
||||
lastCheckedAt: Date.now(),
|
||||
};
|
||||
localStorage.setItem(key(projectId), JSON.stringify(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
export function updateB03AnalysisState(
|
||||
projectId: string,
|
||||
status: B03AnalysisStatus,
|
||||
progress: number,
|
||||
): B03ProjectState {
|
||||
const current = loadB03ProjectState(projectId);
|
||||
const next: B03ProjectState = {
|
||||
projectId,
|
||||
uploadedAt: current?.uploadedAt ?? Date.now(),
|
||||
files: current?.files ?? [],
|
||||
analysisStatus: status,
|
||||
analysisProgress: progress,
|
||||
lastCheckedAt: Date.now(),
|
||||
};
|
||||
localStorage.setItem(key(projectId), JSON.stringify(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
export function restoreB03ProjectState(options: {
|
||||
projectId: string;
|
||||
label: string;
|
||||
container: HTMLElement;
|
||||
poll: (projectId: string) => Promise<boolean>;
|
||||
onComplete: () => void;
|
||||
}): void {
|
||||
const state = loadB03ProjectState(options.projectId);
|
||||
if (!state) return;
|
||||
options.container.replaceChildren();
|
||||
const message = document.createElement("span");
|
||||
message.textContent = `${options.label}: ${state.analysisProgress}%`;
|
||||
options.container.append(message);
|
||||
options.container.classList.add("is-visible");
|
||||
if (state.analysisStatus === "in_progress" || state.analysisStatus === "pending") {
|
||||
void options.poll(options.projectId).then((completed) => {
|
||||
if (completed) options.onComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/* B03 파일 입력 페이지 - 가로형 드롭존 + 슬롯 그리드 + 청크 업로드 */
|
||||
|
||||
import {
|
||||
CURRENT_PROJECT_ID_KEY,
|
||||
PROGRESS_UPDATE_INTERVAL_MS,
|
||||
@@ -9,15 +7,7 @@ import {
|
||||
UPLOAD_MAX_MB,
|
||||
} from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
createTag,
|
||||
createWorkflowShell,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createButton, createTag, createWorkflowShell, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
checkWF1AnalysisStatus,
|
||||
createUploadSession,
|
||||
@@ -27,6 +17,11 @@ import {
|
||||
} from "./B03_FileInput_Api_Fetch";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import {
|
||||
restoreB03ProjectState,
|
||||
saveB03UploadedFile,
|
||||
updateB03AnalysisState,
|
||||
} from "./B03_FileInput_State";
|
||||
import "./B03_FileInput_UI_Style.css";
|
||||
|
||||
type FileSlot = "las_laz" | "prj" | "tfw" | "tif" | "dxf";
|
||||
@@ -196,9 +191,8 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
});
|
||||
shell.root.classList.add("b03-file");
|
||||
|
||||
// 레이아웃 전면 리팩토링: 좌측/우측 패널 구분 없이 하나의 와이드 컴포넌트로 결합
|
||||
shell.leftPanel.remove(); // 기존 좁은 세로 영역 제거
|
||||
|
||||
shell.leftPanel.remove();
|
||||
|
||||
const contentContainer = document.createElement("div");
|
||||
contentContainer.className = "b03-file__main-layout";
|
||||
|
||||
@@ -247,8 +241,7 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
);
|
||||
const cssState = stateName === "failed" ? "error" : stateName;
|
||||
card.classList.add(`b03-file__card--${cssState}`);
|
||||
|
||||
// 배지 상태 연동 업데이트
|
||||
|
||||
const badgeContainer = card.querySelector<HTMLDivElement>(".b03-file__card-badge-container");
|
||||
if (badgeContainer) {
|
||||
badgeContainer.replaceChildren();
|
||||
@@ -542,16 +535,16 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
state.speedMbs > 0 ? (file.size - end) / 1024 / 1024 / state.speedMbs : null;
|
||||
|
||||
const stored: StoredUploadSession = {
|
||||
key: storageKey,
|
||||
projectId,
|
||||
slot: state.slot,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
uploadSessionId: session,
|
||||
chunkSizeBytes,
|
||||
totalChunks,
|
||||
completedChunks: chunkIndex + 1,
|
||||
updatedAt: Date.now(),
|
||||
key: storageKey,
|
||||
projectId,
|
||||
slot: state.slot,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
uploadSessionId: session,
|
||||
chunkSizeBytes,
|
||||
totalChunks,
|
||||
completedChunks: chunkIndex + 1,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
localStorage.setItem(storageKey, JSON.stringify(stored));
|
||||
|
||||
@@ -564,6 +557,11 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
|
||||
const response = await finalizeUploadSession(projectId, session, totalChunks);
|
||||
localStorage.removeItem(storageKey);
|
||||
saveB03UploadedFile(projectId, {
|
||||
slot: state.slot,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
});
|
||||
state.progressBytes = file.size;
|
||||
state.speedMbs =
|
||||
file.size / 1024 / 1024 / Math.max(0.001, (performance.now() - startedAt) / 1000);
|
||||
@@ -577,13 +575,11 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const status = await checkWF1AnalysisStatus(projectId);
|
||||
updateB03AnalysisState(projectId, status.status, status.progress_percent);
|
||||
if (status.status === "completed") {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// 상태 조회 실패는 무시하고 계속 폴링
|
||||
}
|
||||
// 5초마다 폴링 (최대 30분)
|
||||
} catch {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
return false;
|
||||
@@ -599,24 +595,20 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
|
||||
pageError.textContent = "";
|
||||
const uploaded: UploadedFileResult[] = [];
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
for (const state of targetStates) {
|
||||
uploaded.push(...(await uploadOneFile(activeProjectId, state)));
|
||||
}
|
||||
renderUploadResults(uploaded);
|
||||
showToast(L("B03_File_Upload_Success"), "success");
|
||||
hideLoadingOverlay();
|
||||
|
||||
// WF1 분석 상태 폴링 시작
|
||||
showToast("WF1 분석 진행 중... 완료되면 자동으로 이동합니다.", "info");
|
||||
showToast(L("B03_File_Analysis_InProgress"), "info");
|
||||
const analysisComplete = await pollWF1Analysis(activeProjectId);
|
||||
|
||||
if (analysisComplete) {
|
||||
// 분석 완료 시 B04 페이지로 이동
|
||||
navigateTo(ROUTES.B04_WF1_SURFACE);
|
||||
} else {
|
||||
showToast("분석이 진행 중입니다. 잠시 후 새로고침해주세요.", "warning");
|
||||
showToast(L("B03_File_Analysis_StillRunning"), "warning");
|
||||
}
|
||||
} catch (error) {
|
||||
const failed = targetStates.find((state) => state.uploadStatus === "uploading");
|
||||
@@ -624,8 +616,6 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
if (failed) showErrorMessage(failed.slot, detail);
|
||||
pageError.textContent = `${L("B03_File_Upload_Failed")} ${detail}`;
|
||||
showToast(L("B03_File_Upload_Failed"), "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,5 +684,13 @@ export function renderB03FileInput(root: HTMLElement): void {
|
||||
for (const slot of slots.keys()) renderSlot(slot);
|
||||
void registerB03ServiceWorker();
|
||||
void detectPausedUploads();
|
||||
if (activeProjectId) {
|
||||
restoreB03ProjectState({
|
||||
projectId: activeProjectId,
|
||||
label: L("B03_File_Restore_State"),
|
||||
container: resumeBanner,
|
||||
poll: pollWF1Analysis,
|
||||
onComplete: () => navigateTo(ROUTES.B04_WF1_SURFACE),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user