using System.Drawing.Drawing2D; using System.Text.Json; namespace AxKeyEncryptor; /// /// AX Copilot — Settings Encryptor (개발자 전용) /// Ollama / vLLM / Gemini 3종 서비스에 대한 설정값 암호화 UI /// public class MainForm : Form { // ── 색상 테마 ─────────────────────────────────────────────────── private static readonly Color BgDark = Color.FromArgb(24, 24, 32); private static readonly Color BgPanel = Color.FromArgb(32, 34, 46); private static readonly Color BgCard = Color.FromArgb(42, 44, 58); private static readonly Color BgInput = Color.FromArgb(52, 54, 70); private static readonly Color AccentBlue = Color.FromArgb(88, 130, 255); private static readonly Color AccentGreen = Color.FromArgb(80, 200, 120); private static readonly Color AccentOrange= Color.FromArgb(255, 160, 60); private static readonly Color AccentRed = Color.FromArgb(255, 90, 90); private static readonly Color TextPrimary = Color.FromArgb(230, 232, 240); private static readonly Color TextSecondary= Color.FromArgb(140, 145, 170); private static readonly Color BorderColor = Color.FromArgb(60, 64, 80); // ── 서비스별 탭 컨트롤 ────────────────────────────────────────── private TabControl _tabs = null!; // Ollama 탭 컨트롤 private TextBox _ollamaEndpoint = null!; private TextBox _ollamaModel = null!; private TextBox _ollamaEncEndpoint = null!; private TextBox _ollamaEncModel = null!; // vLLM 탭 컨트롤 private TextBox _vllmEndpoint = null!; private TextBox _vllmModel = null!; private TextBox _vllmApiKey = null!; private TextBox _vllmEncEndpoint = null!; private TextBox _vllmEncModel = null!; private TextBox _vllmEncApiKey = null!; // Gemini 탭 컨트롤 private TextBox _geminiApiKey = null!; private TextBox _geminiModel = null!; private TextBox _geminiEncApiKey = null!; private TextBox _geminiEncModel = null!; // 하단 공용 private RichTextBox _outputBox = null!; private TextBox _decryptInput = null!; private TextBox _decryptResult = null!; public MainForm() { InitializeComponent(); } private void InitializeComponent() { // ── 폼 기본 설정 ──────────────────────────────────────── Text = "AX Copilot — Settings Encryptor"; Size = new Size(760, 680); MinimumSize = new Size(720, 620); StartPosition = FormStartPosition.CenterScreen; BackColor = BgDark; ForeColor = TextPrimary; Font = new Font("Segoe UI", 9.5f, FontStyle.Regular); FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; // 아이콘 설정 try { var asm = System.Reflection.Assembly.GetExecutingAssembly(); var stream = asm.GetManifestResourceStream("AxKeyEncryptor.Resources.icon.ico"); if (stream != null) Icon = new Icon(stream); } catch { } // ── 헤더 패널 ────────────────────────────────────────── var headerPanel = new Panel { Dock = DockStyle.Top, Height = 60, BackColor = Color.Transparent }; headerPanel.Paint += (s, e) => { using var brush = new LinearGradientBrush( headerPanel.ClientRectangle, Color.FromArgb(35, 40, 65), Color.FromArgb(24, 24, 32), LinearGradientMode.Vertical); e.Graphics.FillRectangle(brush, headerPanel.ClientRectangle); // 타이틀 using var titleFont = new Font("Segoe UI", 14f, FontStyle.Bold); e.Graphics.DrawString("🔐 AX Copilot — Settings Encryptor", titleFont, new SolidBrush(TextPrimary), 20, 8); // 서브타이틀 using var subFont = new Font("Segoe UI", 8.5f, FontStyle.Regular); e.Graphics.DrawString("AES-256-CBC 앱 공용 키 암호화 · 모든 PC에서 동일 복호화 · 개발자 전용", subFont, new SolidBrush(TextSecondary), 48, 36); }; Controls.Add(headerPanel); // ── 메인 컨텐츠 패널 ──────────────────────────────────── var mainPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(16, 8, 16, 16), BackColor = BgDark }; Controls.Add(mainPanel); // ── 서비스 선택 탭 ────────────────────────────────────── _tabs = new TabControl { Dock = DockStyle.Top, Height = 260, Font = new Font("Segoe UI", 10f, FontStyle.Regular), Padding = new Point(12, 6) }; _tabs.DrawMode = TabDrawMode.OwnerDrawFixed; _tabs.DrawItem += DrawTab; _tabs.SizeMode = TabSizeMode.Fixed; _tabs.ItemSize = new Size(160, 36); // Ollama 탭 var ollamaPage = CreateTabPage("🟢 Ollama", AccentGreen); BuildOllamaTab(ollamaPage); _tabs.TabPages.Add(ollamaPage); // vLLM 탭 var vllmPage = CreateTabPage("🔵 vLLM (OpenAI)", AccentBlue); BuildVllmTab(vllmPage); _tabs.TabPages.Add(vllmPage); // Gemini 탭 var geminiPage = CreateTabPage("🟠 Gemini", AccentOrange); BuildGeminiTab(geminiPage); _tabs.TabPages.Add(geminiPage); // 복호화 검증 탭 추가 var toolsPage = CreateTabPage("🔓 복호화 검증", AccentBlue); BuildToolsTab(toolsPage); _tabs.TabPages.Add(toolsPage); mainPanel.Controls.Add(_tabs); // ── 출력 로그 섹션 ────────────────────────────────────── var outputLabel = CreateLabel("📋 출력 로그", 10f, FontStyle.Bold); outputLabel.Dock = DockStyle.Top; outputLabel.Height = 28; outputLabel.Padding = new Padding(4, 8, 0, 0); mainPanel.Controls.Add(outputLabel); _outputBox = new RichTextBox { Dock = DockStyle.Fill, BackColor = Color.FromArgb(18, 18, 24), ForeColor = TextPrimary, Font = new Font("Cascadia Code", 9.5f, FontStyle.Regular, GraphicsUnit.Point, 0, // fallback if Cascadia Code not installed false), BorderStyle = BorderStyle.None, ReadOnly = true, WordWrap = true, Padding = new Padding(8) }; // Cascadia Code fallback if (_outputBox.Font.Name != "Cascadia Code") _outputBox.Font = new Font("Consolas", 9.5f); mainPanel.Controls.Add(_outputBox); // ── 하단 상태바 ───────────────────────────────────────── var statusBar = new Panel { Dock = DockStyle.Bottom, Height = 30, BackColor = Color.FromArgb(20, 22, 30) }; var statusLabel = CreateLabel(" AES-256-CBC · PBKDF2 100K iterations · 모든 PC에서 동일한 암호화 키", 8f, FontStyle.Regular); statusLabel.ForeColor = TextSecondary; statusLabel.Dock = DockStyle.Fill; statusLabel.TextAlign = ContentAlignment.MiddleLeft; statusBar.Controls.Add(statusLabel); Controls.Add(statusBar); // ── 컨트롤 순서 (뒤에서 앞으로) ────────────────────────── // Panel DockStyle 순서 보장 Controls.SetChildIndex(statusBar, 0); Controls.SetChildIndex(headerPanel, 0); mainPanel.Controls.SetChildIndex(_outputBox, 0); mainPanel.Controls.SetChildIndex(outputLabel, 0); mainPanel.Controls.SetChildIndex(_tabs, 0); AppendLog("AX Copilot Settings Encryptor 시작됨", AccentGreen); AppendLog("", TextSecondary); AppendLog("── 사용 방법 ──────────────────────────────────────────", AccentBlue); AppendLog("1. 상단 탭에서 LLM 서비스(Ollama/vLLM/Gemini)를 선택합니다.", TextPrimary); AppendLog("2. 각 필드에 평문 값(URL, 모델명, API 키)을 입력합니다.", TextPrimary); AppendLog("3. [⚡ 전체 암호화] 버튼으로 모든 필드를 한번에 암호화합니다.", TextPrimary); AppendLog("4. [📋 settings.json 복사] 버튼으로 암호화된 JSON을 클립보드에 복사합니다.", TextPrimary); AppendLog("5. 복사된 JSON을 settings.json 파일의 llm 섹션에 붙여넣으면 완료!", TextPrimary); AppendLog("", TextSecondary); AppendLog("── 암호화 방식 ────────────────────────────────────────", AccentBlue); AppendLog("• AES-256-CBC + PBKDF2(100K iterations, SHA256)", TextSecondary); AppendLog("• 앱 공용 키 방식 — 모든 PC에서 동일한 키로 복호화됩니다.", TextSecondary); AppendLog("• 배포 시 settings.json에 암호화된 값을 넣으면 앱이 자동 복호화합니다.", TextSecondary); AppendLog("", TextSecondary); } // ═════════════════════════════════════════════════════════════════ // Ollama 탭 빌드 // ═════════════════════════════════════════════════════════════════ private void BuildOllamaTab(TabPage page) { int y = 12; var desc = CreateLabel("Ollama는 로컬 LLM 서버입니다. API 키가 필요 없습니다.", 9f, FontStyle.Italic); desc.ForeColor = TextSecondary; desc.Location = new Point(12, y); desc.AutoSize = true; page.Controls.Add(desc); y += 30; // Endpoint AddFieldPair(page, ref y, "Endpoint URL:", "http://localhost:11434", out _ollamaEndpoint, out _ollamaEncEndpoint, "암호화", AccentGreen, (s, e) => { EncryptAndShow(_ollamaEndpoint, _ollamaEncEndpoint, "Ollama Endpoint"); }); // Model AddFieldPair(page, ref y, "Model:", "llama3.1:8b", out _ollamaModel, out _ollamaEncModel, "암호화", AccentGreen, (s, e) => { EncryptAndShow(_ollamaModel, _ollamaEncModel, "Ollama Model"); }); // 전체 암호화 버튼 var btnAll = CreateButton("⚡ 전체 암호화", AccentGreen); btnAll.Location = new Point(12, y + 4); btnAll.Size = new Size(130, 34); btnAll.Click += (s, e) => { EncryptAndShow(_ollamaEndpoint, _ollamaEncEndpoint, "Ollama Endpoint"); EncryptAndShow(_ollamaModel, _ollamaEncModel, "Ollama Model"); AppendLog("[Ollama] 전체 암호화 완료 ✔", AccentGreen); }; page.Controls.Add(btnAll); // JSON 생성 버튼 var btnJson = CreateButton("📋 settings.json 복사", AccentGreen); btnJson.Location = new Point(150, y + 4); btnJson.Size = new Size(200, 34); btnJson.Click += (s, e) => GenerateOllamaJson(); page.Controls.Add(btnJson); } // ═════════════════════════════════════════════════════════════════ // vLLM 탭 빌드 // ═════════════════════════════════════════════════════════════════ private void BuildVllmTab(TabPage page) { int y = 12; var desc = CreateLabel("vLLM은 OpenAI 호환 API 서버입니다. API 키는 선택사항입니다.", 9f, FontStyle.Italic); desc.ForeColor = TextSecondary; desc.Location = new Point(12, y); desc.AutoSize = true; page.Controls.Add(desc); y += 30; // Endpoint AddFieldPair(page, ref y, "Endpoint URL:", "http://localhost:8000", out _vllmEndpoint, out _vllmEncEndpoint, "암호화", AccentBlue, (s, e) => { EncryptAndShow(_vllmEndpoint, _vllmEncEndpoint, "vLLM Endpoint"); }); // Model AddFieldPair(page, ref y, "Model:", "meta-llama/Llama-3.1-8B-Instruct", out _vllmModel, out _vllmEncModel, "암호화", AccentBlue, (s, e) => { EncryptAndShow(_vllmModel, _vllmEncModel, "vLLM Model"); }); // API Key AddFieldPair(page, ref y, "API Key (선택):", "", out _vllmApiKey, out _vllmEncApiKey, "암호화", AccentBlue, (s, e) => { EncryptAndShow(_vllmApiKey, _vllmEncApiKey, "vLLM API Key"); }); var btnAll = CreateButton("⚡ 전체 암호화", AccentBlue); btnAll.Location = new Point(12, y + 4); btnAll.Size = new Size(130, 34); btnAll.Click += (s, e) => { EncryptAndShow(_vllmEndpoint, _vllmEncEndpoint, "vLLM Endpoint"); EncryptAndShow(_vllmModel, _vllmEncModel, "vLLM Model"); if (!string.IsNullOrEmpty(_vllmApiKey.Text.Trim())) EncryptAndShow(_vllmApiKey, _vllmEncApiKey, "vLLM API Key"); AppendLog("[vLLM] 전체 암호화 완료 ✔", AccentGreen); }; page.Controls.Add(btnAll); var btnJson = CreateButton("📋 settings.json 복사", AccentBlue); btnJson.Location = new Point(150, y + 4); btnJson.Size = new Size(200, 34); btnJson.Click += (s, e) => GenerateVllmJson(); page.Controls.Add(btnJson); } // ═════════════════════════════════════════════════════════════════ // Gemini 탭 빌드 // ═════════════════════════════════════════════════════════════════ private void BuildGeminiTab(TabPage page) { int y = 12; var desc = CreateLabel("Google Gemini API를 사용합니다. API Key가 필수입니다.", 9f, FontStyle.Italic); desc.ForeColor = TextSecondary; desc.Location = new Point(12, y); desc.AutoSize = true; page.Controls.Add(desc); y += 30; // API Key AddFieldPair(page, ref y, "API Key (필수):", "", out _geminiApiKey, out _geminiEncApiKey, "암호화", AccentOrange, (s, e) => { EncryptAndShow(_geminiApiKey, _geminiEncApiKey, "Gemini API Key"); }); // Model AddFieldPair(page, ref y, "Model:", "gemini-2.0-flash", out _geminiModel, out _geminiEncModel, "암호화", AccentOrange, (s, e) => { EncryptAndShow(_geminiModel, _geminiEncModel, "Gemini Model"); }); var btnAll = CreateButton("⚡ 전체 암호화", AccentOrange); btnAll.Location = new Point(12, y + 4); btnAll.Size = new Size(130, 34); btnAll.Click += (s, e) => { EncryptAndShow(_geminiApiKey, _geminiEncApiKey, "Gemini API Key"); EncryptAndShow(_geminiModel, _geminiEncModel, "Gemini Model"); AppendLog("[Gemini] 전체 암호화 완료 ✔", AccentGreen); }; page.Controls.Add(btnAll); var btnJson = CreateButton("📋 settings.json 복사", AccentOrange); btnJson.Location = new Point(150, y + 4); btnJson.Size = new Size(200, 34); btnJson.Click += (s, e) => GenerateGeminiJson(); page.Controls.Add(btnJson); } // ═════════════════════════════════════════════════════════════════ // 필드 쌍 헬퍼 (라벨 + 입력 + 암호화 버튼 + 결과) // ═════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════ // 복호화 검증 탭 // ═══════════════════════════════════════════════════════════════════════ private void BuildToolsTab(TabPage page) { int y = 12; var desc = CreateLabel("암호화된 Base64 값을 붙여넣고 원문을 확인합니다.", 9f, FontStyle.Italic); desc.ForeColor = TextSecondary; desc.Location = new Point(12, y); desc.AutoSize = true; page.Controls.Add(desc); y += 30; var inputLabel = CreateLabel("암호화된 값 (Base64):", 9f, FontStyle.Regular); inputLabel.Location = new Point(12, y); inputLabel.AutoSize = true; page.Controls.Add(inputLabel); y += 20; _decryptInput = CreateTextBox(); _decryptInput.Location = new Point(12, y); _decryptInput.Size = new Size(440, 28); page.Controls.Add(_decryptInput); var btnDecrypt = CreateButton("복호화", AccentBlue); btnDecrypt.Location = new Point(460, y - 1); btnDecrypt.Size = new Size(80, 30); btnDecrypt.Click += BtnDecrypt_Click; page.Controls.Add(btnDecrypt); var btnVerify = CreateButton("왕복 검증", Color.FromArgb(130, 100, 220)); btnVerify.Location = new Point(546, y - 1); btnVerify.Size = new Size(90, 30); btnVerify.Click += BtnVerify_Click; page.Controls.Add(btnVerify); y += 40; var resultLabel = CreateLabel("복호화 결과:", 9f, FontStyle.Regular); resultLabel.Location = new Point(12, y); resultLabel.AutoSize = true; page.Controls.Add(resultLabel); y += 20; _decryptResult = CreateTextBox(); _decryptResult.Location = new Point(12, y); _decryptResult.Size = new Size(624, 28); _decryptResult.ReadOnly = true; page.Controls.Add(_decryptResult); } private void AddFieldPair(TabPage page, ref int y, string labelText, string placeholder, out TextBox input, out TextBox encrypted, string buttonText, Color accentColor, EventHandler onClick) { var lbl = CreateLabel(labelText, 9f, FontStyle.Regular); lbl.Location = new Point(12, y); lbl.AutoSize = true; page.Controls.Add(lbl); y += 22; input = CreateTextBox(); input.Location = new Point(12, y); input.Size = new Size(310, 28); input.PlaceholderText = placeholder; if (!string.IsNullOrEmpty(placeholder) && placeholder.StartsWith("http")) input.Text = placeholder; page.Controls.Add(input); var btn = CreateButton(buttonText, accentColor); btn.Location = new Point(328, y - 1); btn.Size = new Size(70, 30); btn.Click += onClick; page.Controls.Add(btn); encrypted = CreateTextBox(); encrypted.Location = new Point(404, y); encrypted.Size = new Size(238, 28); encrypted.ReadOnly = true; encrypted.PlaceholderText = "암호화 결과"; page.Controls.Add(encrypted); // 복사 버튼 var btnCopy = CreateCopyButton(encrypted); btnCopy.Location = new Point(646, y); btnCopy.Size = new Size(28, 28); page.Controls.Add(btnCopy); y += 40; } // ═════════════════════════════════════════════════════════════════ // 암호화 실행 // ═════════════════════════════════════════════════════════════════ private void EncryptAndShow(TextBox input, TextBox output, string fieldName) { var text = input.Text.Trim(); if (string.IsNullOrEmpty(text)) { AppendLog($"[{fieldName}] 입력이 비어 있습니다.", AccentRed); return; } var enc = CryptoHelper.Encrypt(text); output.Text = enc; AppendLog($"[{fieldName}] 암호화 완료 ✔", AccentGreen); AppendLog($" 원본: {text}", TextSecondary); AppendLog($" 결과: {enc}", TextPrimary); } // ═════════════════════════════════════════════════════════════════ // 복호화 // ═════════════════════════════════════════════════════════════════ private void BtnDecrypt_Click(object? sender, EventArgs e) { var b64 = _decryptInput.Text.Trim(); if (string.IsNullOrEmpty(b64)) { AppendLog("[복호화] 입력이 비어 있습니다.", AccentRed); return; } var (success, result) = CryptoHelper.Decrypt(b64); _decryptResult.Text = result; if (success) { _decryptResult.ForeColor = AccentGreen; AppendLog($"[복호화] 성공 ✔ → {result}", AccentGreen); } else { _decryptResult.ForeColor = AccentRed; AppendLog($"[복호화] 실패 ✘ — {result}", AccentRed); } } // ═════════════════════════════════════════════════════════════════ // 왕복 검증 // ═════════════════════════════════════════════════════════════════ private void BtnVerify_Click(object? sender, EventArgs e) { var b64 = _decryptInput.Text.Trim(); string testText; if (string.IsNullOrEmpty(b64)) { testText = "test-api-key-12345"; AppendLog("[왕복 검증] 테스트 문자열 사용: " + testText, TextSecondary); } else { // 이미 암호화된 값이면 복호화 → 재암호화 → 재복호화 var (ok, dec) = CryptoHelper.Decrypt(b64); if (ok) testText = dec; else testText = b64; // 평문으로 간주 } var encrypted = CryptoHelper.Encrypt(testText); var (success, decrypted) = CryptoHelper.Decrypt(encrypted); var match = success && testText == decrypted; _decryptResult.Text = match ? $"✅ 일치 — {decrypted}" : $"❌ 불일치"; _decryptResult.ForeColor = match ? AccentGreen : AccentRed; AppendLog("[왕복 검증]", AccentBlue); AppendLog($" 원본: {testText}", TextSecondary); AppendLog($" 암호화: {encrypted}", TextSecondary); AppendLog($" 복호화: {decrypted}", TextSecondary); AppendLog($" 결과: {(match ? "✅ 성공" : "❌ 실패")}", match ? AccentGreen : AccentRed); } // ═════════════════════════════════════════════════════════════════ // JSON 생성 (서비스별) // ═════════════════════════════════════════════════════════════════ private void GenerateOllamaJson() { var endpoint = _ollamaEncEndpoint.Text; var model = _ollamaEncModel.Text; if (string.IsNullOrEmpty(endpoint)) endpoint = CryptoHelper.Encrypt(_ollamaEndpoint.Text); if (string.IsNullOrEmpty(model)) model = CryptoHelper.Encrypt(_ollamaModel.Text); var json = JsonSerializer.Serialize(new { llm = new { service = "ollama", encryptedEndpoint = endpoint, encryptedModel = model, streaming = true, maxContextTokens = 4096, temperature = 0.7 } }, new JsonSerializerOptions { WriteIndented = true }); CopyToClipboard(json, "Ollama"); } private void GenerateVllmJson() { var endpoint = _vllmEncEndpoint.Text; var model = _vllmEncModel.Text; var apiKey = _vllmEncApiKey.Text; if (string.IsNullOrEmpty(endpoint)) endpoint = CryptoHelper.Encrypt(_vllmEndpoint.Text); if (string.IsNullOrEmpty(model)) model = CryptoHelper.Encrypt(_vllmModel.Text); if (string.IsNullOrEmpty(apiKey) && !string.IsNullOrEmpty(_vllmApiKey.Text)) apiKey = CryptoHelper.Encrypt(_vllmApiKey.Text); var obj = new Dictionary { ["llm"] = new Dictionary { ["service"] = "vllm", ["encryptedEndpoint"] = endpoint, ["encryptedModel"] = model, ["encryptedApiKey"] = apiKey ?? "", ["streaming"] = true, ["maxContextTokens"] = 4096, ["temperature"] = 0.7 } }; var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true }); CopyToClipboard(json, "vLLM"); } private void GenerateGeminiJson() { var apiKey = _geminiEncApiKey.Text; var model = _geminiEncModel.Text; if (string.IsNullOrEmpty(apiKey)) { if (string.IsNullOrEmpty(_geminiApiKey.Text)) { AppendLog("[Gemini] API Key는 필수입니다!", AccentRed); return; } apiKey = CryptoHelper.Encrypt(_geminiApiKey.Text); } if (string.IsNullOrEmpty(model)) model = CryptoHelper.Encrypt(_geminiModel.Text); var json = JsonSerializer.Serialize(new { llm = new { service = "gemini", encryptedApiKey = apiKey, encryptedModel = model, streaming = true, maxContextTokens = 8192, temperature = 0.7 } }, new JsonSerializerOptions { WriteIndented = true }); CopyToClipboard(json, "Gemini"); } private void CopyToClipboard(string json, string serviceName) { Clipboard.SetText(json); AppendLog($"[{serviceName}] settings.json 설정값이 클립보드에 복사되었습니다! 📋", AccentGreen); AppendLog(json, TextSecondary); AppendLog("", TextSecondary); // 짧은 알림 var tip = new ToolTip(); tip.Show($"{serviceName} 설정이 클립보드에 복사됨!", this, Width / 2 - 100, Height - 60, 2000); } // ═════════════════════════════════════════════════════════════════ // 출력 로그 // ═════════════════════════════════════════════════════════════════ private void AppendLog(string text, Color color) { _outputBox.SelectionStart = _outputBox.TextLength; _outputBox.SelectionLength = 0; _outputBox.SelectionColor = color; _outputBox.AppendText(text + Environment.NewLine); _outputBox.ScrollToCaret(); } // ═════════════════════════════════════════════════════════════════ // UI 팩토리 헬퍼 // ═════════════════════════════════════════════════════════════════ private static Label CreateLabel(string text, float fontSize, FontStyle style) { return new Label { Text = text, ForeColor = TextPrimary, Font = new Font("Segoe UI", fontSize, style), AutoSize = true }; } private static TextBox CreateTextBox() { return new TextBox { BackColor = BgInput, ForeColor = TextPrimary, Font = new Font("Segoe UI", 10f), BorderStyle = BorderStyle.FixedSingle }; } private Button CreateButton(string text, Color bgColor) { var btn = new Button { Text = text, BackColor = bgColor, ForeColor = Color.White, FlatStyle = FlatStyle.Flat, Font = new Font("Segoe UI", 9f, FontStyle.Bold), Cursor = Cursors.Hand }; btn.FlatAppearance.BorderSize = 0; btn.FlatAppearance.MouseOverBackColor = ControlPaint.Light(bgColor, 0.15f); btn.FlatAppearance.MouseDownBackColor = ControlPaint.Dark(bgColor, 0.1f); return btn; } private Button CreateCopyButton(TextBox target) { var btn = new Button { Text = "📋", BackColor = BgCard, ForeColor = TextPrimary, FlatStyle = FlatStyle.Flat, Font = new Font("Segoe UI", 9f), Cursor = Cursors.Hand }; btn.FlatAppearance.BorderSize = 1; btn.FlatAppearance.BorderColor = BorderColor; btn.Click += (s, e) => { if (!string.IsNullOrEmpty(target.Text)) { Clipboard.SetText(target.Text); var tip = new ToolTip(); tip.Show("복사됨!", btn, 0, -20, 1200); } }; return btn; } private TabPage CreateTabPage(string title, Color accentColor) { var page = new TabPage(title) { BackColor = BgPanel, ForeColor = TextPrimary, Padding = new Padding(4) }; page.Tag = accentColor; // 탭 드로잉에 사용 return page; } private void DrawTab(object? sender, DrawItemEventArgs e) { if (sender is not TabControl tabs) return; var page = tabs.TabPages[e.Index]; var isSelected = e.Index == tabs.SelectedIndex; var accent = page.Tag is Color c ? c : AccentBlue; // 배경 var bgColor = isSelected ? BgCard : BgPanel; using var bgBrush = new SolidBrush(bgColor); e.Graphics.FillRectangle(bgBrush, e.Bounds); // 선택된 탭 하단 악센트 라인 if (isSelected) { using var lineBrush = new SolidBrush(accent); e.Graphics.FillRectangle(lineBrush, new Rectangle(e.Bounds.Left, e.Bounds.Bottom - 3, e.Bounds.Width, 3)); } // 텍스트 var textColor = isSelected ? TextPrimary : TextSecondary; using var textBrush = new SolidBrush(textColor); var textFont = isSelected ? new Font("Segoe UI", 10f, FontStyle.Bold) : new Font("Segoe UI", 9.5f, FontStyle.Regular); var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; e.Graphics.DrawString(page.Text, textFont, textBrush, e.Bounds, sf); textFont.Dispose(); } }