refactor(B01): 저장소 700줄 분리 + 코드 검토 지적사항 수정
- B01_Dashboard_Repository 에서 회사·가입신청 부분을 B01_Dashboard_Repository_Company 로 분리 (740줄 -> 508줄) - 팀원 초대 메일 본문의 회사명·이름 HTML 이스케이프 (임의 HTML 삽입 차단) - 지도 타일 엔드포인트 z/x/y 범위 검증 추가 - 회사 정보 수정 시 회사명·사업자번호 중복은 409 로 응답 (기존 500) - 값 변경 없는 회사 수정이 404 로 튕기던 문제 수정 - 팀원 미선택 시 안내 문구 한글화 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -324,238 +324,6 @@ async def soft_delete_project(project_id: str, actor_id: int) -> bool:
|
||||
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, 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
|
||||
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]) -> 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 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] | 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())
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""회사·가입 신청 저장소 (B01_Dashboard_Repository 에서 분리, 700줄 제한).
|
||||
|
||||
회사를 만들고 고치고 찾는 일, 그리고 그 회사에 들어가겠다는 신청을 다루는 곳이다.
|
||||
사용자·프로젝트 쪽은 `B01_Dashboard_Repository` 에 남는다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import aiomysql
|
||||
|
||||
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]) -> 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 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] | 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())
|
||||
@@ -1,10 +1,22 @@
|
||||
"""B01_Dashboard 역할별 대시보드 API."""
|
||||
|
||||
import html
|
||||
import mimetypes
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Response, UploadFile
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
File,
|
||||
Form,
|
||||
HTTPException,
|
||||
Path,
|
||||
Query,
|
||||
Response,
|
||||
UploadFile,
|
||||
)
|
||||
from pymysql.err import IntegrityError
|
||||
|
||||
from common_util.common_util_auth import require_company, require_system_admin, verify_session
|
||||
from common_util.common_util_email import send_email
|
||||
@@ -17,26 +29,18 @@ from .B01_Dashboard_Repository import (
|
||||
assign_user_company,
|
||||
change_user_role,
|
||||
count_company_admins,
|
||||
create_company,
|
||||
get_dashboard_me,
|
||||
get_project,
|
||||
get_system_resources,
|
||||
get_user_admin_target,
|
||||
get_user_company,
|
||||
join_company,
|
||||
list_all_companies,
|
||||
list_all_projects,
|
||||
list_all_users,
|
||||
list_audit_logs,
|
||||
list_company_projects,
|
||||
list_join_requests,
|
||||
list_user_projects,
|
||||
process_join_request,
|
||||
search_companies,
|
||||
soft_delete_project,
|
||||
soft_delete_user,
|
||||
update_admin_user,
|
||||
update_company,
|
||||
update_project,
|
||||
update_user_profile,
|
||||
)
|
||||
@@ -51,6 +55,16 @@ from .B01_Dashboard_Repository_Assets import (
|
||||
update_company_asset,
|
||||
write_company_asset_file,
|
||||
)
|
||||
from .B01_Dashboard_Repository_Company import (
|
||||
create_company,
|
||||
get_user_company,
|
||||
join_company,
|
||||
list_all_companies,
|
||||
list_join_requests,
|
||||
process_join_request,
|
||||
search_companies,
|
||||
update_company,
|
||||
)
|
||||
from .B01_Dashboard_Repository_Members import (
|
||||
attach_company_member,
|
||||
check_project_refs,
|
||||
@@ -196,7 +210,13 @@ async def company_update(
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
"""회사 정보 수정 — 시스템관리자만 남의 회사를 지정할 수 있다."""
|
||||
if not await update_company(_scope_company(session, company_id), payload.model_dump()):
|
||||
try:
|
||||
changed = await update_company(_scope_company(session, company_id), payload.model_dump())
|
||||
except IntegrityError as exc:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="같은 회사명 또는 사업자등록번호가 이미 있습니다."
|
||||
) from exc
|
||||
if not changed:
|
||||
raise HTTPException(status_code=404, detail="회사를 찾을 수 없습니다.")
|
||||
return {"status": "success"}
|
||||
|
||||
@@ -216,13 +236,15 @@ async def company_geocode(
|
||||
|
||||
@router.get("/map/tile/{z}/{x}/{y}")
|
||||
async def map_tile(
|
||||
z: int,
|
||||
x: int,
|
||||
y: int,
|
||||
z: int = Path(ge=0, le=19),
|
||||
x: int = Path(ge=0),
|
||||
y: int = Path(ge=0),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
"""배경지도 타일 — VWorld 키가 화면으로 새지 않게 서버가 받아 넘긴다."""
|
||||
_ = session
|
||||
if x >= 2**z or y >= 2**z:
|
||||
raise HTTPException(status_code=400, detail="지도 타일 번호가 범위를 벗어났습니다.")
|
||||
try:
|
||||
tile = await fetch_base_tile(z, x, y)
|
||||
except Exception as exc: # 지도 한 칸이 비는 것은 화면을 막을 일이 아니다
|
||||
@@ -289,10 +311,13 @@ async def admin_invite_member(
|
||||
"""아직 가입하지 않은 사람에게 안내 메일만 보낸다 — 계정을 대신 만들지 않는다."""
|
||||
company = await get_user_company(session.get("company_id"))
|
||||
company_name = (company or {}).get("name") or "회사"
|
||||
# 메일 본문에 사람이 넣은 글자가 그대로 들어가면 남의 편지함에 임의 HTML 을 보낼 수 있다.
|
||||
safe_company = html.escape(company_name)
|
||||
safe_name = html.escape(payload.name or "")
|
||||
sent = await send_email(
|
||||
payload.email,
|
||||
f"[Aislo] {company_name} 팀원 등록 안내",
|
||||
f"<p>{payload.name or ''}님, {company_name} 에서 Aislo 팀원으로 등록하려 합니다.</p>"
|
||||
f"<p>{safe_name}님, {safe_company} 에서 Aislo 팀원으로 등록하려 합니다.</p>"
|
||||
f"<p>아래 주소에서 가입한 뒤 알려 주시면 회사 관리자가 팀원으로 등록합니다.</p>"
|
||||
f'<p><a href="{APP_PUBLIC_BASE_URL}">{APP_PUBLIC_BASE_URL}</a></p>',
|
||||
)
|
||||
|
||||
@@ -663,7 +663,7 @@ export function openAddMemberModal(onCreated?: (member: Member) => void): void {
|
||||
openModal(L("B01_Dashboard_Modal_AddMember"), [query.root, bar, results], async () => {
|
||||
if (!picked) {
|
||||
query.setError("등록할 사람을 먼저 고르십시오.");
|
||||
throw new Error("member not picked");
|
||||
throw new Error("등록할 사람을 먼저 고르십시오.");
|
||||
}
|
||||
const created = await addCompanyMember(picked.id);
|
||||
showToast(L("B01_Dashboard_Saved"), "success");
|
||||
|
||||
Reference in New Issue
Block a user