Files
invyone/frontend/components/admin/provisioning/modals/DeleteCompanyModal.tsx
T
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

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>
);
}