- 회사 정보 수정 API(PUT /dashboard/company) 및 수정 모달 신설, 회사 패널·회사 관리 표에 수정 진입점 추가 - 회사 등록·수정 입력칸 공용화 및 순서 조정 (사업자등록번호 > 회사명 > 대표자명 > 로고 > 주소) - 회사 등록 시 사업자번호·회사명으로 기존 회사 조회, 있으면 가입 신청으로 유도 - 주소 지도 미리보기 추가 — VWorld 주소검색·배경지도 타일을 서버가 중계(키 노출 방지), 지도 라이브러리 없이 타일 3x3 조합 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
470 lines
15 KiB
TypeScript
470 lines
15 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;
|
|
/** 좌측 제목 줄 오른쪽에 붙는 프로젝트 이름 (2026-09-04 사용자 지시). */
|
|
project_name?: string | null;
|
|
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;
|
|
/** 계획노선 사용 범위 (2026-09-04 사용자 지시) — 비우면 전 구간. */
|
|
route_start_m?: number | null;
|
|
route_end_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;
|
|
/** 계획노선 사용 범위 (2026-09-04 사용자 지시) — 비우면 전 구간. */
|
|
route_start_m?: number | null;
|
|
route_end_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`;
|
|
|
|
/** 주소를 좌표로 바꾼다 (회사 주소 지도 미리보기). */
|
|
export async function geocodeAddress(address: string): Promise<{ lat: number; lon: number }> {
|
|
return request(`/dashboard/company/geocode?address=${encodeURIComponent(address)}`);
|
|
}
|
|
|
|
/** 회사 정보 수정 — 시스템관리자만 companyId 로 남의 회사를 지정한다. */
|
|
export function updateCompany(
|
|
payload: {
|
|
name: string;
|
|
business_registration_number: string;
|
|
business_address: string | null;
|
|
business_owner: string | null;
|
|
},
|
|
companyId?: number | null,
|
|
): Promise<unknown> {
|
|
return request(`/dashboard/company${companyQuery(companyId)}`, {
|
|
method: "PUT",
|
|
body: body(payload),
|
|
});
|
|
}
|
|
|
|
/** 팀원으로 부를 수 있는 사람 — 소속 없는 가입자만 (2026-09-06 사용자 확정). */
|
|
export async function searchMemberCandidates(query: string): Promise<Member[]> {
|
|
const data = await request<{ users: Member[] }>(
|
|
`/dashboard/admin/members/candidates?q=${encodeURIComponent(query)}`,
|
|
);
|
|
return data.users;
|
|
}
|
|
|
|
/** 이미 가입한 사람을 회사에 붙인다 — 계정을 대신 만들지 않는다. */
|
|
export function addCompanyMember(userId: number): Promise<{ member: Member }> {
|
|
return request("/dashboard/admin/members", {
|
|
method: "POST",
|
|
body: body({ user_id: userId }),
|
|
});
|
|
}
|
|
|
|
/** 아직 가입하지 않은 사람에게 가입 안내 메일만 보낸다. */
|
|
export function inviteMember(email: string, name?: string | null): Promise<unknown> {
|
|
return request("/dashboard/admin/members/invite", {
|
|
method: "POST",
|
|
body: body({ email, name: name || null }),
|
|
});
|
|
}
|
|
|
|
/** 계정 삭제 (회사에서 빼기가 아니라 계정 자체). 회사 관리자는 자기 회사 사람만. */
|
|
export function deleteDashboardUser(userId: number): Promise<unknown> {
|
|
return request(`/dashboard/admin/users/${userId}`, { method: "DELETE" });
|
|
}
|
|
|
|
/** 회사 대표 로고 지정·변경. 프로젝트가 따로 안 고르면 도면이 이 로고를 쓴다. */
|
|
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 async function fetchProjectWorkflowState(projectId: string): Promise<WorkflowState> {
|
|
// 응답은 `{status, workflow_state}` 껍데기로 온다 — 벗겨서 상태만 넘긴다.
|
|
const data = await request<{ workflow_state?: WorkflowState } & WorkflowState>(
|
|
`/projects/${projectId}/workflow-state`,
|
|
{ method: "GET" },
|
|
);
|
|
return data.workflow_state ?? data;
|
|
}
|