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:
2026-09-06 12:22:40 +09:00
co-authored by Claude Opus 5
parent 2c03dbee3e
commit 39bb1e6754
4 changed files with 293 additions and 247 deletions
-232
View File
@@ -324,238 +324,6 @@ async def soft_delete_project(project_id: str, actor_id: int) -> bool:
return changed 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]]: async def list_all_users() -> list[dict[str, Any]]:
pool = get_db_pool() pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor: 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())
+39 -14
View File
@@ -1,10 +1,22 @@
"""B01_Dashboard 역할별 대시보드 API.""" """B01_Dashboard 역할별 대시보드 API."""
import html
import mimetypes import mimetypes
import os import os
from typing import Any 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_auth import require_company, require_system_admin, verify_session
from common_util.common_util_email import send_email from common_util.common_util_email import send_email
@@ -17,26 +29,18 @@ from .B01_Dashboard_Repository import (
assign_user_company, assign_user_company,
change_user_role, change_user_role,
count_company_admins, count_company_admins,
create_company,
get_dashboard_me, get_dashboard_me,
get_project, get_project,
get_system_resources, get_system_resources,
get_user_admin_target, get_user_admin_target,
get_user_company,
join_company,
list_all_companies,
list_all_projects, list_all_projects,
list_all_users, list_all_users,
list_audit_logs, list_audit_logs,
list_company_projects, list_company_projects,
list_join_requests,
list_user_projects, list_user_projects,
process_join_request,
search_companies,
soft_delete_project, soft_delete_project,
soft_delete_user, soft_delete_user,
update_admin_user, update_admin_user,
update_company,
update_project, update_project,
update_user_profile, update_user_profile,
) )
@@ -51,6 +55,16 @@ from .B01_Dashboard_Repository_Assets import (
update_company_asset, update_company_asset,
write_company_asset_file, 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 ( from .B01_Dashboard_Repository_Members import (
attach_company_member, attach_company_member,
check_project_refs, check_project_refs,
@@ -196,7 +210,13 @@ async def company_update(
session: dict[str, Any] = Depends(require_company_admin), 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="회사를 찾을 수 없습니다.") raise HTTPException(status_code=404, detail="회사를 찾을 수 없습니다.")
return {"status": "success"} return {"status": "success"}
@@ -216,13 +236,15 @@ async def company_geocode(
@router.get("/map/tile/{z}/{x}/{y}") @router.get("/map/tile/{z}/{x}/{y}")
async def map_tile( async def map_tile(
z: int, z: int = Path(ge=0, le=19),
x: int, x: int = Path(ge=0),
y: int, y: int = Path(ge=0),
session: dict[str, Any] = Depends(verify_session), session: dict[str, Any] = Depends(verify_session),
): ):
"""배경지도 타일 — VWorld 키가 화면으로 새지 않게 서버가 받아 넘긴다.""" """배경지도 타일 — VWorld 키가 화면으로 새지 않게 서버가 받아 넘긴다."""
_ = session _ = session
if x >= 2**z or y >= 2**z:
raise HTTPException(status_code=400, detail="지도 타일 번호가 범위를 벗어났습니다.")
try: try:
tile = await fetch_base_tile(z, x, y) tile = await fetch_base_tile(z, x, y)
except Exception as exc: # 지도 한 칸이 비는 것은 화면을 막을 일이 아니다 except Exception as exc: # 지도 한 칸이 비는 것은 화면을 막을 일이 아니다
@@ -289,10 +311,13 @@ async def admin_invite_member(
"""아직 가입하지 않은 사람에게 안내 메일만 보낸다 — 계정을 대신 만들지 않는다.""" """아직 가입하지 않은 사람에게 안내 메일만 보낸다 — 계정을 대신 만들지 않는다."""
company = await get_user_company(session.get("company_id")) company = await get_user_company(session.get("company_id"))
company_name = (company or {}).get("name") or "회사" 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( sent = await send_email(
payload.email, payload.email,
f"[Aislo] {company_name} 팀원 등록 안내", 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>아래 주소에서 가입한 뒤 알려 주시면 회사 관리자가 팀원으로 등록합니다.</p>"
f'<p><a href="{APP_PUBLIC_BASE_URL}">{APP_PUBLIC_BASE_URL}</a></p>', f'<p><a href="{APP_PUBLIC_BASE_URL}">{APP_PUBLIC_BASE_URL}</a></p>',
) )
+1 -1
View File
@@ -663,7 +663,7 @@ export function openAddMemberModal(onCreated?: (member: Member) => void): void {
openModal(L("B01_Dashboard_Modal_AddMember"), [query.root, bar, results], async () => { openModal(L("B01_Dashboard_Modal_AddMember"), [query.root, bar, results], async () => {
if (!picked) { if (!picked) {
query.setError("등록할 사람을 먼저 고르십시오."); query.setError("등록할 사람을 먼저 고르십시오.");
throw new Error("member not picked"); throw new Error("등록할 사람을 먼저 고르십시오.");
} }
const created = await addCompanyMember(picked.id); const created = await addCompanyMember(picked.id);
showToast(L("B01_Dashboard_Saved"), "success"); showToast(L("B01_Dashboard_Saved"), "success");