Files
connectai/src/features/datacollect/scheduling/calendarHelpers.ts
T
koriweb 64d8093080 feat(meet): 회의록 가이드 v2 반영 + 모델 출력 붕괴 복원력 (v2.2.253)
회의록 출력물 개선 (실무 회의록 가이드 v2):
- 섹션 우선순위 재정렬(①결정 ②액션 ③오픈이슈 ④리스크 ⑤논의)
- 논의사항 주제별 bullet 간결화, 오픈 이슈 섹션 복원
- 액션 아이템 산출물 컬럼 추가(담당·작업·기한·산출물 4요소), 담당자 개인 우선
- Executive Summary 결과 중심, 결정사항은 확정된 것만

모델 출력 붕괴(degeneration) 대응:
- callLmSynthesis 재시도 내장(repeat_penalty↑/top_k↓로 반복 억제 강화) + looksDegenerate 감지
- 긴 녹취 조각 실패 시 절반 분할 재귀 재시도(12K→6K→3.5K)
- 부분 회의록 fallback(한 조각 실패해도 전체 중단 안 함)

하위호환: 액션 표 파서 신6컬럼/구5컬럼 모두 파싱, 섹션 번호 무관 탐지,
회의일 추출 일시/날짜 둘 다 인식. 테스트 +13건(전체 659 통과).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:16:55 +09:00

104 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* `/meet` 슬래시 명령의 후처리 — 회의록에서 action items 를 뽑아 캘린더 task 일정을
* 계산하는 stateless helpers. slashRouter 의 inline 블록을 분리.
*
* - addBusinessDays(base, n) — 토·일 제외 영업일 n 일 후 날짜
* - toYmd(d) — Date → 'YYYY-MM-DD'
* - extractMeetingDate(report, fallback) — 회의록에서 회의 일자 추출 (없으면 fallback)
* - resolveTaskDate(due, meetingDate, today) — 'D+3' / 'EOW' 같은 due 문구를 절대 날짜로 변환
* - parseActionItems(report) — 회의록 마크다운 표에서 action items 파싱
*/
// ─── /meet 캘린더 등록 헬퍼 ───
/** 토·일을 제외하고 영업일 n일을 더한 날짜 (공휴일은 고려하지 않음). */
export function addBusinessDays(base: Date, n: number): Date {
const r = new Date(base);
let added = 0;
while (added < n) {
r.setDate(r.getDate() + 1);
const day = r.getDay();
if (day !== 0 && day !== 6) added++;
}
return r;
}
/** Date → 'YYYY-MM-DD' (로컬 기준). */
export function toYmd(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
/** 회의록 본문의 "**일시**: 2026년 05월 08일"(구 형식 "**날짜**:" 도 호환)에서 회의 날짜 추출. 없으면 fallback. */
export function extractMeetingDate(report: string, fallback: Date): Date {
const m = report.match(/(?:일시|날짜)\*{0,2}\s*[:]\s*\*{0,2}\s*(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/);
if (m) {
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
if (!isNaN(d.getTime())) return d;
}
return fallback;
}
/**
* 액션 아이템 '기한' 텍스트 → 캘린더 등록 날짜. 사용자 정의 규칙:
* - 명시 날짜(YYYY-MM-DD / YYYY년 M월 D일) → 그 날짜
* - "차주 / 다음 주 / 내주" → 회의일 +6일
* - "즉시 / 당일 / 금일 / 바로 / 오늘" → 등록일(오늘)
* - 변환 불가 / 빈 값 → 등록일 +영업일 5일, tentative=true (제목에 "(미확정)")
*/
export function resolveTaskDate(due: string, meetingDate: Date, today: Date): { date: string; tentative: boolean } {
const t = (due || '').trim();
const iso = t.match(/(\d{4})-(\d{1,2})-(\d{1,2})/);
if (iso) {
return { date: `${iso[1]}-${iso[2].padStart(2, '0')}-${iso[3].padStart(2, '0')}`, tentative: false };
}
const kor = t.match(/(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/);
if (kor) {
return { date: toYmd(new Date(Number(kor[1]), Number(kor[2]) - 1, Number(kor[3]))), tentative: false };
}
if (/차주|다음\s*주|내주/.test(t)) {
const d = new Date(meetingDate);
d.setDate(d.getDate() + 6);
return { date: toYmd(d), tentative: false };
}
if (/즉시|당일|금일|바로|오늘/.test(t)) {
return { date: toYmd(today), tentative: false };
}
// 변환 불가 — 등록일 + 영업일 5일, "(미확정)" 꼬리표.
return { date: toYmd(addBusinessDays(today, 5)), tentative: true };
}
/**
* 회의록 본문의 "## 5. 액션 아이템" 마크다운 표에서 행을 파싱.
* 5열 신표(담당 | 작업 내용 | 작업 상세 | 기한 | 상태) · 4열(상태 없음) ·
* 구(舊) 3열 표(담당 | 작업 내용 | 기한)를 모두 지원한다. 누락 컬럼은 빈 문자열.
*/
export function parseActionItems(report: string): { owner: string; work: string; detail: string; deliverable: string; due: string; status: string }[] {
const rows: { owner: string; work: string; detail: string; deliverable: string; due: string; status: string }[] = [];
let inSection = false;
for (const line of report.split('\n')) {
if (/^#{1,6}\s*(?:\d+\.\s*)?액션\s*아이템/.test(line)) { inSection = true; continue; }
if (!inSection) continue;
if (/^#{1,6}\s/.test(line)) break; // 다음 섹션 시작 → 종료
if (!/^\s*\|/.test(line)) continue;
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
if (cells.length < 3) continue;
if (/^:?-+:?$/.test(cells[0])) continue; // 표 구분선
if (cells[0] === '담당' || cells[1] === '작업 내용') continue; // 헤더
if (cells.length >= 6) {
// 신 형식: 담당 | 작업 내용 | 작업 상세 | 산출물 | 기한 | 상태
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: cells[3], due: cells[4], status: cells[5] });
} else if (cells.length === 5) {
// 구 형식: 담당 | 작업 내용 | 작업 상세 | 기한 | 상태 (산출물 컬럼 없음)
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: '', due: cells[3], status: cells[4] });
} else if (cells.length === 4) {
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], deliverable: '', due: cells[3], status: '' });
} else {
rows.push({ owner: cells[0], work: cells[1], detail: '', deliverable: '', due: cells[2], status: '' });
}
}
return rows;
}