555

import * as fs from 'fs/promises' import * as path from 'path' import * as vscode from 'vscode' type CryptoTarget = { tableName: string plainColumn: string encColumn: string hashColumn: string javaProperty: string encryptTypeHandler: string hashTypeHandler: string } type ResultMapEntry = { kind: 'id' | 'result' column: string property: string typeHandler?: string } type ResultMapDefinition = { id: string type: string entries: ResultMapEntry[] } type MapperItem = { tag: 'insert' | 'update' | 'select' | 'delete' id: string attrs: string text: string } const DECRYPT_TARGET_COMMENT = '/* 복호화 대상 */' const CRYPTO_TARGET_CHECK_COMMENT = '/* @@@ 암호화 대상 확인 필요 @@@ */' export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('cryptoMapper.copyTypeHandlerRecommendation', async () => { try { await copyTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper 추천 생성 실패: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.copyFullFileTypeHandlerRecommendation', async () => { try { await copyFullFileTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper full XML recommendation failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.markCryptoTargetColumns', async () => { try { await markCryptoTargetColumnsInFullFile() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper target mark failed: ${message}`) } }), ) } export function deactivate() { // no-op } async function copyTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('열려 있는 editor가 없습니다.') if (editor.selection.isEmpty) throw new Error('mapper item 전체를 선택한 뒤 Ctrl+Shift+5를 눌러주세요.') const selectedText = editor.document.getText(editor.selection) const mapperItem = parseSelectedMapperItem(selectedText) const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`프로젝트 루트 아래에서 db.ini를 찾지 못했습니다: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const matchedTargets = targets.filter((target) => matchesTarget(selectedText, target, mapperItem.tag)) if (matchedTargets.length === 0) { throw new Error('선택 영역에서 crypto-targets.json의 테이블명과 컬럼명이 함께 경계 일치하는 대상을 찾지 못했습니다.') } const date = formatLocalDate(new Date()) const fullDocumentText = editor.document.getText() const recommendation = buildRecommendation(mapperItem, selectedText, fullDocumentText, matchedTargets, date, markerUserId) await vscode.env.clipboard.writeText(recommendation.clipboardText) vscode.window.showInformationMessage( `Crypto Mapper 추천을 클립보드에 복사했습니다. 대상 ${matchedTargets.length}개, ${recommendation.supportSnippets.length}개 보조 snippet.`, ) } async function copyFullFileTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const originalXml = editor.document.getText() const date = formatLocalDate(new Date()) const recommendation = buildFullFileRecommendation(originalXml, targets, date, markerUserId) await vscode.env.clipboard.writeText(recommendation.clipboardText) vscode.window.showInformationMessage( `Crypto Mapper full XML recommendation copied. changed mapper items: ${recommendation.changedItemCount}, resultMaps: ${recommendation.supportSnippets.length}`, ) } async function markCryptoTargetColumnsInFullFile() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const targets = await readCryptoTargets(cryptoTargetsPath) const originalXml = editor.document.getText() const result = markCryptoTargetColumns(originalXml, targets) if (result.changedCount === 0) { vscode.window.showInformationMessage('Crypto Mapper target mark: no matching mapper item found.') return } const fullRange = new vscode.Range( editor.document.positionAt(0), editor.document.positionAt(originalXml.length), ) await editor.edit((editBuilder) => { editBuilder.replace(fullRange, result.text) }) vscode.window.showInformationMessage(`Crypto Mapper target mark completed. changed columns: ${result.changedCount}`) } function getWorkspaceRoot(uri: vscode.Uri) { const folder = vscode.workspace.getWorkspaceFolder(uri) ?? vscode.workspace.workspaceFolders?.[0] if (!folder) throw new Error('VS Code workspace folder가 없습니다.') return folder.uri.fsPath } async function findFirstFile(root: string, fileName: string): Promise { const queue = [root] const ignored = new Set(['.git', 'node_modules', 'dist', 'out', '.next', 'target', 'build']) while (queue.length > 0) { const current = queue.shift()! let entries: Array<{ name: string; isDirectory: () => boolean; isFile: () => boolean }> try { entries = await fs.readdir(current, { withFileTypes: true }) } catch { continue } const file = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) if (file) return path.join(current, file.name) for (const entry of entries) { if (entry.isDirectory() && !ignored.has(entry.name)) { queue.push(path.join(current, entry.name)) } } } return undefined } async function readCryptoTargetsPath(dbIniPath: string, workspaceRoot: string) { const text = await fs.readFile(dbIniPath, 'utf8') const targetPath = text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#') && !line.startsWith(';')) .map((line) => line.match(/^targetpath\s*=\s*(.+)$/i)?.[1]?.trim()) .find(Boolean) if (!targetPath) throw new Error(`${dbIniPath} 파일에 targetpath 값이 없습니다.`) const unquoted = targetPath.replace(/^['"]|['"]$/g, '') const candidates = path.isAbsolute(unquoted) ? [unquoted] : [path.resolve(path.dirname(dbIniPath), unquoted), path.resolve(workspaceRoot, unquoted)] for (const candidate of candidates) { try { const stat = await fs.stat(candidate) if (stat.isFile()) return candidate if (stat.isDirectory()) return path.join(candidate, 'crypto-targets.json') } catch { // try next } } throw new Error(`targetpath 경로를 찾지 못했습니다: ${unquoted}`) } async function readDbIniUserId(dbIniPath: string) { const text = await fs.readFile(dbIniPath, 'utf8') return text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#') && !line.startsWith(';')) .map((line) => line.match(/^userid\s*=\s*(.+)$/i)?.[1]?.trim()) .find(Boolean) ?.replace(/^['"]|['"]$/g, '') ?? '' } async function readCryptoTargets(filePath: string): Promise { const text = await fs.readFile(filePath, 'utf8') const parsed = JSON.parse(text) as Partial[] if (!Array.isArray(parsed)) throw new Error(`crypto-targets.json 형식이 배열이 아닙니다: ${filePath}`) return parsed .map((target) => ({ tableName: normalizeRequired(target.tableName), plainColumn: normalizeRequired(target.plainColumn), encColumn: normalizeRequired(target.encColumn), hashColumn: normalizeOptional(target.hashColumn), javaProperty: normalizeOptional(target.javaProperty) || toCamel(normalizeRequired(target.plainColumn)), encryptTypeHandler: normalizeRequired(target.encryptTypeHandler), hashTypeHandler: normalizeOptional(target.hashTypeHandler), })) .filter((target) => target.tableName && target.plainColumn && target.encColumn && target.encryptTypeHandler) } function normalizeRequired(value: unknown) { return String(value ?? '').trim() } function normalizeOptional(value: unknown) { return String(value ?? '').trim() } function parseSelectedMapperItem(text: string): MapperItem { const match = text.trim().match(/^<(insert|update|select|delete)\b([^>]*)>[\s\S]*<\/\1>$/i) if (!match) { throw new Error(', ,