Files
Aislo/B01_Dashboard/B01_Dashboard_Api_Fetch.ts
T
eomsangdonandClaude Opus 5 3a36af9a2c feat(B01): 서명은 사람 계정에, 회사 로고는 회사 등록 단계로 재배치
사용자 확정(2026-09-02) — 이름이 들어가는 자리는 서명도 받고, 담당자 선택에 신규 등록
항목을 두며, 회사 로고는 회사 등록 단계에서 받아 이후 변경.

- 014_company_logo.sql: companies.logo_asset_id 추가 (서명은 company_assets.user_id 로
  이미 표현되어 컬럼 없음). 공유 DB 적용 완료
- 사용자 수정 모달에 「서명 (도면 표제란)」 칸 — createAssetField 에 owner(주인 못박기)·
  onChange(즉시 반영) 추가로 재사용, 주인 고정 시 물리기 체크 잠금
- 담당자 select 3개에 「+ 신규 등록…」 — 계정 생성 모달 뒤 세 select 에 항목 삽입·자동 선택
- POST /admin/members 가 이름·직위·부서 수신, 계정 없으면 status=PENDING 으로 생성
  (로그인 불가 비밀번호). 700줄 제한으로 B01_Dashboard_Repository_Members.py 분리
- 회사 등록 모달에 로고 파일 칸, PUT /company/logo 신설, 회사 패널·회사 목록에서 변경
- 프로젝트 수정 모달의 「설계자 서명」 칸 제거 — signature_asset_id 는 null 로 고정 저장
- 검증(5174 실조작): 신규 등록 구성원 2→3(id 6 PENDING)·select 자동 선택, 서명 자산
  [4,'검증신규 서명',user_id 6] 생성, 회사 logo_asset_id None→1, 검증 자산·계정 정리.
  pytest 143 passed, tsc·ruff·prettier 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:57:16 +09:00

420 lines
12 KiB
TypeScript

import { API_BASE_URL } from "@config/config_frontend";
export interface DashboardUser {
id: number;
email: string;
name: string;
position?: string | null;
department?: string | null;
phone?: string | null;
company_id?: number | null;
company_name?: string | null;
role: "SYSTEM_ADMIN" | "ADMIN" | "USER";
is_master: boolean;
status: string;
/** 서버가 하드 삭제 모드인지 (`PROJECT_DELETE_HARD_ENABLED`). GET /me에서만 채워진다. */
project_delete_hard?: boolean;
}
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;
name: string;
region?: string | null;
road_type?: string | null;
project_year?: number | null;
estimated_length_m?: number | null;
memo?: string | null;
status?: string | null;
/** 도면 표제란·표지에 실리는 값 — 프로그램이 지어낼 수 없어 사람이 넣는다 */
client_org?: string | null;
project_number?: string | null;
work_amount?: string | null;
design_date?: string | null;
/** 담당자는 회사 구성원(users.id), 로고·서명은 회사 공유 자산(company_assets.id) */
pm_user_id?: number | null;
field_lead_user_id?: number | null;
designer_user_id?: number | null;
logo_asset_id?: number | null;
signature_asset_id?: number | null;
owner_name?: string | null;
workflow_stage: number;
progress_percent: number;
workflow_state?: WorkflowState;
updated_at?: string | null;
}
export interface CompanyInfo {
id: number;
name: string;
business_registration_number?: string | null;
business_address?: string | null;
business_owner?: string | null;
business_status?: string | null;
logo_asset_id?: number | null;
user_count?: number;
project_count?: number;
}
export interface Member {
id: number;
email: string;
name: string;
position?: string | null;
department?: string | null;
role: string;
status: string;
}
/** 회사 안에서 공유하는 도면 자산(로고·서명). user_id 가 없으면 회사 공용. */
export interface CompanyAsset {
id: number;
company_id: number;
kind: "LOGO" | "SIGNATURE";
label: string;
user_id?: number | null;
user_name?: string | null;
}
export interface JoinRequest {
id: number;
user_id: number;
company_id: number;
user_email: string;
user_name?: string | null;
company_name?: string | null;
requested_at: string;
status: string;
}
export interface AuditLog {
id: number;
user_id: number;
email?: string | null;
action: string;
resource_type?: string | null;
resource_id?: number | null;
timestamp: string;
}
export interface ResourceSnapshot {
cpu_usage_percent: number | null;
memory_usage_percent: number | null;
disk_usage_percent: number | null;
active_user_count: number;
active_project_count: number;
total_storage_mb: number;
}
export interface ResourceHistoryPoint {
timestamp: string;
cpu_usage_percent: number | null;
memory_usage_percent: number | null;
disk_usage_percent: number | null;
active_user_count: number;
active_project_count: number;
total_storage_mb: number;
}
export interface ResourceData {
current: ResourceSnapshot;
history: ResourceHistoryPoint[];
stats: ResourceSnapshot;
}
export interface UpdateUserRequest {
name: string;
position?: string | null;
department?: string | null;
phone?: string | null;
}
export interface UpdateProjectRequest {
name: string;
region?: string | null;
road_type?: string | null;
project_year?: number | null;
estimated_length_m?: number | null;
memo?: string | null;
status?: string | null;
client_org?: string | null;
project_number?: string | null;
work_amount?: string | null;
design_date?: string | null;
pm_user_id?: number | null;
field_lead_user_id?: number | null;
designer_user_id?: number | null;
logo_asset_id?: number | null;
signature_asset_id?: number | null;
}
export interface AdminUpdateUserRequest extends UpdateUserRequest {
status?: string | null;
}
export interface CreateCompanyRequest {
name: string;
business_registration_number: string;
business_address?: string | null;
business_owner?: string | null;
}
export interface ChangePasswordRequest {
current_password: string;
new_password: string;
new_password_confirm: string;
logout_all: boolean;
}
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
credentials: "include",
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
...init,
});
const data = (await response.json()) as { detail?: string } & T;
if (!response.ok) throw new Error(data.detail ?? "Request failed");
return data;
}
const body = (value: object): string => JSON.stringify(value);
export async function fetchDashboardMe(): Promise<DashboardUser> {
const data = await request<{ user: DashboardUser }>("/dashboard/me");
return data.user;
}
export async function updateUserProfile(payload: UpdateUserRequest): Promise<DashboardUser> {
const data = await request<{ user: DashboardUser }>("/dashboard/me", {
method: "PATCH",
body: body(payload),
});
return data.user;
}
export function changePassword(payload: ChangePasswordRequest): Promise<unknown> {
return request("/auth/password", {
method: "POST",
body: body(payload),
});
}
export async function fetchUserProjects(): Promise<ProjectItem[]> {
const data = await request<{ projects: ProjectItem[] }>("/dashboard/user/projects");
return data.projects;
}
export async function fetchUserCompany(): Promise<CompanyInfo | null> {
const data = await request<{ company: CompanyInfo | null }>("/dashboard/user/company");
return data.company;
}
export async function searchCompanies(query: string): Promise<CompanyInfo[]> {
const data = await request<{ companies: CompanyInfo[] }>(
`/dashboard/user/companies?q=${encodeURIComponent(query)}`,
);
return data.companies;
}
export function createCompany(
payload: CreateCompanyRequest,
): Promise<{ company_id: number; status: string }> {
return request("/dashboard/user/company/create", { method: "POST", body: body(payload) });
}
export function createSystemCompany(payload: CreateCompanyRequest): Promise<unknown> {
return request("/dashboard/admin/companies", { method: "POST", body: body(payload) });
}
export function joinCompany(companyId: number): Promise<unknown> {
return request("/dashboard/user/company/join", {
method: "POST",
body: body({ company_id: companyId }),
});
}
const companyQuery = (companyId?: number | null): string =>
companyId ? `?company_id=${encodeURIComponent(companyId)}` : "";
/** companyId 는 시스템관리자가 남의 회사 프로젝트를 고칠 때만 넘긴다. */
export async function fetchCompanyMembers(companyId?: number | null): Promise<Member[]> {
const data = await request<{ members: Member[] }>(
`/dashboard/admin/members${companyQuery(companyId)}`,
);
return data.members;
}
export async function fetchCompanyAssets(companyId?: number | null): Promise<CompanyAsset[]> {
const data = await request<{ assets: CompanyAsset[] }>(
`/dashboard/company/assets${companyQuery(companyId)}`,
);
return data.assets;
}
/** multipart 업로드라 `request()`의 JSON 헤더를 쓰지 않는다. */
export async function createCompanyAsset(form: FormData): Promise<number> {
const response = await fetch(`${API_BASE_URL}/dashboard/company/assets`, {
method: "POST",
credentials: "include",
body: form,
});
const data = (await response.json()) as { detail?: string; asset_id: number };
if (!response.ok) throw new Error(data.detail ?? "Request failed");
return data.asset_id;
}
/** 자산의 이름·주인을 고친다. 서명을 사람 계정에 물릴 때 쓴다 (2026-09-02). */
export function updateCompanyAsset(
assetId: number,
payload: { label: string; user_id: number | null },
): Promise<unknown> {
return request(`/dashboard/company/assets/${assetId}`, {
method: "PUT",
body: body(payload),
});
}
export function deleteCompanyAsset(assetId: number): Promise<unknown> {
return request(`/dashboard/company/assets/${assetId}`, { method: "DELETE" });
}
export const companyAssetFileUrl = (assetId: number): string =>
`${API_BASE_URL}/dashboard/company/assets/${assetId}/file`;
/** 이름을 함께 주면 계정이 없는 사람도 그 자리에서 만든다 (2026-09-02 사용자 확정). */
export function addCompanyMember(
email: string,
profile?: { name?: string; position?: string | null; department?: string | null },
): Promise<{ member: Member }> {
return request("/dashboard/admin/members", {
method: "POST",
body: body({ email, ...profile }),
});
}
/** 회사 대표 로고 지정·변경. 프로젝트가 따로 안 고르면 도면이 이 로고를 쓴다. */
export function setCompanyLogo(
logoAssetId: number | null,
companyId?: number | null,
): Promise<unknown> {
return request(`/dashboard/company/logo${companyQuery(companyId)}`, {
method: "PUT",
body: body({ logo_asset_id: logoAssetId }),
});
}
export function removeCompanyMember(userId: number): Promise<unknown> {
return request(`/dashboard/admin/members/${userId}`, { method: "DELETE" });
}
export async function fetchCompanyJoinRequests(): Promise<JoinRequest[]> {
const data = await request<{ requests: JoinRequest[] }>("/dashboard/admin/join-requests");
return data.requests;
}
export function processJoinRequest(
requestId: number,
action: "APPROVE" | "REJECT",
): Promise<unknown> {
return request(`/dashboard/admin/join-requests/${requestId}`, {
method: "PATCH",
body: body({ action }),
});
}
export async function fetchCompanyProjects(): Promise<ProjectItem[]> {
const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects");
return data.projects;
}
export async function fetchAllProjects(): Promise<ProjectItem[]> {
const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects-all");
return data.projects;
}
export async function fetchAllCompanies(): Promise<CompanyInfo[]> {
const data = await request<{ companies: CompanyInfo[] }>("/dashboard/admin/companies");
return data.companies;
}
export async function fetchAllUsers(): Promise<DashboardUser[]> {
const data = await request<{ users: DashboardUser[] }>("/dashboard/admin/users");
return data.users;
}
export function changeUserRole(userId: number, role: string): Promise<unknown> {
return request(`/dashboard/admin/users/${userId}/role`, {
method: "PATCH",
body: body({ role }),
});
}
export function updateProject(projectId: string, payload: UpdateProjectRequest): Promise<unknown> {
return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, {
method: "PUT",
body: body(payload),
});
}
export function deleteProject(projectId: string): Promise<unknown> {
return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" });
}
export function updateDashboardUser(
userId: number,
payload: AdminUpdateUserRequest,
): Promise<unknown> {
return request(`/dashboard/admin/users/${userId}`, {
method: "PUT",
body: body(payload),
});
}
export function assignUserToCompany(userId: number, companyId: number | null): Promise<unknown> {
return request(`/dashboard/admin/users/${userId}/company`, {
method: "PATCH",
body: body({ company_id: companyId }),
});
}
export async function fetchAllJoinRequests(): Promise<JoinRequest[]> {
const data = await request<{ requests: JoinRequest[] }>("/dashboard/admin/join-requests-all");
return data.requests;
}
export function systemApproveJoinRequest(requestId: number): Promise<unknown> {
return request(`/dashboard/admin/join-requests/${requestId}/approve`, { method: "PATCH" });
}
export async function fetchAuditLogs(): Promise<AuditLog[]> {
const data = await request<{ items: AuditLog[] }>("/dashboard/admin/audit-logs");
return data.items;
}
export async function fetchSystemResources(days = 30): Promise<ResourceData> {
return request<ResourceData>(`/dashboard/admin/resources?days=${encodeURIComponent(days)}`);
}
export function fetchProjectWorkflowState(projectId: string): Promise<WorkflowState> {
return request(`/projects/${projectId}/workflow-state`, {
method: "GET",
}) as Promise<WorkflowState>;
}