using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L11-1: CSV 뷰어·파서 핸들러. "csv" 프리픽스로 사용합니다. /// /// 예: csv → 클립보드 CSV 파싱 (헤더·행수·컬럼수) /// csv col 2 → 2번째 컬럼 값 목록 추출 /// csv row 3 → 3번째 행 출력 /// csv stats → 숫자 컬럼 합계·평균·최대·최소 /// csv head → 헤더 컬럼명 목록 /// csv tsv → CSV → TSV(탭 구분) 변환 /// Enter → 결과를 클립보드에 복사. /// public class CsvHandler : IActionHandler { public string? Prefix => "csv"; public PluginMetadata Metadata => new( "CSV", "CSV 뷰어·파서 — 컬럼 추출 · 통계 · 형식 변환", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var clip = GetClipboard(); var hasData = !string.IsNullOrWhiteSpace(clip) && LooksCsv(clip); if (string.IsNullOrWhiteSpace(q)) { if (hasData) { items.AddRange(BuildOverviewItems(clip)); } else { items.Add(new LauncherItem("CSV 뷰어", "CSV를 클립보드에 복사 후 'csv' 입력", null, null, Symbol: "\uE8A5")); items.Add(new LauncherItem("csv col 2", "2번째 컬럼 추출", null, null, Symbol: "\uE8A5")); items.Add(new LauncherItem("csv stats", "숫자 컬럼 통계", null, null, Symbol: "\uE8A5")); items.Add(new LauncherItem("csv tsv", "CSV → TSV 변환", null, null, Symbol: "\uE8A5")); } return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); if (!hasData) { items.Add(new LauncherItem("클립보드에 CSV 없음", "CSV 형식 데이터를 클립보드에 복사 후 시도하세요", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } var rows = ParseCsv(clip); if (rows.Count == 0) { items.Add(new LauncherItem("파싱 실패", "유효한 CSV가 아닙니다", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } switch (sub) { case "col": case "column": { var colIdx = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n - 1 : 0; items.AddRange(ExtractColumn(rows, colIdx)); break; } case "row": { var rowIdx = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n - 1 : 0; items.AddRange(ExtractRow(rows, rowIdx)); break; } case "stats": case "stat": { items.AddRange(BuildStatsItems(rows)); break; } case "head": case "header": { if (rows.Count > 0) { var header = rows[0]; var all = string.Join(", ", header); items.Add(new LauncherItem($"헤더 {header.Count}개 컬럼", all, null, ("copy", all), Symbol: "\uE8A5")); for (var i = 0; i < header.Count; i++) items.Add(new LauncherItem($"[{i+1}] {header[i]}", $"컬럼 {i+1}", null, ("copy", header[i]), Symbol: "\uE8A5")); } break; } case "tsv": { var tsv = ConvertToTsv(rows); items.Add(new LauncherItem( $"TSV 변환 {rows.Count}행", "탭 구분자 · Enter 복사", null, ("copy", tsv), Symbol: "\uE8A5")); break; } default: items.AddRange(BuildOverviewItems(clip)); break; } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("copy", string text)) { try { System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.SetText(text)); NotificationService.Notify("CSV", "클립보드에 복사했습니다."); } catch { /* 비핵심 */ } } return Task.CompletedTask; } // ── 빌더 ───────────────────────────────────────────────────────────────── private static IEnumerable BuildOverviewItems(string csv) { var rows = ParseCsv(csv); if (rows.Count == 0) { yield return new LauncherItem("파싱 실패", "유효한 CSV 데이터가 아닙니다", null, null, Symbol: "\uE783"); yield break; } var headerRow = rows[0]; var dataRows = rows.Count > 1 ? rows.Count - 1 : 0; var colCount = headerRow.Count; yield return new LauncherItem( $"CSV {dataRows}행 × {colCount}열", $"헤더: {string.Join(", ", headerRow.Take(5))}{(colCount > 5 ? " …" : "")}", null, ("copy", csv), Symbol: "\uE8A5"); yield return new LauncherItem("컬럼 수", $"{colCount}개", null, null, Symbol: "\uE8A5"); yield return new LauncherItem("데이터 행수", $"{dataRows}행", null, null, Symbol: "\uE8A5"); yield return new LauncherItem("헤더", string.Join(" | ", headerRow.Take(8)), null, ("copy", string.Join(",", headerRow)), Symbol: "\uE8A5"); // 첫 데이터 행 미리보기 if (rows.Count > 1) { var first = rows[1]; yield return new LauncherItem( "첫 번째 행", string.Join(" | ", first.Take(6)), null, ("copy", string.Join(",", first)), Symbol: "\uE8A5"); } } private static IEnumerable ExtractColumn(List> rows, int colIdx) { if (rows.Count == 0) { yield return new LauncherItem("데이터 없음", "", null, null, Symbol: "\uE783"); yield break; } var maxCol = rows.Max(r => r.Count) - 1; colIdx = Math.Clamp(colIdx, 0, maxCol); var header = rows[0].Count > colIdx ? rows[0][colIdx] : $"컬럼{colIdx+1}"; var values = rows.Skip(1).Where(r => r.Count > colIdx).Select(r => r[colIdx]).ToList(); var allText = string.Join("\n", values); yield return new LauncherItem( $"[{colIdx+1}] {header} ({values.Count}개 값)", "전체 복사: Enter", null, ("copy", allText), Symbol: "\uE8A5"); foreach (var v in values.Take(15)) yield return new LauncherItem(v, $"컬럼: {header}", null, ("copy", v), Symbol: "\uE8A5"); } private static IEnumerable ExtractRow(List> rows, int rowIdx) { rowIdx = Math.Clamp(rowIdx, 0, rows.Count - 1); var row = rows[rowIdx]; var header = rows[0]; var allText = string.Join(",", row); yield return new LauncherItem( $"행 {rowIdx+1} ({row.Count}개 값)", allText.Length > 80 ? allText[..80] + "…" : allText, null, ("copy", allText), Symbol: "\uE8A5"); for (var i = 0; i < row.Count; i++) { var colName = header.Count > i ? header[i] : $"컬럼{i+1}"; yield return new LauncherItem($"[{colName}]", row[i], null, ("copy", row[i]), Symbol: "\uE8A5"); } } private static IEnumerable BuildStatsItems(List> rows) { if (rows.Count < 2) { yield return new LauncherItem("데이터 없음", "헤더 포함 2행 이상 필요", null, null, Symbol: "\uE783"); yield break; } var header = rows[0]; var data = rows.Skip(1).ToList(); for (var col = 0; col < header.Count; col++) { var colName = header.Count > col ? header[col] : $"컬럼{col+1}"; var nums = data .Where(r => r.Count > col) .Select(r => r[col]) .Where(v => double.TryParse(v, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out _)) .Select(v => double.Parse(v, System.Globalization.CultureInfo.InvariantCulture)) .ToList(); if (nums.Count == 0) continue; var sum = nums.Sum(); var avg = nums.Average(); var min = nums.Min(); var max = nums.Max(); yield return new LauncherItem( $"[{colName}] 합계: {sum:N2}", $"평균: {avg:N2} 최소: {min:N2} 최대: {max:N2} ({nums.Count}개)", null, ("copy", $"{colName}: sum={sum:N2} avg={avg:N2} min={min:N2} max={max:N2}"), Symbol: "\uE8A5"); } } // ── 변환 헬퍼 ──────────────────────────────────────────────────────────── private static string ConvertToTsv(List> rows) { var sb = new StringBuilder(); foreach (var row in rows) { sb.AppendLine(string.Join("\t", row.Select(EscapeTsv))); } return sb.ToString().TrimEnd(); } private static string EscapeTsv(string v) => v.Replace("\t", " ").Replace("\r", "").Replace("\n", " "); // ── CSV 파서 ───────────────────────────────────────────────────────────── private static List> ParseCsv(string text) { var result = new List>(); // 구분자 자동 감지 (탭 또는 쉼표) var firstLine = text.Split('\n')[0]; var delimiter = firstLine.Count(c => c == '\t') > firstLine.Count(c => c == ',') ? '\t' : ','; using var reader = new System.IO.StringReader(text); string? line; while ((line = reader.ReadLine()) != null) { if (string.IsNullOrWhiteSpace(line)) continue; result.Add(ParseCsvLine(line, delimiter)); } return result; } private static List ParseCsvLine(string line, char delim) { var fields = new List(); var sb = new StringBuilder(); var inQuote = false; for (var i = 0; i < line.Length; i++) { var c = line[i]; if (inQuote) { if (c == '"') { if (i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; } else { inQuote = false; } } else { sb.Append(c); } } else { if (c == '"') { inQuote = true; } else if (c == delim){ fields.Add(sb.ToString()); sb.Clear(); } else { sb.Append(c); } } } fields.Add(sb.ToString()); return fields; } private static bool LooksCsv(string text) { var firstLine = text.Split('\n').FirstOrDefault(l => !string.IsNullOrWhiteSpace(l)) ?? ""; return firstLine.Contains(',') || firstLine.Contains('\t'); } private static string GetClipboard() { try { return System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.ContainsText() ? Clipboard.GetText() : ""); } catch { return ""; } } }