- 기록을 `common_util_audit.record_audit()` 한 곳으로 모음 — 여섯 자리에 흩어져 있던 raw INSERT 제거 - 대상 식별자 칸 신설(`017_audit_log_detail.sql`, 적용 완료) — 기존 `resource_id` 는 INT 라 프로젝트 UUID 를 못 담아 늘 NULL 이었음. 프로젝트 생성·수정·삭제·회사 생성이 대상을 남김 - 접속 주소·브라우저 기록 — 라우터가 요청을 넘겨 주고, 프록시 뒤에서는 X-Forwarded-For 우선 - 보관 기간 `AUDIT_LOG_RETENTION_DAYS` 기본 365일, 임시 보관함 정리 루프에 얹어 함께 정리 - 화면: 시스템 로그 표에 대상·접속 주소 열 추가(대상은 UUID 앞 8자만) 자체검증 — 프로젝트 생성·하드삭제를 실화면에서 돌려 두 줄 모두 `프로젝트 e1040640… · 127.0.0.1 · 2026-09-06 09:54/09:55` 로 남는 것 확인. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
265 lines
11 KiB
Python
265 lines
11 KiB
Python
"""회사·가입 신청 저장소 (B01_Dashboard_Repository 에서 분리, 700줄 제한).
|
|
|
|
회사를 만들고 고치고 찾는 일, 그리고 그 회사에 들어가겠다는 신청을 다루는 곳이다.
|
|
사용자·프로젝트 쪽은 `B01_Dashboard_Repository` 에 남는다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import aiomysql
|
|
|
|
from common_util.common_util_audit import record_audit
|
|
from config.config_db import get_db_pool
|
|
|
|
from .B01_Dashboard_Repository import role_for_company
|
|
|
|
|
|
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, c.logo_asset_id,
|
|
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 update_company(company_id: int, data: dict[str, Any]) -> bool:
|
|
"""회사 정보 수정 (2026-09-06 사용자 지시) — 회사 관리자는 자기 회사, 시스템관리자는 전체."""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"""UPDATE companies
|
|
SET name = %s, business_registration_number = %s,
|
|
business_address = %s, business_owner = %s
|
|
WHERE id = %s AND deleted_at IS NULL""",
|
|
(
|
|
data["name"],
|
|
data["business_registration_number"],
|
|
data.get("business_address"),
|
|
data.get("business_owner"),
|
|
company_id,
|
|
),
|
|
)
|
|
changed = cursor.rowcount > 0
|
|
if not changed:
|
|
# 값이 하나도 안 바뀌면 rowcount 가 0 이다 — 회사가 없는 것과는 다르다.
|
|
await cursor.execute(
|
|
"SELECT 1 FROM companies WHERE id = %s AND deleted_at IS NULL", (company_id,)
|
|
)
|
|
changed = await cursor.fetchone() is not None
|
|
await connection.commit()
|
|
return changed
|
|
|
|
|
|
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], request: Any | None = None
|
|
) -> 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'
|
|
WHERE id = %s""",
|
|
(company_id, user_id),
|
|
)
|
|
await record_audit(
|
|
cursor,
|
|
actor_id=user_id,
|
|
action="COMPANY_CREATE",
|
|
resource_type="company",
|
|
resource_ref=company_id,
|
|
request=request,
|
|
)
|
|
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], request: Any | None = None
|
|
) -> 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 record_audit(
|
|
cursor,
|
|
actor_id=actor_id,
|
|
action="COMPANY_CREATE",
|
|
resource_type="company",
|
|
resource_ref=company_id,
|
|
request=request,
|
|
)
|
|
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] | None:
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
try:
|
|
await connection.begin()
|
|
# 이미 소속이 있는 사람의 재신청은 막는다 (2026-09-06 사용자 지시) —
|
|
# 예전에는 신청 즉시 소속이 풀리고 승인대기로 떨어져 회사를 잃었다.
|
|
await cursor.execute(
|
|
"SELECT company_id FROM users WHERE id = %s AND deleted_at IS NULL FOR UPDATE",
|
|
(user_id,),
|
|
)
|
|
current = await cursor.fetchone()
|
|
if current and current[0]:
|
|
await connection.rollback()
|
|
return None
|
|
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_join_requests(company_id: int | None = None) -> list[dict[str, Any]]:
|
|
# 처리 끝난 신청은 목록에 남기지 않는다 (2026-09-06 사용자 지시) — 승인된 사람은
|
|
# 사용자 관리 목록에 이미 있어 같은 사람이 두 번 보였다.
|
|
where = "WHERE jr.status = 'PENDING'" + (" AND 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:
|
|
# 시스템 회사로 들어오면 역할도 함께 올린다 (2026-09-06 사용자 확정).
|
|
role = await role_for_company(int(row["company_id"]))
|
|
await cursor.execute(
|
|
"""UPDATE users SET company_id = %s, status = 'ACTIVE', role = %s
|
|
WHERE id = %s""",
|
|
(row["company_id"], role, 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.logo_asset_id, 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())
|