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 ""; }
}
}