import { useState, useEffect, useCallback } from 'react'; // ─── 타입 ────────────────────────────────────────────────── type TaskStatus = '완료' | '진행중' | '대기' | '오류'; interface AuditLogEntry { id: string; time: string; operator: string; operatorId: string; action: string; targetData: string; result: string; resultType: '성공' | '실패' | '거부' | '진행중'; ip: string; browser: string; detail: { dataCount: number; rulesApplied: string; processedCount: number; errorCount: number; }; } interface DeidentifyTask { id: string; name: string; target: string; status: TaskStatus; startTime: string; progress: number; createdBy: string; } type SourceType = 'db' | 'file' | 'api'; type ProcessMode = 'immediate' | 'scheduled' | 'oneshot'; type RepeatType = 'daily' | 'weekly' | 'monthly'; type DeidentifyTechnique = | '마스킹' | '삭제' | '범주화' | '암호화' | '샘플링' | '가명처리' | '유지'; interface FieldConfig { name: string; dataType: string; technique: DeidentifyTechnique; configValue: string; selected: boolean; } interface DbConfig { host: string; port: string; database: string; tableName: string; } interface ApiConfig { url: string; method: 'GET' | 'POST'; } interface ScheduleConfig { hour: string; repeatType: RepeatType; weekday: string; startDate: string; notifyOnComplete: boolean; notifyOnError: boolean; } interface OneshotConfig { date: string; hour: string; } interface WizardState { step: number; taskName: string; sourceType: SourceType; dbConfig: DbConfig; apiConfig: ApiConfig; fields: FieldConfig[]; processMode: ProcessMode; scheduleConfig: ScheduleConfig; oneshotConfig: OneshotConfig; saveAsTemplate: boolean; applyTemplate: string; confirmed: boolean; } // ─── Mock 데이터 ──────────────────────────────────────────── const MOCK_TASKS: DeidentifyTask[] = [ { id: '001', name: 'customer_2024', target: '선박/운항 - 선장·선원 성명', status: '완료', startTime: '2026-04-10 14:30', progress: 100, createdBy: '관리자' }, { id: '002', name: 'transaction_04', target: '사고 현장 - 현장사진, 영상내 인물', status: '진행중', startTime: '2026-04-10 14:15', progress: 82, createdBy: '김담당' }, { id: '003', name: 'employee_info', target: '인사정보 - 계정, 로그인 정보', status: '대기', startTime: '2026-04-10 22:00', progress: 0, createdBy: '이담당' }, { id: '004', name: 'vendor_data', target: 'HNS 대응 - 화학물질 취급자, 방제업체 연락처', status: '오류', startTime: '2026-04-09 13:45', progress: 45, createdBy: '관리자' }, { id: '005', name: 'partner_contacts', target: '시스템 운영 - 관리자, 운영자 접속로그', status: '완료', startTime: '2026-04-08 09:00', progress: 100, createdBy: '박담당' }, ]; const DEFAULT_FIELDS: FieldConfig[] = [ { name: '고객ID', dataType: '문자열', technique: '삭제', configValue: '-', selected: true }, { name: '이름', dataType: '문자열', technique: '마스킹', configValue: '*로 치환', selected: true }, { name: '휴대폰', dataType: '문자열', technique: '마스킹', configValue: '010-****-****', selected: true }, { name: '주소', dataType: '문자열', technique: '범주화', configValue: '시/도만 표시', selected: true }, { name: '이메일', dataType: '문자열', technique: '가명처리', configValue: '키: random_001', selected: true }, { name: '생년월일', dataType: '날짜', technique: '범주화', configValue: '연도만 표시', selected: true }, { name: '회사', dataType: '문자열', technique: '유지', configValue: '변경 없음', selected: true }, ]; const TECHNIQUES: DeidentifyTechnique[] = ['마스킹', '삭제', '범주화', '암호화', '샘플링', '가명처리', '유지']; const HOURS = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`); const WEEKDAYS = ['월', '화', '수', '목', '금', '토', '일']; const TEMPLATES = ['기본 개인정보', '금융데이터', '의료데이터']; const MOCK_AUDIT_LOGS: Record = { '001': [ { id: 'LOG_20260410_001', time: '2026-04-10 14:30:45', operator: '김철수', operatorId: 'user_12345', action: '처리완료', targetData: 'customer_2024', result: '성공 (100%)', resultType: '성공', ip: '192.168.1.100', browser: 'Chrome 123.0', detail: { dataCount: 15240, rulesApplied: '마스킹 3, 범주화 2, 삭제 2', processedCount: 15240, errorCount: 0 } }, { id: 'LOG_20260410_002', time: '2026-04-10 14:15:10', operator: '김철수', operatorId: 'user_12345', action: '처리시작', targetData: 'customer_2024', result: '성공', resultType: '성공', ip: '192.168.1.100', browser: 'Chrome 123.0', detail: { dataCount: 15240, rulesApplied: '마스킹 3, 범주화 2, 삭제 2', processedCount: 0, errorCount: 0 } }, { id: 'LOG_20260410_003', time: '2026-04-10 14:10:30', operator: '김철수', operatorId: 'user_12345', action: '규칙설정', targetData: 'customer_2024', result: '성공', resultType: '성공', ip: '192.168.1.100', browser: 'Chrome 123.0', detail: { dataCount: 15240, rulesApplied: '7개 규칙 적용', processedCount: 0, errorCount: 0 } }, ], '002': [ { id: 'LOG_20260410_004', time: '2026-04-10 14:15:22', operator: '이영희', operatorId: 'user_23456', action: '처리시작', targetData: 'transaction_04', result: '진행중 (82%)', resultType: '진행중', ip: '192.168.1.101', browser: 'Firefox 124.0', detail: { dataCount: 8920, rulesApplied: '마스킹 2, 암호화 1, 삭제 3', processedCount: 7314, errorCount: 0 } }, ], '003': [ { id: 'LOG_20260410_005', time: '2026-04-10 13:45:30', operator: '박민준', operatorId: 'user_34567', action: '규칙수정', targetData: 'employee_info', result: '성공', resultType: '성공', ip: '192.168.1.102', browser: 'Chrome 123.0', detail: { dataCount: 3200, rulesApplied: '마스킹 4, 가명처리 1', processedCount: 0, errorCount: 0 } }, ], '004': [ { id: 'LOG_20260409_001', time: '2026-04-09 13:45:30', operator: '관리자', operatorId: 'user_admin', action: '처리오류', targetData: 'vendor_data', result: '오류 (45%)', resultType: '실패', ip: '192.168.1.100', browser: 'Chrome 123.0', detail: { dataCount: 5100, rulesApplied: '마스킹 2, 범주화 1, 삭제 1', processedCount: 2295, errorCount: 12 } }, { id: 'LOG_20260409_002', time: '2026-04-09 13:40:15', operator: '김철수', operatorId: 'user_12345', action: '규칙조회', targetData: 'vendor_data', result: '성공', resultType: '성공', ip: '192.168.1.100', browser: 'Chrome 123.0', detail: { dataCount: 5100, rulesApplied: '4개 규칙', processedCount: 0, errorCount: 0 } }, { id: 'LOG_20260409_003', time: '2026-04-09 09:25:00', operator: '이영희', operatorId: 'user_23456', action: '삭제시도', targetData: 'vendor_data', result: '거부 (권한부족)', resultType: '거부', ip: '192.168.1.101', browser: 'Firefox 124.0', detail: { dataCount: 5100, rulesApplied: '-', processedCount: 0, errorCount: 0 } }, ], '005': [ { id: 'LOG_20260408_001', time: '2026-04-08 09:15:00', operator: '박담당', operatorId: 'user_45678', action: '처리완료', targetData: 'partner_contacts', result: '성공 (100%)', resultType: '성공', ip: '192.168.1.103', browser: 'Edge 122.0', detail: { dataCount: 1850, rulesApplied: '마스킹 2, 유지 3', processedCount: 1850, errorCount: 0 } }, ], }; function fetchTasks(): Promise { return new Promise((resolve) => { setTimeout(() => resolve(MOCK_TASKS), 300); }); } // ─── 상태 뱃지 ───────────────────────────────────────────── function getStatusBadgeClass(status: TaskStatus): string { switch (status) { case '완료': return 'text-emerald-400 bg-emerald-500/10'; case '진행중': return 'text-cyan-400 bg-cyan-500/10'; case '대기': return 'text-yellow-400 bg-yellow-500/10'; case '오류': return 'text-red-400 bg-red-500/10'; } } // ─── 진행률 바 ───────────────────────────────────────────── function ProgressBar({ value }: { value: number }) { const colorClass = value === 100 ? 'bg-emerald-500' : value > 0 ? 'bg-cyan-500' : 'bg-bg-elevated'; return (
{value}%
); } // ─── 작업 테이블 ──────────────────────────────────────────── const TABLE_HEADERS = ['작업ID', '작업명', '대상', '상태', '시작시간', '진행률', '등록자', '액션']; interface TaskTableProps { rows: DeidentifyTask[]; loading: boolean; onAction: (action: string, task: DeidentifyTask) => void; } function TaskTable({ rows, loading, onAction }: TaskTableProps) { return (
{TABLE_HEADERS.map((h) => ( ))} {loading && rows.length === 0 ? Array.from({ length: 5 }).map((_, i) => ( {TABLE_HEADERS.map((_, j) => ( ))} )) : rows.map((row) => ( ))}
{h}
{row.id} {row.name} {row.target} {row.status} {row.startTime} {row.createdBy}
); } // ─── 마법사: 단계 표시기 ──────────────────────────────────── const STEP_LABELS = ['소스선택', '데이터검증', '비식별화규칙', '처리방식', '최종확인']; function StepIndicator({ current }: { current: number }) { return (
{STEP_LABELS.map((label, i) => { const stepNum = i + 1; const isDone = stepNum < current; const isActive = stepNum === current; return (
{isDone ? ( ) : ( stepNum )}
{stepNum}.{label}
{i < STEP_LABELS.length - 1 && (
)}
); })}
); } // ─── 마법사: Step 1 ───────────────────────────────────────── interface Step1Props { wizard: WizardState; onChange: (patch: Partial) => void; } function Step1({ wizard, onChange }: Step1Props) { const handleDbChange = (key: keyof DbConfig, value: string) => { onChange({ dbConfig: { ...wizard.dbConfig, [key]: value } }); }; const handleApiChange = (key: keyof ApiConfig, value: string) => { onChange({ apiConfig: { ...wizard.apiConfig, [key]: value } }); }; return (
onChange({ taskName: e.target.value })} placeholder="작업 이름을 입력하세요" className="w-full px-3 py-2 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500" />
{([ ['db', '데이터베이스 연결'], ['file', '파일 업로드'], ['api', 'API 호출'], ] as [SourceType, string][]).map(([val, label]) => ( ))}
{wizard.sourceType === 'db' && (
{( [ ['host', '호스트', 'localhost'], ['port', '포트', '5432'], ['database', '데이터베이스', 'wing'], ['tableName', '테이블명', 'public.customers'], ] as [keyof DbConfig, string, string][] ).map(([key, labelText, placeholder]) => (
handleDbChange(key, e.target.value)} placeholder={placeholder} className="w-full px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500" />
))}
)} {wizard.sourceType === 'file' && (

파일을 드래그하거나 클릭하여 업로드

CSV, XLSX, JSON 지원 (최대 500MB)

)} {wizard.sourceType === 'api' && (
handleApiChange('url', e.target.value)} placeholder="https://api.example.com/data" className="w-full px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500" />
)}
); } // ─── 마법사: Step 2 ───────────────────────────────────────── interface Step2Props { wizard: WizardState; onChange: (patch: Partial) => void; } function Step2({ wizard, onChange }: Step2Props) { const toggleField = (idx: number) => { const updated = wizard.fields.map((f, i) => i === idx ? { ...f, selected: !f.selected } : f, ); onChange({ fields: updated }); }; return (
{[ { label: '총 데이터 건수', value: '15,240건', color: 'text-t1' }, { label: '중복', value: '0건', color: 'text-emerald-400' }, { label: '누락값', value: '23건', color: 'text-yellow-400' }, ].map((stat) => (

{stat.label}

{stat.value}

))}

스키마 분석 결과 — 포함 필드 선택

{wizard.fields.map((field, idx) => ( ))}
f.selected)} onChange={(e) => onChange({ fields: wizard.fields.map((f) => ({ ...f, selected: e.target.checked })) }) } className="accent-cyan-500" /> 필드명 데이터 타입
toggleField(idx)} className="accent-cyan-500" /> {field.name} {field.dataType}

{wizard.fields.filter((f) => f.selected).length}개 선택됨 (전체 {wizard.fields.length}개)

); } // ─── 마법사: Step 3 ───────────────────────────────────────── interface Step3Props { wizard: WizardState; onChange: (patch: Partial) => void; } function Step3({ wizard, onChange }: Step3Props) { const updateField = (idx: number, key: keyof FieldConfig, value: string | boolean) => { const updated = wizard.fields.map((f, i) => i === idx ? { ...f, [key]: value } : f, ); onChange({ fields: updated }); }; const selectedFields = wizard.fields.filter((f) => f.selected); return (
{selectedFields.map((field) => { const globalIdx = wizard.fields.findIndex((f) => f.name === field.name); return ( ); })}
필드명 데이터타입 선택된 기법 설정값
{field.name} {field.dataType} updateField(globalIdx, 'configValue', e.target.value)} className="w-full px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500" />
이전 템플릿 적용:
); } // ─── 마법사: Step 4 ───────────────────────────────────────── interface Step4Props { wizard: WizardState; onChange: (patch: Partial) => void; } function Step4({ wizard, onChange }: Step4Props) { const handleScheduleChange = (key: keyof ScheduleConfig, value: string | boolean) => { onChange({ scheduleConfig: { ...wizard.scheduleConfig, [key]: value } }); }; const handleOneshotChange = (key: keyof OneshotConfig, value: string) => { onChange({ oneshotConfig: { ...wizard.oneshotConfig, [key]: value } }); }; return (
{( [ ['immediate', '즉시 처리', '지금 바로 데이터를 비식별화합니다.'], ['scheduled', '배치 처리 - 정기 스케줄링', '반복 일정에 따라 자동으로 처리합니다.'], ['oneshot', '배치 처리 - 일회성', '지정한 날짜/시간에 한 번 처리합니다.'], ] as [ProcessMode, string, string][] ).map(([val, label, desc]) => (
{val === 'scheduled' && wizard.processMode === 'scheduled' && (
{( [ ['daily', '매일'], ['weekly', '주 1회'], ['monthly', '월 1회'], ] as [RepeatType, string][] ).map(([rt, rl]) => (
handleScheduleChange('repeatType', rt)} className="accent-cyan-500" /> {rl} {rt === 'weekly' && wizard.scheduleConfig.repeatType === 'weekly' && ( )}
))}
handleScheduleChange('startDate', e.target.value)} className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500" />
)} {val === 'oneshot' && wizard.processMode === 'oneshot' && (
handleOneshotChange('date', e.target.value)} className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500" />
)}
))}
); } // ─── 마법사: Step 5 ───────────────────────────────────────── interface Step5Props { wizard: WizardState; onChange: (patch: Partial) => void; } function Step5({ wizard, onChange }: Step5Props) { const selectedCount = wizard.fields.filter((f) => f.selected).length; const ruleCount = wizard.fields.filter((f) => f.selected && f.technique !== '유지').length; const processModeLabel: Record = { immediate: '즉시 처리', scheduled: `배치 - 정기 (${wizard.scheduleConfig.hour} / ${wizard.scheduleConfig.repeatType === 'daily' ? '매일' : wizard.scheduleConfig.repeatType === 'weekly' ? `주1회 ${wizard.scheduleConfig.weekday}요일` : '월1회'})`, oneshot: `배치 - 일회성 (${wizard.oneshotConfig.date} ${wizard.oneshotConfig.hour})`, }; const summaryRows = [ { label: '작업명', value: wizard.taskName || '(미입력)' }, { label: '소스', value: wizard.sourceType === 'db' ? `DB: ${wizard.dbConfig.database}.${wizard.dbConfig.tableName}` : wizard.sourceType === 'file' ? '파일 업로드' : `API: ${wizard.apiConfig.url}` }, { label: '데이터 건수', value: '15,240건' }, { label: '선택 필드 수', value: `${selectedCount}개` }, { label: '비식별화 규칙 수', value: `${ruleCount}개` }, { label: '처리 방식', value: processModeLabel[wizard.processMode] }, { label: '예상 처리시간', value: '약 3~5분' }, ]; return (
{summaryRows.map(({ label, value }) => ( ))}
{label} {value}
); } // ─── 마법사 모달 ───────────────────────────────────────────── const INITIAL_WIZARD: WizardState = { step: 1, taskName: '', sourceType: 'db', dbConfig: { host: '', port: '5432', database: '', tableName: '' }, apiConfig: { url: '', method: 'GET' }, fields: DEFAULT_FIELDS, processMode: 'immediate', scheduleConfig: { hour: '02:00', repeatType: 'daily', weekday: '월', startDate: '', notifyOnComplete: true, notifyOnError: true, }, oneshotConfig: { date: '', hour: '02:00' }, saveAsTemplate: false, applyTemplate: '', confirmed: false, }; // ─── 감사로그 모달 ───────────────────────────────────────── function getAuditResultClass(type: AuditLogEntry['resultType']): string { switch (type) { case '성공': return 'text-emerald-400 bg-emerald-500/10'; case '진행중': return 'text-cyan-400 bg-cyan-500/10'; case '실패': return 'text-red-400 bg-red-500/10'; case '거부': return 'text-yellow-400 bg-yellow-500/10'; } } interface AuditLogModalProps { task: DeidentifyTask; onClose: () => void; } function AuditLogModal({ task, onClose }: AuditLogModalProps) { const logs = MOCK_AUDIT_LOGS[task.id] ?? []; const [selectedLog, setSelectedLog] = useState(null); const [filterOperator, setFilterOperator] = useState('모두'); const [startDate, setStartDate] = useState('2026-04-01'); const [endDate, setEndDate] = useState('2026-04-11'); const operators = ['모두', ...Array.from(new Set(logs.map((l) => l.operator)))]; const filteredLogs = logs.filter((l) => { if (filterOperator !== '모두' && l.operator !== filterOperator) return false; return true; }); return (
{/* 헤더 */}

감시 감독 (감사로그) — {task.name}

{/* 필터 바 */}
기간: setStartDate(e.target.value)} className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500" /> ~ setEndDate(e.target.value)} className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500" /> 작업자:
{/* 로그 테이블 */}
{['시간', '작업자', '작업', '대상 데이터', '결과', '상세'].map((h) => ( ))} {filteredLogs.length === 0 ? ( ) : ( filteredLogs.map((log) => ( setSelectedLog(log)} > )) )}
{h}
감사로그가 없습니다.
{log.time.split(' ')[1]} {log.operator} {log.action} {log.targetData} {log.result}
{/* 로그 상세 정보 */} {selectedLog && (

로그 상세 정보

로그ID: {selectedLog.id}
타임스탬프: {selectedLog.time}
작업자: {selectedLog.operator} ({selectedLog.operatorId})
작업 유형: {selectedLog.action}
대상: {selectedLog.targetData} ({selectedLog.detail.dataCount.toLocaleString()}건)
적용 규칙: {selectedLog.detail.rulesApplied}
결과: {selectedLog.result} (처리: {selectedLog.detail.processedCount.toLocaleString()}, 오류: {selectedLog.detail.errorCount})
IP 주소: {selectedLog.ip}
브라우저: {selectedLog.browser}
)} {/* 하단 버튼 */}
); } // ─── 마법사 모달 ─────────────────────────────────────────── interface WizardModalProps { onClose: () => void; onSubmit: (wizard: WizardState) => void; } function WizardModal({ onClose, onSubmit }: WizardModalProps) { const [wizard, setWizard] = useState(INITIAL_WIZARD); const patch = useCallback((update: Partial) => { setWizard((prev) => ({ ...prev, ...update })); }, []); const handleNext = () => { if (wizard.step < 5) patch({ step: wizard.step + 1 }); }; const handlePrev = () => { if (wizard.step > 1) patch({ step: wizard.step - 1 }); }; const handleSubmit = () => { onSubmit(wizard); onClose(); }; const canProceed = () => { if (wizard.step === 1) return wizard.taskName.trim().length > 0; if (wizard.step === 2) return wizard.fields.some((f) => f.selected); if (wizard.step === 5) return wizard.confirmed; return true; }; return (
{/* 모달 헤더 */}

새 비식별화 작업

{/* 단계 표시기 */} {/* 단계 내용 */}
{wizard.step === 1 && } {wizard.step === 2 && } {wizard.step === 3 && } {wizard.step === 4 && } {wizard.step === 5 && }
{/* 푸터 버튼 */}
{wizard.step < 5 ? ( ) : ( )}
); } // ─── 메인 패널 ────────────────────────────────────────────── type FilterStatus = '모두' | TaskStatus; export default function DeidentifyPanel() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [showWizard, setShowWizard] = useState(false); const [auditTask, setAuditTask] = useState(null); const [searchName, setSearchName] = useState(''); const [filterStatus, setFilterStatus] = useState('모두'); const [filterPeriod, setFilterPeriod] = useState<'7' | '30' | '90'>('30'); const loadTasks = useCallback(async () => { setLoading(true); const data = await fetchTasks(); setTasks(data); setLoading(false); }, []); useEffect(() => { let isMounted = true; if (tasks.length === 0) { void Promise.resolve().then(() => { if (isMounted) void loadTasks(); }); } return () => { isMounted = false; }; }, [tasks.length, loadTasks]); const handleAction = useCallback((action: string, task: DeidentifyTask) => { // TODO: 실제 API 연동 시 각 액션에 맞는 API 호출로 교체 if (action === 'delete') { setTasks((prev) => prev.filter((t) => t.id !== task.id)); } else if (action === 'audit') { setAuditTask(task); } }, []); const handleWizardSubmit = useCallback((wizard: WizardState) => { const selectedFields = wizard.fields.filter((f) => f.selected).map((f) => f.name); const newTask: DeidentifyTask = { id: String(tasks.length + 1).padStart(3, '0'), name: wizard.taskName, target: selectedFields.join(', ') || '-', status: wizard.processMode === 'immediate' ? '진행중' : '대기', startTime: new Date().toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).replace(/\. /g, '-').replace('.', ''), progress: 0, createdBy: '관리자', }; setTasks((prev) => [newTask, ...prev]); }, [tasks.length]); const filteredTasks = tasks.filter((t) => { if (searchName && !t.name.includes(searchName)) return false; if (filterStatus !== '모두' && t.status !== filterStatus) return false; return true; }); const completedCount = tasks.filter((t) => t.status === '완료').length; const inProgressCount = tasks.filter((t) => t.status === '진행중').length; const errorCount = tasks.filter((t) => t.status === '오류').length; return (
{/* 헤더 */}

비식별화조치

{/* 상태 요약 */}
완료 {completedCount}건 진행중 {inProgressCount}건 {errorCount > 0 && ( 오류 {errorCount}건 )} 전체 {tasks.length}건
{/* 검색/필터 */}
setSearchName(e.target.value)} placeholder="작업명 검색" className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500 w-40" />
{/* 테이블 */}
{/* 감사로그 모달 */} {auditTask && ( setAuditTask(null)} /> )} {/* 마법사 모달 */} {showWizard && ( setShowWizard(false)} onSubmit={handleWizardSubmit} /> )}
); }