feat: add admin test data reset

This commit is contained in:
unknown
2026-07-20 19:48:40 +09:00
parent 7617c9f371
commit c45c47109c
4 changed files with 127 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ import {
PublicPass,
Team,
VisitRequestCreate,
VisitRequestResetResult,
VisitRequestView,
AdminUser,
Watcher1Settings,
@@ -194,6 +195,8 @@ export const runSmsDiagnostics = () =>
request<SmsDiagnosticsResult>('/admin/sms/diagnostics', { method: 'POST' });
export const runEmailDiagnostics = () =>
request<EmailDiagnosticsResult>('/admin/email/diagnostics', { method: 'POST' });
export const resetVisitRequestTestData = (confirm: string) =>
request<VisitRequestResetResult>('/admin/visit-requests/reset-test-data', jsonInit('POST', { confirm }));
export const listAdminUsers = () => request<AdminUser[]>('/admin/users');
export const updateAdminUserRoles = (id: number, roles: Role[]) =>
request<AdminUser>(`/admin/users/${id}/roles`, jsonInit('PUT', { roles }));

View File

@@ -14,14 +14,15 @@ import {
updateAdminUserRoles,
runEmailDiagnostics,
runSmsDiagnostics,
resetVisitRequestTestData,
uploadAdminTeams,
uploadAdminUserRoles,
updateWatcher1Settings,
getWatcher1Settings,
} from '../api';
import { AdminExcelImportResult, AdminUser, EmailDiagnosticsResult, PurposeCode, Role, SmsDiagnosticsResult, Team, Watcher1Settings } from '../types';
import { AdminExcelImportResult, AdminUser, EmailDiagnosticsResult, PurposeCode, Role, SmsDiagnosticsResult, Team, VisitRequestResetResult, Watcher1Settings } from '../types';
type Tab = 'watcher' | 'purpose' | 'teams' | 'roles' | 'sms' | 'email';
type Tab = 'watcher' | 'purpose' | 'teams' | 'roles' | 'sms' | 'email' | 'data';
const EMPTY_PURPOSE = {
code: '',
@@ -141,6 +142,8 @@ export const AdminManagementPage: React.FC = () => {
const [userRoleUploadResult, setUserRoleUploadResult] = useState<AdminExcelImportResult | null>(null);
const [smsDiagnostics, setSmsDiagnostics] = useState<SmsDiagnosticsResult | null>(null);
const [emailDiagnostics, setEmailDiagnostics] = useState<EmailDiagnosticsResult | null>(null);
const [resetConfirm, setResetConfirm] = useState('');
const [resetResult, setResetResult] = useState<VisitRequestResetResult | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
@@ -203,6 +206,27 @@ export const AdminManagementPage: React.FC = () => {
}
};
const resetVisitData = async () => {
if (resetConfirm.trim() !== '초기화') {
setError('확인 입력란에 "초기화"를 입력하세요.');
return;
}
setBusy(true);
setError(null);
setNotice(null);
setResetResult(null);
try {
const result = await resetVisitRequestTestData(resetConfirm);
setResetResult(result);
setResetConfirm('');
setNotice(`출입신청 테스트 데이터 초기화 완료: 신청 ${result.visitRequests}건 삭제`);
} catch (err) {
setError(err instanceof Error ? err.message : '테스트 데이터 초기화 실패');
} finally {
setBusy(false);
}
};
const editPurpose = (p: PurposeCode) => {
setEditingPurposeId(p.id);
setPurposeForm({
@@ -440,6 +464,7 @@ export const AdminManagementPage: React.FC = () => {
<button className={tab === 'roles' ? 'tab-active' : ''} onClick={() => setTab('roles')}></button>
<button className={tab === 'sms' ? 'tab-active' : ''} onClick={() => setTab('sms')}>SMS </button>
<button className={tab === 'email' ? 'tab-active' : ''} onClick={() => setTab('email')}>Email </button>
<button className={tab === 'data' ? 'tab-active' : ''} onClick={() => setTab('data')}> </button>
</div>
{tab === 'watcher' && (
@@ -616,6 +641,43 @@ export const AdminManagementPage: React.FC = () => {
</>
)}
{tab === 'data' && (
<div className="card">
<div className="admin-upload-head">
<h3> </h3>
</div>
<p className="muted">
, , / , . , , , , , .
</p>
<div className="admin-upload-controls">
<input
value={resetConfirm}
onChange={(e) => setResetConfirm(e.target.value)}
placeholder="초기화"
/>
<button
className="btn-danger"
type="button"
disabled={busy || resetConfirm.trim() !== '초기화'}
onClick={() => void resetVisitData()}
>
</button>
</div>
{resetResult && (
<table className="table diagnostic-table">
<tbody>
<tr><th></th><td>{resetResult.visitRequests}</td></tr>
<tr><th></th><td>{resetResult.visitors}</td></tr>
<tr><th> </th><td>{resetResult.approvals}</td></tr>
<tr><th>/ </th><td>{resetResult.accessEvents}</td></tr>
<tr><th> </th><td>{resetResult.passDeliveries}</td></tr>
</tbody>
</table>
)}
</div>
)}
{tab === 'sms' && (
<div className="card">
<div className="admin-upload-head">

View File

@@ -197,6 +197,14 @@ export interface AdminExcelImportResult {
rows: AdminExcelImportRow[];
}
export interface VisitRequestResetResult {
visitRequests: number;
visitors: number;
approvals: number;
accessEvents: number;
passDeliveries: number;
}
export interface InsideVisitor {
visitRequestId: number;
visitorName: string;

View File

@@ -62,6 +62,14 @@ interface ImportResult {
rows: ImportRow[];
}
interface VisitRequestResetResult {
visitRequests: number;
visitors: number;
approvals: number;
accessEvents: number;
passDeliveries: number;
}
interface AdminRouterDeps {
query: QueryFn;
}
@@ -481,6 +489,50 @@ export function createAdminRouter(deps: AdminRouterDeps = { query }): Router {
ok(res, await diagnoseEmailConnectivity());
}));
router.post('/visit-requests/reset-test-data', asyncRoute(async (req, res) => {
const actor = await requireAdmin(dbQuery, req);
if (String(req.body?.confirm ?? '').trim() !== '초기화') {
throw new ApiError(400, '확인 입력란에 "초기화"를 입력하세요.');
}
const targetRequests = await dbQuery<{ id: string; visitor_id: string }>(
'SELECT id, visitor_id FROM visit_requests',
);
const visitorIds = [...new Set(targetRequests.rows.map((row) => Number(row.visitor_id)))];
const passDeliveries = await dbQuery<{ id: string }>('DELETE FROM pass_deliveries RETURNING id');
const approvals = await dbQuery<{ id: string }>('DELETE FROM approvals RETURNING id');
const accessEvents = await dbQuery<{ id: string }>('DELETE FROM access_events RETURNING id');
const visitRequests = await dbQuery<{ id: string }>('DELETE FROM visit_requests RETURNING id');
const visitors = visitorIds.length > 0
? await dbQuery<{ id: string }>(
`
DELETE FROM visitors v
WHERE v.id = ANY($1::bigint[])
AND NOT EXISTS (SELECT 1 FROM visit_requests vr WHERE vr.visitor_id = v.id)
RETURNING id
`,
[visitorIds],
)
: { rows: [] };
const result: VisitRequestResetResult = {
visitRequests: visitRequests.rows.length,
visitors: visitors.rows.length,
approvals: approvals.rows.length,
accessEvents: accessEvents.rows.length,
passDeliveries: passDeliveries.rows.length,
};
await audit(
dbQuery,
actor,
'VISIT_REQUEST',
null,
`출입신청 테스트 데이터 초기화: 신청 ${result.visitRequests}건, 방문자 ${result.visitors}건, 승인 ${result.approvals}건, 입퇴장 ${result.accessEvents}건, 발송 ${result.passDeliveries}`,
);
ok(res, result);
}));
router.get('/teams', asyncRoute(async (req, res) => {
await requireAdmin(dbQuery, req);
ok(res, await listTeams(dbQuery));