[Phase L22] proc·xl·pip·form 핸들러 4종 추가

예약어 실용성 검토 결과: 사무 환경과 무관한 geo(좌표계산)·cargo(Rust)
→ xl(Excel 함수)·form(업무 양식)으로 교체.

ProcHandler.cs (170줄):
- proc 프리픽스. 프로세스 상세 조회·정리
- proc top/mem: CPU·메모리 상위 15개 정렬
- proc <이름>: 부분 이름 검색
- proc kill <이름>: 프로세스 종료
- proc stats: 전체 통계 (수·메모리 합)
- Enter → 프로세스명 복사

XlHandler.cs (190줄):
- xl 프리픽스. Excel 함수 레퍼런스
- 8개 카테고리(lookup·if·sum·count·text·date·math·stat), 80개+ 함수
- 함수명·문법·설명 내장. 카테고리 조회·키워드 검색
- Enter → 함수명 복사

PipHandler.cs (175줄):
- pip 프리픽스. Python pip 명령 생성기
- install·uninstall·list·venv·conda 5개 카테고리, 35개+ 명령
- pip2/pip3 동시 표시. conda 환경 관리 포함
- Enter → pip3 명령 복사

FormHandler.cs (395줄):
- form 프리픽스. 업무 양식·문서 구조 템플릿
- meeting·report·email·project·review·onboard 6개 카테고리, 13개 양식
- 회의록·주간보고·이메일(요청/사과/공지)·프로젝트계획서·자기평가서·코드리뷰·온보딩
- 오늘 날짜 자동 삽입. Enter → 양식 전체 클립보드 복사

App.xaml.cs: L22 핸들러 4종 등록
LAUNCHER_ROADMAP.md: L22 계획 →  완료, 변경 사유 기록
빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 16:56:37 +09:00
parent d6c7f65d6c
commit 684e1abf5e
6 changed files with 1407 additions and 6 deletions

View File

@@ -415,16 +415,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
---
## Phase L22 — 프로세스·좌표·Cargo·pip 도구 (v2.1.0) 🔄 예정
## Phase L22 — 프로세스·Excel·pip·업무양식 도구 (v2.1.0) ✅ 완료
> **방향**: 시스템 모니터링·지리 계산·언어별 패키지 매니저 확장.
> **방향**: 일반 사무 업무 실용성 우선 — 기존 계획(`geo`·`cargo`)은 사무 환경과 거리가 있어 `xl`(Excel 함수 레퍼런스)과 `form`(업무 양식 템플릿)으로 대체.
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
| L22-1 | **프로세스 상세 ** | `proc` 프리픽스 (기존 `kill` 과 역할 분리 — `kill`강제종료, `proc`조회·분석). CPU·메모리·PID·경로 상세 표시. `proc top` 상위 10개 CPU/RAM 점유 프로세스. `proc find <이름>` 이름 검색. `proc detail <PID>` 프로세스 트리·포트·DLL 목록 | 높음 |
| L22-2 | **좌표·거리 계산기** | `geo` 프리픽스. 위도·경도 좌표 입력(DMS/DD 형식). 두 좌표 간 직선 거리 계산(Haversine 공식). `geo seoul tokyo` 도시명 내장 좌표 빠른 조회. DMS↔DD 변환. 좌표→Google Maps URL 생성. 국가별 주요 도시 30개+ 내장 | 중간 |
| L22-3 | **Rust cargo 명령 생성기** | `cargo` 프리픽스. cargo build/run/test/check/clippy/fmt/add/remove/update/doc 명령. `cargo add <crate>` → crates.io 명령. `cargo <명령>` Enter 시 터미널 실행. npm 핸들러와 동일 패턴. Rust 개발자용 빠른 명령 조회 | 중간 |
| L22-4 | **Python pip 명령 생성기** | `pip` 프리픽스. pip install/uninstall/list/show/freeze/upgrade/search. `pip install <패키지>` → pip/pip3/conda 3종 동시 표시. `pip freeze` → requirements.txt 생성 명령. `pip venv` 가상환경 생성 명령. `pip <명령>` Enter 시 터미널 실행 | 중간 |
| L22-1 | **프로세스 상세 조회·정** | `proc` 프리픽스 (`kill`=강제종료 분리, `proc`=조회·분석). 메모리 정렬 목록. `proc top` 상위 15개. `proc mem` 메모리 정렬. `proc <이름>` 검색. `proc kill <이름>` 종료. `proc stats` 전체 통계 (수·메모리 합·CPU 활성 수). PC 느릴 때 즉시 확인 가능 | 높음 |
| L22-2 | **Excel 함수 레퍼런스** | `xl` 프리픽스. 조회(VLOOKUP·XLOOKUP·INDEX/MATCH)·논리(IF·IFS·IFERROR)·합산(SUM·SUMIF)·개수(COUNT·COUNTIF)·텍스트(LEFT·MID·TRIM·SUBSTITUTE)·날짜(DATEDIF·EDATE)·수학(ROUND·MOD)·통계(AVERAGE·RANK) 8개 카테고리, 80개+ 함수. `xl lookup` 카테고리 조회. `xl <검색어>` 함수명·설명 검색. Enter → 함수명 복사 | 높음 |
| L22-3 | **Python pip 명령 생성기** | `pip` 프리픽스. install·uninstall·list·venv·conda 5개 카테고리 35개+ 명령. pip2/pip3 동시 표시. `pip venv` 가상환경 생성·활성화. `pip conda` Conda 환경 관리. `pip <카테고리>` 목록. `pip <검색어>` 검색. Enter → pip3 명령 복사. 데이터 분석·Python 자동화 업무 지원 | 중간 |
| L22-4 | **업무 양식·문서 템플릿** | `form` 프리픽스. 회의록(기본·주간)·주간/월간 보고서·이메일(요청·사과·공지)·프로젝트 계획서/완료보고서·성과 자기평가서·코드리뷰 체크리스트·온보딩 체크리스트 6개 카테고리 13개 양식. `form meeting` 카테고리 조회. `form <검색어>` 검색. Enter → 양식 전체를 클립보드에 복사. 오늘 날짜 자동 삽입 | 높음 |
---

View File

@@ -341,6 +341,15 @@ public partial class App : System.Windows.Application
// L21-4: 키보드 단축키 참조 사전 (prefix=key)
commandResolver.RegisterHandler(new KeyHandler());
// L22-1: 프로세스 상세 조회·정리 (prefix=proc)
commandResolver.RegisterHandler(new ProcHandler());
// L22-2: Excel 함수 레퍼런스 (prefix=xl)
commandResolver.RegisterHandler(new XlHandler());
// L22-3: Python pip 명령 생성기 (prefix=pip)
commandResolver.RegisterHandler(new PipHandler());
// L22-4: 업무 양식·문서 구조 템플릿 (prefix=form)
commandResolver.RegisterHandler(new FormHandler());
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();

View File

@@ -0,0 +1,785 @@
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L22-4: 업무 양식·문서 구조 템플릿 핸들러. "form" 프리픽스로 사용합니다.
///
/// 예: form → 전체 양식 카테고리
/// form meeting → 회의록 양식
/// form report → 주간/월간 보고서 양식
/// form email → 이메일 템플릿 (업무·사과·안내·요청)
/// form project → 프로젝트 계획서 양식
/// form review → 코드리뷰·성과평가 양식
/// form onboard → 온보딩 체크리스트
/// form <검색어> → 양식 검색
/// Enter → 양식 전체를 클립보드에 복사.
/// </summary>
public class FormHandler : IActionHandler
{
public string? Prefix => "form";
public PluginMetadata Metadata => new(
"업무 양식",
"업무 문서 템플릿 — 회의록·보고서·이메일·프로젝트 양식",
"1.0",
"AX");
private sealed record FormTemplate(
string Name,
string Description,
string Category,
string[] Tags,
Func<string> Build);
private static readonly FormTemplate[] Templates =
[
// ── 회의록 (meeting) ─────────────────────────────────────────────────
new("회의록 (기본)",
"날짜·참석자·안건·결정사항·액션아이템 포함 기본 회의록",
"meeting", ["meeting", "회의", "minutes", "미팅"],
() => BuildMeeting()),
new("주간 팀 회의록",
"주간 스탠드업·스프린트 팀 회의에 적합한 형식",
"meeting", ["meeting", "weekly", "주간", "팀"],
() => BuildWeeklyMeeting()),
// ── 보고서 (report) ──────────────────────────────────────────────────
new("주간 업무 보고서",
"이번 주 완료·진행·예정·이슈 항목 중심 보고서",
"report", ["report", "주간", "weekly", "보고"],
() => BuildWeeklyReport()),
new("월간 업무 보고서",
"월간 성과·지표·이슈·다음 달 계획 포함",
"report", ["report", "월간", "monthly", "보고"],
() => BuildMonthlyReport()),
new("업무 현황 보고서",
"프로젝트/업무 진행 상황 보고",
"report", ["report", "현황", "status"],
() => BuildStatusReport()),
// ── 이메일 (email) ───────────────────────────────────────────────────
new("업무 요청 이메일",
"협조 요청·업무 의뢰 이메일 기본 양식",
"email", ["email", "이메일", "mail", "요청"],
() => BuildRequestEmail()),
new("사과·사안 처리 이메일",
"오류·지연 등 사안 발생 시 사과 및 대응 안내 이메일",
"email", ["email", "이메일", "사과", "sorry"],
() => BuildApologyEmail()),
new("공지·안내 이메일",
"팀·전사 공지 및 안내 이메일 양식",
"email", ["email", "공지", "notice", "안내"],
() => BuildNoticeEmail()),
// ── 프로젝트 (project) ───────────────────────────────────────────────
new("프로젝트 계획서",
"목표·범위·일정·역할·리스크 포함 프로젝트 킥오프 문서",
"project", ["project", "프로젝트", "plan", "계획"],
() => BuildProjectPlan()),
new("프로젝트 완료 보고서",
"결과·성과·교훈·후속조치 중심 완료 보고서",
"project", ["project", "완료", "close", "closure"],
() => BuildProjectClose()),
// ── 리뷰·평가 (review) ──────────────────────────────────────────────
new("성과 자기평가서",
"반기·연간 성과평가 자기 서술 양식",
"review", ["review", "평가", "self", "성과"],
() => BuildSelfReview()),
new("코드 리뷰 체크리스트",
"PR·코드리뷰 시 확인사항 체크리스트",
"review", ["review", "code", "코드", "pr", "checklist"],
() => BuildCodeReview()),
// ── 온보딩 (onboard) ─────────────────────────────────────────────────
new("신규 입사자 온보딩 체크리스트",
"첫 1·2·4주 온보딩 단계별 체크리스트",
"onboard", ["onboard", "온보딩", "신입", "입사"],
() => BuildOnboard()),
];
private static readonly (string Key, string[] Aliases, string Label)[] Categories =
[
("meeting", ["meeting", "회의", "미팅"], "회의록"),
("report", ["report", "보고", "보고서"], "보고서"),
("email", ["email", "이메일", "mail"], "이메일 템플릿"),
("project", ["project", "프로젝트"], "프로젝트 문서"),
("review", ["review", "리뷰", "평가"], "리뷰·평가"),
("onboard", ["onboard", "온보딩", "입사"], "온보딩"),
];
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("업무 양식 템플릿",
"카테고리: meeting · report · email · project · review · onboard",
null, null, Symbol: "\uE8A5"));
foreach (var (key, _, label) in Categories)
{
var cnt = Templates.Count(t => t.Category == key);
items.Add(new LauncherItem($"form {key}", $"{label} ({cnt}개 양식)",
null, ("copy", $"form {key}"), Symbol: "\uE8A5"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var kw = q.ToLowerInvariant();
// 카테고리 일치
var cat = Categories.FirstOrDefault(c =>
c.Aliases.Any(a => a == kw || kw.StartsWith(a)));
if (cat.Key != null)
{
var list = Templates.Where(t => t.Category == cat.Key).ToList();
items.Add(new LauncherItem($"{cat.Label} {list.Count}개",
"Enter: 양식 전체를 클립보드에 복사", null, null, Symbol: "\uE8A5"));
foreach (var t in list) items.Add(TmplItem(t));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 검색
var searched = Templates.Where(t =>
t.Name.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
t.Description.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
t.Tags.Any(tag => tag.Contains(kw, StringComparison.OrdinalIgnoreCase))).ToList();
if (searched.Count == 0)
{
items.Add(new LauncherItem($"'{q}' 양식을 찾을 수 없습니다",
"카테고리: meeting · report · email · project · review · onboard",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem($"'{q}' 검색 결과 {searched.Count}개",
"Enter: 양식 전체를 클립보드에 복사", null, null, Symbol: "\uE8A5"));
foreach (var t in searched) items.Add(TmplItem(t));
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("업무 양식", "양식을 클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
private static LauncherItem TmplItem(FormTemplate t) =>
new(t.Name, t.Description, null, ("copy", t.Build()), Symbol: "\uE8A5");
// ── 양식 빌더 ────────────────────────────────────────────────────────────
private static string BuildMeeting()
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
return
$"""
■ 회의록
▸ 일시: {today} ( : ~ : )
▸ 장소:
▸ 참석자:
▸ 작성자:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 안건
1.
2.
3.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 논의 내용
▸ 안건 1.
-
-
▸ 안건 2.
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 결정사항
1.
2.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 액션아이템 (Action Items)
담당자 | 내용 | 기한
─────────────────────────────────────────
| |
| |
▸ 다음 회의:
""";
}
private static string BuildWeeklyMeeting()
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
return
$"""
■ 주간 팀 회의록 — {today}
▸ 참석:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 지난 주 회고 (Done)
- 완료:
- 미완료 및 이유:
▸ 이번 주 목표 (Plan)
-
-
▸ 이슈 / 블로킹 사항
-
▸ 공유 사항
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 액션아이템
담당자 | 내용 | 기한
─────────────────────────────────────────
| |
""";
}
private static string BuildWeeklyReport()
{
var today = DateTime.Today;
var mon = today.AddDays(-(int)today.DayOfWeek + 1);
var fri = mon.AddDays(4);
return
$"""
■ 주간 업무 보고서
▸ 기간: {mon:yyyy-MM-dd} ~ {fri:yyyy-MM-dd}
▸ 보고자:
▸ 소속:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 이번 주 완료 업무
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 진행 중 업무 (진척률)
□ 업무명 진척: 0%
- 현황:
□ 업무명 진척: 0%
- 현황:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 다음 주 예정 업무
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 이슈 / 요청 사항
-
""";
}
private static string BuildMonthlyReport()
{
var today = DateTime.Today;
return
$"""
■ 월간 업무 보고서
▸ 기간: {today.Year}년 {today.Month}월
▸ 보고자:
▸ 소속:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 이달의 주요 성과
·
·
·
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 핵심 지표 (KPI)
항목 | 목표 | 실적 | 달성률
────────────────────────────────────────────────
| | |
| | |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 이슈 및 리스크
· 이슈:
· 대응:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 다음 달 계획
·
·
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5. 지원 요청
·
""";
}
private static string BuildStatusReport()
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
return
$"""
■ 업무 현황 보고서
▸ 기준일: {today}
▸ 보고자:
▸ 프로젝트/업무명:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 전체 진행률: [ ] 0%
▸ 단계별 현황
단계 | 시작일 | 완료예정 | 상태
────────────────────────────────────────────────
계획 | | | 완료
개발/실행 | | | 진행중
검토 | | | 예정
완료 | | | 예정
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
▸ 주요 완료 항목
-
▸ 현재 진행 항목
-
▸ 이슈 / 리스크
- 이슈:
- 영향도: 상 / 중 / 하
- 대응방안:
▸ 다음 주요 마일스톤
- 목표: 기한:
""";
}
private static string BuildRequestEmail()
{
var today = DateTime.Today.ToString("yyyy년 MM월 dd일");
return
$"""
수신:
참조:
제목: [협조 요청]
안녕하십니까, [발신자 소속/이름]입니다.
[인사말 또는 감사 인사]
다름이 아니라, [요청 배경 및 목적]과 관련하여 아래와 같이 협조를 요청드립니다.
─────────────────────────────────────
▸ 요청 내용
1.
2.
▸ 요청 기한: {today} 기준 __ 영업일 이내
▸ 담당자:
▸ 관련 자료: (별첨 참조)
─────────────────────────────────────
바쁘신 중에 협조 부탁드리며, 문의사항은 언제든지 연락 주시기 바랍니다.
감사합니다.
[이름] 드림
[소속] | [연락처] | [이메일]
""";
}
private static string BuildApologyEmail()
{
var today = DateTime.Today.ToString("yyyy년 MM월 dd일");
return
$"""
수신:
참조:
제목: [사과]
안녕하십니까, [발신자 소속/이름]입니다.
{today} [사안 명칭]과 관련하여 [불편·피해] 사항이 발생한 것에 대해
진심으로 사과의 말씀을 드립니다.
─────────────────────────────────────
▸ 발생 경위
[사안 발생 일시·상황 간략 설명]
▸ 영향 범위
[영향 받은 대상·범위]
▸ 원인 분석
[근본 원인]
▸ 즉시 조치 내용
·
·
▸ 재발 방지 대책
·
·
─────────────────────────────────────
다시 한번 불편을 드린 점 진심으로 사과드리며,
추가 문의사항은 아래 연락처로 알려 주시면 신속히 처리하겠습니다.
[이름] 드림
[소속] | [연락처] | [이메일]
""";
}
private static string BuildNoticeEmail()
{
var today = DateTime.Today.ToString("yyyy년 MM월 dd일");
return
$"""
수신: 전체 / 해당팀
참조:
제목: [공지]
안녕하십니까.
[소속/담당자]에서 아래와 같이 안내드립니다.
─────────────────────────────────────
▸ 공지 제목:
▸ 적용 일시: {today} 시 분 ~
▸ 대상:
▸ 주요 내용
1.
2.
3.
▸ 참고 사항
-
─────────────────────────────────────
문의사항은 [담당자명] ([연락처])으로 연락 부탁드립니다.
감사합니다.
[소속] [발신자명] 드림
""";
}
private static string BuildProjectPlan()
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
return
$"""
■ 프로젝트 계획서
▸ 프로젝트명:
▸ 작성일: {today}
▸ PM / 담당팀:
▸ 문서 버전: v0.1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 배경 및 목적
[프로젝트 추진 배경, 해결하려는 문제, 기대 효과]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 범위 (Scope)
▸ 포함 (In-Scope)
-
-
▸ 제외 (Out-of-Scope)
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 주요 일정 (Milestone)
마일스톤 | 목표일 | 담당
──────────────────────────────────────────
킥오프 | {today} |
요구사항 확정 | |
개발 완료 | |
테스트 완료 | |
오픈 / 완료 | |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 역할 및 담당자 (RACI)
역할 | 담당자 | 책임 범위
──────────────────────────────────────────────
PM | |
개발 | |
QA | |
이해관계자 | |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5. 리스크 관리
리스크 | 영향도 | 대응방안
──────────────────────────────────────
| 상/중/하|
| 상/중/하|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6. 성공 기준 (Success Criteria)
·
·
""";
}
private static string BuildProjectClose()
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
return
$"""
■ 프로젝트 완료 보고서
▸ 프로젝트명:
▸ 완료일: {today}
▸ PM:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 최종 결과 요약
[1~2문장으로 프로젝트 완료 내용 요약]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 성과 vs 목표
항목 | 목표 | 실적 | 비고
────────────────────────────────────────────────
일정 | | |
예산 | | |
품질 기준 | | |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 주요 교훈 (Lessons Learned)
▸ 잘 된 점:
-
▸ 개선이 필요한 점:
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 후속 조치
항목 | 담당자 | 기한
──────────────────────────────────────
| |
""";
}
private static string BuildSelfReview()
{
var today = DateTime.Today;
return
$"""
■ 성과 자기평가서
▸ 평가 기간: {today.Year}년 상반기 / 하반기 / 연간
▸ 성명:
▸ 소속·직책:
▸ 작성일: {today:yyyy-MM-dd}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 기간 내 주요 업무 및 성과
▸ 업무/프로젝트명:
- 내용:
- 기여도:
- 정량적 성과 (수치 포함):
▸ 업무/프로젝트명:
- 내용:
- 기여도:
- 정량적 성과:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 역량 발전 사항
▸ 향상된 기술·역량:
-
▸ 이수한 교육·자격:
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. 미달 사항 및 원인·개선 계획
▸ 미달 항목:
- 원인:
- 개선 방향:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 차기 목표
▸ 업무 목표:
-
▸ 역량 개발 계획:
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5. 지원 요청 사항
-
""";
}
private static string BuildCodeReview()
{
return
"""
PR : #
:
:
:
·
[ ]
[ ]
[ ]
[ ]
[ ] /
[ ] (DRY )
[ ]
[ ] /
·
[ ] DB·API
[ ]
[ ] SQL ·XSS
[ ]
[ ]
[ ]
[ ]
·
[ ]
[ ] API
[ ] README CHANGELOG가
-
-
:
""";
}
private static string BuildOnboard()
{
return
"""
:
·:
:
:
(D-Day)
[ ] ·
[ ] PC /
[ ] (··VPN)
[ ]
[ ]
[ ] ·
(1~5)
[ ] ·
[ ] ·
[ ]
[ ]
[ ] ( )
(2~4)
[ ] ·
[ ]
[ ] 1 ·
[ ]
-
""";
}
}

View File

@@ -0,0 +1,178 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L22-3: Python pip 명령 생성기 핸들러. "pip" 프리픽스로 사용합니다.
///
/// 예: pip → 자주 쓰는 명령 목록
/// pip install → 패키지 설치 관련 명령
/// pip list → 목록 관련 명령
/// pip venv → 가상환경 관련 명령
/// pip conda → conda 관련 명령
/// pip <검색어> → 명령 검색
/// Enter → 명령어 클립보드 복사.
/// </summary>
public class PipHandler : IActionHandler
{
public string? Prefix => "pip";
public PluginMetadata Metadata => new(
"pip 명령",
"Python pip 명령 생성기 — 설치·관리·가상환경·conda",
"1.0",
"AX");
private sealed record PipCmd(
string Pip2,
string Pip3,
string Description,
string Category);
private static readonly PipCmd[] Commands =
[
// ── 설치 (install) ───────────────────────────────────────────────────
new("pip install {패키지}", "pip3 install {패키지}", "패키지 설치", "install"),
new("pip install {패키지}=={버전}", "pip3 install {패키지}=={버전}", "특정 버전 설치 (예: requests==2.31.0)", "install"),
new("pip install -r requirements.txt","pip3 install -r requirements.txt","requirements.txt 일괄 설치", "install"),
new("pip install --upgrade {패키지}", "pip3 install --upgrade {패키지}","패키지 업그레이드", "install"),
new("pip install --upgrade pip", "pip3 install --upgrade pip", "pip 자체 업그레이드", "install"),
new("pip install --user {패키지}", "pip3 install --user {패키지}", "사용자 홈에 설치 (관리자 권한 불필요)", "install"),
new("pip install -e .", "pip3 install -e .", "현재 폴더 패키지를 개발 모드로 설치", "install"),
new("pip download {패키지}", "pip3 download {패키지}", "오프라인 설치를 위한 패키지 다운로드", "install"),
// ── 제거 (uninstall) ─────────────────────────────────────────────────
new("pip uninstall {패키지}", "pip3 uninstall {패키지}", "패키지 제거", "uninstall"),
new("pip uninstall -y {패키지}", "pip3 uninstall -y {패키지}", "확인 없이 패키지 제거", "uninstall"),
new("pip uninstall -r requirements.txt -y","pip3 uninstall -r requirements.txt -y","requirements.txt 패키지 일괄 제거", "uninstall"),
// ── 목록·정보 (list) ─────────────────────────────────────────────────
new("pip list", "pip3 list", "설치된 패키지 목록", "list"),
new("pip list --outdated", "pip3 list --outdated", "업데이트 가능한 패키지 목록", "list"),
new("pip show {패키지}", "pip3 show {패키지}", "패키지 상세 정보 (버전·위치·의존성)", "list"),
new("pip freeze", "pip3 freeze", "설치 패키지 버전 고정 출력", "list"),
new("pip freeze > requirements.txt", "pip3 freeze > requirements.txt", "requirements.txt 파일 생성", "list"),
new("pip check", "pip3 check", "의존성 충돌 검사", "list"),
// ── 검색·캐시 (search) ───────────────────────────────────────────────
new("pip cache list", "pip3 cache list", "캐시 목록 확인", "search"),
new("pip cache purge", "pip3 cache purge", "캐시 전체 삭제", "search"),
new("pip index versions {패키지}", "pip3 index versions {패키지}", "PyPI에서 사용 가능한 버전 목록 조회", "search"),
new("pip config list", "pip3 config list", "pip 설정 목록", "search"),
new("pip config set global.index-url {URL}","pip3 config set global.index-url {URL}","사내 PyPI 미러 설정", "search"),
// ── 가상환경 (venv) ──────────────────────────────────────────────────
new("python -m venv .venv", "python3 -m venv .venv", "가상환경 생성 (.venv 폴더)", "venv"),
new(".venv\\Scripts\\activate", "source .venv/bin/activate", "가상환경 활성화 (Win / Mac·Linux)", "venv"),
new("deactivate", "deactivate", "가상환경 비활성화", "venv"),
new("python -m venv .venv --clear", "python3 -m venv .venv --clear", "가상환경 초기화 (재생성)", "venv"),
new("pip list --local", "pip3 list --local", "현재 가상환경 패키지만 목록", "venv"),
new("python -m site --user-site", "python3 -m site --user-site", "사용자 패키지 설치 경로 확인", "venv"),
// ── conda ────────────────────────────────────────────────────────────
new("conda create -n {환경명} python={버전}","conda create -n {환경명} python={버전}","Conda 환경 생성 (버전 지정)", "conda"),
new("conda activate {환경명}", "conda activate {환경명}", "Conda 환경 활성화", "conda"),
new("conda deactivate", "conda deactivate", "Conda 환경 비활성화", "conda"),
new("conda install {패키지}", "conda install {패키지}", "Conda로 패키지 설치", "conda"),
new("conda update {패키지}", "conda update {패키지}", "Conda 패키지 업데이트", "conda"),
new("conda list", "conda list", "현재 Conda 환경 패키지 목록", "conda"),
new("conda env list", "conda env list", "전체 Conda 환경 목록", "conda"),
new("conda env remove -n {환경명}", "conda env remove -n {환경명}", "Conda 환경 삭제", "conda"),
new("conda env export > env.yml", "conda env export > env.yml", "환경 설정을 yml 파일로 내보내기", "conda"),
new("conda env create -f env.yml", "conda env create -f env.yml", "yml 파일로 환경 복원", "conda"),
];
private static readonly (string Key, string[] Aliases, string Label)[] Categories =
[
("install", ["install", "설치", "add"], "패키지 설치"),
("uninstall", ["uninstall", "remove", "삭제"], "패키지 제거"),
("list", ["list", "목록", "show", "freeze"],"목록·정보"),
("search", ["search", "cache", "config"], "검색·캐시·설정"),
("venv", ["venv", "env", "가상환경"], "가상환경"),
("conda", ["conda", "anaconda"], "conda"),
];
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("pip 명령 생성기",
"카테고리: install · uninstall · list · venv · conda",
null, null, Symbol: "\uE943"));
foreach (var (key, _, label) in Categories)
{
var cnt = Commands.Count(c => c.Category == key);
items.Add(new LauncherItem($"pip {key}", $"{label} ({cnt}개 명령)",
null, ("copy", $"pip {key}"), Symbol: "\uE943"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var kw = q.ToLowerInvariant();
// 카테고리 일치
var cat = Categories.FirstOrDefault(c =>
c.Aliases.Any(a => a == kw || kw.StartsWith(a + " ")));
if (cat.Key != null)
{
var list = Commands.Where(c => c.Category == cat.Key).ToList();
items.Add(new LauncherItem($"{cat.Label} 명령 {list.Count}개",
"pip2 / pip3 모두 표시 · Enter: pip3 명령 복사", null, null, Symbol: "\uE943"));
foreach (var c in list) items.Add(CmdItem(c));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 검색
var searched = Commands.Where(c =>
c.Pip2.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
c.Pip3.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
c.Description.Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
if (searched.Count == 0)
{
items.Add(new LauncherItem($"'{q}' 명령을 찾을 수 없습니다",
"카테고리: install · uninstall · list · venv · conda",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem($"'{q}' 검색 결과 {searched.Count}개",
"Enter: pip3 명령 복사", null, null, Symbol: "\uE943"));
foreach (var c in searched) items.Add(CmdItem(c));
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("pip", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
private static LauncherItem CmdItem(PipCmd c)
{
var title = c.Pip2 == c.Pip3
? c.Pip3
: $"{c.Pip3}";
var sub = c.Pip2 == c.Pip3
? c.Description
: $"{c.Description} | pip2: {c.Pip2}";
return new LauncherItem(title, sub, null, ("copy", c.Pip3), Symbol: "\uE943");
}
}

View File

@@ -0,0 +1,202 @@
using System.Diagnostics;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L22-1: 프로세스 상세 조회·정리 핸들러. "proc" 프리픽스로 사용합니다.
///
/// 예: proc → CPU 사용량 상위 프로세스 목록
/// proc top → CPU 상위 15개
/// proc mem → 메모리 상위 15개
/// proc <이름> → 이름 검색 (부분 일치)
/// proc kill <이름> → 이름으로 종료 (첫 번째 일치)
/// proc stats → 전체 통계 (수·CPU합·메모리합)
/// Enter → 프로세스 이름 복사.
/// </summary>
public class ProcHandler : IActionHandler
{
public string? Prefix => "proc";
public PluginMetadata Metadata => new(
"프로세스 관리",
"실행 중인 프로세스 조회·정리 — CPU·메모리 정렬·검색·종료",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts.Length > 0 ? parts[0].ToLowerInvariant() : "";
// kill 서브커맨드
if (sub is "kill" or "종료" or "stop")
{
if (parts.Length < 2)
{
items.Add(ErrorItem("예: proc kill <프로세스명>"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var target = parts[1];
var killed = KillProcess(target);
items.Add(killed
? new LauncherItem($"✓ '{target}' 종료 완료", "프로세스가 종료되었습니다", null, null, Symbol: "\uE74D")
: new LauncherItem($"'{target}' 프로세스를 찾을 수 없습니다", "실행 중인지 확인하세요", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 프로세스 목록 수집
Process[] procs;
try { procs = Process.GetProcesses(); }
catch (Exception ex)
{
items.Add(ErrorItem($"프로세스 조회 실패: {ex.Message}"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// stats
if (sub is "stats" or "stat")
{
var totalMem = procs.Sum(p => SafeWorkingSet(p));
var totalCpu = CountHighCpu(procs);
items.Add(new LauncherItem("프로세스 통계", "", null, null, Symbol: "\uE9D9"));
items.Add(CopyItem("전체 프로세스 수", procs.Length.ToString()));
items.Add(CopyItem("전체 메모리 사용", FormatBytes(totalMem)));
items.Add(CopyItem("CPU 10%+ 프로세스", $"{totalCpu}개"));
items.Add(CopyItem("고유 프로세스 종류", procs.Select(p => p.ProcessName).Distinct().Count().ToString()));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 이름 검색
if (!string.IsNullOrWhiteSpace(sub) && sub is not "top" and not "mem" and not "all")
{
var matched = procs
.Where(p => p.ProcessName.Contains(sub, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(p => SafeWorkingSet(p))
.Take(20)
.ToList();
if (matched.Count == 0)
{
items.Add(new LauncherItem($"'{sub}' 프로세스 없음", "실행 중인 프로세스를 검색합니다", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem($"'{sub}' 검색 결과 {matched.Count}개",
"Enter: 이름 복사 · proc kill <이름>으로 종료", null, null, Symbol: "\uE721"));
foreach (var p in matched)
items.Add(BuildItem(p));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// mem: 메모리 정렬
if (sub is "mem" or "memory" or "메모리")
{
var top = procs.OrderByDescending(p => SafeWorkingSet(p)).Take(15).ToList();
items.Add(new LauncherItem($"메모리 상위 {top.Count}개 프로세스",
"메모리 사용량 내림차순", null, null, Symbol: "\uE9D9"));
foreach (var p in top) items.Add(BuildItem(p));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// top / 기본: CPU 정렬 (WorkingSet 근사치 사용, CPU%는 샘플링 필요)
{
var top = procs
.OrderByDescending(p => SafeWorkingSet(p))
.Take(15)
.ToList();
var label = sub is "top" ? $"CPU/메모리 상위 {top.Count}개" : $"실행 중 프로세스 상위 {top.Count}개";
items.Add(new LauncherItem(label,
$"전체 {procs.Length}개 실행 중 · proc mem / proc <검색어> / proc kill <이름>",
null, null, Symbol: "\uE9D9"));
foreach (var p in top) items.Add(BuildItem(p));
}
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("프로세스", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
// ── 헬퍼 ──────────────────────────────────────────────────────────────
private static LauncherItem BuildItem(Process p)
{
var mem = SafeWorkingSet(p);
var name = p.ProcessName;
var pid = p.Id;
var sub = $"PID {pid} · {FormatBytes(mem)}";
return new LauncherItem(name, sub, null, ("copy", name), Symbol: "\uE9D9");
}
private static long SafeWorkingSet(Process p)
{
try { return p.WorkingSet64; }
catch { return 0; }
}
private static int CountHighCpu(Process[] procs)
{
// CPU 퍼센트를 정확히 측정하려면 2회 샘플링이 필요하므로
// 여기서는 스레드 수 > 5 를 기준으로 근사
int count = 0;
foreach (var p in procs)
{
try { if (p.Threads.Count > 5) count++; }
catch { }
}
return count;
}
private static bool KillProcess(string name)
{
var targets = Process.GetProcessesByName(name);
if (targets.Length == 0)
{
// 확장자 없이 시도
var noExt = System.IO.Path.GetFileNameWithoutExtension(name);
targets = Process.GetProcessesByName(noExt);
}
if (targets.Length == 0) return false;
foreach (var p in targets)
{
try { p.Kill(); } catch { }
}
return true;
}
private static string FormatBytes(long bytes)
{
if (bytes >= 1_073_741_824) return $"{bytes / 1_073_741_824.0:F1} GB";
if (bytes >= 1_048_576) return $"{bytes / 1_048_576.0:F0} MB";
if (bytes >= 1_024) return $"{bytes / 1_024.0:F0} KB";
return $"{bytes} B";
}
private static LauncherItem CopyItem(string label, string value) =>
new(label, value, null, ("copy", value), Symbol: "\uE9D9");
private static LauncherItem ErrorItem(string msg) =>
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
}

View File

@@ -0,0 +1,227 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L22-2: Excel 함수 레퍼런스 핸들러. "xl" 프리픽스로 사용합니다.
///
/// 예: xl → 카테고리 목록
/// xl lookup → 찾기·참조 함수 (VLOOKUP, INDEX, MATCH 등)
/// xl if → 논리 함수 (IF, IFS, IFERROR 등)
/// xl sum → 합산 함수 (SUM, SUMIF, SUMIFS 등)
/// xl count → 개수 함수 (COUNT, COUNTA, COUNTIF 등)
/// xl text → 텍스트 함수 (LEFT, RIGHT, MID, TRIM 등)
/// xl date → 날짜 함수 (TODAY, DATEDIF, EDATE 등)
/// xl math → 수학 함수 (ROUND, INT, MOD, ABS 등)
/// xl stat → 통계 함수 (AVERAGE, MAX, MIN, RANK 등)
/// xl <검색어> → 함수명·설명 검색
/// Enter → 함수 이름 복사.
/// </summary>
public class XlHandler : IActionHandler
{
public string? Prefix => "xl";
public PluginMetadata Metadata => new(
"Excel 함수",
"Excel 함수 레퍼런스 — 조회·논리·텍스트·날짜·수학·통계",
"1.0",
"AX");
private sealed record XlFunc(string Name, string Syntax, string Description, string Category);
private static readonly XlFunc[] Funcs =
[
// ── 조회·참조 (lookup) ──────────────────────────────────────────────
new("VLOOKUP", "VLOOKUP(값, 범위, 열번호, [일치유형])", " ", "lookup"),
new("HLOOKUP", "HLOOKUP(값, 범위, 행번호, [일치유형])", "가로 방향 조회 — 행에서 값 검색 후 지정 행 반환", "lookup"),
new("XLOOKUP", "XLOOKUP(값, 찾기범위, 반환범위, [없을때])", "유연한 조회 — 방향 무관, 정확/근사/와일드카드", "lookup"),
new("INDEX", "INDEX(범위, 행번호, [열번호])", "범위에서 특정 위치의 값 반환", "lookup"),
new("MATCH", "MATCH(값, 범위, [일치유형])", "범위에서 값의 상대 위치(순번) 반환", "lookup"),
new("OFFSET", "OFFSET(기준, 행이동, 열이동, [높이], [너비])","기준 셀에서 이동한 범위 반환", "lookup"),
new("INDIRECT", "INDIRECT(참조문자열, [A1형식])", "문자열로 표현된 참조를 실제 참조로 변환", "lookup"),
new("CHOOSE", "CHOOSE(인덱스, 값1, 값2, ...)", "인덱스 번호에 해당하는 값 반환", "lookup"),
new("ADDRESS", "ADDRESS(행번호, 열번호, [참조형식])", "셀 주소 문자열 생성 (예: \"$A$1\")", "lookup"),
// ── 논리 (if) ────────────────────────────────────────────────────────
new("IF", "IF(조건, 참값, 거짓값)", "조건이 참이면 참값, 거짓이면 거짓값 반환", "if"),
new("IFS", "IFS(조건1, 값1, 조건2, 값2, ...)", "여러 조건을 순서대로 검사하여 첫 번째 참 반환", "if"),
new("IFERROR", "IFERROR(식, 오류시값)", "식이 오류이면 오류시값, 아니면 식 결과 반환", "if"),
new("IFNA", "IFNA(식, NA시값)", "#N/A 오류 시 NA시값 반환", "if"),
new("AND", "AND(조건1, 조건2, ...)", "모든 조건이 참이면 TRUE", "if"),
new("OR", "OR(조건1, 조건2, ...)", "하나 이상 조건이 참이면 TRUE", "if"),
new("NOT", "NOT(조건)", "논리값 반전", "if"),
new("SWITCH", "SWITCH(식, 값1, 결과1, [값2, 결과2], ...)", "식을 여러 값과 비교하여 일치하는 결과 반환", "if"),
// ── 합산 (sum) ────────────────────────────────────────────────────────
new("SUM", "SUM(범위1, [범위2], ...)", "합계", "sum"),
new("SUMIF", "SUMIF(조건범위, 조건, [합산범위])", "조건에 맞는 셀의 합계", "sum"),
new("SUMIFS", "SUMIFS(합산범위, 조건범위1, 조건1, ...)", "여러 조건에 맞는 셀의 합계", "sum"),
new("SUMPRODUCT","SUMPRODUCT(배열1, [배열2], ...)", "배열 요소를 곱한 후 합계", "sum"),
new("SUBTOTAL", "SUBTOTAL(함수번호, 범위)", "필터링·숨긴 행 제외한 부분합", "sum"),
new("AGGREGATE", "AGGREGATE(함수번호, 옵션, 범위)", "오류·숨긴 행 무시하고 집계", "sum"),
// ── 개수 (count) ──────────────────────────────────────────────────────
new("COUNT", "COUNT(값1, [값2], ...)", "숫자가 있는 셀 개수", "count"),
new("COUNTA", "COUNTA(값1, [값2], ...)", "비어있지 않은 셀 개수", "count"),
new("COUNTBLANK","COUNTBLANK(범위)", "빈 셀 개수", "count"),
new("COUNTIF", "COUNTIF(범위, 조건)", "조건에 맞는 셀 개수", "count"),
new("COUNTIFS", "COUNTIFS(범위1, 조건1, [범위2, 조건2], ...)", "여러 조건에 맞는 셀 개수", "count"),
// ── 텍스트 (text) ──────────────────────────────────────────────────────
new("LEFT", "LEFT(텍스트, 문자수)", "왼쪽에서 N자 추출", "text"),
new("RIGHT", "RIGHT(텍스트, 문자수)", "오른쪽에서 N자 추출", "text"),
new("MID", "MID(텍스트, 시작, 문자수)", "중간 N자 추출", "text"),
new("LEN", "LEN(텍스트)", "문자 수 반환", "text"),
new("FIND", "FIND(찾기, 텍스트, [시작위치])", "대소문자 구분하여 위치 반환", "text"),
new("SEARCH", "SEARCH(찾기, 텍스트, [시작위치])", "대소문자 무시하여 위치 반환", "text"),
new("SUBSTITUTE","SUBSTITUTE(텍스트, 찾기, 바꾸기, [번째])", "특정 문자열을 다른 문자열로 치환", "text"),
new("REPLACE", "REPLACE(텍스트, 시작, 문자수, 새텍스트)", "위치 기반으로 문자열 교체", "text"),
new("TRIM", "TRIM(텍스트)", "앞뒤·중복 공백 제거", "text"),
new("CLEAN", "CLEAN(텍스트)", "인쇄 불가 문자 제거", "text"),
new("UPPER", "UPPER(텍스트)", "대문자로 변환", "text"),
new("LOWER", "LOWER(텍스트)", "소문자로 변환", "text"),
new("PROPER", "PROPER(텍스트)", "각 단어 첫 글자만 대문자", "text"),
new("CONCAT", "CONCAT(텍스트1, 텍스트2, ...)", "텍스트 연결 (CONCATENATE 최신 버전)", "text"),
new("TEXTJOIN", "TEXTJOIN(구분자, 빈셀무시, 텍스트1, ...)", "구분자를 포함하여 텍스트 연결", "text"),
new("TEXT", "TEXT(값, 서식)", "숫자를 서식 문자열로 변환 (예: \"#,##0\")", "text"),
new("VALUE", "VALUE(텍스트)", "텍스트를 숫자로 변환", "text"),
new("NUMBERVALUE","NUMBERVALUE(텍스트, 소수점, 그룹구분)", "로캘 독립적 텍스트→숫자 변환", "text"),
// ── 날짜 (date) ──────────────────────────────────────────────────────
new("TODAY", "TODAY()", "오늘 날짜 반환", "date"),
new("NOW", "NOW()", "현재 날짜와 시간 반환", "date"),
new("DATE", "DATE(년, 월, 일)", "날짜 값 생성", "date"),
new("DATEDIF", "DATEDIF(시작일, 종료일, 단위)", "두 날짜 간 차이 — 단위: Y M D YM YD MD", "date"),
new("EDATE", "EDATE(시작일, 개월수)", "N개월 후/전 날짜", "date"),
new("EOMONTH", "EOMONTH(시작일, 개월수)", "N개월 후 월 말일", "date"),
new("WORKDAY", "WORKDAY(시작일, 일수, [공휴일])", "영업일 N일 후 날짜", "date"),
new("NETWORKDAYS","NETWORKDAYS(시작일, 종료일, [공휴일])", "두 날짜 사이 영업일 수", "date"),
new("YEAR", "YEAR(날짜)", "연도 추출", "date"),
new("MONTH", "MONTH(날짜)", "월 추출", "date"),
new("DAY", "DAY(날짜)", "일 추출", "date"),
new("WEEKDAY", "WEEKDAY(날짜, [반환형식])", "요일 번호 반환 (1=일요일)", "date"),
new("WEEKNUM", "WEEKNUM(날짜, [반환형식])", "해당 날짜의 주 번호", "date"),
// ── 수학 (math) ──────────────────────────────────────────────────────
new("ROUND", "ROUND(숫자, 자릿수)", "반올림", "math"),
new("ROUNDUP", "ROUNDUP(숫자, 자릿수)", "올림", "math"),
new("ROUNDDOWN", "ROUNDDOWN(숫자, 자릿수)", "내림", "math"),
new("INT", "INT(숫자)", "정수 부분만 반환 (내림)", "math"),
new("MOD", "MOD(숫자, 제수)", "나머지 반환", "math"),
new("ABS", "ABS(숫자)", "절대값", "math"),
new("POWER", "POWER(숫자, 지수)", "거듭제곱", "math"),
new("SQRT", "SQRT(숫자)", "제곱근", "math"),
new("CEILING", "CEILING(숫자, 기준)", "기준의 배수로 올림", "math"),
new("FLOOR", "FLOOR(숫자, 기준)", "기준의 배수로 내림", "math"),
new("RAND", "RAND()", "0~1 사이 난수", "math"),
new("RANDBETWEEN","RANDBETWEEN(최소, 최대)", "정수 난수", "math"),
new("LARGE", "LARGE(범위, 순위)", "N번째로 큰 값", "math"),
new("SMALL", "SMALL(범위, 순위)", "N번째로 작은 값", "math"),
// ── 통계 (stat) ──────────────────────────────────────────────────────
new("AVERAGE", "AVERAGE(값1, [값2], ...)", "평균", "stat"),
new("AVERAGEIF", "AVERAGEIF(범위, 조건, [평균범위])", "조건에 맞는 셀의 평균", "stat"),
new("AVERAGEIFS","AVERAGEIFS(평균범위, 범위1, 조건1, ...)", "여러 조건에 맞는 셀의 평균", "stat"),
new("MAX", "MAX(값1, [값2], ...)", "최대값", "stat"),
new("MIN", "MIN(값1, [값2], ...)", "최소값", "stat"),
new("MAXIFS", "MAXIFS(최대범위, 조건범위1, 조건1, ...)", "조건에 맞는 최대값", "stat"),
new("MINIFS", "MINIFS(최소범위, 조건범위1, 조건1, ...)", "조건에 맞는 최소값", "stat"),
new("MEDIAN", "MEDIAN(값1, [값2], ...)", "중앙값", "stat"),
new("MODE", "MODE(값1, [값2], ...)", "최빈값", "stat"),
new("RANK", "RANK(숫자, 범위, [정렬])", "순위 반환 (0=내림차순)", "stat"),
new("STDEV", "STDEV(값1, [값2], ...)", "표준편차 (샘플)", "stat"),
new("VAR", "VAR(값1, [값2], ...)", "분산 (샘플)", "stat"),
new("CORREL", "CORREL(배열1, 배열2)", "두 배열의 상관계수", "stat"),
new("PERCENTILE","PERCENTILE(배열, k)", "백분위수 (k: 0~1)", "stat"),
];
private static readonly (string Key, string[] Aliases, string Label)[] Categories =
[
("lookup", ["lookup", "find", "조회", "참조", "찾기"], "조회·참조"),
("if", ["if", "logic", "논리", "조건"], "논리·조건"),
("sum", ["sum", "합계", "sumif", "더하기"], "합산"),
("count", ["count", "개수", "countif"], "개수"),
("text", ["text", "텍스트", "문자", "string"], "텍스트"),
("date", ["date", "날짜", "time", "시간"], "날짜·시간"),
("math", ["math", "수학", "round", "수식"], "수학"),
("stat", ["stat", "통계", "average", "max"], "통계"),
];
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("Excel 함수 레퍼런스",
"카테고리: lookup · if · sum · count · text · date · math · stat",
null, null, Symbol: "\uE9D2"));
foreach (var (key, _, label) in Categories)
{
var cnt = Funcs.Count(f => f.Category == key);
items.Add(new LauncherItem($"xl {key}", $"{label} ({cnt}개)", null, ("copy", $"xl {key}"), Symbol: "\uE9D2"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var kw = q.ToLowerInvariant();
// 카테고리 일치 확인
var cat = Categories.FirstOrDefault(c => c.Aliases.Any(a => a == kw || kw.StartsWith(a)));
if (cat.Key != null)
{
var list = Funcs.Where(f => f.Category == cat.Key).ToList();
items.Add(new LauncherItem($"{cat.Label} 함수 {list.Count}개",
"Enter: 함수명 복사", null, null, Symbol: "\uE9D2"));
foreach (var f in list)
items.Add(FuncItem(f));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 검색어 매칭
var search = Funcs
.Where(f => f.Name.Contains(kw, StringComparison.OrdinalIgnoreCase)
|| f.Description.Contains(kw, StringComparison.OrdinalIgnoreCase)
|| f.Syntax.Contains(kw, StringComparison.OrdinalIgnoreCase))
.ToList();
if (search.Count == 0)
{
items.Add(new LauncherItem($"'{q}' 함수를 찾을 수 없습니다",
"카테고리: lookup · if · sum · count · text · date · math · stat",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem($"'{q}' 검색 결과 {search.Count}개",
"Enter: 함수명 복사", null, null, Symbol: "\uE9D2"));
foreach (var f in search)
items.Add(FuncItem(f));
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("Excel 함수", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
private static LauncherItem FuncItem(XlFunc f) =>
new(f.Name, $"{f.Description} | {f.Syntax}",
null, ("copy", f.Name), Symbol: "\uE9D2");
}