v2.2.258: /meet 화자 팀/역할 정규화 + 헤더 전조각 주입 + 검증 5종
STT 화자번호(참석자 N) 박멸→회사 표준 팀/역할 귀속, 회의 헤더 전 청크 주입, 전역 헤드라인 추출, 결정 게이트·담화 상태 태깅, 슬림 6섹션 포맷, 타임스탬프 근거, parseActionItems 헤더명 기반 재작성, 검증 패스 5종. 전체 698 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,23 @@ export function toYmd(d: Date): string {
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 녹취록 본문(첫 화자 발언) 앞의 헤더 블록을 추출한다.
|
||||
* STT 다이어라이제이션 줄("참석자 3 00:46" / "화자 1 0:12") 직전까지를 헤더로 본다.
|
||||
* 참석자 명단·일시·장소·녹취 길이가 보통 여기 있다. 헤더가 없거나 비정상적으로
|
||||
* 길면(다이어라이제이션 없는 일반 문서) 빈 문자열을 반환한다.
|
||||
*/
|
||||
export function extractMeetingHeader(transcript: string): string {
|
||||
const lines = transcript.split('\n');
|
||||
let cut = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/^\s*(?:참석자|화자|발언자)\s*\d+\s+\d{1,2}:\d{2}/.test(lines[i])) { cut = i; break; }
|
||||
}
|
||||
if (cut <= 0) return '';
|
||||
const header = lines.slice(0, cut).join('\n').trim();
|
||||
return header.length > 0 && header.length <= 1200 ? `[회의 헤더]\n${header}` : '';
|
||||
}
|
||||
|
||||
/** 회의록 본문의 "**일시**: 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*일/);
|
||||
@@ -70,34 +87,74 @@ export function resolveTaskDate(due: string, meetingDate: Date, today: Date): {
|
||||
return { date: toYmd(addBusinessDays(today, 5)), tentative: true };
|
||||
}
|
||||
|
||||
export interface ParsedActionRow { owner: string; work: string; detail: string; deliverable: string; due: string; status: string; source: string }
|
||||
|
||||
/** 표 머리글 셀 → 표준 필드 키. 컬럼 순서가 바뀌어도 이름으로 안전하게 매핑한다. */
|
||||
function headerKey(cell: string): keyof ParsedActionRow | null {
|
||||
const c = cell.replace(/\s+/g, '');
|
||||
if (c === '담당') return 'owner';
|
||||
if (c === '액션' || c === '작업내용') return 'work';
|
||||
if (c === '작업상세') return 'detail';
|
||||
if (c === '산출물') return 'deliverable';
|
||||
if (c === '기한') return 'due';
|
||||
if (c === '상태') return 'status';
|
||||
if (c === '출처' || c === '근거') return 'source';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 본문의 "## 5. 액션 아이템" 마크다운 표에서 행을 파싱.
|
||||
* 5열 신표(담당 | 작업 내용 | 작업 상세 | 기한 | 상태) · 4열(상태 없음) ·
|
||||
* 구(舊) 3열 표(담당 | 작업 내용 | 기한)를 모두 지원한다. 누락 컬럼은 빈 문자열.
|
||||
* 회의록 본문의 "## 액션 아이템" 마크다운 표에서 행을 파싱.
|
||||
* **머리글 이름 기반 매핑**이라 컬럼 순서·개수가 달라도 안전하다:
|
||||
* - 신 형식(v2.2.258): 담당 | 액션 | 기한 | 상태 | 출처
|
||||
* - 구 형식: 담당 | 작업 내용 | 작업 상세 | (산출물) | 기한 | 상태
|
||||
* 머리글을 못 찾으면 위치 기반으로 폴백(구형 호환). 누락 컬럼은 빈 문자열.
|
||||
*/
|
||||
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 }[] = [];
|
||||
export function parseActionItems(report: string): ParsedActionRow[] {
|
||||
const rows: ParsedActionRow[] = [];
|
||||
const empty = (): ParsedActionRow => ({ owner: '', work: '', detail: '', deliverable: '', due: '', status: '', source: '' });
|
||||
let inSection = false;
|
||||
let colMap: Partial<Record<keyof ParsedActionRow, number>> | null = null;
|
||||
for (const line of report.split('\n')) {
|
||||
if (/^#{1,6}\s*(?:\d+\.\s*)?액션\s*아이템/.test(line)) { inSection = true; continue; }
|
||||
if (/^#{1,6}\s*(?:\d+\.\s*)?액션\s*아이템/.test(line)) { inSection = true; colMap = null; 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: '' });
|
||||
// 머리글 행 감지 → 컬럼 인덱스 매핑 기록
|
||||
const isHeader = cells.some((c) => c === '담당') && cells.some((c) => headerKey(c) === 'work');
|
||||
if (isHeader) {
|
||||
colMap = {};
|
||||
cells.forEach((c, i) => { const k = headerKey(c); if (k && colMap![k] === undefined) colMap![k] = i; });
|
||||
continue;
|
||||
}
|
||||
if (colMap) {
|
||||
// '—'/빈 칸은 미정 신호 → 빈 문자열로 정규화(다운스트림 게이트가 보류 처리).
|
||||
const get = (k: keyof ParsedActionRow): string => {
|
||||
const i = colMap![k];
|
||||
if (i === undefined) return '';
|
||||
const v = (cells[i] ?? '').trim();
|
||||
return v === '—' || v === '-' ? '' : v;
|
||||
};
|
||||
const row = empty();
|
||||
row.owner = get('owner'); row.work = get('work'); row.detail = get('detail');
|
||||
row.deliverable = get('deliverable'); row.due = get('due'); row.status = get('status'); row.source = get('source');
|
||||
if (row.work) rows.push(row);
|
||||
continue;
|
||||
}
|
||||
// 머리글 없음 — 위치 기반 폴백(구형 회의록 호환)
|
||||
const row = empty();
|
||||
if (cells.length >= 6) {
|
||||
[row.owner, row.work, row.detail, row.deliverable, row.due, row.status] = [cells[0], cells[1], cells[2], cells[3], cells[4], cells[5]];
|
||||
} else if (cells.length === 5) {
|
||||
[row.owner, row.work, row.detail, row.due, row.status] = [cells[0], cells[1], cells[2], cells[3], cells[4]];
|
||||
} else if (cells.length === 4) {
|
||||
[row.owner, row.work, row.detail, row.due] = [cells[0], cells[1], cells[2], cells[3]];
|
||||
} else {
|
||||
[row.owner, row.work, row.due] = [cells[0], cells[1], cells[2]];
|
||||
}
|
||||
if (row.work) rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user