Files
gbpark 68f85f3736 회사 관리 기능 확장 + 테넌트/비번 보안 하드닝
- 첫 로그인 비번 강제 변경 (RUN_082): FORCE_PASSWORD_CHANGE 컬럼,
  ForcePasswordChangeGuardFilter, /auth/change-password API + 페이지
- 테넌트 일관성 가드: TenantConsistencyGuardFilter 로 JWT.company_code
  ↔ 서브도메인 company_code 대조, CompanyResolver 가 (db_name, company_code)
  동시 반환
- 회사 관리 확장 (RUN_083 audit log, RUN_084 lifecycle 컬럼):
  CompanyAdmin/Members/Templates/Lifecycle/AuditLog 서비스 +
  CompanyMgmtController + SuperAdminGuard
- 회사 관리 UI: CompanyAccordionRow 탭화 + 모달 4종
  (AdminInfo/Deactivate/Delete/RecopyTemplates) + AuditLogDrawer + csvExport
- 프로비저닝 마법사: force_password_change 토글 반영
- 프론트 인증: storage 이벤트 멀티탭 동기화, 403 errorCode
  (PASSWORD_CHANGE_REQUIRED / CROSS_TENANT_REJECTED / TENANT_NOT_RESOLVED)
  전역 리다이렉트
- 기타: StartupSchemaMigrator, OS별 도커 기동 스크립트, CLAUDE.md 트래킹

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:36:05 +09:00

152 lines
5.1 KiB
TypeScript

/**
* Phase 3-A 프로비저닝 API 클라이언트.
* 백엔드: /api/admin/provisioning/*
* - GET /table-groups - 마법사 Step 2 체크박스 렌더
* - GET /check - subdomain/db_prefix/company_code 실시간 검증
* - POST /companies - 회사 생성 (202 accepted)
* - GET /status/{id} - 진행 상태 폴링
* - GET /companies-stats - 메인 화면 accordion 렌더용 (derived 필드 포함)
*/
import { apiClient } from "./client";
export type CompanyStats = Record<string, any>;
export async function getCompaniesStats(): Promise<CompanyStats[]> {
const { data } = await apiClient.get("/admin/provisioning/companies-stats");
return Array.isArray(data) ? data : [];
}
export async function getTableGroups(): Promise<Record<string, any>[]> {
const { data } = await apiClient.get("/admin/provisioning/table-groups");
return Array.isArray(data) ? data : [];
}
export interface CheckParams {
subdomain?: string;
dbPrefix?: string;
companyCode?: string;
}
export async function checkAvailability(params: CheckParams): Promise<Record<string, any>> {
const q = new URLSearchParams();
if (params.subdomain) q.set("subdomain", params.subdomain);
if (params.dbPrefix) q.set("dbPrefix", params.dbPrefix);
if (params.companyCode) q.set("companyCode", params.companyCode);
const { data } = await apiClient.get(`/admin/provisioning/check?${q.toString()}`);
return data || {};
}
export interface CreateCompanyRequest {
company_code: string;
company_name: string;
subdomain: string;
db_prefix: string;
business_registration_number?: string;
representative_name?: string;
representative_phone?: string;
email?: string;
website?: string;
address?: string;
selected_groups?: string[];
initial_password?: string;
force_password_change?: boolean;
}
export interface CreateCompanyResponse {
provisioning_id: string;
company_code: string;
db_name: string;
subdomain: string;
admin_user_id: string;
initial_password: string;
status_url: string;
}
export async function createCompany(req: CreateCompanyRequest): Promise<CreateCompanyResponse> {
const { data } = await apiClient.post("/admin/provisioning/companies", req);
return data;
}
export async function getProvisioningStatus(jobId: string): Promise<Record<string, any>> {
const { data } = await apiClient.get(`/admin/provisioning/status/${jobId}`);
return data || {};
}
// ───────────────── 회사 관리 (lifecycle / admin / members / templates / audit) ─────────────────
export async function getCompanyAdmin(companyCode: string): Promise<Record<string, any>> {
const { data } = await apiClient.get(`/admin/provisioning/companies/${companyCode}/admin`);
return data || {};
}
export interface ResetAdminPasswordResponse {
admin_user_id?: string;
new_password?: string;
force_password_change?: boolean;
error?: string;
}
export async function resetAdminPassword(companyCode: string): Promise<ResetAdminPasswordResponse> {
const { data } = await apiClient.post(`/admin/provisioning/companies/${companyCode}/admin/reset-password`);
return data || {};
}
export async function getCompanyMembers(companyCode: string): Promise<Record<string, any>> {
const { data } = await apiClient.get(`/admin/provisioning/companies/${companyCode}/members`);
return data || {};
}
export async function getInstalledGroups(companyCode: string): Promise<Record<string, any>[]> {
const { data } = await apiClient.get(`/admin/provisioning/companies/${companyCode}/installed-groups`);
return Array.isArray(data) ? data : [];
}
export async function recopyTemplates(companyCode: string, selectedGroups: string[]): Promise<Record<string, any>> {
const { data } = await apiClient.post(`/admin/provisioning/companies/${companyCode}/re-copy`, {
selected_groups: selectedGroups,
});
return data || {};
}
export async function patchCompanyStatus(
companyCode: string,
status: "active" | "suspended",
reason?: string,
): Promise<Record<string, any>> {
const { data } = await apiClient.patch(`/admin/provisioning/companies/${companyCode}/status`, {
status,
reason,
});
return data || {};
}
/** 영구 삭제 — 서브도메인 타이핑 확인 필수 */
export async function deleteCompany(companyCode: string, confirmSubdomain: string): Promise<Record<string, any>> {
const { data } = await apiClient.delete(`/admin/provisioning/companies/${companyCode}`, {
data: { confirm_subdomain: confirmSubdomain },
});
return data || {};
}
export async function getCompanyAuditLog(
companyCode: string,
page = 1,
limit = 50,
): Promise<Record<string, any>> {
const { data } = await apiClient.get(`/admin/provisioning/companies/${companyCode}/audit-log`, {
params: { page, limit },
});
return data || {};
}
export async function getGlobalAuditLog(
page = 1,
limit = 50,
action?: string,
): Promise<Record<string, any>> {
const { data } = await apiClient.get(`/admin/provisioning/audit-log`, {
params: { page, limit, action },
});
return data || {};
}