feat(meet): 확신 게이트 등록 + /meet confirm + 데일리 브리핑 (v2.2.216)

캘린더 등록 정책을 "확신 없으면 등록 대신 질문"으로 전환:
- 액션 표에 상태 컬럼(확정/진행미정/기한미정/조건부:선행/반복:주기) — LLM 분류.
- 확정+기한만 자동 등록. 진행미정·기한미정·조건부는 보류 목록으로 질문,
  `/meet confirm 1=6/20 2=ok 3=skip` 답변으로 등록 완결 (/meet pending 재확인).
- 조건부 규칙: ok=날짜 없는 Tasks 로 [조건부] 등록(선행조건 노트 명시),
  날짜=그날을 '조건 확인일'로 등록 — 의존 대상이 제목/노트에서 즉시 인지됨.
- 반복 업무: 반복 등록 없이 첫 1회만(다음 해당 요일) — 까먹음 방지.
- 기한 해석 불가 확정건: 구버전의 +5일 추측 등록 제거 → 보류 질문.
- 과거 날짜(옛 녹취): 과거 날짜 그대로 등록 + "과거자료·완료확인 필요" 표기.
- 중복 방지: 녹취 sha256 해시 레지스트리(.astra/meet_registered.json)로
  같은 녹취 재실행 시 이중 등록 차단.
- tasksApi: due 옵션화(날짜 없는 task 지원).

데일리 브리핑 (신규):
- 평일 KST 09:30(설정 가능) 오늘의 캘린더 일정 + Tasks(오늘 마감/기한 경과/
  조건부 대기)를 텔레그램 발송. 텔레그램·캘린더 미연결 시 조용히 skip.
- g1nation.dailyBriefing.enabled(기본 true) / .time("09:30").

테스트: meetRegistration 15건 (분류 게이트·confirm 파싱·날짜 정규화·중복 키).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:22:19 +09:00
parent 4eb8bf03f7
commit 70ea421827
10 changed files with 777 additions and 60 deletions
@@ -72,11 +72,11 @@ export function resolveTaskDate(due: string, meetingDate: Date, today: Date): {
/**
* 회의록 본문의 "## 5. 액션 아이템" 마크다운 표에서 행을 파싱.
* 4열 표(담당 | 작업 내용 | 작업 상세 | 기한)와 구(舊) 3열 표(담당 | 작업 내용 | 기한)를
* 모두 지원한다. 3열일 때 detail 은 빈 문자열.
* 5표(담당 | 작업 내용 | 작업 상세 | 기한 | 상태) · 4열(상태 없음) ·
* 구(舊) 3열 표(담당 | 작업 내용 | 기한)를 모두 지원한다. 누락 컬럼은 빈 문자열.
*/
export function parseActionItems(report: string): { owner: string; work: string; detail: string; due: string }[] {
const rows: { owner: string; work: string; detail: string; due: string }[] = [];
export function parseActionItems(report: string): { owner: string; work: string; detail: string; due: string; status: string }[] {
const rows: { owner: string; work: string; detail: string; due: string; status: string }[] = [];
let inSection = false;
for (const line of report.split('\n')) {
if (/^#{1,6}\s*5\.\s*액션\s*아이템/.test(line)) { inSection = true; continue; }
@@ -87,10 +87,12 @@ export function parseActionItems(report: string): { owner: string; work: string;
if (cells.length < 3) continue;
if (/^:?-+:?$/.test(cells[0])) continue; // 표 구분선
if (cells[0] === '담당' || cells[1] === '작업 내용') continue; // 헤더
if (cells.length >= 4) {
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3] });
if (cells.length >= 5) {
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3], status: cells[4] });
} else if (cells.length === 4) {
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3], status: '' });
} else {
rows.push({ owner: cells[0], work: cells[1], detail: '', due: cells[2] });
rows.push({ owner: cells[0], work: cells[1], detail: '', due: cells[2], status: '' });
}
}
return rows;