"""B01_Dashboard aiomysql Raw SQL 저장소.""" from __future__ import annotations import json import shutil from datetime import datetime, timedelta from typing import Any import aiomysql import psutil from config.config_db import get_db_pool def _role(value: str | None) -> str: return {"MASTER": "ADMIN", "MEMBER": "USER"}.get(value or "USER", value or "USER") def _stage_from_status(status: str | None) -> tuple[int, int]: value = status or "NEW" if value in {"WF1_ANALYZING", "WF1_FAILED"}: return 1, round(1 / 7 * 100) order = [ ("FILE_UPLOADED", 1), ("WF1_COMPLETE", 2), ("WF2_COMPLETE", 3), ("WF3_COMPLETE", 4), ("WF4_COMPLETE", 5), ("WF5_COMPLETE", 6), ("WF6_COMPLETE", 7), ("DONE", 7), ("CONFIRMED", 7), ] stage = 0 for token, idx in order: if token in value: stage = max(stage, idx) return stage, round(stage / 7 * 100) def _project_row(row: dict[str, Any]) -> dict[str, Any]: stage, progress = _stage_from_status(row.get("status")) return {**row, "workflow_stage": stage, "progress_percent": progress} async def get_dashboard_me(user_id: int) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT u.id, u.email, u.name, u.position, u.department, u.phone, u.company_id, u.role, u.is_master, u.status, u.last_login, u.auth_expires_at, c.name AS company_name FROM users u LEFT JOIN companies c ON c.id = u.company_id WHERE u.id = %s AND u.deleted_at IS NULL""", (user_id,), ) row = await cursor.fetchone() if row: row["role"] = _role(row.get("role")) row["is_master"] = bool(row.get("is_master")) return row async def update_user_profile(user_id: int, data: dict[str, Any]) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( """UPDATE users SET name = %s, position = %s, department = %s, phone = %s WHERE id = %s AND deleted_at IS NULL""", ( data["name"], data.get("position"), data.get("department"), data.get("phone"), user_id, ), ) await connection.commit() return await get_dashboard_me(user_id) 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: await cursor.execute( """SELECT id, company_id, name, region, road_type, project_year, estimated_length_m, memo, status, updated_at, created_at FROM projects WHERE user_id = %s AND deleted_at IS NULL ORDER BY updated_at DESC, created_at DESC""", (user_id,), ) return [_project_row(row) for row in await cursor.fetchall()] async def list_company_projects(company_id: int) -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT p.id, p.company_id, p.name, p.region, p.road_type, p.project_year, p.estimated_length_m, p.memo, p.status, p.updated_at, p.created_at, u.name AS owner_name, u.email AS owner_email FROM projects p LEFT JOIN users u ON u.id = p.user_id WHERE p.company_id = %s AND p.deleted_at IS NULL ORDER BY p.updated_at DESC, p.created_at DESC""", (company_id,), ) return [_project_row(row) for row in await cursor.fetchall()] async def list_all_projects() -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT p.id, p.company_id, p.name, p.region, p.road_type, p.project_year, p.estimated_length_m, p.memo, p.status, p.updated_at, p.created_at, u.name AS owner_name, u.email AS owner_email FROM projects p LEFT JOIN users u ON u.id = p.user_id 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()] async def get_project(project_id: str) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT id, user_id, company_id, name, region, road_type, project_year, estimated_length_m, memo, status FROM projects WHERE id = %s AND deleted_at IS NULL""", (project_id,), ) return await cursor.fetchone() async def update_project(project_id: str, data: dict[str, Any], actor_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await connection.begin() await cursor.execute( """UPDATE projects SET name = %s, region = %s, road_type = %s, project_year = %s, estimated_length_m = %s, memo = %s, status = COALESCE(%s, status) WHERE id = %s AND deleted_at IS NULL""", ( data["name"], data.get("region"), data.get("road_type"), data.get("project_year"), data.get("estimated_length_m"), data.get("memo"), data.get("status"), project_id, ), ) changed = cursor.rowcount > 0 if changed: await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'PROJECT_UPDATE', 'project', NULL)""", (actor_id,), ) await connection.commit() return changed async def soft_delete_project(project_id: str, actor_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await connection.begin() await cursor.execute( """UPDATE projects SET deleted_at = CURRENT_TIMESTAMP WHERE id = %s AND deleted_at IS NULL""", (project_id,), ) changed = cursor.rowcount > 0 if changed: await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'PROJECT_DELETE', 'project', NULL)""", (actor_id,), ) await connection.commit() return changed async def get_user_company(company_id: int | None) -> dict[str, Any] | None: if company_id is None: return None pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT c.id, c.name, c.business_registration_number, c.business_address, c.business_owner, c.business_status, COUNT(DISTINCT u.id) AS user_count, COUNT(DISTINCT p.id) AS project_count FROM companies c LEFT JOIN users u ON u.company_id = c.id AND u.deleted_at IS NULL LEFT JOIN projects p ON p.company_id = c.id AND p.deleted_at IS NULL WHERE c.id = %s AND c.deleted_at IS NULL GROUP BY c.id""", (company_id,), ) return await cursor.fetchone() async def search_companies(query: str) -> list[dict[str, Any]]: pool = get_db_pool() pattern = f"%{query}%" async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT id, name, business_registration_number, business_status FROM companies WHERE deleted_at IS NULL AND (name LIKE %s OR business_registration_number LIKE %s) ORDER BY name LIMIT 20""", (pattern, pattern), ) return list(await cursor.fetchall()) async def create_company(user_id: int, data: dict[str, Any]) -> dict[str, Any]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: try: await connection.begin() await cursor.execute( """INSERT INTO companies (name, business_registration_number, business_address, business_owner, business_status, master_user_id, created_by, status) VALUES (%s, %s, %s, %s, '활동중', %s, %s, 'ACTIVE')""", ( data["name"], data["business_registration_number"], data.get("business_address"), data.get("business_owner"), user_id, user_id, ), ) company_id = cursor.lastrowid await cursor.execute( """UPDATE users SET company_id = %s, role = 'ADMIN', is_master = TRUE, status = 'ACTIVE', auth_expires_at = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 3 MONTH) WHERE id = %s""", (company_id, user_id), ) await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'COMPANY_CREATE', 'company', %s)""", (user_id, company_id), ) await connection.commit() return {"company_id": company_id, "status": "ACTIVE"} except Exception: await connection.rollback() raise async def create_system_company(actor_id: int, data: dict[str, Any]) -> dict[str, Any]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: try: await connection.begin() await cursor.execute( """INSERT INTO companies (name, business_registration_number, business_address, business_owner, business_status, created_by, status) VALUES (%s, %s, %s, %s, '활동중', %s, 'ACTIVE')""", ( data["name"], data["business_registration_number"], data.get("business_address"), data.get("business_owner"), actor_id, ), ) company_id = cursor.lastrowid await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'COMPANY_CREATE', 'company', %s)""", (actor_id, company_id), ) await connection.commit() return {"company_id": company_id, "status": "ACTIVE"} except Exception: await connection.rollback() raise async def join_company(user_id: int, company_id: int) -> dict[str, Any]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: try: await connection.begin() await cursor.execute( """INSERT INTO join_requests (user_id, company_id, status) VALUES (%s, %s, 'PENDING') ON DUPLICATE KEY UPDATE status = 'PENDING', requested_at = CURRENT_TIMESTAMP, reviewed_by = NULL, reviewed_at = NULL""", (user_id, company_id), ) request_id = cursor.lastrowid await cursor.execute( "UPDATE users SET status = 'PENDING', company_id = NULL WHERE id = %s", (user_id,) ) await connection.commit() return {"join_request_id": request_id, "status": "PENDING"} except Exception: await connection.rollback() raise async def list_company_members(company_id: int) -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT id, email, name, position, department, role, is_master, status FROM users WHERE company_id = %s AND deleted_at IS NULL ORDER BY name, email""", (company_id,), ) rows = list(await cursor.fetchall()) for row in rows: row["role"] = _role(row.get("role")) row["is_master"] = bool(row.get("is_master")) return rows async def add_company_member(company_id: int, email: str) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await connection.begin() await cursor.execute( """SELECT id, company_id FROM users WHERE email = %s AND deleted_at IS NULL FOR UPDATE""", (email.lower(),), ) user = await cursor.fetchone() if not user or user["company_id"] == company_id: await connection.rollback() return None await cursor.execute( """UPDATE users SET company_id = %s, status = 'ACTIVE', role = 'USER', is_master = FALSE WHERE id = %s""", (company_id, user["id"]), ) await connection.commit() return await get_dashboard_me(user["id"]) async def remove_company_member(company_id: int, user_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( """UPDATE users SET company_id = NULL, status = 'NO_COMPANY', role = 'USER', is_master = FALSE WHERE id = %s AND company_id = %s AND is_master = FALSE""", (user_id, company_id), ) changed = cursor.rowcount > 0 await connection.commit() return changed async def list_join_requests(company_id: int | None = None) -> list[dict[str, Any]]: where = "WHERE jr.company_id = %s" if company_id else "" params = (company_id,) if company_id else () pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( f"""SELECT jr.id, jr.user_id, jr.company_id, jr.requested_at, jr.status, u.email AS user_email, u.name AS user_name, c.name AS company_name FROM join_requests jr JOIN users u ON u.id = jr.user_id JOIN companies c ON c.id = jr.company_id {where} ORDER BY jr.requested_at DESC LIMIT 100""", params, ) return list(await cursor.fetchall()) async def process_join_request( request_id: int, reviewer_id: int, approved: bool, company_id: int | None = None ) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await connection.begin() where = "id = %s AND status = 'PENDING'" params: tuple[Any, ...] = (request_id,) if company_id is not None: where += " AND company_id = %s" params = (request_id, company_id) await cursor.execute(f"SELECT * FROM join_requests WHERE {where} FOR UPDATE", params) row = await cursor.fetchone() if not row: await connection.rollback() return False status = "APPROVED" if approved else "REJECTED" await cursor.execute( """UPDATE join_requests SET status = %s, reviewed_by = %s, reviewed_at = CURRENT_TIMESTAMP WHERE id = %s""", (status, reviewer_id, request_id), ) if approved: await cursor.execute( """UPDATE users SET company_id = %s, status = 'ACTIVE' WHERE id = %s""", (row["company_id"], row["user_id"]), ) else: await cursor.execute( "UPDATE users SET status = 'REJECTED' WHERE id = %s", (row["user_id"],) ) await connection.commit() return True async def list_all_companies() -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT c.id, c.name, c.business_registration_number, c.business_status, c.created_at, COUNT(DISTINCT u.id) AS user_count, COUNT(DISTINCT p.id) AS project_count FROM companies c LEFT JOIN users u ON u.company_id = c.id AND u.deleted_at IS NULL LEFT JOIN projects p ON p.company_id = c.id AND p.deleted_at IS NULL WHERE c.deleted_at IS NULL GROUP BY c.id ORDER BY c.created_at DESC""" ) return list(await cursor.fetchall()) async def list_all_users() -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT u.id, u.email, u.name, u.position, u.department, u.phone, u.role, u.status, u.company_id, c.name AS company_name, u.last_login FROM users u LEFT JOIN companies c ON c.id = u.company_id WHERE u.deleted_at IS NULL ORDER BY u.created_at DESC LIMIT 200""" ) rows = list(await cursor.fetchall()) for row in rows: row["role"] = _role(row.get("role")) return rows async def change_user_role(user_id: int, role: str) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute("UPDATE users SET role = %s WHERE id = %s", (role, user_id)) changed = cursor.rowcount > 0 await connection.commit() return changed async def get_user_admin_target(user_id: int) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT id, company_id, role, is_master, status FROM users WHERE id = %s AND deleted_at IS NULL""", (user_id,), ) row = await cursor.fetchone() if row: row["role"] = _role(row.get("role")) row["is_master"] = bool(row.get("is_master")) return row async def update_admin_user(user_id: int, data: dict[str, Any]) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( """UPDATE users SET name = %s, position = %s, department = %s, phone = %s, status = COALESCE(%s, status) WHERE id = %s AND deleted_at IS NULL""", ( data["name"], data.get("position"), data.get("department"), data.get("phone"), data.get("status"), user_id, ), ) changed = cursor.rowcount > 0 await connection.commit() return changed async def count_company_admins(company_id: int) -> int: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT COUNT(*) AS cnt FROM users WHERE company_id = %s AND role = 'ADMIN' AND deleted_at IS NULL""", (company_id,), ) row = await cursor.fetchone() return int(row["cnt"] if row else 0) async def assign_user_company(user_id: int, company_id: int | None) -> bool: status = "ACTIVE" if company_id else "NO_COMPANY" pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( "UPDATE users SET company_id = %s, status = %s WHERE id = %s", (company_id, status, user_id), ) changed = cursor.rowcount > 0 await connection.commit() return changed async def list_audit_logs(limit: int, offset: int) -> dict[str, Any]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute("SELECT COUNT(*) AS total FROM system_audit_logs") total = (await cursor.fetchone())["total"] await cursor.execute( """SELECT l.id, l.user_id, u.email, l.action, l.resource_type, l.resource_id, l.timestamp FROM system_audit_logs l LEFT JOIN users u ON u.id = l.user_id ORDER BY l.timestamp DESC LIMIT %s OFFSET %s""", (limit, offset), ) return {"total": total, "items": list(await cursor.fetchall())} async def get_system_resources(days: int = 30) -> dict[str, Any]: total, used, _free = shutil.disk_usage(".") disk_percent = used / total * 100 if total else 0 cpu_percent = psutil.cpu_percent(interval=0.1) memory_percent = psutil.virtual_memory().percent pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT (SELECT COUNT(*) FROM sessions WHERE expires_at > CURRENT_TIMESTAMP) AS active_user_count, (SELECT COUNT(*) FROM projects WHERE deleted_at IS NULL) AS active_project_count""" ) stats = await cursor.fetchone() current = { "cpu_usage_percent": round(cpu_percent, 2), "memory_usage_percent": round(memory_percent, 2), "disk_usage_percent": round(disk_percent, 2), "active_user_count": stats["active_user_count"], "active_project_count": stats["active_project_count"], "total_storage_mb": round(used / 1024 / 1024, 2), } since = datetime.utcnow() - timedelta(days=days) # 다운샘플링: 조회 구간을 최대 TARGET_POINTS개 시간버킷으로 나눠 평균. # 계측 간격(2분)보다 버킷이 작으면 원본 그대로(버킷=간격) 반환. target_points = 300 await cursor.execute( "SELECT COUNT(*) AS cnt FROM system_resources WHERE timestamp >= %s", (since,), ) row_cnt_res = await cursor.fetchone() row_count = row_cnt_res["cnt"] if row_cnt_res else 0 if row_count <= target_points: bucket_seconds = 120 else: bucket_seconds = max(120, (days * 86400) // target_points) date_fmt = "%%Y-%%m-%%dT%%H:%%i:%%s" await cursor.execute( f"""SELECT DATE_FORMAT( FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(timestamp) / %s) * %s), '{date_fmt}' ) AS timestamp, ROUND(AVG(cpu_usage_percent), 2) AS cpu_usage_percent, ROUND(AVG(memory_usage_percent), 2) AS memory_usage_percent, ROUND(AVG(disk_usage_percent), 2) AS disk_usage_percent, MAX(active_user_count) AS active_user_count, MAX(active_project_count) AS active_project_count, ROUND(AVG(total_storage_mb), 2) AS total_storage_mb FROM system_resources WHERE timestamp >= %s GROUP BY FLOOR(UNIX_TIMESTAMP(timestamp) / %s) ORDER BY timestamp""", (bucket_seconds, bucket_seconds, since, bucket_seconds), ) return {"current": current, "history": list(await cursor.fetchall()), "stats": current} async def list_project_automations(project_id: str) -> list[dict[str, Any]]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT id, project_id, name, logic_type, config_json, status, last_executed_at, created_at, updated_at FROM project_automations WHERE project_id = %s AND deleted_at IS NULL ORDER BY updated_at DESC, created_at DESC""", (project_id,), ) return list(await cursor.fetchall()) async def create_project_automation( project_id: str, actor_id: int, data: dict[str, Any] ) -> dict[str, Any]: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await connection.begin() await cursor.execute( """INSERT INTO project_automations (project_id, name, logic_type, config_json, status, created_by, updated_by) VALUES (%s, %s, %s, %s, %s, %s, %s)""", ( project_id, data["name"], data["logic_type"], json.dumps(data["config_json"], ensure_ascii=False), data["status"], actor_id, actor_id, ), ) automation_id = cursor.lastrowid await connection.commit() await cursor.execute("SELECT * FROM project_automations WHERE id = %s", (automation_id,)) return await cursor.fetchone() async def get_project_automation(automation_id: int) -> dict[str, Any] | None: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( """SELECT a.*, p.company_id FROM project_automations a JOIN projects p ON p.id = a.project_id WHERE a.id = %s AND a.deleted_at IS NULL AND p.deleted_at IS NULL""", (automation_id,), ) return await cursor.fetchone() async def update_project_automation( automation_id: int, actor_id: int, data: dict[str, Any] ) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( """UPDATE project_automations SET name = %s, logic_type = %s, config_json = %s, status = %s, updated_by = %s WHERE id = %s AND deleted_at IS NULL""", ( data["name"], data["logic_type"], json.dumps(data["config_json"], ensure_ascii=False), data["status"], actor_id, automation_id, ), ) changed = cursor.rowcount > 0 await connection.commit() return changed async def delete_project_automation(automation_id: int, actor_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await connection.begin() await cursor.execute( """UPDATE project_automations SET deleted_at = CURRENT_TIMESTAMP, updated_by = %s WHERE id = %s AND deleted_at IS NULL""", (actor_id, automation_id), ) changed = cursor.rowcount > 0 if changed: await cursor.execute( """INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id) VALUES (%s, 'AUTOMATION_DELETE', 'automation', %s)""", (actor_id, automation_id), ) await connection.commit() return changed async def mark_project_automation_executed(automation_id: int, actor_id: int) -> bool: pool = get_db_pool() async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute( """UPDATE project_automations SET last_executed_at = CURRENT_TIMESTAMP, updated_by = %s WHERE id = %s AND deleted_at IS NULL""", (actor_id, automation_id), ) changed = cursor.rowcount > 0 await connection.commit() return changed