Files
AX-Copilot-Codex/src/AxKeyEncryptor/MainForm.cs

755 lines
32 KiB
C#

using System.Drawing.Drawing2D;
using System.Text.Json;
namespace AxKeyEncryptor;
/// <summary>
/// AX Copilot — Settings Encryptor (개발자 전용)
/// Ollama / vLLM / Gemini 3종 서비스에 대한 설정값 암호화 UI
/// </summary>
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<string, object>
{
["llm"] = new Dictionary<string, object>
{
["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();
}
}