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; /** 참여자 (2026-09-06 사용자 확정) — 여기 든 사람은 일반 사용자여도 수정할 수 있다. */ member_user_ids?: number[]; owner_name?: string | null; workflow_stage: 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; /** 대상 식별자 문자열 — 프로젝트는 UUID 라 숫자 칸에 못 담는다 (2026-09-06). */ resource_ref?: string | null; ip_address?: string | 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; member_user_ids?: 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(path: string, init: RequestInit = {}): Promise { 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 { const data = await request<{ user: DashboardUser }>("/dashboard/me"); return data.user; } export async function updateUserProfile(payload: UpdateUserRequest): Promise { const data = await request<{ user: DashboardUser }>("/dashboard/me", { method: "PATCH", body: body(payload), }); return data.user; } export function changePassword(payload: ChangePasswordRequest): Promise { return request("/auth/password", { method: "POST", body: body(payload), }); } export async function fetchUserProjects(): Promise { const data = await request<{ projects: ProjectItem[] }>("/dashboard/user/projects"); return data.projects; } export async function fetchUserCompany(): Promise { const data = await request<{ company: CompanyInfo | null }>("/dashboard/user/company"); return data.company; } export async function searchCompanies(query: string): Promise { 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 { return request("/dashboard/admin/companies", { method: "POST", body: body(payload) }); } export function joinCompany(companyId: number): Promise { 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 { const data = await request<{ members: Member[] }>( `/dashboard/admin/members${companyQuery(companyId)}`, ); return data.members; } export async function fetchCompanyAssets(companyId?: number | null): Promise { const data = await request<{ assets: CompanyAsset[] }>( `/dashboard/company/assets${companyQuery(companyId)}`, ); return data.assets; } /** multipart 업로드라 `request()`의 JSON 헤더를 쓰지 않는다. */ export async function createCompanyAsset(form: FormData): Promise { 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 { return request(`/dashboard/company/assets/${assetId}`, { method: "PUT", body: body(payload), }); } export function deleteCompanyAsset(assetId: number): Promise { 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)}`); } /** 주소 후보 한 줄 — 검색해서 골라 넣는 용도. */ export interface AddressCandidate { zipcode: string; road: string; parcel: string; building: string; category: string; lat: number; lon: number; } /** 주소를 검색해 후보를 받는다 (도로명 먼저, 없으면 지번). */ export async function searchAddress(query: string): Promise { const body = await request<{ items?: AddressCandidate[] }>( `/dashboard/company/address/search?query=${encodeURIComponent(query)}`, ); return body.items ?? []; } /** 회사 정보 수정 — 시스템관리자만 companyId 로 남의 회사를 지정한다. */ export function updateCompany( payload: { name: string; business_registration_number: string; business_address: string | null; business_owner: string | null; }, companyId?: number | null, ): Promise { return request(`/dashboard/company${companyQuery(companyId)}`, { method: "PUT", body: body(payload), }); } /** 팀원으로 부를 수 있는 사람 — 소속 없는 가입자만 (2026-09-06 사용자 확정). */ export async function searchMemberCandidates(query: string): Promise { 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 { return request("/dashboard/admin/members/invite", { method: "POST", body: body({ email, name: name || null }), }); } /** 계정 삭제 (회사에서 빼기가 아니라 계정 자체). 회사 관리자는 자기 회사 사람만. */ export function deleteDashboardUser(userId: number): Promise { return request(`/dashboard/admin/users/${userId}`, { method: "DELETE" }); } /** 회사 대표 로고 지정·변경. 프로젝트가 따로 안 고르면 도면이 이 로고를 쓴다. */ export function setCompanyLogo( logoAssetId: number | null, companyId?: number | null, ): Promise { return request(`/dashboard/company/logo${companyQuery(companyId)}`, { method: "PUT", body: body({ logo_asset_id: logoAssetId }), }); } export function removeCompanyMember(userId: number): Promise { return request(`/dashboard/admin/members/${userId}`, { method: "DELETE" }); } export async function fetchCompanyJoinRequests(): Promise { const data = await request<{ requests: JoinRequest[] }>("/dashboard/admin/join-requests"); return data.requests; } export function processJoinRequest( requestId: number, action: "APPROVE" | "REJECT", ): Promise { return request(`/dashboard/admin/join-requests/${requestId}`, { method: "PATCH", body: body({ action }), }); } export async function fetchCompanyProjects(): Promise { const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects"); return data.projects; } export async function fetchAllProjects(): Promise { const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects-all"); return data.projects; } export async function fetchAllCompanies(): Promise { const data = await request<{ companies: CompanyInfo[] }>("/dashboard/admin/companies"); return data.companies; } export async function fetchAllUsers(): Promise { const data = await request<{ users: DashboardUser[] }>("/dashboard/admin/users"); return data.users; } export function changeUserRole(userId: number, role: string): Promise { return request(`/dashboard/admin/users/${userId}/role`, { method: "PATCH", body: body({ role }), }); } export function updateProject(projectId: string, payload: UpdateProjectRequest): Promise { return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, { method: "PUT", body: body(payload), }); } export function deleteProject(projectId: string): Promise { return request(`/dashboard/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" }); } export function updateDashboardUser( userId: number, payload: AdminUpdateUserRequest, ): Promise { return request(`/dashboard/admin/users/${userId}`, { method: "PUT", body: body(payload), }); } export function assignUserToCompany(userId: number, companyId: number | null): Promise { return request(`/dashboard/admin/users/${userId}/company`, { method: "PATCH", body: body({ company_id: companyId }), }); } export async function fetchAllJoinRequests(): Promise { const data = await request<{ requests: JoinRequest[] }>("/dashboard/admin/join-requests-all"); return data.requests; } export function systemApproveJoinRequest(requestId: number): Promise { return request(`/dashboard/admin/join-requests/${requestId}/approve`, { method: "PATCH" }); } export async function fetchAuditLogs(): Promise { const data = await request<{ items: AuditLog[] }>("/dashboard/admin/audit-logs"); return data.items; } export async function fetchSystemResources(days = 30): Promise { return request(`/dashboard/admin/resources?days=${encodeURIComponent(days)}`); } export async function fetchProjectWorkflowState(projectId: string): Promise { // 응답은 `{status, workflow_state}` 껍데기로 온다 — 벗겨서 상태만 넘긴다. const data = await request<{ workflow_state?: WorkflowState } & WorkflowState>( `/projects/${projectId}/workflow-state`, { method: "GET" }, ); return data.workflow_state ?? data; }