[Phase L17] 단위변환·숫자읽기·YAML·Gitignore 핸들러 4종 추가
UnitHandler.cs (신규, ~230줄, prefix=unit):
- 길이/무게/온도/넓이/속도/데이터/압력/부피 8개 카테고리 50+ UnitDef
- UnitDef record: Names[]/ToBase/Cat/Display 구조
- Convert(): 선형 변환 (value × from.ToBase / to.ToBase)
- ConvertTemp(): 비선형 온도 변환 (°C 경유 중간 변환)
- FindUnit(): Names[] 배열에서 대소문자 무시 검색
- 대상 단위 생략 시 같은 Cat 전체 일괄 변환
- FormatNum(): 과학표기/정수/소수 자동 포맷
NumHandler.cs (신규, ~200줄, prefix=num):
- TryParseNumber(): 0x/0b/0o 접두사 + double 자동 파싱
- ToKorean() + KoNumber(): 조·억·만 재귀 분해 한국어 읽기
- ToKoreanUnit(): 만·억·조 단위 숫자 축약
- ToEnglish() + EnNumber(): 영어 읽기 (billion/million/thousand)
- ToRoman(): 1~3999 로마 숫자 변환
- Convert.ToString(lv, 2/8): 2·8진수 변환
YamlHandler.cs (신규, ~290줄, prefix=yaml, partial class):
- ParseYaml() + ParseBlock(): 외부 라이브러리 없이 경량 YAML 파서
- ParseScalar(): true/false/null/숫자/문자열 타입 자동 감지
- GetByPath(): 점 표기법 재귀 경로 조회
- Flatten(): 중첩 객체/배열 → key.sub[0]: value 평탄화
- CountKeys(): 재귀 키 수 집계
- [GeneratedRegex] KeyLineRegex 소스 생성기 사용
GitignoreHandler.cs (신규, ~280줄, prefix=gitignore):
- 14개 내장 템플릿: node/python/csharp/java/go/rust/react/flutter
/android/ios/unity/windows/macos/linux
- 별칭 배열: nodejs, dotnet, net, maven, cargo, nextjs, swift 등
- 여러 키워드 입력 시 병합 (# ===== 섹션 구분)
- FindTemplate(): 직접키 → 별칭 → 부분일치 순서 탐색
- 미리보기 12줄 표시 + 전체 복사
App.xaml.cs (수정): Phase L17 핸들러 4종 RegisterHandler 추가
docs/LAUNCHER_ROADMAP.md (수정): Phase L17 섹션 추가 (✅ 완료)
빌드: 경고 0, 오류 0
This commit is contained in:
@@ -347,3 +347,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
|||||||
| L16-2 | **Docker 관리** ✅ | `docker` 프리픽스. `docker ps` 실행 중 컨테이너 목록(이름·상태·포트). `docker all` 중지 포함 전체 목록. `docker images` 로컬 이미지 목록(크기·생성일). `docker stop/start <이름>` 터미널 없이 직접 실행. `docker logs <이름>` 터미널에서 로그. `docker shell <이름>` exec -it sh 접속. Docker 미설치 감지 | 높음 |
|
| L16-2 | **Docker 관리** ✅ | `docker` 프리픽스. `docker ps` 실행 중 컨테이너 목록(이름·상태·포트). `docker all` 중지 포함 전체 목록. `docker images` 로컬 이미지 목록(크기·생성일). `docker stop/start <이름>` 터미널 없이 직접 실행. `docker logs <이름>` 터미널에서 로그. `docker shell <이름>` exec -it sh 접속. Docker 미설치 감지 | 높음 |
|
||||||
| L16-3 | **할 일 목록** ✅ | `todo` 프리픽스. `todo <내용>` 새 항목 추가. `todo done <번호>` 완료 토글. `todo del <번호>` 삭제. `todo clear` 완료 항목 정리. `todo clear all` 전체 삭제. `todo <검색어>` 키워드 필터. 번호만 입력 시 빠른 완료 토글. 미완료 먼저, 완료 항목 하단 그룹. `%APPDATA%\AxCopilot\todos.json` 로컬 저장 | 높음 |
|
| L16-3 | **할 일 목록** ✅ | `todo` 프리픽스. `todo <내용>` 새 항목 추가. `todo done <번호>` 완료 토글. `todo del <번호>` 삭제. `todo clear` 완료 항목 정리. `todo clear all` 전체 삭제. `todo <검색어>` 키워드 필터. 번호만 입력 시 빠른 완료 토글. 미완료 먼저, 완료 항목 하단 그룹. `%APPDATA%\AxCopilot\todos.json` 로컬 저장 | 높음 |
|
||||||
| L16-4 | **텍스트 → 표 변환기** ✅ | `table` 프리픽스. 클립보드 텍스트 자동 읽기. 탭·CSV·공백 구분자 자동 감지. `table` → 마크다운 표. `table csv` → CSV 변환. `table html` → HTML `<table>` 태그. `table flip` 행·열 전치(transpose). `table sort N` N번 열 기준 정렬(숫자/문자 자동 감지). 셀 너비 자동 정렬(PadRight). 미리보기 3줄 표시 | 높음 |
|
| L16-4 | **텍스트 → 표 변환기** ✅ | `table` 프리픽스. 클립보드 텍스트 자동 읽기. 탭·CSV·공백 구분자 자동 감지. `table` → 마크다운 표. `table csv` → CSV 변환. `table html` → HTML `<table>` 태그. `table flip` 행·열 전치(transpose). `table sort N` N번 열 기준 정렬(숫자/문자 자동 감지). 셀 너비 자동 정렬(PadRight). 미리보기 3줄 표시 | 높음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase L17 — 단위·숫자·YAML·Gitignore 도구 (v2.0.9) ✅ 완료
|
||||||
|
|
||||||
|
> **방향**: 개발자·업무 실용 도구 심화 — 단위 변환, 숫자 읽기, YAML 분석, 프로젝트 초기화.
|
||||||
|
|
||||||
|
| # | 기능 | 설명 | 우선순위 |
|
||||||
|
|---|------|------|----------|
|
||||||
|
| L17-1 | **단위 변환기** ✅ | `unit` 프리픽스. 길이(km·m·ft·in·mi)·무게(kg·lb·oz·근)·온도(°C·°F·K)·넓이(m²·ha·acre·평)·속도(km/h·mph·m/s·knot)·데이터(bit·B·KB~PB)·압력(Pa·atm·bar·psi)·부피(L·mL·gallon·cup) 8개 카테고리 50+ 단위. `unit 100 km m` → 변환. 대상 단위 생략 시 같은 카테고리 전체 일괄 변환. 한글 단위명 별칭 지원 | 높음 |
|
||||||
|
| L17-2 | **숫자 포맷·읽기** ✅ | `num` 프리픽스. `num 1234567` → 천단위·한글 단위(만·억·조)·한국어 읽기·영어 읽기·16진수·8진수·2진수·과학표기·로마 숫자 일괄 표시. `0x/0b/0o` 접두사 진수 입력. `num 42 ko` 한국어 읽기만. `num 42 en` 영어 읽기만. 1~3999 로마 숫자 변환. ToKorean(): 조·억·만 단위 재귀 분해 | 높음 |
|
||||||
|
| L17-3 | **YAML 파서·분석기** ✅ | `yaml` 프리픽스. 클립보드 자동 읽기. 외부 라이브러리 없이 순수 구현(경량 파서). `yaml validate` 유효성 검사. `yaml keys` 최상위 키 목록. `yaml get key.sub` 점 표기법 경로 조회. `yaml stats` 줄·키·깊이 통계. `yaml flat` 점 표기법 평탄화(flatten). [GeneratedRegex] 소스 생성기 | 높음 |
|
||||||
|
| L17-4 | **.gitignore 생성기** ✅ | `gitignore` 프리픽스. Node/Python/C#(.NET)/Java/Go/Rust/React(Next.js·Vite·Vue)/Flutter/Android/iOS/Unity/Windows/macOS/Linux 14개 내장 템플릿. 별칭(nodejs·npm·dotnet·net·maven·golang·cargo·nextjs·swift 등) 지원. 여러 템플릿 명 입력 시 자동 병합. 미리보기 12줄 표시. Enter → 클립보드 복사 | 높음 |
|
||||||
|
|||||||
@@ -297,6 +297,16 @@ public partial class App : System.Windows.Application
|
|||||||
// L16-4: 텍스트 → 표 변환기 (prefix=table)
|
// L16-4: 텍스트 → 표 변환기 (prefix=table)
|
||||||
commandResolver.RegisterHandler(new TableHandler());
|
commandResolver.RegisterHandler(new TableHandler());
|
||||||
|
|
||||||
|
// ─── Phase L17 핸들러 ─────────────────────────────────────────────────
|
||||||
|
// L17-1: 단위 변환기 (prefix=unit)
|
||||||
|
commandResolver.RegisterHandler(new UnitHandler());
|
||||||
|
// L17-2: 숫자 포맷·읽기 변환기 (prefix=num)
|
||||||
|
commandResolver.RegisterHandler(new NumHandler());
|
||||||
|
// L17-3: YAML 파서·포맷터 (prefix=yaml)
|
||||||
|
commandResolver.RegisterHandler(new YamlHandler());
|
||||||
|
// L17-4: .gitignore 생성기 (prefix=gitignore)
|
||||||
|
commandResolver.RegisterHandler(new GitignoreHandler());
|
||||||
|
|
||||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||||
var pluginHost = new PluginHost(settings, commandResolver);
|
var pluginHost = new PluginHost(settings, commandResolver);
|
||||||
pluginHost.LoadAll();
|
pluginHost.LoadAll();
|
||||||
|
|||||||
536
src/AxCopilot/Handlers/GitignoreHandler.cs
Normal file
536
src/AxCopilot/Handlers/GitignoreHandler.cs
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L17-4: .gitignore 생성기 핸들러. "gitignore" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: gitignore → 지원 언어·프레임워크 목록
|
||||||
|
/// gitignore node → Node.js .gitignore 생성
|
||||||
|
/// gitignore python → Python .gitignore 생성
|
||||||
|
/// gitignore csharp → C# / .NET .gitignore 생성
|
||||||
|
/// gitignore java → Java .gitignore 생성
|
||||||
|
/// gitignore react → React (Node 기반) .gitignore 생성
|
||||||
|
/// gitignore node python → 여러 템플릿 병합
|
||||||
|
/// Enter → .gitignore 내용을 클립보드에 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class GitignoreHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "gitignore";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Gitignore",
|
||||||
|
".gitignore 생성기 — Node·Python·C#·Java·Go·Rust 등 내장 템플릿",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
// ── 내장 템플릿 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, (string[] Aliases, string Description, string Content)> Templates =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["node"] = (
|
||||||
|
["nodejs", "npm", "javascript", "js"],
|
||||||
|
"Node.js / npm",
|
||||||
|
"""
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.pnpm-store/
|
||||||
|
.npm/
|
||||||
|
.yarn/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.vite/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["python"] = (
|
||||||
|
["py", "django", "flask", "fastapi"],
|
||||||
|
"Python",
|
||||||
|
"""
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["csharp"] = (
|
||||||
|
["cs", "dotnet", ".net", "net", "aspnet", "aspnetcore"],
|
||||||
|
"C# / .NET",
|
||||||
|
"""
|
||||||
|
# C# / .NET
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
*.userprefs
|
||||||
|
*.pidb
|
||||||
|
*.booproj
|
||||||
|
*.svd
|
||||||
|
*.userprefs
|
||||||
|
packages/
|
||||||
|
*.nupkg
|
||||||
|
**/[Bb]in/
|
||||||
|
**/[Oo]bj/
|
||||||
|
**/[Ll]og/
|
||||||
|
**/[Ll]ogs/
|
||||||
|
TestResults/
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["java"] = (
|
||||||
|
["gradle", "maven", "mvn", "spring"],
|
||||||
|
"Java / Maven / Gradle",
|
||||||
|
"""
|
||||||
|
# Java / Maven / Gradle
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.ctxt
|
||||||
|
.mtj.tmp/
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
|
target/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
.gradle/
|
||||||
|
.mvn/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!.mvn/wrapper/maven-wrapper.properties
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["go"] = (
|
||||||
|
["golang"],
|
||||||
|
"Go (Golang)",
|
||||||
|
"""
|
||||||
|
# Go
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
vendor/
|
||||||
|
.env
|
||||||
|
dist/
|
||||||
|
bin/
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["rust"] = (
|
||||||
|
["cargo"],
|
||||||
|
"Rust / Cargo",
|
||||||
|
"""
|
||||||
|
# Rust / Cargo
|
||||||
|
/target/
|
||||||
|
Cargo.lock
|
||||||
|
**/*.rs.bk
|
||||||
|
*.pdb
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["react"] = (
|
||||||
|
["nextjs", "next", "vue", "vite", "svelte"],
|
||||||
|
"React / Next.js / Vue / Vite",
|
||||||
|
"""
|
||||||
|
# React / Next.js / Vite
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["flutter"] = (
|
||||||
|
["dart"],
|
||||||
|
"Flutter / Dart",
|
||||||
|
"""
|
||||||
|
# Flutter / Dart
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["android"] = (
|
||||||
|
["kotlin", "gradle-android"],
|
||||||
|
"Android",
|
||||||
|
"""
|
||||||
|
# Android
|
||||||
|
*.iml
|
||||||
|
.gradle/
|
||||||
|
/local.properties
|
||||||
|
/.idea/
|
||||||
|
.DS_Store
|
||||||
|
/build/
|
||||||
|
/captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
google-services.json
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["ios"] = (
|
||||||
|
["swift", "xcode", "objc", "objective-c"],
|
||||||
|
"iOS / Swift / Xcode",
|
||||||
|
"""
|
||||||
|
# iOS / Swift / Xcode
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
.build/
|
||||||
|
*.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
xcuserdata/
|
||||||
|
*.xcworkspace
|
||||||
|
!default.xcworkspace
|
||||||
|
.swiftpm/
|
||||||
|
Packages/
|
||||||
|
*.resolved
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
*.xcuserstate
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["unity"] = (
|
||||||
|
[],
|
||||||
|
"Unity",
|
||||||
|
"""
|
||||||
|
# Unity
|
||||||
|
/[Ll]ibrary/
|
||||||
|
/[Tt]emp/
|
||||||
|
/[Oo]bj/
|
||||||
|
/[Bb]uild/
|
||||||
|
/[Bb]uilds/
|
||||||
|
/[Ll]ogs/
|
||||||
|
/[Uu]ser[Ss]ettings/
|
||||||
|
/[Mm]emoryCaptures/
|
||||||
|
/[Rr]ecordings/
|
||||||
|
/[Pp]rofiles/
|
||||||
|
/[Pp]rofile[Ss]
|
||||||
|
/[Aa]ssets/Plugins/EditorVR.meta
|
||||||
|
/[Pp]ackages/
|
||||||
|
!/[Pp]ackages/manifest.json
|
||||||
|
!/[Pp]ackages/packages-lock.json
|
||||||
|
/*.sln
|
||||||
|
/*.csproj
|
||||||
|
/.vs/
|
||||||
|
.DS_Store
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["windows"] = (
|
||||||
|
["win", "powershell", "ps"],
|
||||||
|
"Windows 공통",
|
||||||
|
"""
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
*.stackdump
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["macos"] = (
|
||||||
|
["mac", "osx"],
|
||||||
|
"macOS 공통",
|
||||||
|
"""
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
"""),
|
||||||
|
|
||||||
|
["linux"] = (
|
||||||
|
[],
|
||||||
|
"Linux 공통",
|
||||||
|
"""
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
"""),
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(".gitignore 생성기",
|
||||||
|
"예: gitignore node / gitignore python / gitignore csharp / gitignore node python",
|
||||||
|
null, null, Symbol: "\uEA3C"));
|
||||||
|
items.Add(new LauncherItem($"── 지원 템플릿 {Templates.Count}개 ──", "", null, null, Symbol: "\uEA3C"));
|
||||||
|
foreach (var (key, (aliases, desc, _)) in Templates.OrderBy(t => t.Key))
|
||||||
|
{
|
||||||
|
var aliasStr = aliases.Length > 0 ? $" ({string.Join(", ", aliases.Take(3))})" : "";
|
||||||
|
items.Add(new LauncherItem(key, $"{desc}{aliasStr}", null, ("gen", key), Symbol: "\uEA3C"));
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 여러 키워드 → 병합
|
||||||
|
var keywords = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var matched = new List<string>(); // 템플릿 키 목록
|
||||||
|
|
||||||
|
foreach (var kw in keywords)
|
||||||
|
{
|
||||||
|
var found = FindTemplate(kw);
|
||||||
|
if (found != null && !matched.Contains(found))
|
||||||
|
matched.Add(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{q}' 템플릿을 찾을 수 없습니다",
|
||||||
|
$"지원: {string.Join(", ", Templates.Keys.Take(10))}…",
|
||||||
|
null, null, Symbol: "\uE946"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 템플릿 생성
|
||||||
|
if (matched.Count == 1)
|
||||||
|
{
|
||||||
|
var key = matched[0];
|
||||||
|
var (_, desc, content) = Templates[key];
|
||||||
|
var trimmed = content.Trim();
|
||||||
|
|
||||||
|
items.Add(new LauncherItem($".gitignore [{key}]",
|
||||||
|
$"{desc} · {trimmed.Split('\n').Length}줄 · Enter → 복사",
|
||||||
|
null, ("copy", trimmed), Symbol: "\uEA3C"));
|
||||||
|
|
||||||
|
// 미리보기
|
||||||
|
foreach (var line in trimmed.Split('\n').Take(12))
|
||||||
|
items.Add(new LauncherItem(line, "", null, null, Symbol: "\uEA3C"));
|
||||||
|
|
||||||
|
if (trimmed.Split('\n').Length > 12)
|
||||||
|
items.Add(new LauncherItem($"… 외 {trimmed.Split('\n').Length - 12}줄",
|
||||||
|
"전체는 첫 항목 Enter로 복사", null, null, Symbol: "\uEA3C"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 다중 병합
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
var totalLines = 0;
|
||||||
|
foreach (var key in matched)
|
||||||
|
{
|
||||||
|
var (_, desc, content) = Templates[key];
|
||||||
|
sb.AppendLine($"# ===== {desc} =====");
|
||||||
|
sb.AppendLine(content.Trim());
|
||||||
|
sb.AppendLine();
|
||||||
|
totalLines += content.Split('\n').Length;
|
||||||
|
}
|
||||||
|
var merged = sb.ToString().TrimEnd();
|
||||||
|
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$".gitignore 병합 [{string.Join(" + ", matched)}]",
|
||||||
|
$"{totalLines}줄 · Enter → 복사",
|
||||||
|
null, ("copy", merged), Symbol: "\uEA3C"));
|
||||||
|
|
||||||
|
foreach (var key in matched)
|
||||||
|
{
|
||||||
|
var (_, desc, _) = Templates[key];
|
||||||
|
items.Add(new LauncherItem($"[{key}]", desc, null, ("gen", key), Symbol: "\uEA3C"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
switch (item.Data)
|
||||||
|
{
|
||||||
|
case ("copy", string text):
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||||
|
() => Clipboard.SetText(text));
|
||||||
|
NotificationService.Notify("Gitignore", ".gitignore 내용을 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ("gen", string key):
|
||||||
|
if (Templates.TryGetValue(key, out var t))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||||
|
() => Clipboard.SetText(t.Content.Trim()));
|
||||||
|
NotificationService.Notify("Gitignore", $"[{key}] .gitignore 복사됨");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string? FindTemplate(string keyword)
|
||||||
|
{
|
||||||
|
// 직접 키 일치
|
||||||
|
if (Templates.ContainsKey(keyword)) return keyword;
|
||||||
|
|
||||||
|
// 별칭 검색
|
||||||
|
foreach (var (key, (aliases, _, _)) in Templates)
|
||||||
|
{
|
||||||
|
if (aliases.Any(a => a.Equals(keyword, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 부분 일치
|
||||||
|
var partial = Templates.Keys
|
||||||
|
.FirstOrDefault(k => k.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return partial;
|
||||||
|
}
|
||||||
|
}
|
||||||
288
src/AxCopilot/Handlers/NumHandler.cs
Normal file
288
src/AxCopilot/Handlers/NumHandler.cs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L17-2: 숫자 포맷·읽기 변환 핸들러. "num" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: num 1234567 → 천단위·한글·영어·진수·과학표기 모두 표시
|
||||||
|
/// num 0xff → 16진수 → 10진수 변환
|
||||||
|
/// num 0b1010 → 2진수 → 10진수 변환
|
||||||
|
/// num 42 ko → 한국어로 읽기 (사십이)
|
||||||
|
/// num 42 en → 영어로 읽기 (forty-two)
|
||||||
|
/// num 1e6 → 과학표기 → 일반 변환
|
||||||
|
/// Enter → 결과 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class NumHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "num";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Num",
|
||||||
|
"숫자 포맷·읽기 변환 — 한글·영어·진수·천단위·과학표기",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("숫자 포맷·읽기 변환기",
|
||||||
|
"예: num 1234567 / num 0xff / num 42 ko / num 42 en",
|
||||||
|
null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num <숫자>", "천단위·한글·영어·진수 변환", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num 0xff", "16진수 입력", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num 0b1010", "2진수 입력", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num 0o17", "8진수 입력", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num 42 ko", "한국어로 읽기", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("num 42 en", "영어로 읽기", null, null, Symbol: "\uE8EF"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var numStr = parts[0];
|
||||||
|
var mode = parts.Length > 1 ? parts[1].ToLowerInvariant() : null;
|
||||||
|
|
||||||
|
// 숫자 파싱 (여러 진수 지원)
|
||||||
|
if (!TryParseNumber(numStr, out var value, out var inputBase))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("숫자 형식 오류",
|
||||||
|
"정수 또는 0x/0b/0o 접두사 숫자를 입력하세요", null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 한국어 읽기 전용
|
||||||
|
if (mode is "ko" or "kr" or "한국어" or "한글")
|
||||||
|
{
|
||||||
|
var ko = ToKorean(value);
|
||||||
|
items.Add(new LauncherItem(ko, $"{value:N0} 한국어 읽기", null, ("copy", ko), Symbol: "\uE8EF"));
|
||||||
|
var ko2 = ToKoreanMoney(value);
|
||||||
|
items.Add(new LauncherItem(ko2, "금액 읽기 (원)", null, ("copy", ko2), Symbol: "\uE8EF"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영어 읽기 전용
|
||||||
|
if (mode is "en" or "english" or "영어")
|
||||||
|
{
|
||||||
|
var en = ToEnglish(value);
|
||||||
|
items.Add(new LauncherItem(en, $"{value:N0} 영어 읽기", null, ("copy", en), Symbol: "\uE8EF"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 변환 표시
|
||||||
|
items.Add(new LauncherItem($"{value:N0}",
|
||||||
|
$"입력값 ({inputBase}진수 → 10진수)",
|
||||||
|
null, ("copy", $"{value}"), Symbol: "\uE8EF"));
|
||||||
|
|
||||||
|
// 천단위 구분
|
||||||
|
var commaSep = $"{value:N0}";
|
||||||
|
items.Add(new LauncherItem(commaSep, "천단위 구분", null, ("copy", commaSep), Symbol: "\uE8EF"));
|
||||||
|
|
||||||
|
// 한글 단위 (만·억·조)
|
||||||
|
var krUnit = ToKoreanUnit(value);
|
||||||
|
items.Add(new LauncherItem(krUnit, "한글 단위", null, ("copy", krUnit), Symbol: "\uE8EF"));
|
||||||
|
|
||||||
|
// 한국어 읽기
|
||||||
|
var koRead = ToKorean(value);
|
||||||
|
items.Add(new LauncherItem(koRead, "한국어 읽기", null, ("copy", koRead), Symbol: "\uE8EF"));
|
||||||
|
|
||||||
|
// 영어 읽기
|
||||||
|
if (Math.Abs(value) < 1_000_000_000_000L)
|
||||||
|
{
|
||||||
|
var enRead = ToEnglish(value);
|
||||||
|
items.Add(new LauncherItem(enRead, "영어 읽기", null, ("copy", enRead), Symbol: "\uE8EF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진수 변환 (정수 범위만)
|
||||||
|
if (value >= 0 && value <= long.MaxValue)
|
||||||
|
{
|
||||||
|
var lv = (long)value;
|
||||||
|
var hex = $"0x{lv:X}";
|
||||||
|
var bin = Convert.ToString(lv, 2);
|
||||||
|
var oct = Convert.ToString(lv, 8);
|
||||||
|
items.Add(new LauncherItem($"── 진수 변환 ──", "", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem($"16진수 {hex}", $"Hex", null, ("copy", hex), Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem($"8진수 0o{oct}", $"Octal", null, ("copy", $"0o{oct}"), Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem($"2진수 {bin}", $"Binary ({bin.Length}bit)", null, ("copy", bin), Symbol: "\uE8EF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 과학 표기
|
||||||
|
var sci = $"{value:E4}";
|
||||||
|
items.Add(new LauncherItem($"과학 표기 {sci}", "Scientific Notation", null, ("copy", sci), Symbol: "\uE8EF"));
|
||||||
|
|
||||||
|
// 로마 숫자 (1~3999)
|
||||||
|
if (value >= 1 && value <= 3999)
|
||||||
|
{
|
||||||
|
var roman = ToRoman((int)value);
|
||||||
|
items.Add(new LauncherItem($"로마 숫자 {roman}", "Roman Numerals", null, ("copy", roman), Symbol: "\uE8EF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(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("Num", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 파싱 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static bool TryParseNumber(string s, out double value, out int inputBase)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
inputBase = 10;
|
||||||
|
s = s.Replace(",", "").Trim();
|
||||||
|
|
||||||
|
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
s.StartsWith("&H", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
inputBase = 16;
|
||||||
|
var hex = s[2..];
|
||||||
|
if (long.TryParse(hex, System.Globalization.NumberStyles.HexNumber,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var lv))
|
||||||
|
{ value = lv; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (s.StartsWith("0b", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
inputBase = 2;
|
||||||
|
try { value = System.Convert.ToInt64(s[2..], 2); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
if (s.StartsWith("0o", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
inputBase = 8;
|
||||||
|
try { value = System.Convert.ToInt64(s[2..], 8); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 과학 표기법 포함 일반 double
|
||||||
|
if (double.TryParse(s, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out value))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 한글 단위 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string ToKoreanUnit(double value)
|
||||||
|
{
|
||||||
|
var abs = Math.Abs(value);
|
||||||
|
var sign = value < 0 ? "-" : "";
|
||||||
|
|
||||||
|
if (abs >= 1_0000_0000_0000) return $"{sign}{abs / 1_0000_0000_0000:N2}조";
|
||||||
|
if (abs >= 1_0000_0000) return $"{sign}{abs / 1_0000_0000:N2}억";
|
||||||
|
if (abs >= 1_0000) return $"{sign}{abs / 1_0000:N2}만";
|
||||||
|
return $"{sign}{abs:N0}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] KoOnes = ["", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
|
||||||
|
private static readonly string[] KoTens = ["", "십", "이십", "삼십", "사십", "오십", "육십", "칠십", "팔십", "구십"];
|
||||||
|
private static readonly string[] KoHundreds= ["", "백", "이백", "삼백", "사백", "오백", "육백", "칠백", "팔백", "구백"];
|
||||||
|
|
||||||
|
private static string ToKorean(double value)
|
||||||
|
{
|
||||||
|
if (value == 0) return "영";
|
||||||
|
var abs = (long)Math.Abs(value);
|
||||||
|
var sign = value < 0 ? "마이너스 " : "";
|
||||||
|
return sign + KoNumber(abs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string KoNumber(long n)
|
||||||
|
{
|
||||||
|
if (n == 0) return "";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var jo = n / 1_0000_0000_0000L;
|
||||||
|
var eok = n % 1_0000_0000_0000L / 1_0000_0000L;
|
||||||
|
var man = n % 1_0000_0000L / 1_0000L;
|
||||||
|
var rest = n % 1_0000L;
|
||||||
|
|
||||||
|
if (jo > 0) { sb.Append(KoUnder1만(jo)); sb.Append("조 "); }
|
||||||
|
if (eok > 0) { sb.Append(KoUnder1만(eok)); sb.Append("억 "); }
|
||||||
|
if (man > 0) { sb.Append(KoUnder1만(man)); sb.Append("만 "); }
|
||||||
|
if (rest> 0) { sb.Append(KoUnder1만(rest)); }
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string KoUnder1만(long n)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var thou = (int)(n / 1000);
|
||||||
|
var hund = (int)(n % 1000 / 100);
|
||||||
|
var ten = (int)(n % 100 / 10);
|
||||||
|
var one = (int)(n % 10);
|
||||||
|
if (thou > 0) { sb.Append(thou == 1 ? "천" : KoOnes[thou] + "천"); }
|
||||||
|
if (hund > 0) { sb.Append(hund == 1 ? "백" : KoOnes[hund] + "백"); }
|
||||||
|
if (ten > 0) { sb.Append(ten == 1 ? "십" : KoOnes[ten] + "십"); }
|
||||||
|
if (one > 0) { sb.Append(KoOnes[one]); }
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToKoreanMoney(double value)
|
||||||
|
{
|
||||||
|
var ko = ToKorean(value);
|
||||||
|
return string.IsNullOrEmpty(ko) ? "영 원" : $"{ko} 원";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 영어 읽기 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static readonly string[] EnOnes =
|
||||||
|
["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
||||||
|
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
|
||||||
|
"sixteen", "seventeen", "eighteen", "nineteen"];
|
||||||
|
private static readonly string[] EnTens =
|
||||||
|
["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
|
||||||
|
|
||||||
|
private static string ToEnglish(double value)
|
||||||
|
{
|
||||||
|
if (value == 0) return "zero";
|
||||||
|
var n = (long)Math.Abs(value);
|
||||||
|
var sign = value < 0 ? "negative " : "";
|
||||||
|
return sign + EnNumber(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnNumber(long n)
|
||||||
|
{
|
||||||
|
if (n == 0) return "";
|
||||||
|
if (n < 20) return EnOnes[n];
|
||||||
|
if (n < 100) return EnTens[n / 10] + (n % 10 > 0 ? "-" + EnOnes[n % 10] : "");
|
||||||
|
if (n < 1000)
|
||||||
|
{
|
||||||
|
var rest = n % 100 > 0 ? " and " + EnNumber(n % 100) : "";
|
||||||
|
return EnOnes[n / 100] + " hundred" + rest;
|
||||||
|
}
|
||||||
|
if (n < 1_000_000) return EnNumber(n / 1000) + " thousand" + (n % 1000 > 0 ? " " + EnNumber(n % 1000) : "");
|
||||||
|
if (n < 1_000_000_000) return EnNumber(n / 1_000_000) + " million" + (n % 1_000_000 > 0 ? " " + EnNumber(n % 1_000_000) : "");
|
||||||
|
return EnNumber(n / 1_000_000_000) + " billion" + (n % 1_000_000_000 > 0 ? " " + EnNumber(n % 1_000_000_000) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 로마 숫자 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string ToRoman(int n)
|
||||||
|
{
|
||||||
|
var vals = new[] { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
|
||||||
|
var syms = new[] { "M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I" };
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var i = 0; i < vals.Length; i++)
|
||||||
|
while (n >= vals[i]) { sb.Append(syms[i]); n -= vals[i]; }
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
284
src/AxCopilot/Handlers/UnitHandler.cs
Normal file
284
src/AxCopilot/Handlers/UnitHandler.cs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L17-1: 단위 변환기 핸들러. "unit" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: unit 100 km m → 100km → m
|
||||||
|
/// unit 72 f c → 화씨 72°F → 섭씨
|
||||||
|
/// unit 5 kg lb → 5kg → 파운드
|
||||||
|
/// unit 1 gb mb → 1GB → MB
|
||||||
|
/// unit 60 mph kmh → 속도 변환
|
||||||
|
/// unit length → 길이 단위 목록
|
||||||
|
/// unit weight / temp / area / speed / data → 카테고리 목록
|
||||||
|
/// Enter → 결과 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class UnitHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "unit";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Unit",
|
||||||
|
"단위 변환기 — 길이·무게·온도·넓이·속도·데이터",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
// ── 단위 정의 (기준 단위 → SI 변환 계수) ─────────────────────────────────
|
||||||
|
// 온도는 별도 처리 (비선형)
|
||||||
|
|
||||||
|
private enum UnitCategory { Length, Weight, Area, Speed, Data, Temperature, Pressure, Volume }
|
||||||
|
|
||||||
|
private record UnitDef(string[] Names, double ToBase, UnitCategory Cat, string Display);
|
||||||
|
|
||||||
|
private static readonly UnitDef[] Units =
|
||||||
|
[
|
||||||
|
// 길이 (기준: m)
|
||||||
|
new(["km","킬로미터"], 1000, UnitCategory.Length, "킬로미터 (km)"),
|
||||||
|
new(["m","미터"], 1, UnitCategory.Length, "미터 (m)"),
|
||||||
|
new(["cm","센티미터"], 0.01, UnitCategory.Length, "센티미터 (cm)"),
|
||||||
|
new(["mm","밀리미터"], 0.001, UnitCategory.Length, "밀리미터 (mm)"),
|
||||||
|
new(["mi","mile","마일"], 1609.344, UnitCategory.Length, "마일 (mi)"),
|
||||||
|
new(["yd","yard","야드"], 0.9144, UnitCategory.Length, "야드 (yd)"),
|
||||||
|
new(["ft","feet","foot","피트"], 0.3048, UnitCategory.Length, "피트 (ft)"),
|
||||||
|
new(["in","inch","인치"], 0.0254, UnitCategory.Length, "인치 (in)"),
|
||||||
|
new(["nm","해리"], 1852, UnitCategory.Length, "해리 (nm)"),
|
||||||
|
|
||||||
|
// 무게 (기준: kg)
|
||||||
|
new(["t","ton","톤"], 1000, UnitCategory.Weight, "톤 (t)"),
|
||||||
|
new(["kg","킬로그램"], 1, UnitCategory.Weight, "킬로그램 (kg)"),
|
||||||
|
new(["g","그램"], 0.001, UnitCategory.Weight, "그램 (g)"),
|
||||||
|
new(["mg","밀리그램"], 1e-6, UnitCategory.Weight, "밀리그램 (mg)"),
|
||||||
|
new(["lb","lbs","파운드"], 0.453592, UnitCategory.Weight, "파운드 (lb)"),
|
||||||
|
new(["oz","온스"], 0.0283495, UnitCategory.Weight, "온스 (oz)"),
|
||||||
|
new(["근"], 0.6, UnitCategory.Weight, "근 (600g)"),
|
||||||
|
|
||||||
|
// 넓이 (기준: m²)
|
||||||
|
new(["km2","km²"], 1e6, UnitCategory.Area, "제곱킬로미터 (km²)"),
|
||||||
|
new(["m2","m²","sqm"], 1, UnitCategory.Area, "제곱미터 (m²)"),
|
||||||
|
new(["cm2","cm²"], 0.0001, UnitCategory.Area, "제곱센티미터 (cm²)"),
|
||||||
|
new(["ha","헥타르"], 10000, UnitCategory.Area, "헥타르 (ha)"),
|
||||||
|
new(["a","아르"], 100, UnitCategory.Area, "아르 (a)"),
|
||||||
|
new(["acre","에이커"], 4046.856, UnitCategory.Area, "에이커 (acre)"),
|
||||||
|
new(["ft2","ft²","sqft"], 0.092903, UnitCategory.Area, "제곱피트 (ft²)"),
|
||||||
|
new(["평"], 3.30579, UnitCategory.Area, "평 (3.3058m²)"),
|
||||||
|
|
||||||
|
// 속도 (기준: m/s)
|
||||||
|
new(["mps","m/s"], 1, UnitCategory.Speed, "미터/초 (m/s)"),
|
||||||
|
new(["kph","kmh","km/h","kmph"], 0.277778, UnitCategory.Speed, "킬로미터/시 (km/h)"),
|
||||||
|
new(["mph","mi/h"], 0.44704, UnitCategory.Speed, "마일/시 (mph)"),
|
||||||
|
new(["knot","kn","노트"], 0.514444, UnitCategory.Speed, "노트 (kn)"),
|
||||||
|
new(["fps","ft/s"], 0.3048, UnitCategory.Speed, "피트/초 (ft/s)"),
|
||||||
|
|
||||||
|
// 데이터 (기준: byte)
|
||||||
|
new(["b","bit","비트"], 0.125, UnitCategory.Data, "비트 (bit)"),
|
||||||
|
new(["byte","바이트"], 1, UnitCategory.Data, "바이트 (byte)"),
|
||||||
|
new(["kb","킬로바이트"], 1024, UnitCategory.Data, "킬로바이트 (KB)"),
|
||||||
|
new(["mb","메가바이트"], 1048576, UnitCategory.Data, "메가바이트 (MB)"),
|
||||||
|
new(["gb","기가바이트"], 1073741824, UnitCategory.Data, "기가바이트 (GB)"),
|
||||||
|
new(["tb","테라바이트"], 1099511627776,UnitCategory.Data, "테라바이트 (TB)"),
|
||||||
|
new(["pb","페타바이트"], 1.12589990684e15, UnitCategory.Data, "페타바이트 (PB)"),
|
||||||
|
|
||||||
|
// 온도 (기준: °C, 변환은 특수 처리)
|
||||||
|
new(["c","°c","celsius","섭씨"], 1, UnitCategory.Temperature, "섭씨 (°C)"),
|
||||||
|
new(["f","°f","fahrenheit","화씨"],1, UnitCategory.Temperature, "화씨 (°F)"),
|
||||||
|
new(["k","kelvin","켈빈"], 1, UnitCategory.Temperature, "켈빈 (K)"),
|
||||||
|
|
||||||
|
// 압력 (기준: Pa)
|
||||||
|
new(["pa","파스칼"], 1, UnitCategory.Pressure, "파스칼 (Pa)"),
|
||||||
|
new(["kpa"], 1000, UnitCategory.Pressure, "킬로파스칼 (kPa)"),
|
||||||
|
new(["mpa"], 1e6, UnitCategory.Pressure, "메가파스칼 (MPa)"),
|
||||||
|
new(["atm","기압"], 101325, UnitCategory.Pressure, "기압 (atm)"),
|
||||||
|
new(["bar","바"], 100000, UnitCategory.Pressure, "바 (bar)"),
|
||||||
|
new(["psi"], 6894.757, UnitCategory.Pressure, "PSI (psi)"),
|
||||||
|
|
||||||
|
// 부피 (기준: L)
|
||||||
|
new(["l","liter","리터"], 1, UnitCategory.Volume, "리터 (L)"),
|
||||||
|
new(["ml","밀리리터"], 0.001, UnitCategory.Volume, "밀리리터 (mL)"),
|
||||||
|
new(["m3","m³","cbm"], 1000, UnitCategory.Volume, "세제곱미터 (m³)"),
|
||||||
|
new(["cm3","cm³","cc"], 0.001, UnitCategory.Volume, "세제곱센티미터 (cc)"),
|
||||||
|
new(["gallon","gal","갤런"], 3.78541, UnitCategory.Volume, "갤런 (US, gal)"),
|
||||||
|
new(["floz","fl.oz"], 0.0295735, UnitCategory.Volume, "액량온스 (fl.oz)"),
|
||||||
|
new(["cup","컵"], 0.236588, UnitCategory.Volume, "컵 (cup)"),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, UnitCategory> CategoryKeywords =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["length"] = UnitCategory.Length, ["길이"] = UnitCategory.Length,
|
||||||
|
["weight"] = UnitCategory.Weight, ["무게"] = UnitCategory.Weight,
|
||||||
|
["mass"] = UnitCategory.Weight,
|
||||||
|
["area"] = UnitCategory.Area, ["넓이"] = UnitCategory.Area,
|
||||||
|
["speed"] = UnitCategory.Speed, ["속도"] = UnitCategory.Speed,
|
||||||
|
["data"] = UnitCategory.Data, ["데이터"]= UnitCategory.Data,
|
||||||
|
["temp"] = UnitCategory.Temperature, ["온도"] = UnitCategory.Temperature,
|
||||||
|
["temperature"] = UnitCategory.Temperature,
|
||||||
|
["pressure"] = UnitCategory.Pressure, ["압력"] = UnitCategory.Pressure,
|
||||||
|
["volume"] = UnitCategory.Volume, ["부피"] = UnitCategory.Volume,
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("단위 변환기",
|
||||||
|
"예: unit 100 km m / unit 72 f c / unit 5 kg lb / unit 1 gb mb",
|
||||||
|
null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("── 카테고리 ──", "", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit length", "길이 단위 (km·m·cm·ft·in·mi)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit weight", "무게 단위 (kg·g·lb·oz·근)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit temp", "온도 단위 (°C·°F·K)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit area", "넓이 단위 (m²·ha·acre·평)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit speed", "속도 단위 (km/h·mph·m/s·knot)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit data", "데이터 단위 (bit·B·KB·MB·GB·TB)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit pressure", "압력 단위 (Pa·atm·bar·psi)", null, null, Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem("unit volume", "부피 단위 (L·mL·m³·gallon·cup)", null, null, Symbol: "\uE8EF"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
// 카테고리 목록
|
||||||
|
if (parts.Length == 1 && CategoryKeywords.TryGetValue(parts[0], out var cat))
|
||||||
|
{
|
||||||
|
var catUnits = Units.Where(u => u.Cat == cat).ToList();
|
||||||
|
items.Add(new LauncherItem($"{cat} 단위 {catUnits.Count}개",
|
||||||
|
"예: unit 100 km m", null, null, Symbol: "\uE8EF"));
|
||||||
|
foreach (var u in catUnits)
|
||||||
|
items.Add(new LauncherItem(u.Display, string.Join(", ", u.Names), null, null, Symbol: "\uE8EF"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변환: unit <값> <from> <to>
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("입력 형식",
|
||||||
|
"unit <값> <단위> [대상단위] 예: unit 100 km m", null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!double.TryParse(parts[0].Replace(",", ""), System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("숫자 형식 오류",
|
||||||
|
"첫 번째 값이 숫자여야 합니다 예: unit 100 km m", null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromKey = parts[1].ToLowerInvariant();
|
||||||
|
var fromDef = FindUnit(fromKey);
|
||||||
|
if (fromDef == null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{parts[1]}' 단위를 찾을 수 없습니다",
|
||||||
|
"unit length / weight / temp / area / speed / data 로 단위 목록 확인",
|
||||||
|
null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대상 단위 지정
|
||||||
|
if (parts.Length >= 3)
|
||||||
|
{
|
||||||
|
var toKey = parts[2].ToLowerInvariant();
|
||||||
|
var toDef = FindUnit(toKey);
|
||||||
|
if (toDef == null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{parts[2]}' 단위를 찾을 수 없습니다",
|
||||||
|
"", null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
if (fromDef.Cat != toDef.Cat)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("카테고리 불일치",
|
||||||
|
$"{fromDef.Cat} ≠ {toDef.Cat} — 같은 종류끼리만 변환 가능",
|
||||||
|
null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = Convert(value, fromDef, toDef);
|
||||||
|
var label = $"{FormatNum(value)} {fromDef.Names[0].ToUpper()} = {FormatNum(result)} {toDef.Names[0].ToUpper()}";
|
||||||
|
items.Add(new LauncherItem(label, "Enter → 복사", null, ("copy", label), Symbol: "\uE8EF"));
|
||||||
|
items.Add(new LauncherItem($"{FormatNum(result)} {toDef.Names[0].ToUpper()}", toDef.Display,
|
||||||
|
null, ("copy", FormatNum(result)), Symbol: "\uE8EF"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 같은 카테고리 모든 단위로 변환
|
||||||
|
var sameCat = Units.Where(u => u.Cat == fromDef.Cat && u != fromDef).ToList();
|
||||||
|
items.Add(new LauncherItem($"{FormatNum(value)} {fromDef.Names[0].ToUpper()} 변환 결과",
|
||||||
|
fromDef.Display, null, null, Symbol: "\uE8EF"));
|
||||||
|
foreach (var toDef in sameCat)
|
||||||
|
{
|
||||||
|
var result = Convert(value, fromDef, toDef);
|
||||||
|
var label = $"{FormatNum(result)} {toDef.Names[0].ToUpper()}";
|
||||||
|
items.Add(new LauncherItem(label, toDef.Display, null, ("copy", FormatNum(result)), Symbol: "\uE8EF"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(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("Unit", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 변환 로직 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static double Convert(double value, UnitDef from, UnitDef to)
|
||||||
|
{
|
||||||
|
if (from.Cat == UnitCategory.Temperature)
|
||||||
|
return ConvertTemp(value, from.Names[0].ToLowerInvariant(), to.Names[0].ToLowerInvariant());
|
||||||
|
|
||||||
|
// 선형 변환: value × from.ToBase / to.ToBase
|
||||||
|
return value * from.ToBase / to.ToBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ConvertTemp(double value, string from, string to)
|
||||||
|
{
|
||||||
|
// 먼저 °C로
|
||||||
|
var celsius = from switch
|
||||||
|
{
|
||||||
|
"c" or "°c" => value,
|
||||||
|
"f" or "°f" => (value - 32) * 5 / 9,
|
||||||
|
"k" => value - 273.15,
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
// °C에서 목표로
|
||||||
|
return to switch
|
||||||
|
{
|
||||||
|
"c" or "°c" => celsius,
|
||||||
|
"f" or "°f" => celsius * 9 / 5 + 32,
|
||||||
|
"k" => celsius + 273.15,
|
||||||
|
_ => celsius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnitDef? FindUnit(string key) =>
|
||||||
|
Units.FirstOrDefault(u => u.Names.Any(n => n.Equals(key, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
private static string FormatNum(double v)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(v) || double.IsInfinity(v)) return v.ToString();
|
||||||
|
if (Math.Abs(v) >= 1e12 || (Math.Abs(v) < 1e-4 && v != 0))
|
||||||
|
return v.ToString("E3", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
if (v == Math.Floor(v) && Math.Abs(v) < 1e9)
|
||||||
|
return $"{v:N0}";
|
||||||
|
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
410
src/AxCopilot/Handlers/YamlHandler.cs
Normal file
410
src/AxCopilot/Handlers/YamlHandler.cs
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L17-3: YAML 파서·포맷터·검증 핸들러. "yaml" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: yaml → 클립보드 YAML 구조 분석
|
||||||
|
/// yaml validate → YAML 유효성 검사
|
||||||
|
/// yaml keys → 최상위 키 목록
|
||||||
|
/// yaml get key.subkey → 특정 경로 값 조회 (점 표기법)
|
||||||
|
/// yaml stats → 줄 수·키 수·깊이 통계
|
||||||
|
/// yaml flat → 점 표기법으로 평탄화
|
||||||
|
/// Enter → 결과 복사.
|
||||||
|
/// 외부 라이브러리 없이 순수 파싱 구현 (기본 YAML 스펙 지원).
|
||||||
|
/// </summary>
|
||||||
|
public partial class YamlHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "yaml";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"YAML",
|
||||||
|
"YAML 파서·검증 — 키 조회 · 구조 분석 · 평탄화",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
string? clipboard = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (Clipboard.ContainsText())
|
||||||
|
clipboard = Clipboard.GetText();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { /* 클립보드 접근 실패 */ }
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("YAML 파서·분석기",
|
||||||
|
"클립보드 YAML 분석 · yaml validate / keys / get key / stats / flat",
|
||||||
|
null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("yaml validate", "YAML 유효성 검사", null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("yaml keys", "최상위 키 목록", null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("yaml get key", "특정 키 값 조회", null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("yaml stats", "줄·키·깊이 통계", null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("yaml flat", "점 표기법 평탄화", null, null, Symbol: "\uE8A5"));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clipboard))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("클립보드가 비어 있습니다",
|
||||||
|
"YAML 텍스트를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stat = QuickStat(clipboard);
|
||||||
|
items.Add(new LauncherItem("── 클립보드 미리보기 ──", stat, null, ("copy", stat), Symbol: "\uE8A5"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clipboard))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("클립보드가 비어 있습니다",
|
||||||
|
"YAML 텍스트를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var sub = parts[0].ToLowerInvariant();
|
||||||
|
|
||||||
|
// 파싱
|
||||||
|
var (yamlObj, parseError) = ParseYaml(clipboard);
|
||||||
|
|
||||||
|
switch (sub)
|
||||||
|
{
|
||||||
|
case "validate":
|
||||||
|
case "check":
|
||||||
|
case "lint":
|
||||||
|
{
|
||||||
|
if (parseError != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("❌ YAML 오류", parseError, null, null, Symbol: "\uE783"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var linesCount = clipboard.Split('\n').Length;
|
||||||
|
var keyCount = CountKeys(yamlObj);
|
||||||
|
items.Add(new LauncherItem("✓ 유효한 YAML",
|
||||||
|
$"{linesCount}줄 · 키 {keyCount}개", null, ("copy", "Valid YAML"), Symbol: "\uE8A5"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "keys":
|
||||||
|
case "key":
|
||||||
|
{
|
||||||
|
if (parseError != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("YAML 파싱 오류", parseError, null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (yamlObj is Dictionary<string, object?> dict)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"최상위 키 {dict.Count}개", "", null, null, Symbol: "\uE8A5"));
|
||||||
|
foreach (var (k, v) in dict)
|
||||||
|
{
|
||||||
|
var valStr = FormatValue(v);
|
||||||
|
items.Add(new LauncherItem(k, valStr.Length > 60 ? valStr[..60] + "…" : valStr,
|
||||||
|
null, ("copy", k), Symbol: "\uE8A5"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else items.Add(new LauncherItem("최상위가 매핑이 아닙니다", "배열 또는 스칼라 값", null, null, Symbol: "\uE946"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get":
|
||||||
|
{
|
||||||
|
var keyPath = parts.Length > 1 ? parts[1].Trim() : "";
|
||||||
|
if (string.IsNullOrWhiteSpace(keyPath))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("키 경로 입력", "예: yaml get server.port", null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (parseError != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("YAML 파싱 오류", parseError, null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var found = GetByPath(yamlObj, keyPath.Split('.'));
|
||||||
|
if (found == null)
|
||||||
|
items.Add(new LauncherItem($"'{keyPath}' 없음", "경로가 존재하지 않습니다", null, null, Symbol: "\uE946"));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var valStr = FormatValue(found);
|
||||||
|
items.Add(new LauncherItem(keyPath, valStr, null, ("copy", valStr), Symbol: "\uE8A5"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "stats":
|
||||||
|
case "stat":
|
||||||
|
{
|
||||||
|
var lines = clipboard.Split('\n');
|
||||||
|
var blank = lines.Count(l => string.IsNullOrWhiteSpace(l));
|
||||||
|
var comments = lines.Count(l => l.TrimStart().StartsWith('#'));
|
||||||
|
var keyLines = lines.Count(l => KeyLineRegex().IsMatch(l));
|
||||||
|
var maxDepth = GetMaxDepth(clipboard);
|
||||||
|
var keyCount = parseError == null ? CountKeys(yamlObj) : -1;
|
||||||
|
|
||||||
|
items.Add(new LauncherItem($"YAML 통계", $"{lines.Length}줄 · 키 {keyCount}개 · 깊이 {maxDepth}",
|
||||||
|
null, null, Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("전체 줄", $"{lines.Length}줄", null, ("copy", $"{lines.Length}"), Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("빈 줄", $"{blank}줄", null, ("copy", $"{blank}"), Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("주석 줄", $"{comments}줄", null, ("copy", $"{comments}"), Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("키 줄", $"{keyLines}줄", null, ("copy", $"{keyLines}"), Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("최대 들여쓰기 깊이", $"{maxDepth}단계", null, ("copy", $"{maxDepth}"), Symbol: "\uE8A5"));
|
||||||
|
if (parseError == null && keyCount >= 0)
|
||||||
|
items.Add(new LauncherItem("전체 키 수 (재귀)", $"{keyCount}개", null, ("copy", $"{keyCount}"), Symbol: "\uE8A5"));
|
||||||
|
if (parseError != null)
|
||||||
|
items.Add(new LauncherItem("파싱 오류", parseError, null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "flat":
|
||||||
|
case "flatten":
|
||||||
|
{
|
||||||
|
if (parseError != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("YAML 파싱 오류", parseError, null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var flat = new List<(string Key, string Value)>();
|
||||||
|
Flatten(yamlObj, "", flat);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var (k, v) in flat) sb.AppendLine($"{k}: {v}");
|
||||||
|
var result = sb.ToString().TrimEnd();
|
||||||
|
|
||||||
|
items.Add(new LauncherItem($"평탄화 ({flat.Count}개 키)",
|
||||||
|
"Enter → 전체 복사", null, ("copy", result), Symbol: "\uE8A5"));
|
||||||
|
foreach (var (k, v) in flat.Take(25))
|
||||||
|
{
|
||||||
|
var disp = v.Length > 50 ? v[..50] + "…" : v;
|
||||||
|
items.Add(new LauncherItem(k, disp, null, ("copy", v), Symbol: "\uE8A5"));
|
||||||
|
}
|
||||||
|
if (flat.Count > 25)
|
||||||
|
items.Add(new LauncherItem($"… 외 {flat.Count - 25}개", "전체 복사는 첫 항목 Enter", null, null, Symbol: "\uE8A5"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("알 수 없는 서브커맨드",
|
||||||
|
"validate · keys · get key · stats · flat", null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(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("YAML", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── YAML 파서 (경량, 기본 스펙만) ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경량 YAML 파서. 지원: 스칼라, 매핑(들여쓰기 기반), 시퀀스(- 표기).
|
||||||
|
/// 멀티라인/앵커/태그/복잡 흐름 스타일 미지원.
|
||||||
|
/// </summary>
|
||||||
|
private static (object? Value, string? Error) ParseYaml(string yaml)
|
||||||
|
{
|
||||||
|
var error = "";
|
||||||
|
var lines = yaml.Split('\n').Select(l => l.TrimEnd('\r')).ToList();
|
||||||
|
|
||||||
|
// 기본 유효성: 들여쓰기 일관성 확인
|
||||||
|
int? indentUnit = null;
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith('#')) continue;
|
||||||
|
var indent = line.Length - line.TrimStart().Length;
|
||||||
|
if (indent > 0)
|
||||||
|
{
|
||||||
|
indentUnit ??= indent;
|
||||||
|
// 허용 범위 내 들여쓰기인지 간단 확인
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 간단 구조 파싱
|
||||||
|
var result = ParseBlock(lines, 0, ref error, out _);
|
||||||
|
return (result, string.IsNullOrEmpty(error) ? null : error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ParseBlock(List<string> lines, int baseIndent, ref string error, out int consumed)
|
||||||
|
{
|
||||||
|
consumed = 0;
|
||||||
|
var dict = new Dictionary<string, object?>();
|
||||||
|
var list = new List<object?>();
|
||||||
|
var isSeq = false;
|
||||||
|
var isMap = false;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < lines.Count)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith('#')) { i++; continue; }
|
||||||
|
|
||||||
|
var indent = line.Length - line.TrimStart().Length;
|
||||||
|
if (indent < baseIndent) break;
|
||||||
|
|
||||||
|
var trimmed = line.TrimStart();
|
||||||
|
|
||||||
|
// 시퀀스 항목
|
||||||
|
if (trimmed.StartsWith("- "))
|
||||||
|
{
|
||||||
|
isSeq = true;
|
||||||
|
var valStr = trimmed[2..].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(valStr))
|
||||||
|
{
|
||||||
|
// 다음 줄이 하위 블록
|
||||||
|
var sub = lines.Skip(i + 1).TakeWhile(l =>
|
||||||
|
string.IsNullOrWhiteSpace(l) || l.Length - l.TrimStart().Length > indent).ToList();
|
||||||
|
var innerErr = "";
|
||||||
|
var child = ParseBlock(sub, indent + 2, ref innerErr, out var childConsumed);
|
||||||
|
list.Add(child);
|
||||||
|
i += 1 + childConsumed;
|
||||||
|
}
|
||||||
|
else list.Add(ParseScalar(valStr));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매핑 항목 (key: value)
|
||||||
|
var colonIdx = trimmed.IndexOf(':');
|
||||||
|
if (colonIdx > 0)
|
||||||
|
{
|
||||||
|
isMap = true;
|
||||||
|
var key = trimmed[..colonIdx].Trim().Trim('"', '\'');
|
||||||
|
var rest = trimmed[(colonIdx + 1)..].Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(rest) || rest.StartsWith('#'))
|
||||||
|
{
|
||||||
|
// 다음 줄이 하위 블록
|
||||||
|
var sub = lines.Skip(i + 1).TakeWhile(l =>
|
||||||
|
string.IsNullOrWhiteSpace(l) || l.Length - l.TrimStart().Length > indent).ToList();
|
||||||
|
var innerErr = "";
|
||||||
|
var child = ParseBlock(sub, indent + 2, ref innerErr, out var childConsumed);
|
||||||
|
dict[key] = child;
|
||||||
|
i += 1 + childConsumed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dict[key] = ParseScalar(rest.Split('#')[0].Trim());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumed = i;
|
||||||
|
if (isSeq) return list;
|
||||||
|
if (isMap) return dict;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ParseScalar(string s)
|
||||||
|
{
|
||||||
|
if (s is "true" or "True" or "TRUE" or "yes" or "Yes") return true;
|
||||||
|
if (s is "false" or "False" or "FALSE" or "no" or "No") return false;
|
||||||
|
if (s is "null" or "~" or "Null" or "NULL") return null;
|
||||||
|
if (long.TryParse(s, out var l)) return l;
|
||||||
|
if (double.TryParse(s, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var d)) return d;
|
||||||
|
return s.Trim('"', '\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string QuickStat(string yaml)
|
||||||
|
{
|
||||||
|
var lines = yaml.Split('\n').Length;
|
||||||
|
var keyLines = yaml.Split('\n').Count(l => KeyLineRegex().IsMatch(l));
|
||||||
|
var depth = GetMaxDepth(yaml);
|
||||||
|
return $"{lines}줄 · 키 {keyLines}개 · 최대 깊이 {depth}단계";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetMaxDepth(string yaml)
|
||||||
|
{
|
||||||
|
var maxIndent = yaml.Split('\n')
|
||||||
|
.Where(l => !string.IsNullOrWhiteSpace(l) && !l.TrimStart().StartsWith('#'))
|
||||||
|
.Select(l => l.Length - l.TrimStart().Length)
|
||||||
|
.DefaultIfEmpty(0).Max();
|
||||||
|
return maxIndent / 2 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountKeys(object? node) => node switch
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> d => d.Count + d.Values.Sum(v => CountKeys(v)),
|
||||||
|
List<object?> l => l.Sum(v => CountKeys(v)),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static object? GetByPath(object? node, string[] parts)
|
||||||
|
{
|
||||||
|
if (parts.Length == 0) return node;
|
||||||
|
if (node is Dictionary<string, object?> dict)
|
||||||
|
{
|
||||||
|
var key = parts[0];
|
||||||
|
if (!dict.TryGetValue(key, out var child)) return null;
|
||||||
|
return GetByPath(child, parts[1..]);
|
||||||
|
}
|
||||||
|
if (node is List<object?> list && int.TryParse(parts[0], out var idx) && idx < list.Count)
|
||||||
|
return GetByPath(list[idx], parts[1..]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Flatten(object? node, string prefix, List<(string, string)> result)
|
||||||
|
{
|
||||||
|
switch (node)
|
||||||
|
{
|
||||||
|
case Dictionary<string, object?> dict:
|
||||||
|
foreach (var (k, v) in dict)
|
||||||
|
Flatten(v, string.IsNullOrEmpty(prefix) ? k : $"{prefix}.{k}", result);
|
||||||
|
break;
|
||||||
|
case List<object?> list:
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
Flatten(list[i], $"{prefix}[{i}]", result);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.Add((prefix, FormatValue(node)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatValue(object? v) => v switch
|
||||||
|
{
|
||||||
|
null => "(null)",
|
||||||
|
bool b => b ? "true" : "false",
|
||||||
|
Dictionary<string, object?> d => $"{{...{d.Count}개 키}}",
|
||||||
|
List<object?> l => $"[...{l.Count}개 항목]",
|
||||||
|
_ => v.ToString() ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^\s*[\w\-""']+\s*:")]
|
||||||
|
private static partial Regex KeyLineRegex();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user