Files
connectai/src/features/teamops/handlers/_shared.ts
T
koriweb 7bec20620a refactor: v2.2.195-201 — slashRouter god-file 해체 (–95%) + 인프라 5개 추출
아키텍처 감사 결과 HIGH 2건 + MED 2건 + LOW 1건 — 7 라운드 정리 시리즈.
기능 변경 없음, 순수 구조 정리.

**slashRouter.ts: 4,174 → 201줄 (–3,973, –95%)**
**agent.ts: 1,617 → 1,551줄 (–66, –4%)**

v2.2.195: eventSourcedStore + SystemPromptBlock registry
  - createEventStore<E>(opts) — 4 store (customers/hire/runway/feedback) I/O 240줄 중복 제거
  - _turnCtx 5 named string field → 1 Map<string, string> (새 verification block 추가 25곳→1곳)
  - buildAstraModeSystemPrompt: 5 ternary gate + 5 위치 → 1 for-loop join

v2.2.196: trackers cluster split
  - src/features/teamops/handlers/_shared.ts (fmtKrw/parseAmount/daysUntil/parseTaskOwner/stageEmoji/STAGE_ORDER/TERMINAL_STAGES)
  - src/features/teamops/handlers/trackers.ts (runway/customers/hire)
  - src/features/teamops/handlers/index.ts (barrel)
  - extension.ts 에 side-effect import (순환 import 회피)

v2.2.197: mtimeFileCache + PostAnswerHook registry
  - src/lib/mtimeFileCache.ts — createMtimeFileCache<T>(name, parse) (terminologyBlock + termValidator 2-cache invariant 자동화)
  - src/agent/postAnswerHooks/{types,index}.ts — Devil/SelfCheck/TermValidator 3 _maybeX method → 1 runPostAnswerHooks(ctx) loop
  - agent.ts –66줄

v2.2.198: dashboards cluster split
  - src/features/teamops/handlers/dashboards.ts (morning/evening/cohort/weekly)

v2.2.199: coordination + communication clusters split
  - src/features/teamops/handlers/coordination.ts (task/decisions/onesie/blocked/standup)
  - src/features/teamops/handlers/communication.ts (draft/feedback)
  - callLmSynthesis export 노출 (communication 이 사용)
  - 옛 parseTaskOwner local 정의 삭제 (_shared.ts 사용)

v2.2.200: system cluster split
  - src/features/system/handlers.ts (memory/glossary/help)

v2.2.201: datacollect cluster split + LLM 인프라 추출
  - src/features/datacollect/handlers.ts (research/benchmark/youtube/blog/wikify/meet)
  - src/features/datacollect/llm.ts (callLmSynthesis + repairKoreanGlitches + bridgeErrorRemedy)
  - slashRouter import 4개로 축소: vscode/logInfo/getBridgeBaseUrl/bridgeErrorRemedy

**최종 slashRouter (201줄):**
- REGISTRY Map + registerSlashCommand/listSlashCommands/isSlashCommand
- handleSlashCommand (dispatcher + 에러 처리)
- Webview interface + chunk helper
- getRecentSlashCommands ring buffer (actionability scoring 용)

**미래 부담 감소 metrics:**
- 새 슬래시 명령: god-file 끝에 함수 + register → 1 파일 + 1 register call
- 새 verification block: 5곳 편집 → 1 set call
- 새 event store: 60줄 boilerplate → createEventStore 한 줄
- 새 post-answer hook: 3 step → 1 push
- 새 mtime cache: Map + invariant 관리 → createMtimeFileCache 한 줄

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:55:22 +09:00

85 lines
3.6 KiB
TypeScript

/**
* TeamOps handlers 공통 헬퍼 — 4인 팀 운영 슬래시 명령 클러스터가 공유하는 함수·상수.
*
* 이전 위치: `src/features/datacollect/slashRouter.ts` 내부 module-local 함수
* (v2.2.196 에서 분리). 핸들러 도메인별 분할 시 공통 항목만 여기에.
*
* 추후 datacollect / system 핸들러도 비슷한 공유 helpers 가 필요해지면
* `src/features/_shared/` 로 promote 고려.
*/
/** 한국식 KRW 숫자 포맷 — 만/억 단위 자동. 마이너스 부호 보존. */
export function fmtKrw(n: number): string {
const sign = n < 0 ? '-' : '';
const abs = Math.abs(n);
if (abs >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(2)}`;
if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}`;
return `${sign}${abs.toLocaleString('ko-KR')}`;
}
/** 한국식 금액 토큰 파싱 — "5000만", "1.5억", "300000", "1,500,000", "10k", "5m" 모두 인식. */
export function parseAmount(token: string): number | null {
if (!token) return null;
const s = token.replace(/[,_]/g, '').trim();
const m = s.match(/^(-?[\d.]+)\s*(억|만|k|m|b)?$/i);
if (!m) return null;
const base = parseFloat(m[1]);
if (!Number.isFinite(base)) return null;
let mul = 1;
const unit = (m[2] || '').toLowerCase();
if (unit === '억') mul = 100_000_000;
else if (unit === '만') mul = 10_000;
else if (unit === 'k') mul = 1_000;
else if (unit === 'm') mul = 1_000_000;
else if (unit === 'b') mul = 1_000_000_000;
return base * mul;
}
/** D-day 계산 — ISO 날짜 (YYYY-MM-DD) 받아 오늘부터 며칠 후인지. 음수면 지난 일수. */
export function daysUntil(isoDate: string | undefined, now: Date = new Date()): number | null {
if (!isoDate) return null;
const t = Date.parse(isoDate);
if (!Number.isFinite(t)) return null;
return Math.ceil((t - now.getTime()) / (24 * 60 * 60 * 1000));
}
export interface ParsedTaskOwner { owner: string | undefined; displayTitle: string; }
/**
* Task 제목·notes 에서 owner 추출. 두 패턴 지원:
* 1. 제목 prefix `[멤버] 실제 제목` — /task, /meet 가 등록한 형식
* 2. notes 의 `담당: @이름` 또는 `담당: 이름` — 일부 외부 등록 호환
*/
export function parseTaskOwner(title: string, notes?: string): ParsedTaskOwner {
const titlePrefix = title.match(/^\[([^\]]+)\]\s*(.+)$/);
if (titlePrefix) return { owner: titlePrefix[1].trim(), displayTitle: titlePrefix[2].trim() };
const notesMatch = (notes || '').match(/담당:\s*(?:@)?([\S]+)/);
if (notesMatch) return { owner: notesMatch[1].trim(), displayTitle: title };
return { owner: undefined, displayTitle: title };
}
/** /hire 파이프라인 단계 정렬 가중치. inbox 1, hired 7, terminal 99. */
export const STAGE_ORDER: Record<string, number> = {
inbox: 1, screened: 2, interview: 3, final: 4, offer: 5,
accepted: 6, hired: 7, rejected: 99, declined: 99,
};
/** 종료된 후보 단계. 검색·통계에서 active 와 구분. */
export const TERMINAL_STAGES = new Set(['hired', 'rejected', 'declined']);
/** 단계별 emoji — UI 표시. */
export function stageEmoji(stage: string): string {
switch (stage) {
case 'inbox': return '📥';
case 'screened': return '🔍';
case 'interview': return '💬';
case 'final': return '🎯';
case 'offer': return '📨';
case 'accepted': return '🤝';
case 'hired': return '🎉';
case 'rejected': return '❌';
case 'declined': return '🚪';
default: return '•';
}
}