This commit is contained in:
2026-07-10 19:01:33 +09:00
parent e3d66e717c
commit b98affbf99
22 changed files with 1681 additions and 752 deletions
+24
View File
@@ -14,6 +14,23 @@ export interface DashboardUser {
status: string;
}
export interface WorkflowStageState {
stage_no: number;
stage_key: string;
state: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETE" | "FAILED" | "STALE";
progress_percent: number;
params?: any | null;
message?: string | null;
started_at?: string | null;
completed_at?: string | null;
}
export interface WorkflowState {
project_id: string;
current_stage: number;
stages: WorkflowStageState[];
}
export interface ProjectItem {
id: string;
company_id?: number | null;
@@ -27,6 +44,7 @@ export interface ProjectItem {
owner_name?: string | null;
workflow_stage: number;
progress_percent: number;
workflow_state?: WorkflowState;
updated_at?: string | null;
}
@@ -348,3 +366,9 @@ export function deleteProjectAutomation(automationId: number): Promise<unknown>
export function executeProjectAutomation(automationId: number): Promise<unknown> {
return request(`/dashboard/automations/${automationId}/execute`, { method: "POST" });
}
export function fetchProjectWorkflowState(projectId: string): Promise<WorkflowState> {
return request(`/projects/${projectId}/workflow-state`, {
method: "GET",
}) as Promise<WorkflowState>;
}
+88 -3
View File
@@ -80,6 +80,67 @@ async def update_user_profile(user_id: int, data: dict[str, Any]) -> dict[str, A
return await get_dashboard_me(user_id)
async def get_workflow_states_for_projects(
cursor: aiomysql.DictCursor, project_ids: list[str]
) -> dict[str, dict[str, Any]]:
if not project_ids:
return {}
format_strings = ",".join(["%s"] * len(project_ids))
await cursor.execute(
f"""
SELECT project_id, stage_no, stage_key, state, progress_percent, params, message,
DATE_FORMAT(started_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS started_at,
DATE_FORMAT(completed_at, '%%Y-%%m-%%dT%%H:%%i:%%s') AS completed_at
FROM project_workflow_stages
WHERE project_id IN ({format_strings})
ORDER BY project_id, stage_no ASC
""",
tuple(project_ids),
)
rows = await cursor.fetchall()
project_stages = {}
for r in rows:
pid = r["project_id"]
if pid not in project_stages:
project_stages[pid] = []
params_val = None
if r.get("params"):
try:
params_val = (
json.loads(r["params"]) if isinstance(r["params"], str) else r["params"]
)
except Exception:
params_val = r["params"]
project_stages[pid].append(
{
"stage_no": r["stage_no"],
"stage_key": r["stage_key"],
"state": r["state"],
"progress_percent": r["progress_percent"],
"params": params_val,
"message": r["message"],
"started_at": r["started_at"],
"completed_at": r["completed_at"],
}
)
result = {}
for pid in project_ids:
stages = project_stages.get(pid, [])
current_stage = 0
for stage in stages:
if stage["stage_no"] == 0:
continue
prev_stage = stages[stage["stage_no"] - 1]
if prev_stage["state"] == "COMPLETE":
current_stage = stage["stage_no"]
else:
break
result[pid] = {"current_stage": current_stage, "stages": stages}
return result
async def list_user_projects(user_id: int) -> list[dict[str, Any]]:
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
@@ -90,7 +151,15 @@ async def list_user_projects(user_id: int) -> list[dict[str, Any]]:
ORDER BY updated_at DESC, created_at DESC""",
(user_id,),
)
return [_project_row(row) for row in await cursor.fetchall()]
rows = await cursor.fetchall()
pids = [r["id"] for r in rows]
states = await get_workflow_states_for_projects(cursor, pids)
result = []
for r in rows:
p_row = _project_row(r)
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
result.append(p_row)
return result
async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
@@ -105,7 +174,15 @@ async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
ORDER BY p.updated_at DESC, p.created_at DESC""",
(company_id,),
)
return [_project_row(row) for row in await cursor.fetchall()]
rows = await cursor.fetchall()
pids = [r["id"] for r in rows]
states = await get_workflow_states_for_projects(cursor, pids)
result = []
for r in rows:
p_row = _project_row(r)
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
result.append(p_row)
return result
async def list_all_projects() -> list[dict[str, Any]]:
@@ -119,7 +196,15 @@ async def list_all_projects() -> list[dict[str, Any]]:
WHERE p.deleted_at IS NULL
ORDER BY p.updated_at DESC, p.created_at DESC"""
)
return [_project_row(row) for row in await cursor.fetchall()]
rows = await cursor.fetchall()
pids = [r["id"] for r in rows]
states = await get_workflow_states_for_projects(cursor, pids)
result = []
for r in rows:
p_row = _project_row(r)
p_row["workflow_state"] = states.get(r["id"], {"current_stage": 0, "stages": []})
result.append(p_row)
return result
async def get_project(project_id: str) -> dict[str, Any] | None:
+28 -1
View File
@@ -380,12 +380,22 @@ function workflow(project: ProjectItem): HTMLElement {
];
const box = document.createElement("div");
box.className = "b01-dashboard__workflow";
const stages = project.workflow_state?.stages;
routes.forEach((route, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "b01-dashboard__step";
button.textContent = `B${String(index + 3).padStart(2, "0")}`;
const enabled = index + 1 <= Math.max(activeStage, 1);
let enabled = false;
if (stages && stages.length > 0) {
enabled = index === 0 || stages[index - 1]?.state === "COMPLETE";
} else {
enabled = index + 1 <= Math.max(activeStage, 1);
}
button.disabled = !enabled;
if (enabled) {
button.classList.add("is-enabled");
@@ -394,6 +404,23 @@ function workflow(project: ProjectItem): HTMLElement {
navigateTo(route);
});
}
if (stages && stages[index]) {
const state = stages[index].state;
button.classList.add(`state-${state.toLowerCase()}`);
if (state === "STALE") {
button.title = "Stale (하위 단계 변경으로 무효화됨)";
} else if (state === "FAILED") {
button.title = "Failed (실패)";
} else if (state === "COMPLETE") {
button.title = "Complete (완료)";
} else if (state === "IN_PROGRESS") {
button.title = "In Progress (진행 중)";
} else {
button.title = "Not Started (미실행)";
}
}
box.append(button);
});
return box;