nextjs all 0722

============= app/page.tsx ============= 'use client' import { Fragment, type MouseEvent, type ReactNode, useEffect, useMemo, useRef, useState } from 'react' import SqlViewer from './components/SqlViewer' import VerificationTree from './components/VerificationTree' import { applyRewrite, loadMapperSqlIds, loadRewriteCandidates, loadScenarioFile, loadScenarioUiState, loadSettings, makeVerificationRunId, previewRewrite, runScenario, runVerification, saveScenarioFile, saveScenarioUiState, saveSettings, } from './lib/spring-api' import type { CryptoTarget, CryptoTypeHandlerMapping, MapperSqlItem, RewriteCandidate, ScenarioDefinition, ScenarioUiState, ScenarioVerificationHistory, Status, VerificationNode } from './lib/types' type Tab = 'verify' | 'targets' | 'scenarios' | 'settings' type ScenarioViewMode = 'json' | 'grid' type ColumnKind = 'encrypt' | 'hash' type ColumnAction = 'add' | 'drop' const targetMenuWidth = 220 const viewportMenuMargin = 12 const queryLogStorageKey = 'crypto-verify-query-logs' const queryLogChannelName = 'crypto-verify-query-log-channel' interface OptionGuideState { title: string description: string steps: string[] checks?: string[] } interface ContextMenuState { node: VerificationNode x: number y: number } interface TargetMenuState { x: number y: number } type QueryLogEntry = { scenarioId: string scenarioLabel: string queryKey: string queryId?: string mode?: string command?: string statement?: string fullSql: string parameter?: unknown updatedAt: string } type ScenarioSeed = Partial & { xmlText?: string } const emptyScenarioUiState: ScenarioUiState = { collapsedMapperFiles: [], collapsedSqlIds: [], verificationHistory: {}, cleanupInsertedRows: true, cleanupInsertedRowsByScenario: {}, } type ColumnLengthLookup = { loading?: boolean message?: string target?: string enc?: string hash?: string } type RandomColumnSample = { loading?: boolean message?: string values?: string[] } type PrimaryKeyColumn = { columnName: string dataType: string dataLength?: number charLength?: number | null charUsed?: string | null position?: number } function emptyTarget(): CryptoTarget { return { tableName: '', plainColumn: '', cryptoType: '', encColumn: '', hashColumn: '', javaProperty: '', encryptTypeHandler: '', hashTypeHandler: '', } } function emptyCryptoTypeHandlerMapping(): CryptoTypeHandlerMapping { return { cryptoType: '', encryptTypeHandler: '', hashTypeHandler: '', } } function normalizeCryptoTarget(target: Partial): CryptoTarget { return { ...emptyTarget(), ...target, tableName: (target.tableName ?? '').trim().toUpperCase(), plainColumn: (target.plainColumn ?? '').trim().toUpperCase(), cryptoType: (target.cryptoType ?? '').trim(), encColumn: (target.encColumn ?? '').trim().toUpperCase(), hashColumn: (target.hashColumn ?? '').trim().toUpperCase(), javaProperty: target.javaProperty ?? '', encryptTypeHandler: target.encryptTypeHandler ?? '', hashTypeHandler: target.hashTypeHandler ?? '', } } function normalizeCryptoTypeHandlerMapping(mapping: Partial): CryptoTypeHandlerMapping { return { cryptoType: (mapping.cryptoType ?? '').trim(), encryptTypeHandler: (mapping.encryptTypeHandler ?? '').trim(), hashTypeHandler: (mapping.hashTypeHandler ?? '').trim(), } } function cryptoTargetKey(target: CryptoTarget) { const tableName = target.tableName.trim().toUpperCase() const plainColumn = target.plainColumn.trim().toUpperCase() if (!tableName && !plainColumn) return '' return `${tableName}.${plainColumn}` } function uniqueCryptoTargets(targets: Array>) { const keyed = new Map() const keyless: CryptoTarget[] = [] for (const item of targets) { const target = normalizeCryptoTarget(item) const key = cryptoTargetKey(target) if (!key) { if (target.tableName || target.plainColumn || target.cryptoType || target.encColumn || target.hashColumn || target.javaProperty || target.encryptTypeHandler || target.hashTypeHandler) { keyless.push(target) } continue } keyed.set(key, target) } return [...keyed.values(), ...keyless] } function mergeCryptoTargets(currentTargets: CryptoTarget[], importedTargets: CryptoTarget[]) { return uniqueCryptoTargets([...currentTargets, ...importedTargets]) } function cryptoTypeHandlerMappingKey(mapping: CryptoTypeHandlerMapping) { return mapping.cryptoType.trim().toUpperCase() } function uniqueCryptoTypeHandlerMappings(mappings: Array>) { const keyed = new Map() const keyless: CryptoTypeHandlerMapping[] = [] for (const item of mappings) { const mapping = normalizeCryptoTypeHandlerMapping(item) const key = cryptoTypeHandlerMappingKey(mapping) if (!key) { if (mapping.encryptTypeHandler || mapping.hashTypeHandler) keyless.push(mapping) continue } keyed.set(key, mapping) } return [...keyed.values(), ...keyless] } function cryptoTypeMappingFor(mappings: CryptoTypeHandlerMapping[], cryptoType: string) { const normalized = cryptoType.trim().toUpperCase() if (!normalized) return undefined return mappings.find((mapping) => mapping.cryptoType.trim().toUpperCase() === normalized) } function applyCryptoTypeHandlersToTarget(target: CryptoTarget, mappings: CryptoTypeHandlerMapping[]): CryptoTarget { const mapping = cryptoTypeMappingFor(mappings, target.cryptoType) if (!mapping) return target return { ...target, encryptTypeHandler: mapping.encryptTypeHandler || target.encryptTypeHandler, hashTypeHandler: target.hashColumn ? mapping.hashTypeHandler || target.hashTypeHandler : target.hashTypeHandler, } } function toCamel(column: string) { return column .toLowerCase() .replace(/_([a-z])/g, (_, char: string) => char.toUpperCase()) } function sanitizeId(value: string) { return value.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase() || 'ROW' } function inferMaxLength(column: string) { const normalized = column.toUpperCase() if (normalized.includes('ADDRESS')) return 500 if (normalized.includes('PHONE')) return 40 if (normalized.includes('NAME')) return 300 return 100 } function inferEncryptTypeHandler(column: string) { const normalized = column.toUpperCase() if (normalized.includes('PHONE')) return 'PhoneEncryptTypeHandler' if (normalized.includes('ADDRESS')) return 'AddressEncryptTypeHandler' if (normalized.includes('NAME') || normalized.includes('BUYER') || normalized.includes('CUSTOMER')) return 'NameEncryptTypeHandler' return '' } function inferHashTypeHandler(column: string, encryptTypeHandler?: string) { if (encryptTypeHandler?.includes('EncryptTypeHandler')) { return encryptTypeHandler.replace('EncryptTypeHandler', 'HashTypeHandler') } const normalized = column.toUpperCase() if (normalized.includes('PHONE')) return 'PhoneHashTypeHandler' if (normalized.includes('ADDRESS')) return 'AddressHashTypeHandler' if (normalized.includes('NAME') || normalized.includes('BUYER') || normalized.includes('CUSTOMER')) return 'NameHashTypeHandler' return '' } function targetFromTableColumn(tableName: string, plainColumn: string, cryptoType = '', useHash = false): CryptoTarget { const table = tableName.trim().toUpperCase() const column = plainColumn.trim().toUpperCase() const encryptTypeHandler = inferEncryptTypeHandler(column) return { tableName: table, plainColumn: column, cryptoType: cryptoType.trim(), encColumn: column ? `${column}_ENC` : '', hashColumn: useHash && column ? `${column}_HASH` : '', javaProperty: toCamel(column), encryptTypeHandler, hashTypeHandler: useHash ? inferHashTypeHandler(column, encryptTypeHandler) : '', } } function parseCryptoTargetsText(text: string): CryptoTarget[] { const trimmed = text.trim() if (!trimmed) return [] if (trimmed.startsWith('[')) return (JSON.parse(trimmed) as Array>).map(normalizeCryptoTarget) const isHeaderLine = (line: string) => { const [tableName = '', columnName = '', cryptoType = ''] = line.split('|').map((part) => part.trim().toUpperCase()) const tableHeader = ['TABLE', 'TABLE_NAME', 'TABLENAME', '?뚯씠釉?].includes(tableName) const columnHeader = ['COLUMN', 'COLUMN_NAME', 'COLUMNNAME', 'COL', 'COLNAME', '而щ읆', '而щ읆紐?].includes(columnName) const cryptoTypeHeader = !cryptoType || ['CRYPTOTYPE', 'CRYPTO_TYPE', '?뷀샇?뷀???, '?뷀샇???€??].includes(cryptoType) return tableHeader && columnHeader && cryptoTypeHeader } return trimmed .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#') && !isHeaderLine(line)) .map((line) => { const [tableName = '', plainColumn = '', third = '', fourth = ''] = line.split('|').map((part) => part.trim()) if (!tableName || !plainColumn) { throw new Error(`TXT ?뺤떇 ?ㅻ쪟: ${line}`) } const thirdIsHashFlag = /^Y(?:ES)?$/i.test(third) const cryptoType = thirdIsHashFlag ? '' : third const useHash = thirdIsHashFlag || /^Y(?:ES)?$/i.test(fourth) return targetFromTableColumn(tableName, plainColumn, cryptoType, useHash) }) } function inferScenarioType(sqlCommandType: string | undefined, xmlText?: string) { const command = (sqlCommandType || '').toUpperCase() const xml = xmlText || '' if (command === 'INSERT' || command === 'UPDATE') return 'MAX_LENGTH_INSERT_UPDATE' if (command === 'SELECT' && /\bJOIN\b/i.test(xml)) return 'JOIN_HASH_COLUMN_MATCH' if (command === 'SELECT' && /_HASH\b/i.test(xml)) return 'HASH_WHERE_EXACT_MATCH' if (command === 'SELECT' && /_ENC\s+AS\s+/i.test(xml)) return 'SELECT_ENC_ALIAS_DECRYPT_MAPPING' return 'MAX_LENGTH_INSERT_UPDATE' } function inferTargetFromXml(xmlText: string | undefined, targets: CryptoTarget[]) { if (!xmlText) return undefined const xml = xmlText.toUpperCase() return targets .map((target) => { const tokens = [ target.tableName, target.plainColumn, target.encColumn, target.hashColumn, target.encryptTypeHandler, target.hashTypeHandler, ].filter(Boolean) const score = tokens.reduce((sum, token) => sum + (xml.includes(token.toUpperCase()) ? 1 : 0), 0) return { target, score } }) .filter((item) => item.score > 0) .sort((left, right) => right.score - left.score)[0]?.target } function hasEncryptHint(xmlText?: string) { return !!xmlText && /(typeHandler\s*=|_ENC\b|_HASH\b)/i.test(xmlText) } function mapperBase(mapperFile?: string) { const fileName = (mapperFile || 'UserMapper.xml').split(/[\\/]/).pop() || 'UserMapper.xml' return fileName.replace(/Mapper\.xml$/i, '') } function mapperNamespace(mapperFile?: string) { const base = mapperBase(mapperFile) return `com.samsung.cryptverify.business.${base.toLowerCase()}.${base}Mapper` } function dtoType(mapperFile?: string) { const base = mapperBase(mapperFile) return `com.samsung.cryptverify.business.${base.toLowerCase()}.${base}Dto` } function idColumnFor(tableName?: string) { if (tableName === 'TB_CUSTOMER') return 'CUSTOMER_ID' if (tableName === 'TB_ORDER') return 'ORDER_ID' return 'USER_ID' } function idPropertyFor(tableName?: string) { if (tableName === 'TB_CUSTOMER') return 'customerId' if (tableName === 'TB_ORDER') return 'orderId' return 'userId' } function defaultInsertSqlId(scenario: Pick) { if (scenario.sqlCommandType === 'INSERT' && scenario.sqlId) return scenario.sqlId if (scenario.targetTable === 'TB_CUSTOMER') return 'insertCustomer' if (scenario.targetTable === 'TB_ORDER') return 'insertOrder' return 'insertUser' } function defaultUpdateSqlId(targetTable?: string) { if (targetTable === 'TB_CUSTOMER') return 'updateCustomer' if (targetTable === 'TB_USER') return 'updateUserDynamic' return '' } function defaultSelectSqlId(targetTable?: string) { if (targetTable === 'TB_CUSTOMER') return 'selectCustomerByNameHash' if (targetTable === 'TB_ORDER') return 'selectOrdersByBuyerJoin' return 'selectUserByNameHash' } function defaultSelectStatementSqlId(scenario: Pick) { if (scenario.sqlCommandType === 'SELECT' && scenario.sqlId) return scenario.sqlId return defaultSelectSqlId(scenario.targetTable) } function dtoParameterForScenario(scenario: ScenarioDefinition, plainExpression: string) { const idProperty = idPropertyFor(scenario.targetTable) const parameter: Record = { [idProperty]: `\${${idProperty}}`, [scenario.javaProperty || 'plain']: plainExpression, verificationRunId: '${verificationRunId}', } if (scenario.targetTable === 'TB_USER') { parameter.phone = '${phone}' parameter.email = '${email}' } else if (scenario.targetTable === 'TB_CUSTOMER') { parameter.address = '${address}' parameter.gradeCode = '${gradeCode}' } else if (scenario.targetTable === 'TB_ORDER') { parameter.userId = '${userId}' parameter.amount = '${amount}' } return parameter } function rawOperationForScenario(scenario: ScenarioDefinition, name: string) { const idColumn = idColumnFor(scenario.targetTable) const idProperty = idPropertyFor(scenario.targetTable) return { name, mode: 'DYNAMIC_SQL', command: 'SELECT_ONE', table: scenario.targetTable, columns: [idColumn, scenario.targetColumn, scenario.encColumn, scenario.hashColumn, 'VERIFICATION_RUN_ID'].filter(Boolean), where: { [idColumn]: idProperty }, parameter: { [idProperty]: `\${${idProperty}}` }, resultKey: name, } } function primaryKeyColumnsForScenario(scenario: ScenarioDefinition): PrimaryKeyColumn[] { const value = scenario.defaultParameters?.__primaryKeyColumns return Array.isArray(value) ? value as PrimaryKeyColumn[] : [] } function pkPropertyForColumn(columnName: string) { return toCamel(columnName) } function randomPkTemplate(column: PrimaryKeyColumn) { const dataType = column.dataType.toUpperCase() if (/(NUMBER|INTEGER|DECIMAL|NUMERIC|FLOAT|DOUBLE)/.test(dataType)) { return `${Date.now() % 100000000}\${index}` } const maxLength = Number(column.charLength ?? column.dataLength ?? 40) const prefix = `CV${Date.now().toString(36).toUpperCase()}` if (maxLength <= 12) return `${prefix.slice(0, Math.max(2, maxLength - 3))}\${index}` return `${prefix}_\${index}` } function cleanupOperationForScenario(scenario: ScenarioDefinition) { const pkColumns = primaryKeyColumnsForScenario(scenario) if (pkColumns.length === 0) return null const where: Record = {} const parameter: Record = {} pkColumns.forEach((column) => { const property = pkPropertyForColumn(column.columnName) where[column.columnName] = property parameter[property] = `\${${property}}` }) return { name: 'cleanupByPk', mode: 'DYNAMIC_SQL', command: 'DELETE', table: scenario.targetTable, where, parameter, cleanup: true, resultKey: 'cleanupCount', } } function defaultOperationsForScenario(scenario: ScenarioDefinition): Array> { if (scenario.scenarioType === 'TYPEHANDLER_MISSING_DETECTION') return [] const namespace = mapperNamespace(scenario.mapperFile) const operations: Array> = [ { name: 'insert', mode: 'SQL_SESSION', command: 'INSERT', statement: `${namespace}.${defaultInsertSqlId(scenario)}`, parameterType: dtoType(scenario.mapperFile), parameter: dtoParameterForScenario(scenario, scenario.scenarioType === 'MAX_LENGTH_INSERT_UPDATE' ? `\${${scenario.javaProperty}}` : `\${${scenario.javaProperty}}`), resultKey: 'insertCount', }, rawOperationForScenario(scenario, 'raw'), ] const updateSqlId = scenario.scenarioType === 'MAX_LENGTH_INSERT_UPDATE' ? (scenario.sqlCommandType === 'UPDATE' && scenario.sqlId ? scenario.sqlId : defaultUpdateSqlId(scenario.targetTable)) : '' if (updateSqlId) { operations.push({ name: 'update', mode: 'SQL_SESSION', command: 'UPDATE', statement: `${namespace}.${updateSqlId}`, parameterType: dtoType(scenario.mapperFile), parameter: dtoParameterForScenario(scenario, '${updatedPlain}'), resultKey: 'updateCount', }) operations.push(rawOperationForScenario(scenario, 'rawAfterUpdate')) } operations.push({ name: 'decrypted', mode: 'SQL_SESSION', command: 'SELECT_FIRST', statement: `${namespace}.${defaultSelectStatementSqlId(scenario)}`, parameter: { [scenario.javaProperty || 'plain']: updateSqlId ? '${updatedPlain}' : `\${${scenario.javaProperty}}` }, resultKey: 'decrypted', }) const cleanup = cleanupOperationForScenario(scenario) if (cleanup) { operations.push(cleanup) } return operations } function defaultVerificationForScenario(scenario: ScenarioDefinition): Record | null { if (scenario.scenarioType === 'TYPEHANDLER_MISSING_DETECTION') return null const hasUpdate = scenario.scenarioType === 'MAX_LENGTH_INSERT_UPDATE' && !!defaultUpdateSqlId(scenario.targetTable) return { plain: hasUpdate ? '${updatedPlain}' : `\${${scenario.javaProperty}}`, rawResult: hasUpdate ? 'rawAfterUpdate' : 'raw', decryptedResult: 'decrypted', cipherColumn: scenario.encColumn, hashColumn: scenario.hashColumn, decryptedProperty: scenario.javaProperty, checks: [ 'CIPHER_NOT_PLAIN', 'HASH_NOT_PLAIN', 'DECRYPTED_EQUALS_PLAIN', 'ENCRYPT_TYPEHANDLER_SET_CALLED', 'HASH_TYPEHANDLER_SET_CALLED', 'ENCRYPT_TYPEHANDLER_GET_CALLED', ], } } function defaultParametersForScenario(scenario: Pick): Record { const idProperty = idPropertyFor(scenario.targetTable) return { [idProperty]: `${idProperty.toUpperCase()}_\${index}`, [scenario.javaProperty || toCamel(scenario.targetColumn || 'plain')]: `${toCamel(scenario.targetColumn || 'plain')}_verify_\${index}`, phone: '010-1234-0001', email: 'verify@example.com', address: 'address_verify_${index}', gradeCode: 'A', amount: 2000, verificationRunId: '${testRunId}', maxLength: inferMaxLength(scenario.targetColumn || ''), } } function hasOperations(value: unknown): value is Array> { return Array.isArray(value) && value.length > 0 } function regenerateScenarioExecution(scenario: ScenarioDefinition): ScenarioDefinition { return { ...scenario, operations: defaultOperationsForScenario(scenario), verification: defaultVerificationForScenario(scenario), } } function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function stripXmlNoise(xmlText?: string) { return (xmlText ?? '') .replace(//g, ' ') .replace(/]*>/gi, ' ') .replace(/<\/?(select|insert|update|delete)\b[^>]*>/gi, ' ') .replace(/\s+/g, ' ') } function tableAliases(sqlText: string, tableName: string) { const aliases = new Set([tableName.toUpperCase()]) const table = escapeRegExp(tableName) const patterns = [ new RegExp(`\\b(?:FROM|JOIN|INTO)\\s+${table}\\b(?:\\s+(?:AS\\s+)?([A-Z_][A-Z0-9_]*))?`, 'gi'), new RegExp(`\\bUPDATE\\s+${table}\\b(?:\\s+(?:AS\\s+)?([A-Z_][A-Z0-9_]*))?`, 'gi'), ] for (const pattern of patterns) { for (const match of sqlText.matchAll(pattern)) { const alias = match[1]?.toUpperCase() if (alias && !['SET', 'WHERE', 'VALUES', 'SELECT', 'ON'].includes(alias)) aliases.add(alias) } } return [...aliases] } function hasColumnReference(sqlText: string, columnName?: string, aliases: string[] = []) { if (!columnName) return false const column = escapeRegExp(columnName.toUpperCase()) if (new RegExp(`\\b${column}\\b`, 'i').test(sqlText)) return true return aliases.some((alias) => new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*${column}\\b`, 'i').test(sqlText)) } function hasColumnAssignment(sqlText: string, columnName?: string, aliases: string[] = []) { if (!columnName) return false const column = escapeRegExp(columnName.toUpperCase()) if (new RegExp(`\\b${column}\\b\\s*=`, 'i').test(sqlText)) return true return aliases.some((alias) => new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*${column}\\b\\s*=`, 'i').test(sqlText)) } function whereFragments(sqlText: string) { const fragments: string[] = [] const pattern = /\bWHERE\b([\s\S]*?)(?=\bGROUP\s+BY\b|\bORDER\s+BY\b|\bHAVING\b|\bUNION\b|<\/(?:select|update|delete)>|$)/gi for (const match of sqlText.matchAll(pattern)) { fragments.push(match[1]) } return fragments } function columnUsedInWhere(xmlText: string | undefined, tableName: string, columnName: string) { if (!xmlText || !tableName || !columnName) return false const sql = stripXmlNoise(xmlText).toUpperCase() if (!new RegExp(`\\b${escapeRegExp(tableName.toUpperCase())}\\b`, 'i').test(sql)) return false const aliases = tableAliases(sql, tableName) const column = escapeRegExp(columnName.toUpperCase()) const columnPattern = new RegExp(`\\b${column}\\b`, 'i') const aliasPatterns = aliases.map((alias) => new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*${column}\\b`, 'i')) return whereFragments(sql).some((where) => columnPattern.test(where) || aliasPatterns.some((pattern) => pattern.test(where))) } function targetAppearsInMapper(item: MapperSqlItem, target: CryptoTarget) { const sql = stripXmlNoise(item.xmlText).toUpperCase() const aliases = tableAliases(sql, target.tableName) const tableMentioned = new RegExp(`\\b${escapeRegExp(target.tableName.toUpperCase())}\\b`, 'i').test(sql) const columnMentioned = [ target.plainColumn, target.encColumn, target.hashColumn, ].some((column) => hasColumnReference(sql, column, aliases)) return tableMentioned && columnMentioned } function scenarioTypesForMapperItem(item: MapperSqlItem, target: CryptoTarget) { const command = item.sqlCommandType.toUpperCase() const sql = stripXmlNoise(item.xmlText).toUpperCase() const aliases = tableAliases(sql, target.tableName) const hasHash = !!target.hashColumn?.trim() const types = new Set() if (command === 'INSERT') { types.add('INSERT_ROUND_TRIP') types.add('MAX_LENGTH_INSERT_UPDATE') } if (command === 'UPDATE') { types.add('MAX_LENGTH_INSERT_UPDATE') const updatesEncryptedTarget = hasColumnAssignment(sql, target.encColumn, aliases) || hasColumnAssignment(sql, target.plainColumn, aliases) if (hasHash && updatesEncryptedTarget) { types.add('TYPEHANDLER_MISSING_DETECTION') } } if (command === 'SELECT') { if (/\bJOIN\b/i.test(sql) && hasHash && hasColumnReference(sql, target.hashColumn, aliases)) { types.add('JOIN_HASH_COLUMN_MATCH') } if (hasHash && /\bWHERE\b/i.test(sql) && hasColumnReference(sql, target.hashColumn, aliases)) { types.add('HASH_WHERE_EXACT_MATCH') } if ( hasColumnReference(sql, target.encColumn, aliases) || new RegExp(`${escapeRegExp(target.encColumn.toUpperCase())}\\s+AS\\s+${escapeRegExp(target.plainColumn.toUpperCase())}`, 'i').test(sql) ) { types.add('SELECT_ENC_ALIAS_DECRYPT_MAPPING') } } return [...types] } function discoveredScenarioFromMapperItem(item: MapperSqlItem, target: CryptoTarget, scenarioType: string): ScenarioDefinition { const base: ScenarioDefinition = { scenarioId: `AUTO_${sanitizeId(item.mapperFile)}_${sanitizeId(item.sqlId)}_${sanitizeId(target.plainColumn)}_${sanitizeId(scenarioType)}`, displayName: `${item.sqlId} ${scenarioTypeLabel(scenarioType)}`, mapperFile: item.mapperFile, sqlId: item.sqlId, sqlCommandType: item.sqlCommandType, scenarioType, targetTable: target.tableName, targetColumn: target.plainColumn, encColumn: target.encColumn, hashColumn: target.hashColumn, javaProperty: target.javaProperty, encryptTypeHandler: target.encryptTypeHandler, hashTypeHandler: target.hashTypeHandler, executionMode: scenarioType === 'TYPEHANDLER_MISSING_DETECTION' ? undefined : 'SQL_SESSION', defaultParameters: {}, } base.defaultParameters = { ...defaultParametersForScenario(base), discoveredFrom: 'mapper-rescan', } return regenerateScenarioExecution(base) } function discoverScenariosFromMappers(mapperItems: MapperSqlItem[], targets: CryptoTarget[]) { const scenarios: ScenarioDefinition[] = [] const seen = new Set() for (const item of mapperItems) { if (!['INSERT', 'UPDATE', 'SELECT'].includes(item.sqlCommandType.toUpperCase())) continue for (const target of targets) { if (!target.tableName || !target.plainColumn || !target.encColumn) continue if (!targetAppearsInMapper(item, target)) continue for (const scenarioType of scenarioTypesForMapperItem(item, target)) { const scenario = discoveredScenarioFromMapperItem(item, target, scenarioType) if (seen.has(scenario.scenarioId)) continue seen.add(scenario.scenarioId) scenarios.push(scenario) } } } return scenarios } async function loadLocalTargets() { const response = await fetch('/api/targets', { cache: 'no-store' }) if (!response.ok) throw new Error(await response.text()) const items = await response.json() as Array> return uniqueCryptoTargets(items) } async function saveLocalTargets(targets: CryptoTarget[]) { const normalizedTargets = uniqueCryptoTargets(targets) const response = await fetch('/api/targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(normalizedTargets), }) if (!response.ok) throw new Error(await response.text()) const items = await response.json() as Array> return uniqueCryptoTargets(items) } async function loadCryptoTypeHandlerMappings() { const response = await fetch('/api/crypto-type-handler-mappings', { cache: 'no-store' }) if (!response.ok) throw new Error(await response.text()) const items = await response.json() as Array> return uniqueCryptoTypeHandlerMappings(items) } async function saveCryptoTypeHandlerMappings(mappings: CryptoTypeHandlerMapping[]) { const normalizedMappings = uniqueCryptoTypeHandlerMappings(mappings) const response = await fetch('/api/crypto-type-handler-mappings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(normalizedMappings), }) if (!response.ok) throw new Error(await response.text()) const items = await response.json() as Array> return uniqueCryptoTypeHandlerMappings(items) } async function loadTreeState() { const response = await fetch('/api/tree-state', { cache: 'no-store' }) if (!response.ok) return { expandedIds: ['root'] } return response.json() as Promise<{ expandedIds: string[] }> } async function saveTreeState(expandedIds: string[]) { await fetch('/api/tree-state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expandedIds }), }) } export default function Page() { const [tab, setTab] = useState('targets') const [running, setRunning] = useState(false) const [verificationRunId, setVerificationRunId] = useState('') const [plainValue, setPlainValue] = useState('') const [tree, setTree] = useState([]) const [selected, setSelected] = useState() const [expandedIds, setExpandedIds] = useState(['root']) const [contextMenu, setContextMenu] = useState() const [optionGuide, setOptionGuide] = useState() const [queryLogs, setQueryLogs] = useState([]) const [operationRunningKeys, setOperationRunningKeys] = useState>({}) const [operationQueryCaptures, setOperationQueryCaptures] = useState>({}) const [targets, setTargets] = useState([]) const [cryptoTypeHandlerMappings, setCryptoTypeHandlerMappings] = useState([]) const [targetMessage, setTargetMessage] = useState('') const [targetError, setTargetError] = useState('') const [targetMenu, setTargetMenu] = useState() const [columnJob, setColumnJob] = useState<{ action: ColumnAction, kind: ColumnKind } | null>(null) const [targetSamples, setTargetSamples] = useState>({}) const [scenarioJson, setScenarioJson] = useState('') const [scenarioFileName, setScenarioFileName] = useState('verification-scenarios.json') const [scenarioViewMode, setScenarioViewMode] = useState('grid') const [scenarioMessage, setScenarioMessage] = useState('') const [scenarioRunning, setScenarioRunning] = useState(false) const [scenarioUiState, setScenarioUiState] = useState(emptyScenarioUiState) const [scenarioUiStateLoaded, setScenarioUiStateLoaded] = useState(false) const [mapperSqlItems, setMapperSqlItems] = useState([]) const [mapperRoot, setMapperRoot] = useState('') const [settingsMessage, setSettingsMessage] = useState('') const [settingsError, setSettingsError] = useState('') const [rewrites, setRewrites] = useState([]) const [selectedRewrite, setSelectedRewrite] = useState() const [rewriteAfterSql, setRewriteAfterSql] = useState('') const [error, setError] = useState('') const fileInputRef = useRef(null) const scenarioFileInputRef = useRef(null) const scenarios = useMemo(() => { try { const parsed = JSON.parse(scenarioJson) as ScenarioDefinition[] return Array.isArray(parsed) ? parsed : [] } catch { return [] } }, [scenarioJson]) const detailJson = useMemo(() => { if (!selected?.details) return '' return JSON.stringify(selected.details, null, 2) }, [selected]) const selectedScenario = useMemo(() => { if (!selected) return undefined return findScenarioForNode(selected, scenarios) }, [selected, scenarios]) function cleanupEnabledForScenario(scenarioId: string) { return scenarioUiState.cleanupInsertedRowsByScenario?.[scenarioId] ?? scenarioUiState.cleanupInsertedRows ?? true } function verificationOverrides(scenarioIds: string[] = scenarios.map((scenario) => scenario.scenarioId)) { return { verificationRunId, userName: plainValue, buyerName: plainValue, cleanupInsertedRows: scenarioUiState.cleanupInsertedRows ?? true, cleanupInsertedRowsByScenario: Object.fromEntries(scenarioIds.map((scenarioId) => [scenarioId, cleanupEnabledForScenario(scenarioId)])), } } function appendQueryLogsFromResult(result: VerificationNode) { const entries = collectQueryLogs(result) if (entries.length === 0) return setQueryLogs((current) => [...entries, ...current].slice(0, 200)) } function openQueryLogWindow() { window.localStorage.setItem(queryLogStorageKey, JSON.stringify(queryLogs)) window.open('/query-log', 'cryptoVerifyQueryLog', 'width=1180,height=820,menubar=no,toolbar=no,location=no,status=no') } function operationUiKey(scenarioId: string, operationIndex: number) { return `${scenarioId}:${operationIndex}` } async function runScenarioOperation(scenario: ScenarioDefinition, operation: Record, operationIndex: number) { const uiKey = operationUiKey(scenario.scenarioId, operationIndex) const resultKey = String(operation.resultKey ?? operation.name ?? `operation${operationIndex + 1}`) setError('') setOperationRunningKeys((current) => ({ ...current, [uiKey]: true })) try { const operationScenarioId = `${scenario.scenarioId}__OP_${operationIndex + 1}` const cleanupEnabled = cleanupEnabledForScenario(scenario.scenarioId) const operationScenario: ScenarioDefinition = { ...scenario, scenarioId: operationScenarioId, displayName: `${scenario.displayName || scenario.scenarioId} / ${operation.name ?? resultKey}`, operations: [operation], verification: { checks: [], rawResult: resultKey, decryptedResult: resultKey }, } const result = await runScenario(operationScenarioId, { scenario: operationScenario, overrides: { ...verificationOverrides([operationScenarioId]), cleanupInsertedRows: cleanupEnabled, cleanupInsertedRowsByScenario: { [operationScenarioId]: cleanupEnabled }, }, }) appendQueryLogsFromResult(result) const capture = firstExecutedQueryCapture(result, resultKey) if (capture) { setOperationQueryCaptures((current) => ({ ...current, [uiKey]: capture })) } setScenarioMessage(`?ㅽ띁?덉씠???ㅽ뻾 ?꾨즺: ${scenario.scenarioId} / ${resultKey}`) } catch (error) { setError(error instanceof Error ? error.message : '?ㅽ띁?덉씠???ㅽ뻾 ?ㅽ뙣') } finally { setOperationRunningKeys((current) => ({ ...current, [uiKey]: false })) } } async function runAll() { setRunning(true) setError('') try { const result = await runVerification({ scenarios, overrides: verificationOverrides(), }) appendQueryLogsFromResult(result) const nextTree = recalculateTree([result]) setTree(nextTree) const ids = collectExpandableIds(nextTree) setExpandedIds(ids) await saveTreeState(ids) setSelected(result.children?.[0]?.children?.[0]?.children?.[0] ?? result) } catch (e) { setError(e instanceof Error ? e.message : '寃€利??ㅽ뻾 ?ㅽ뙣') } finally { setRunning(false) } } async function runTreeNode(node: VerificationNode) { setError('') try { if (node.type === 'SCENARIO') { const scenario = scenarios.find((item) => item.scenarioId === node.id) if (!scenario) throw new Error(`Scenario not found in Next.js JSON: ${node.id}`) const result = await runScenario(node.id, { scenario, overrides: verificationOverrides([node.id]), }) appendQueryLogsFromResult(result) setTree((items) => recalculateTree(replaceNode(items, result))) setSelected(result) return } const scenarioIds = collectScenarioIds(node) if (scenarioIds.length === 0) { setSelected(node) return } const results = await Promise.all(scenarioIds.map((scenarioId) => { const scenario = scenarios.find((item) => item.scenarioId === scenarioId) if (!scenario) throw new Error(`Scenario not found in Next.js JSON: ${scenarioId}`) return runScenario(scenarioId, { scenario, overrides: verificationOverrides([scenarioId]), }) })) results.forEach(appendQueryLogsFromResult) setTree((items) => recalculateTree(results.reduce((next, result) => replaceNode(next, result), items))) setSelected(results[0]) } catch (e) { setError(e instanceof Error ? e.message : '媛쒕퀎 寃€利??ㅽ뙣') } } async function runScenarioIdsSequentially(scenarioIds: string[]) { const uniqueIds = [...new Set(scenarioIds)].filter(Boolean) if (uniqueIds.length === 0 || scenarioRunning) return setScenarioRunning(true) setScenarioMessage('') setError('') try { for (const scenarioId of uniqueIds) { const scenario = scenarios.find((item) => item.scenarioId === scenarioId) if (!scenario) { setScenarioUiState((current) => ({ ...current, verificationHistory: { ...current.verificationHistory, [scenarioId]: { status: 'FAIL', progress: 100, updatedAt: new Date().toISOString(), label: scenarioId, exceptionMessage: `Scenario not found in Next.js JSON: ${scenarioId}`, }, }, })) continue } setScenarioUiState((current) => ({ ...current, verificationHistory: { ...current.verificationHistory, [scenarioId]: { ...(current.verificationHistory[scenarioId] ?? {}), status: 'RUNNING', progress: 0, updatedAt: new Date().toISOString(), label: scenario.displayName || scenario.scenarioId, }, }, })) try { const result = await runScenario(scenarioId, { scenario, overrides: verificationOverrides([scenarioId]), }) appendQueryLogsFromResult(result) const history: ScenarioVerificationHistory = { status: result.status, progress: result.progress ?? 100, updatedAt: new Date().toISOString(), label: result.label || scenario.displayName || scenario.scenarioId, elapsedMs: result.elapsedMs, exceptionMessage: result.exceptionMessage, details: result.details, } setScenarioUiState((current) => ({ ...current, verificationHistory: { ...current.verificationHistory, [scenarioId]: history, }, })) setTree((items) => recalculateTree(replaceNode(items, result))) setSelected(result) } catch (e) { setScenarioUiState((current) => ({ ...current, verificationHistory: { ...current.verificationHistory, [scenarioId]: { status: 'FAIL', progress: 100, updatedAt: new Date().toISOString(), label: scenario.displayName || scenario.scenarioId, exceptionMessage: e instanceof Error ? e.message : '寃€利??ㅽ뙣', }, }, })) } } setScenarioMessage(`?쒖감 寃€利??꾨즺: ${uniqueIds.length}嫄?) } finally { setScenarioRunning(false) } } function toggleNode(nodeId: string) { setExpandedIds((current) => { const next = current.includes(nodeId) ? current.filter((id) => id !== nodeId) : [...current, nodeId] saveTreeState(next) return next }) } function openContextMenu(node: VerificationNode, x: number, y: number) { setContextMenu({ node, x, y }) } function openTargetMenu(event: MouseEvent) { const rect = event.currentTarget.getBoundingClientRect() const maxLeft = window.innerWidth - targetMenuWidth - viewportMenuMargin setTargetMenu({ x: Math.max(viewportMenuMargin, Math.min(rect.right - targetMenuWidth, maxLeft)), y: Math.max(viewportMenuMargin, rect.top), }) } function syncTarget(index: number, plainColumn: string) { setTargets((items) => items.map((item, i) => { if (i !== index) return item const base = plainColumn.toUpperCase() const encryptTypeHandler = item.encryptTypeHandler || inferEncryptTypeHandler(base) return applyCryptoTypeHandlersToTarget({ ...item, plainColumn: base, encColumn: `${base}_ENC`, hashColumn: item.hashColumn ? `${base}_HASH` : '', javaProperty: toCamel(base), encryptTypeHandler, hashTypeHandler: item.hashTypeHandler || (item.hashColumn ? inferHashTypeHandler(base, encryptTypeHandler) : ''), }, cryptoTypeHandlerMappings) })) } function updateTarget(index: number, patch: Partial, applyHandlerMapping = false) { setTargets((items) => items.map((item, i) => { if (i !== index) return item const next = { ...item, ...patch } return applyHandlerMapping ? applyCryptoTypeHandlersToTarget(next, cryptoTypeHandlerMappings) : next })) } function addCryptoTypeHandlerMappingRow() { setCryptoTypeHandlerMappings((items) => [...items, emptyCryptoTypeHandlerMapping()]) } function updateCryptoTypeHandlerMapping(index: number, patch: Partial) { setCryptoTypeHandlerMappings((items) => { const next = items.map((item, i) => i === index ? normalizeCryptoTypeHandlerMapping({ ...item, ...patch }) : item) setTargets((currentTargets) => currentTargets.map((target) => applyCryptoTypeHandlersToTarget(target, next))) return next }) } function deleteCryptoTypeHandlerMappingRow(index: number) { setCryptoTypeHandlerMappings((items) => items.filter((_, i) => i !== index)) } function addTargetRow() { setTargets((items) => [...items, emptyTarget()]) } function deleteTargetRow(index: number) { setTargets((items) => items.filter((_, i) => i !== index)) } async function runCryptoColumnJob(action: ColumnAction, kind: ColumnKind) { const columnLabel = kind === 'encrypt' ? '?뷀샇?붿뺄?? : '?댁떆而щ읆' const actionLabel = action === 'add' ? '?쇨큵異붽?' : '?쇨큵??젣' if (action === 'drop' && !window.confirm(`${columnLabel}${actionLabel}瑜??ㅽ뻾?좉퉴?? DB 而щ읆????젣?⑸땲??`)) return setTargetMessage(`${columnLabel}${actionLabel} ?ㅽ뻾以?..`) setTargetError('') setTargetMenu(undefined) setColumnJob({ action, kind }) try { const response = await fetch('/api/db-add-crypto-columns', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, kind, targets }), }) const payload = await response.json() as { message?: string summary?: { created: number, dropped: number, skipped: number, failed: number } results?: Array<{ tableName: string, newColumn: string, status: string, message?: string, columnType?: string }> } if (!response.ok) throw new Error(payload.message || `${columnLabel}${actionLabel} ?ㅽ뙣`) const summary = payload.summary ?? { created: 0, dropped: 0, skipped: 0, failed: 0 } const changed = action === 'add' ? `?앹꽦 ${summary.created}媛? : `??젣 ${summary.dropped}媛? setTargetMessage(`${columnLabel}${actionLabel} ?꾨즺: ${changed}, 嫄대꼫?€ ${summary.skipped}媛? ?ㅽ뙣 ${summary.failed}媛?) const failed = (payload.results ?? []).filter((result) => result.status === 'failed') setTargetError(failed.map((result) => `${result.tableName}.${result.newColumn}: ${result.message}`).join('\n')) } catch (e) { setTargetMessage('') setTargetError(e instanceof Error ? e.message : `${columnLabel}${actionLabel} ?ㅽ뙣`) } finally { setColumnJob(null) } } async function saveTargetsToJson() { setTargetMessage('') setTargetError('') try { const [saved, savedMappings] = await Promise.all([ saveLocalTargets(targets), saveCryptoTypeHandlerMappings(cryptoTypeHandlerMappings), ]) setTargets(saved) setCryptoTypeHandlerMappings(savedMappings) setTargetMessage(`verify-next/data/crypto-targets.json ?€???꾨즺: ${saved.length}媛? verify-next/data/handler-mapping.json ?€???꾨즺: ${savedMappings.length}媛?) } catch (e) { setTargetError(e instanceof Error ? e.message : '?뷀샇???€???€???ㅽ뙣') } } async function importTargets(file: File | undefined) { if (!file) return setTargetMessage('') setTargetError('') try { const parsed = parseCryptoTargetsText(await file.text()) if (!Array.isArray(parsed)) throw new Error('JSON 理쒖긽??援ъ“媛€ 諛곗뿴???꾨떃?덈떎.') const mappedParsed = parsed.map((target) => applyCryptoTypeHandlersToTarget(target, cryptoTypeHandlerMappings)) const saved = await saveLocalTargets(mergeCryptoTargets(targets, mappedParsed)) setTargets(saved) setTargetMessage(`JSON 遺덈윭?ㅺ린 諛?濡쒖뺄 ?€???꾨즺: ${saved.length}媛?) } catch (e) { setTargetError(e instanceof Error ? e.message : 'JSON 遺덈윭?ㅺ린 ?ㅽ뙣') } finally { if (fileInputRef.current) fileInputRef.current.value = '' } } async function autoDetectHashColumns() { setTargetMessage('') setTargetError('') try { const savedSettings = await saveSettings({ mapperRoot }) setMapperRoot(savedSettings.mapperRoot) const items = await loadMapperSqlIds() setMapperSqlItems(items) let detected = 0 const nextTargets = targets.map((target) => { if (!target.tableName || !target.plainColumn) return target const used = items.some((item) => columnUsedInWhere(item.xmlText, target.tableName, target.plainColumn)) if (!used) return target const hashColumn = target.hashColumn || `${target.plainColumn}_HASH` const hashTypeHandler = target.hashTypeHandler || inferHashTypeHandler(target.plainColumn, target.encryptTypeHandler) if (hashColumn !== target.hashColumn || hashTypeHandler !== target.hashTypeHandler) detected += 1 return applyCryptoTypeHandlersToTarget({ ...target, hashColumn, hashTypeHandler }, cryptoTypeHandlerMappings) }) const saved = await saveLocalTargets(nextTargets) setTargets(saved) setTargetMessage(`?댁돩而щ읆 ?먮룞?먯깋 ?꾨즺: ${detected}媛?媛깆떊`) } catch (e) { setTargetError(e instanceof Error ? e.message : '?댁돩而щ읆 ?먮룞?먯깋 ?ㅽ뙣') } } async function fetchRandomColumnSample(target: CryptoTarget, key: string) { if (!target.tableName || !target.plainColumn) return setTargetSamples((current) => ({ ...current, [key]: { ...(current[key] ?? {}), loading: true, message: '?쒕뜡媛?議고쉶以? } })) try { const response = await fetch(`/api/db-random-column-values?table=${encodeURIComponent(target.tableName)}&column=${encodeURIComponent(target.plainColumn)}`, { cache: 'no-store' }) const payload = await response.json() as { values?: string[], message?: string } if (!response.ok) throw new Error(payload.message || '?쒕뜡 而щ읆 媛?議고쉶 ?ㅽ뙣') setTargetSamples((current) => ({ ...current, [key]: { loading: false, values: payload.values ?? [], message: (payload.values ?? []).length > 0 ? undefined : '議고쉶??媛??놁쓬', }, })) } catch (e) { setTargetSamples((current) => ({ ...current, [key]: { loading: false, values: [], message: e instanceof Error ? e.message : '?쒕뜡 而щ읆 媛?議고쉶 ?ㅽ뙣' }, })) } } async function saveScenarioJson() { setScenarioMessage('') const parsed = JSON.parse(scenarioJson) as ScenarioDefinition[] const saved = await saveScenarioFile(scenarioFileName, parsed) setScenarioFileName(saved.fileName) await saveScenarioUiState(scenarioUiState) setScenarioMessage(`${saved.fileName} ?€???꾨즺`) } async function importScenarioJson(file: File | undefined) { if (!file) return setScenarioMessage('') try { const parsed = JSON.parse(await file.text()) as ScenarioDefinition[] if (!Array.isArray(parsed)) throw new Error('寃€利앹떆?섎━??JSON?€ 諛곗뿴?댁뼱???⑸땲??') const saved = await saveScenarioFile(file.name, parsed) setScenarioFileName(saved.fileName) setScenarioJson(JSON.stringify(saved.scenarios, null, 2)) const nextUiState = emptyScenarioUiState setScenarioUiState(nextUiState) await saveScenarioUiState(nextUiState) setTree((items) => syncTreeWithScenarios(items, saved.scenarios)) setScenarioMessage(`${saved.fileName} 濡쒕뱶 ?꾨즺`) } catch (e) { setScenarioMessage(e instanceof Error ? e.message : '?쒕굹由ъ삤 JSON 濡쒕뱶 ?ㅽ뙣') } finally { if (scenarioFileInputRef.current) scenarioFileInputRef.current.value = '' } } function updateScenario(index: number, patch: Partial) { const shouldRegenerate = [ 'scenarioType', 'targetTable', 'targetColumn', 'encColumn', 'hashColumn', 'javaProperty', 'mapperFile', 'sqlId', 'sqlCommandType', 'encryptTypeHandler', 'hashTypeHandler', ].some((key) => Object.prototype.hasOwnProperty.call(patch, key)) const next = scenarios.map((scenario, i) => { if (i !== index) return scenario const merged: ScenarioDefinition = { ...scenario, ...patch } if (patch.targetTable || patch.targetColumn) { const target = targets.find((item) => item.tableName === merged.targetTable && item.plainColumn === merged.targetColumn) if (target) { merged.encColumn = target.encColumn merged.hashColumn = target.hashColumn merged.javaProperty = target.javaProperty merged.encryptTypeHandler = target.encryptTypeHandler merged.hashTypeHandler = target.hashTypeHandler } else if (patch.targetColumn) { const base = merged.targetColumn.toUpperCase() merged.targetColumn = base merged.encColumn = `${base}_ENC` merged.hashColumn = `${base}_HASH` merged.javaProperty = toCamel(base) } } if (patch.scenarioType || patch.targetTable || patch.targetColumn || patch.javaProperty) { merged.defaultParameters = { ...(merged.defaultParameters ?? {}), ...defaultParametersForScenario(merged), } } if (shouldRegenerate) { return regenerateScenarioExecution(merged) } return merged }) setScenarioJson(JSON.stringify(next, null, 2)) } function addScenario(seed: ScenarioSeed) { const inferredTarget = inferTargetFromXml(seed.xmlText, targets) const target = targets.find((item) => item.tableName === seed.targetTable && item.plainColumn === seed.targetColumn) ?? inferredTarget ?? targets[0] const mapperBase = seed.mapperFile || 'ManualMapper.xml' const sqlBase = seed.sqlId || 'manualSqlId' const timestamp = Date.now() const inferredScenarioType = inferScenarioType(seed.sqlCommandType, seed.xmlText) const seedCommand = (seed.sqlCommandType || '').toUpperCase() const scenarioType = seedCommand === 'INSERT' || seedCommand === 'UPDATE' ? inferredScenarioType : seed.scenarioType || inferredScenarioType const nextScenarioBase: ScenarioDefinition = { scenarioId: `MANUAL_${sanitizeId(mapperBase)}_${sanitizeId(sqlBase)}_${timestamp}`, displayName: `${sqlBase} ${scenarioTypeLabel(scenarioType)}`, mapperFile: mapperBase, sqlId: sqlBase, sqlCommandType: seed.sqlCommandType || 'UPDATE', scenarioType, targetTable: seed.targetTable || target?.tableName || '', targetColumn: seed.targetColumn || target?.plainColumn || '', encColumn: seed.encColumn || target?.encColumn || '', hashColumn: seed.hashColumn || target?.hashColumn || '', javaProperty: seed.javaProperty || target?.javaProperty || '', encryptTypeHandler: seed.encryptTypeHandler || target?.encryptTypeHandler || '', hashTypeHandler: seed.hashTypeHandler || target?.hashTypeHandler || '', executionMode: scenarioType === 'TYPEHANDLER_MISSING_DETECTION' ? undefined : 'SQL_SESSION', defaultParameters: {}, } nextScenarioBase.defaultParameters = { ...defaultParametersForScenario(nextScenarioBase), ...(seed.defaultParameters ?? {}), } const nextScenario: ScenarioDefinition = { ...nextScenarioBase, operations: hasOperations(seed.operations) ? seed.operations : defaultOperationsForScenario(nextScenarioBase), verification: seed.verification && Object.keys(seed.verification).length > 0 ? seed.verification : defaultVerificationForScenario(nextScenarioBase), } setScenarioJson(JSON.stringify([...scenarios, nextScenario], null, 2)) setScenarioMessage(`${mapperBase} 쨌 ${sqlBase} row 異붽? ?꾨즺`) } function deleteScenario(index: number) { const scenario = scenarios[index] if (!scenario) return const next = scenarios.filter((_, i) => i !== index) setScenarioJson(JSON.stringify(next, null, 2)) setScenarioMessage(`${scenario.displayName || scenario.scenarioId} row ??젣 ?꾨즺`) setScenarioUiState((current) => { const { [scenario.scenarioId]: _removed, ...verificationHistory } = current.verificationHistory return { ...current, verificationHistory } }) if (selected?.id === scenario.scenarioId) { setSelected(undefined) } setTree((items) => removeScenarioFromTree(items, scenario.scenarioId)) } async function refreshMapperSqlItems() { setMapperSqlItems(await loadMapperSqlIds()) } async function saveMapperRootSetting() { setSettingsMessage('') setSettingsError('') try { const saved = await saveSettings({ mapperRoot }) setMapperRoot(saved.mapperRoot) await refreshMapperSqlItems() await refreshRewrite() setSettingsMessage('Mapper XML 猷⑦듃 寃쎈줈 ?€??諛?SQL ID ?ъ뒪罹??꾨즺') } catch (e) { setSettingsError(e instanceof Error ? e.message : '?ㅼ젙 ?€???ㅽ뙣') } } async function rescanAndGenerateScenarios() { const confirmed = window.confirm('湲곗〈 寃€利??뺣낫?ㅼ쓣 諛깆뾽?섏꽭?? ?뺤씤?섎㈃ ?꾩옱 寃€利앹떆?섎━??JSON怨?寃€利?湲곕줉????뼱?⑥꽌 吏€?뚯쭛?덈떎.') if (!confirmed) return setScenarioMessage('') try { const savedSettings = await saveSettings({ mapperRoot }) setMapperRoot(savedSettings.mapperRoot) const items = await loadMapperSqlIds() setMapperSqlItems(items) const generated = discoverScenariosFromMappers(items, targets) const nextUiState = emptyScenarioUiState setScenarioJson(JSON.stringify(generated, null, 2)) setScenarioUiState(nextUiState) await saveScenarioFile(scenarioFileName, generated) await saveScenarioUiState(nextUiState) setTree((items) => syncTreeWithScenarios(items, generated)) setScenarioMessage(`Mapper ?ы깘??諛?寃€利앹떆?섎━???먮룞 ?앹꽦 ?꾨즺: ${generated.length}媛?) } catch (e) { setScenarioMessage(e instanceof Error ? e.message : 'Mapper ?ы깘??諛??쒕굹由ъ삤 ?먮룞 ?앹꽦 ?ㅽ뙣') } } async function cleanAndGenerateScenarios() { const confirmed = window.confirm('Clean ???꾩옱 crypto-targets.json 湲곗??쇰줈 ?쒕굹由ъ삤 JSON???꾩쟾???덈줈 ?앹꽦?⑸땲?? 湲곗〈 ?쒕굹由ъ삤?€ 寃€利?湲곕줉?€ 珥덇린?붾맗?덈떎.') if (!confirmed) return setScenarioMessage('') try { const savedSettings = await saveSettings({ mapperRoot }) setMapperRoot(savedSettings.mapperRoot) const [latestTargets, items] = await Promise.all([ loadLocalTargets(), loadMapperSqlIds(), ]) setTargets(latestTargets) setMapperSqlItems(items) const generated = discoverScenariosFromMappers(items, latestTargets) const nextUiState = emptyScenarioUiState setSelected(undefined) setTree((items) => syncTreeWithScenarios(items, generated)) setScenarioJson(JSON.stringify(generated, null, 2)) setScenarioUiState(nextUiState) await saveScenarioFile(scenarioFileName, generated) await saveScenarioUiState(nextUiState) setScenarioMessage(`Clean ?꾨즺: ?꾩옱 crypto-targets.json 湲곗??쇰줈 ${generated.length}媛??쒕굹由ъ삤瑜??덈줈 ?앹꽦?덉뒿?덈떎.`) } catch (e) { setScenarioMessage(e instanceof Error ? e.message : 'Clean 諛??쒕굹由ъ삤 ?ъ깮???ㅽ뙣') } } async function refreshRewrite() { setError('') try { setRewrites(await loadRewriteCandidates()) } catch (e) { setError(e instanceof Error ? e.message : '移섑솚 ?꾨낫 議고쉶 ?ㅽ뙣') } } async function selectRewrite(candidate: RewriteCandidate) { const preview = await previewRewrite(candidate) setSelectedRewrite(preview) setRewriteAfterSql(preview.afterSql) } async function applySelectedRewrite() { if (!selectedRewrite) return const applied = await applyRewrite({ ...selectedRewrite, afterSql: rewriteAfterSql }) setSelectedRewrite(applied) setRewriteAfterSql(applied.afterSql) } async function openRewritePopup(node: VerificationNode) { setError('') try { const mapperName = node.mapperName || findMapperName(node) const sqlId = node.sqlId || findSqlId(node) const candidate = rewrites.find((item) => item.mapperFile === mapperName && item.sqlId === sqlId) if (!candidate) { setError(`移섑솚 ?꾨낫媛€ ?놁뒿?덈떎. mapper=${mapperName || '-'} sqlId=${sqlId || '-'}`) return } await selectRewrite(candidate) } catch (e) { setError(e instanceof Error ? e.message : '移섑솚 誘몃━蹂닿린 ?ㅽ뙣') } } useEffect(() => { const runId = makeVerificationRunId() setVerificationRunId(runId) setPlainValue(`?띻만??${runId}`) loadCryptoTypeHandlerMappings() .then((mappings) => { setCryptoTypeHandlerMappings(mappings) return loadLocalTargets().then((items) => setTargets(items.map((target) => applyCryptoTypeHandlersToTarget(target, mappings)))) }) .catch((e) => { setCryptoTypeHandlerMappings([]) loadLocalTargets().then(setTargets).catch(() => setTargetError(e instanceof Error ? e.message : '?뷀샇???€??濡쒕뱶 ?ㅽ뙣')) }) loadScenarioFile() .then((payload) => { setScenarioFileName(payload.fileName) setScenarioJson(JSON.stringify(payload.scenarios, null, 2)) }) .catch(() => setScenarioJson('[]')) loadMapperSqlIds().then(setMapperSqlItems).catch(() => setMapperSqlItems([])) loadSettings().then((settings) => setMapperRoot(settings.mapperRoot)).catch(() => setMapperRoot('src/main/resources/mapper/business')) loadTreeState().then((state) => setExpandedIds(state.expandedIds?.length ? state.expandedIds : ['root'])) loadScenarioUiState() .then((state) => setScenarioUiState({ ...emptyScenarioUiState, ...state, verificationHistory: state.verificationHistory ?? {}, cleanupInsertedRows: state.cleanupInsertedRows ?? true, cleanupInsertedRowsByScenario: state.cleanupInsertedRowsByScenario ?? {}, })) .catch(() => setScenarioUiState(emptyScenarioUiState)) .finally(() => setScenarioUiStateLoaded(true)) refreshRewrite() }, []) useEffect(() => { if (!scenarioUiStateLoaded) return const timeout = window.setTimeout(() => { saveScenarioUiState(scenarioUiState).catch(() => undefined) }, 700) return () => window.clearTimeout(timeout) }, [scenarioUiState, scenarioUiStateLoaded]) useEffect(() => { window.localStorage.setItem(queryLogStorageKey, JSON.stringify(queryLogs)) const channel = new BroadcastChannel(queryLogChannelName) channel.postMessage(queryLogs) channel.close() }, [queryLogs]) useEffect(() => { if (!scenarioJson.trim()) return const timeout = window.setTimeout(() => { try { const parsed = JSON.parse(scenarioJson) as ScenarioDefinition[] if (Array.isArray(parsed)) { saveScenarioFile(scenarioFileName, parsed).catch(() => undefined) } } catch { // Keep the editor responsive while the user is in the middle of typing invalid JSON. } }, 700) return () => window.clearTimeout(timeout) }, [scenarioJson, scenarioFileName]) useEffect(() => { setTree((items) => syncTreeWithScenarios(items, scenarios)) }, [scenarios]) useEffect(() => { const timeout = window.setTimeout(() => { targets.forEach((target, index) => { if (target.tableName && target.plainColumn) { void fetchRandomColumnSample(target, `${index}:${target.tableName}:${target.plainColumn}`) } }) }, 500) return () => window.clearTimeout(timeout) }, [targets]) return (
{error &&
{error}
} {contextMenu && (
setContextMenu(undefined)} >
)} {targetMenu && (
setTargetMenu(undefined)} >
)} {optionGuide && (
setOptionGuide(undefined)}>
event.stopPropagation()}>

{optionGuide.title}

{optionGuide.description}

寃€利?怨쇱젙

    {optionGuide.steps.map((step) =>
  1. {step}
  2. )}
{!!optionGuide.checks?.length && ( <>

?듦낵 湲곗?

    {optionGuide.checks.map((check) =>
  • {check}
  • )}
)}
)} {tab === 'verify' && ( <>
setVerificationRunId(e.target.value)} />
setPlainValue(e.target.value)} />
{selected ? ( <>

{selected.label}

{selected.mapperName || '-'} 쨌 {selected.sqlId || '-'}

{selected.status}
{selected.exceptionMessage &&
{localizedFailureMessage({ status: 'FAIL', progress: 100, updatedAt: '', exceptionMessage: selected.exceptionMessage, details: selected.details })}
} {detailJson &&
{detailJson}
} ) : (
?곸꽭 ?뺣낫媛€ ?놁뒿?덈떎. 寃€利앹쓣 ?ㅽ뻾?????몃━ ?몃뱶瑜??좏깮?섏꽭??
)}
)} {tab === 'targets' && (

?뷀샇???€??而щ읆

Spring ?쒕쾭媛€ ?꾨땲??Next.js 濡쒖뺄 JSON ?뚯씪???€?λ맗?덈떎.

importTargets(e.target.files?.[0])} />

cryptoType Handler 留ㅽ븨

?뷀샇???€?낅퀎 Encrypt Handler?€ Hash Handler瑜?吏€?뺥빀?덈떎.

{cryptoTypeHandlerMappings.map((mapping, index) => ( ))} {cryptoTypeHandlerMappings.length === 0 && ( )}
cryptoTypeEncrypt HandlerHash Handler??젣
updateCryptoTypeHandlerMapping(index, { cryptoType: e.target.value })} /> updateCryptoTypeHandlerMapping(index, { encryptTypeHandler: e.target.value })} /> updateCryptoTypeHandlerMapping(index, { hashTypeHandler: e.target.value })} />
?깅줉??cryptoType handler 留ㅽ븨???놁뒿?덈떎.
{targetMessage &&
{targetMessage}
} {targetError &&
{targetError}
}
{targets.map((target, index) => { const sampleKey = `${index}:${target.tableName}:${target.plainColumn}` const sample = targetSamples[sampleKey] const sampleText = sample?.loading ? '議고쉶以?..' : sample?.message || sample?.values?.join(', ') || '-' return ( ) })}
?뚯씠釉?/th>?됰Ц 而щ읆?뷀샇???€??/th>?뷀샇 而щ읆?댁떆 而щ읆Java PropertyEncrypt HandlerHash Handler??젣
setTargets((items) => items.map((item, i) => i === index ? { ...item, tableName: e.target.value.toUpperCase() } : item))} /> syncTarget(index, e.target.value)} /> updateTarget(index, { cryptoType: e.target.value }, true)} /> setTargets((items) => items.map((item, i) => i === index ? { ...item, encColumn: e.target.value.toUpperCase() } : item))} /> updateTarget(index, { hashColumn: e.target.value.toUpperCase() }, true)} /> setTargets((items) => items.map((item, i) => i === index ? { ...item, javaProperty: e.target.value } : item))} /> setTargets((items) => items.map((item, i) => i === index ? { ...item, encryptTypeHandler: e.target.value } : item))} /> setTargets((items) => items.map((item, i) => i === index ? { ...item, hashTypeHandler: e.target.value } : item))} />
?쒕뜡 ROW 媛?/b> {sampleText}
)} {tab === 'scenarios' && (

寃€利??쒕굹由ъ삤 JSON

?꾩옱 寃€利??붿쭊 ?쒕굹由ъ삤??Spring API?€ ?곕룞?⑸땲??

?꾩옱 ?뚯씪: {scenarioFileName}
importScenarioJson(e.target.files?.[0])} />
{scenarioMessage &&
{scenarioMessage}
} {scenarioViewMode === 'json' ? (