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>
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Trash2, AlertOctagon } from "lucide-react";
|
|
import { deleteCompany } from "@/lib/api/provisioning";
|
|
import ModalShell, { ModalBtn } from "./ModalShell";
|
|
|
|
/**
|
|
* 영구 삭제 — 서브도메인 타이핑 확인 + 2-step.
|
|
* 1단계: 경고
|
|
* 2단계: 서브도메인 타이핑 + 최종 삭제 버튼
|
|
*/
|
|
export default function DeleteCompanyModal({
|
|
companyCode,
|
|
companyName,
|
|
subdomain,
|
|
dbName,
|
|
onClose,
|
|
}: {
|
|
companyCode: string;
|
|
companyName?: string;
|
|
subdomain: string;
|
|
dbName: string;
|
|
onClose: () => void;
|
|
}) {
|
|
const [stage, setStage] = useState<1 | 2>(1);
|
|
const [typed, setTyped] = useState("");
|
|
const qc = useQueryClient();
|
|
|
|
const typedOk = typed.trim() === subdomain;
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: () => deleteCompany(companyCode, typed.trim()),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ["companies-stats"] });
|
|
onClose();
|
|
},
|
|
});
|
|
|
|
return (
|
|
<ModalShell
|
|
title={<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}><AlertOctagon size={18} strokeWidth={1.75} color="var(--v5-red)" /> 회사 영구 삭제</span>}
|
|
subtitle={companyName || companyCode}
|
|
onClose={onClose}
|
|
width={560}
|
|
footer={
|
|
stage === 1 ? (
|
|
<>
|
|
<ModalBtn variant="ghost" onClick={onClose}>취소</ModalBtn>
|
|
<ModalBtn variant="danger" onClick={() => setStage(2)} icon={<Trash2 size={13} strokeWidth={1.75} />}>
|
|
이해했습니다, 계속
|
|
</ModalBtn>
|
|
</>
|
|
) : (
|
|
<>
|
|
<ModalBtn variant="ghost" onClick={onClose} disabled={mutation.isPending}>취소</ModalBtn>
|
|
<ModalBtn
|
|
variant="danger"
|
|
onClick={() => mutation.mutate()}
|
|
disabled={!typedOk || mutation.isPending}
|
|
icon={<Trash2 size={13} strokeWidth={1.75} />}
|
|
>
|
|
{mutation.isPending ? "삭제 중..." : "영구 삭제"}
|
|
</ModalBtn>
|
|
</>
|
|
)
|
|
}
|
|
>
|
|
{stage === 1 && (
|
|
<div style={{ fontSize: "0.85rem", color: "var(--v5-text)", lineHeight: 1.7 }}>
|
|
<div style={{
|
|
padding: "0.9rem 1rem",
|
|
background: "rgba(var(--v5-red-rgb), 0.08)",
|
|
border: "1px solid rgba(var(--v5-red-rgb), 0.4)",
|
|
borderRadius: 8,
|
|
marginBottom: "1rem",
|
|
fontWeight: 600,
|
|
color: "var(--v5-red)",
|
|
}}>
|
|
⚠ 이 작업은 <b>되돌릴 수 없습니다.</b>
|
|
</div>
|
|
<p>다음 리소스가 <b>즉시 영구 삭제</b>됩니다:</p>
|
|
<ul style={{ paddingLeft: 22, margin: "0.5rem 0 0.8rem" }}>
|
|
<li>테넌트 DB <b style={{ fontFamily: "var(--v5-font-mono)", color: "var(--v5-red)" }}>{dbName}</b> (<code>DROP DATABASE</code>)</li>
|
|
<li>해당 회사의 모든 사용자 · 권한 · 데이터 · 업로드 파일</li>
|
|
<li>메타 DB 의 COMPANY_MNG row</li>
|
|
<li>서브도메인 <b style={{ fontFamily: "var(--v5-font-mono)" }}>{subdomain}.invyone.com</b> 라우팅</li>
|
|
</ul>
|
|
<p style={{ color: "var(--v5-text-sec)", fontSize: "0.8rem" }}>
|
|
감사 로그에는 삭제 기록이 남지만 데이터 자체는 복구 불가입니다. 백업이 필요하면 먼저 비활성화만 수행하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{stage === 2 && (
|
|
<div>
|
|
<p style={{ fontSize: "0.85rem", color: "var(--v5-text)", lineHeight: 1.6, marginBottom: "0.8rem" }}>
|
|
확인을 위해 아래 입력란에 <b style={{ color: "var(--v5-red)" }}>서브도메인</b>을 정확히 입력하세요.
|
|
</p>
|
|
<div style={{
|
|
padding: "0.55rem 0.7rem",
|
|
background: "var(--v5-bg-subtle)",
|
|
border: "1px solid var(--v5-border)",
|
|
borderRadius: 7,
|
|
fontFamily: "var(--v5-font-mono)",
|
|
fontSize: "0.85rem",
|
|
color: "var(--v5-text-sec)",
|
|
marginBottom: "0.6rem",
|
|
userSelect: "all",
|
|
}}>
|
|
입력할 값: <b style={{ color: "var(--v5-red)" }}>{subdomain}</b>
|
|
</div>
|
|
<input
|
|
value={typed}
|
|
onChange={(e) => setTyped(e.target.value)}
|
|
placeholder={subdomain}
|
|
autoFocus
|
|
style={{
|
|
width: "100%",
|
|
padding: "0.6rem 0.75rem",
|
|
background: "var(--v5-surface-solid)",
|
|
border: `1px solid ${typed.length === 0 ? "var(--v5-border)" : typedOk ? "rgb(var(--v5-green-rgb))" : "var(--v5-red)"}`,
|
|
borderRadius: 7,
|
|
fontSize: "0.9rem",
|
|
fontFamily: "var(--v5-font-mono)",
|
|
color: "var(--v5-text)",
|
|
outline: "none",
|
|
transition: "all 0.18s ease",
|
|
}}
|
|
/>
|
|
{mutation.isError && (
|
|
<div style={{ fontSize: "0.8rem", color: "var(--v5-red)", marginTop: 10 }}>
|
|
오류: {(mutation.error as Error)?.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</ModalShell>
|
|
);
|
|
}
|