feat: add admin test data reset
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
PublicPass,
|
PublicPass,
|
||||||
Team,
|
Team,
|
||||||
VisitRequestCreate,
|
VisitRequestCreate,
|
||||||
|
VisitRequestResetResult,
|
||||||
VisitRequestView,
|
VisitRequestView,
|
||||||
AdminUser,
|
AdminUser,
|
||||||
Watcher1Settings,
|
Watcher1Settings,
|
||||||
@@ -194,6 +195,8 @@ export const runSmsDiagnostics = () =>
|
|||||||
request<SmsDiagnosticsResult>('/admin/sms/diagnostics', { method: 'POST' });
|
request<SmsDiagnosticsResult>('/admin/sms/diagnostics', { method: 'POST' });
|
||||||
export const runEmailDiagnostics = () =>
|
export const runEmailDiagnostics = () =>
|
||||||
request<EmailDiagnosticsResult>('/admin/email/diagnostics', { method: 'POST' });
|
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 listAdminUsers = () => request<AdminUser[]>('/admin/users');
|
||||||
export const updateAdminUserRoles = (id: number, roles: Role[]) =>
|
export const updateAdminUserRoles = (id: number, roles: Role[]) =>
|
||||||
request<AdminUser>(`/admin/users/${id}/roles`, jsonInit('PUT', { roles }));
|
request<AdminUser>(`/admin/users/${id}/roles`, jsonInit('PUT', { roles }));
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ import {
|
|||||||
updateAdminUserRoles,
|
updateAdminUserRoles,
|
||||||
runEmailDiagnostics,
|
runEmailDiagnostics,
|
||||||
runSmsDiagnostics,
|
runSmsDiagnostics,
|
||||||
|
resetVisitRequestTestData,
|
||||||
uploadAdminTeams,
|
uploadAdminTeams,
|
||||||
uploadAdminUserRoles,
|
uploadAdminUserRoles,
|
||||||
updateWatcher1Settings,
|
updateWatcher1Settings,
|
||||||
getWatcher1Settings,
|
getWatcher1Settings,
|
||||||
} from '../api';
|
} 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 = {
|
const EMPTY_PURPOSE = {
|
||||||
code: '',
|
code: '',
|
||||||
@@ -141,6 +142,8 @@ export const AdminManagementPage: React.FC = () => {
|
|||||||
const [userRoleUploadResult, setUserRoleUploadResult] = useState<AdminExcelImportResult | null>(null);
|
const [userRoleUploadResult, setUserRoleUploadResult] = useState<AdminExcelImportResult | null>(null);
|
||||||
const [smsDiagnostics, setSmsDiagnostics] = useState<SmsDiagnosticsResult | null>(null);
|
const [smsDiagnostics, setSmsDiagnostics] = useState<SmsDiagnosticsResult | null>(null);
|
||||||
const [emailDiagnostics, setEmailDiagnostics] = useState<EmailDiagnosticsResult | 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 [notice, setNotice] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
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) => {
|
const editPurpose = (p: PurposeCode) => {
|
||||||
setEditingPurposeId(p.id);
|
setEditingPurposeId(p.id);
|
||||||
setPurposeForm({
|
setPurposeForm({
|
||||||
@@ -440,6 +464,7 @@ export const AdminManagementPage: React.FC = () => {
|
|||||||
<button className={tab === 'roles' ? 'tab-active' : ''} onClick={() => setTab('roles')}>권한관리</button>
|
<button className={tab === 'roles' ? 'tab-active' : ''} onClick={() => setTab('roles')}>권한관리</button>
|
||||||
<button className={tab === 'sms' ? 'tab-active' : ''} onClick={() => setTab('sms')}>SMS 점검</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 === 'email' ? 'tab-active' : ''} onClick={() => setTab('email')}>Email 점검</button>
|
||||||
|
<button className={tab === 'data' ? 'tab-active' : ''} onClick={() => setTab('data')}>데이터 정리</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === 'watcher' && (
|
{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' && (
|
{tab === 'sms' && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="admin-upload-head">
|
<div className="admin-upload-head">
|
||||||
|
|||||||
@@ -197,6 +197,14 @@ export interface AdminExcelImportResult {
|
|||||||
rows: AdminExcelImportRow[];
|
rows: AdminExcelImportRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VisitRequestResetResult {
|
||||||
|
visitRequests: number;
|
||||||
|
visitors: number;
|
||||||
|
approvals: number;
|
||||||
|
accessEvents: number;
|
||||||
|
passDeliveries: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InsideVisitor {
|
export interface InsideVisitor {
|
||||||
visitRequestId: number;
|
visitRequestId: number;
|
||||||
visitorName: string;
|
visitorName: string;
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ interface ImportResult {
|
|||||||
rows: ImportRow[];
|
rows: ImportRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VisitRequestResetResult {
|
||||||
|
visitRequests: number;
|
||||||
|
visitors: number;
|
||||||
|
approvals: number;
|
||||||
|
accessEvents: number;
|
||||||
|
passDeliveries: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface AdminRouterDeps {
|
interface AdminRouterDeps {
|
||||||
query: QueryFn;
|
query: QueryFn;
|
||||||
}
|
}
|
||||||
@@ -481,6 +489,50 @@ export function createAdminRouter(deps: AdminRouterDeps = { query }): Router {
|
|||||||
ok(res, await diagnoseEmailConnectivity());
|
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) => {
|
router.get('/teams', asyncRoute(async (req, res) => {
|
||||||
await requireAdmin(dbQuery, req);
|
await requireAdmin(dbQuery, req);
|
||||||
ok(res, await listTeams(dbQuery));
|
ok(res, await listTeams(dbQuery));
|
||||||
|
|||||||
Reference in New Issue
Block a user