mirror of
https://github.com/nad4-su/meeting-minutes.git
synced 2026-10-11 15:50:13 +09:00
feat: 회의록 섹션을 내용 기반으로 생성하고 액션 아이템에 담당자 표기 (#13)
고정 제목(## 주요 논의 사항)을 강요하던 프롬프트를, 회의가 실제로 다룬 주제에서 섹션 제목을 도출하도록 바꿨다. 제목만 훑어도 내용이 파악된다. - meeting·one_on_one: 본문 섹션 제목을 AI가 직접 짓도록 지시 - brainstorm: 카테고리도 미리 정해진 목록이 아님을 명시 - 액션 아이템에 (담당자)·기한 표기 요청 + 예시 제공 - COMMON_RULES 신설 — 환각 방지, 발화자 표기 활용, 영어 기술 용어 원문 유지(한영 코드 스위칭 대응). 프리셋에만 적용하고 custom 프롬프트는 사용자가 전적으로 제어하도록 유지 - LIVE_MODIFIER에 섹션 제목이 갱신될 수 있다는 안내 추가 버그 수정: 기존 meeting 프롬프트는 액션 아이템을 "담당자와 기한이 명확한 항목만"으로 제한해, 담당자를 정하지 않은 회의에서는 액션 아이템이 하나도 추출되지 않았다. 담당자가 불분명해도 포함하도록 변경. `- [ ]` 체크박스 형식은 그대로 유지해 parseActionItems와 호환된다. 테스트 102 → 113. Co-authored-by: csbae <csbae@RP-002.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+166
-32
No files matched your search
@@ -69,14 +69,25 @@ docker compose up -d
|
|||||||
|
|
||||||
| 템플릿 | 생성되는 구조 | 기본 강도 | 라이브 요약 |
|
| 템플릿 | 생성되는 구조 | 기본 강도 | 라이브 요약 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 🗂️ 회의록 | 요약 / 논의 / 액션 / 결정 | 표준 | ✅ |
|
| 🗂️ 회의록 | 요약 / **내용 기반 주제 섹션** / 액션 / 결정 | 표준 | ✅ |
|
||||||
| 🎓 강의·세미나 노트 | 핵심 개념 / 예시 / 인용 / 후속 질문 | 상세 | ✅ |
|
| 🎓 강의·세미나 노트 | 핵심 개념 / 예시 / 인용 / 후속 질문 | 상세 | ✅ |
|
||||||
| 🤝 1:1 미팅 | 주제 / 고민 / 피드백 / 다음 액션 | 표준 | ✅ |
|
| 🤝 1:1 미팅 | 요지 / **내용 기반 주제 섹션** / 고민 / 피드백 / 다음 액션 | 표준 | ✅ |
|
||||||
| 💡 브레인스토밍 | 카테고리별 아이디어 / 즉시 시도 / 보류 | 상세 | ✅ |
|
| 💡 브레인스토밍 | 카테고리별 아이디어 / 즉시 시도 / 보류 | 상세 | ✅ |
|
||||||
| 🎤 인터뷰 | Q&A 포맷 / 인상적 발언 / 종합 | 표준 | ❌ |
|
| 🎤 인터뷰 | Q&A 포맷 / 인상적 발언 / 종합 | 표준 | ❌ |
|
||||||
| 📝 원문 정리 | 요약 없이 문단화·오탈자 정리만 | 상세 고정 | ❌ |
|
| 📝 원문 정리 | 요약 없이 문단화·오탈자 정리만 | 상세 고정 | ❌ |
|
||||||
| ⚙️ 커스텀 | 자유 프롬프트 입력 | - | ✅ |
|
| ⚙️ 커스텀 | 자유 프롬프트 입력 | - | ✅ |
|
||||||
|
|
||||||
|
**내용 기반 주제 섹션**
|
||||||
|
|
||||||
|
회의록·1:1 템플릿은 `## 주요 논의 사항` 같은 고정 제목을 쓰지 않습니다. 그 회의가 실제로 무엇을
|
||||||
|
다뤘는지에 따라 AI가 섹션 제목을 직접 짓습니다 — 예를 들어 `## 단계별 개발 계획`,
|
||||||
|
`## 수익화 방안`, `## 기술적 고려사항` 처럼. 제목만 훑어도 회의 내용이 파악됩니다.
|
||||||
|
|
||||||
|
**액션 아이템 담당자**
|
||||||
|
|
||||||
|
발화에서 담당자가 파악되면 `- [ ] (김지훈) 경쟁 사이트 리스트업 — 5/17까지` 형태로 이름과 기한이 붙습니다.
|
||||||
|
담당자가 정해지지 않은 항목도 버리지 않고 그대로 수집합니다.
|
||||||
|
|
||||||
**강도 3단계**
|
**강도 3단계**
|
||||||
- **간결** — 각 섹션 3줄 이내
|
- **간결** — 각 섹션 3줄 이내
|
||||||
- **표준** — 맥락이 이해될 정도
|
- **표준** — 맥락이 이해될 정도
|
||||||
|
|||||||
@@ -164,3 +164,119 @@ describe('liveEnabledFor / depthAdjustableFor', () => {
|
|||||||
expect(depthAdjustableFor('custom')).toBe(false)
|
expect(depthAdjustableFor('custom')).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('내용 기반 섹션 구조', () => {
|
||||||
|
const transcript = '이번에는 가격 비교 사이트를 만들어 봅시다'
|
||||||
|
|
||||||
|
it('meeting은 섹션 제목을 직접 짓도록 지시한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'meeting',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('섹션 제목을 직접 지어서')
|
||||||
|
// 고정 제목을 쓰지 말라는 지시가 함께 있어야 한다
|
||||||
|
expect(prompt).toContain('일반적인 제목은 쓰지 마세요')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('one_on_one도 주제별 제목을 직접 짓도록 지시한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'one_on_one',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('제목을 직접 지어')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('raw는 구조적 제목을 덧붙이지 말라는 지시를 유지한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'raw',
|
||||||
|
depth: 'detailed',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('구조적 제목(## 섹션)을 덧붙이지 마세요')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('액션 아이템 담당자 표기', () => {
|
||||||
|
const transcript = '다음 주까지 정리해 주세요'
|
||||||
|
|
||||||
|
it('담당자 표기를 요구한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'meeting',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('(담당자)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('담당자가 불분명해도 항목을 버리지 않도록 지시한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'meeting',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('담당자가 불분명해도')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('체크박스 형식을 유지해 액션 아이템 파서와 호환된다', () => {
|
||||||
|
for (const id of ['meeting', 'one_on_one', 'brainstorm'] as const) {
|
||||||
|
const prompt = buildPrompt({ templateId: id, depth: 'standard', transcript })
|
||||||
|
expect(prompt).toContain('- [ ]')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('공통 규칙', () => {
|
||||||
|
const transcript = '테스트 발화'
|
||||||
|
|
||||||
|
it('프리셋 템플릿 전체에 공통 규칙이 붙는다', () => {
|
||||||
|
for (const id of Object.keys(TEMPLATES) as (keyof typeof TEMPLATES)[]) {
|
||||||
|
const prompt = buildPrompt({ templateId: id, depth: 'standard', transcript })
|
||||||
|
expect(prompt).toContain('지어내지 마세요')
|
||||||
|
expect(prompt).toContain('한국어로 작성하세요')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('발화자 표기가 있으면 활용하도록 지시한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'meeting',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('발화자 표기가 있다면')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('영어 기술 용어는 원문 표기를 유지하도록 지시한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'meeting',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('원문 표기를 유지하세요')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('custom 프롬프트에는 공통 규칙을 덧붙이지 않는다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'custom',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
customPrompt: '한 줄로만 요약해줘',
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('한 줄로만 요약해줘')
|
||||||
|
expect(prompt).not.toContain('지어내지 마세요')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('custom 프롬프트가 비어 있으면 meeting 템플릿 + 공통 규칙으로 대체한다', () => {
|
||||||
|
const prompt = buildPrompt({
|
||||||
|
templateId: 'custom',
|
||||||
|
depth: 'standard',
|
||||||
|
transcript,
|
||||||
|
customPrompt: ' ',
|
||||||
|
})
|
||||||
|
expect(prompt).toContain('회의록 작성 전문가')
|
||||||
|
expect(prompt).toContain('지어내지 마세요')
|
||||||
|
// 기존 동작 유지 — custom 경로에는 강도/라이브 모디파이어를 적용하지 않는다
|
||||||
|
expect(prompt).not.toContain('작성 강도')
|
||||||
|
})
|
||||||
|
})
|
||||||
+37
-30
@@ -34,20 +34,24 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
depthAdjustable: true,
|
depthAdjustable: true,
|
||||||
basePrompt: `당신은 회의록 작성 전문가입니다. 아래 발화 내용을 분석하여 마크다운 회의록을 작성하세요.
|
basePrompt: `당신은 회의록 작성 전문가입니다. 아래 발화 내용을 분석하여 마크다운 회의록을 작성하세요.
|
||||||
|
|
||||||
포함할 섹션:
|
|
||||||
## 요약
|
## 요약
|
||||||
(3~5줄 핵심 내용)
|
회의 전체를 3~5줄로. 무엇을 논의했고 무엇이 정해졌는지.
|
||||||
|
|
||||||
## 주요 논의 사항
|
이어지는 본문은 **섹션 제목을 직접 지어서** 구성하세요:
|
||||||
1. 순번 매기기
|
- 실제로 논의된 주제를 스스로 파악해 \`##\` 제목을 붙여 나눕니다.
|
||||||
|
- "주요 논의 사항", "논의 내용" 같은 일반적인 제목은 쓰지 마세요.
|
||||||
|
이 회의가 무엇을 다뤘는지 제목만 봐도 알 수 있어야 합니다.
|
||||||
|
(예: "단계별 개발 계획", "수익화 방안", "기술적 고려사항")
|
||||||
|
- 주제 수는 내용에 따라 정하되 보통 3~6개입니다.
|
||||||
|
|
||||||
## 액션 아이템
|
## 액션 아이템
|
||||||
- [ ] 담당자와 기한이 명확한 항목만
|
반드시 \`- [ ]\` 체크박스 형식으로 작성하세요.
|
||||||
|
담당자가 파악되면 \`(담당자)\`를 앞에, 기한이 언급됐으면 뒤에 덧붙입니다.
|
||||||
|
예: \`- [ ] (김지훈) 경쟁 사이트 리스트업 — 5/17까지\`
|
||||||
|
담당자가 불분명해도 해야 할 일이 명확하면 포함하세요.
|
||||||
|
|
||||||
## 결정 사항
|
## 결정 사항
|
||||||
- 합의 또는 확정된 것만
|
- 합의되었거나 확정된 것만. 없으면 이 섹션을 생략하세요.`,
|
||||||
|
|
||||||
한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
lecture: {
|
lecture: {
|
||||||
@@ -60,20 +64,18 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
depthAdjustable: true,
|
depthAdjustable: true,
|
||||||
basePrompt: `당신은 학습용 노트를 작성하는 전문가입니다. 강의/세미나 녹취를 구조화된 학습 자료로 정리하세요.
|
basePrompt: `당신은 학습용 노트를 작성하는 전문가입니다. 강의/세미나 녹취를 구조화된 학습 자료로 정리하세요.
|
||||||
|
|
||||||
포함할 섹션:
|
|
||||||
## 주요 주제
|
## 주요 주제
|
||||||
(한 줄 요약)
|
(한 줄 요약)
|
||||||
|
|
||||||
## 핵심 개념
|
## 핵심 개념
|
||||||
각 개념마다 ### 소제목 + 설명 + 구체 예시
|
각 개념마다 ### 소제목 + 설명 + 구체 예시
|
||||||
|
소제목은 강의에서 실제로 다룬 개념 이름을 그대로 쓰세요.
|
||||||
|
|
||||||
## 기억할 인용·예시
|
## 기억할 인용·예시
|
||||||
> 중요한 문장 인용 형식으로
|
> 중요한 문장 인용 형식으로
|
||||||
|
|
||||||
## 후속 질문
|
## 후속 질문
|
||||||
- 스스로 탐구해볼 만한 질문
|
- 스스로 탐구해볼 만한 질문`,
|
||||||
|
|
||||||
한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
one_on_one: {
|
one_on_one: {
|
||||||
@@ -86,12 +88,11 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
depthAdjustable: true,
|
depthAdjustable: true,
|
||||||
basePrompt: `당신은 1:1 미팅 정리 전문가입니다. 개인적이고 신뢰 기반의 대화임을 존중하며 정리하세요.
|
basePrompt: `당신은 1:1 미팅 정리 전문가입니다. 개인적이고 신뢰 기반의 대화임을 존중하며 정리하세요.
|
||||||
|
|
||||||
포함할 섹션:
|
|
||||||
## 이번 세션 요지
|
## 이번 세션 요지
|
||||||
(2~3줄)
|
(2~3줄)
|
||||||
|
|
||||||
## 논의한 주제
|
이어서 대화에서 실제로 다룬 주제마다 **\`##\` 제목을 직접 지어** 나누세요.
|
||||||
- 주요 대화 흐름
|
"논의한 주제" 같은 일반적인 제목 대신, 무엇에 대한 이야기였는지 드러내는 제목을 쓰세요.
|
||||||
|
|
||||||
## 고민·블로커
|
## 고민·블로커
|
||||||
- 공유된 어려움
|
- 공유된 어려움
|
||||||
@@ -100,9 +101,7 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
- 건설적 피드백 위주
|
- 건설적 피드백 위주
|
||||||
|
|
||||||
## 다음 미팅까지 할 일
|
## 다음 미팅까지 할 일
|
||||||
- [ ] 합의된 후속 조치
|
- [ ] 합의된 후속 조치. 담당자가 있으면 \`(담당자)\` 표기`,
|
||||||
|
|
||||||
한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
brainstorm: {
|
brainstorm: {
|
||||||
@@ -115,12 +114,12 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
depthAdjustable: true,
|
depthAdjustable: true,
|
||||||
basePrompt: `당신은 브레인스토밍 세션을 정리하는 전문가입니다. 나온 아이디어를 분류하고 실행 가능성 관점에서 정리하세요.
|
basePrompt: `당신은 브레인스토밍 세션을 정리하는 전문가입니다. 나온 아이디어를 분류하고 실행 가능성 관점에서 정리하세요.
|
||||||
|
|
||||||
포함할 섹션:
|
|
||||||
## 세션 개요
|
## 세션 개요
|
||||||
(1~2줄 — 주제/목표)
|
(1~2줄 — 주제/목표)
|
||||||
|
|
||||||
## 아이디어 (카테고리별)
|
## 아이디어 (카테고리별)
|
||||||
### [카테고리 이름]
|
### [카테고리 이름]
|
||||||
|
카테고리는 미리 정해진 목록이 아니라, 나온 아이디어를 보고 직접 묶어서 이름 붙이세요.
|
||||||
- 아이디어 요약
|
- 아이디어 요약
|
||||||
|
|
||||||
## 즉시 시도 가능
|
## 즉시 시도 가능
|
||||||
@@ -130,9 +129,7 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
- 사유 간단히
|
- 사유 간단히
|
||||||
|
|
||||||
## 다음 스텝
|
## 다음 스텝
|
||||||
- 구체적인 후속 행동
|
- [ ] 구체적인 후속 행동. 담당자가 있으면 \`(담당자)\` 표기`,
|
||||||
|
|
||||||
한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
interview: {
|
interview: {
|
||||||
@@ -145,7 +142,6 @@ export const TEMPLATES: Record<Exclude<TemplateId, 'custom'>, Template> = {
|
|||||||
depthAdjustable: true,
|
depthAdjustable: true,
|
||||||
basePrompt: `당신은 인터뷰 녹취를 Q&A 형식으로 정리하는 전문가입니다.
|
basePrompt: `당신은 인터뷰 녹취를 Q&A 형식으로 정리하는 전문가입니다.
|
||||||
|
|
||||||
포함할 섹션:
|
|
||||||
## 개요
|
## 개요
|
||||||
(인터뷰 대상/주제 1~2줄)
|
(인터뷰 대상/주제 1~2줄)
|
||||||
|
|
||||||
@@ -159,9 +155,7 @@ A: [답변 요약]
|
|||||||
> 직접 인용
|
> 직접 인용
|
||||||
|
|
||||||
## 종합 인상
|
## 종합 인상
|
||||||
- 전반적 테마 / 놓치지 말 것
|
- 전반적 테마 / 놓치지 말 것`,
|
||||||
|
|
||||||
한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
raw: {
|
raw: {
|
||||||
@@ -181,8 +175,7 @@ A: [답변 요약]
|
|||||||
엄격한 규칙:
|
엄격한 규칙:
|
||||||
- 내용을 삭제하거나 축약하지 마세요
|
- 내용을 삭제하거나 축약하지 마세요
|
||||||
- 구조적 제목(## 섹션)을 덧붙이지 마세요
|
- 구조적 제목(## 섹션)을 덧붙이지 마세요
|
||||||
- 타임스탬프가 있으면 그대로 유지하세요
|
- 타임스탬프가 있으면 그대로 유지하세요`,
|
||||||
- 한국어로 작성하세요.`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +188,19 @@ const DEPTH_MODIFIERS: Record<SummaryDepth, string> = {
|
|||||||
'작성 강도: **상세**. 원문의 주요 세부사항·수치·고유명사를 누락 없이 포함하세요. 구조만 바꾸고 내용은 최대한 보존합니다.',
|
'작성 강도: **상세**. 원문의 주요 세부사항·수치·고유명사를 누락 없이 포함하세요. 구조만 바꾸고 내용은 최대한 보존합니다.',
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIVE_MODIFIER = `회의가 아직 진행 중입니다. 지금까지의 내용을 기반으로 **중간 정리**를 작성하세요. 확정되지 않은 결정은 "(논의 중)"으로 표시하세요.`
|
const LIVE_MODIFIER = `회의가 아직 진행 중입니다. 지금까지의 내용을 기반으로 **중간 정리**를 작성하세요. 확정되지 않은 결정은 "(논의 중)"으로 표시하세요.
|
||||||
|
섹션 제목은 지금까지 나온 주제를 기준으로 붙이되, 이후 갱신될 수 있으므로 간결하게 유지하세요.`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프리셋 템플릿 전체에 공통으로 덧붙는 규칙.
|
||||||
|
*
|
||||||
|
* custom 프롬프트에는 적용하지 않는다 — 사용자가 전적으로 제어하는 영역이기 때문.
|
||||||
|
*/
|
||||||
|
const COMMON_RULES = `공통 규칙:
|
||||||
|
- 발화에 없는 내용을 지어내지 마세요. 불확실하면 쓰지 마세요.
|
||||||
|
- 발화자 표기가 있다면 누가 무엇을 주장하고 약속했는지 반영하세요.
|
||||||
|
- 영어 기술 용어·제품명은 번역하거나 음차하지 말고 원문 표기를 유지하세요 (예: latency, deployment).
|
||||||
|
- 한국어로 작성하세요.`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 증분 갱신 모드.
|
* 증분 갱신 모드.
|
||||||
@@ -252,7 +257,7 @@ export function buildPrompt(args: BuildPromptArgs): string {
|
|||||||
if (templateId === 'custom') {
|
if (templateId === 'custom') {
|
||||||
const custom = customPrompt?.trim()
|
const custom = customPrompt?.trim()
|
||||||
if (!custom) {
|
if (!custom) {
|
||||||
instruction = TEMPLATES.meeting.basePrompt
|
instruction = `${TEMPLATES.meeting.basePrompt}\n\n${COMMON_RULES}`
|
||||||
} else {
|
} else {
|
||||||
instruction = custom
|
instruction = custom
|
||||||
}
|
}
|
||||||
@@ -272,6 +277,8 @@ export function buildPrompt(args: BuildPromptArgs): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts.push(COMMON_RULES)
|
||||||
|
|
||||||
instruction = parts.join('\n\n')
|
instruction = parts.join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user