Feat: AI 서비스 통합 및 회의록 기능 개선

- AI 서비스와 Meeting 서비스 통합 개선
  - AgendaSummaryDTO에 decisions 필드 추가 (안건별 결정사항 배열)
  - EndMeetingService에서 AI 서비스 타임아웃 처리 개선
  - AIServiceClient에 상세한 에러 로깅 추가

- 회의록 consolidate 프롬프트 개선
  - Todo 추출 로직 강화 (자연스러운 표현 인식)
  - 안건별 decisions 필드 추가 (대시보드 표시용)
  - 담당자 패턴 인식 개선

- Kubernetes 배포 설정 개선
  - meeting-service.yaml에 AI_SERVICE_URL 환경변수 추가
  - AI_SERVICE_TIMEOUT 설정 추가

- 데이터베이스 관리 SQL 스크립트 추가
  - check-agenda-sections.sql: 안건 섹션 확인
  - cleanup-test-data.sql: 테스트 데이터 정리
  - insert-test-data-final.sql: 최종 테스트 데이터

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Minseo-Jo 2025-10-30 18:07:57 +09:00
parent 4a87be88f0
commit 0caa1ec3b6
11 changed files with 458 additions and 54 deletions

View File

@ -30,6 +30,7 @@ class AgendaSummary(BaseModel):
agenda_title: str = Field(..., description="안건 제목")
summary_short: str = Field(..., description="AI 생성 짧은 요약 (1줄, 20자 이내)")
summary: str = Field(..., description="안건별 회의록 요약 (논의사항+결정사항, 사용자 수정 가능)")
decisions: List[str] = Field(default_factory=list, description="안건별 결정사항 배열 (대시보드 표시용)")
pending: List[str] = Field(default_factory=list, description="보류 사항")
todos: List[ExtractedTodo] = Field(default_factory=list, description="Todo 목록 (제목만)")

View File

@ -49,13 +49,23 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
- **agenda_number**: 안건 번호 (1, 2, 3...)
- **agenda_title**: 안건 제목 (간결하게)
- **summary_short**: AI가 생성한 1 요약 (20 이내, 사용자 수정 불가)
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항포함한 전체 요약)
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항 모두 포함)
* 회의록 수정 페이지에서 사용자가 수정할 있는 입력 필드
* 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2"
* 사용자가 자유롭게 편집할 있도록 구조화된 텍스트로 작성
- **decisions**: 안건별 결정사항 배열 (대시보드 표시용, summary의 결정사항 부분을 배열로 추출)
* 형식: ["결정사항1", "결정사항2", "결정사항3"]
* 회의에서 최종 결정된 사항만 포함
- **pending**: 보류 사항 배열 (추가 논의 필요 사항)
- **todos**: Todo 배열 (제목만, 담당자/마감일/우선순위 없음)
- title: Todo 제목만 추출 (: "시장 조사 보고서 작성")
- **todos**: Todo 배열 (제목과 담당자 추출)
- title: Todo 제목 (: "시장 조사 보고서 작성")
- assignee: 담당자 이름 (있는 경우에만, : "김대리", "박과장")
**Todo 추출 가이드:**
- 자연스러운 표현도 인식: "김대리가 ~하기로 함", "박과장은 ~준비합니다", "이차장님께서 ~하시기로 하셨습니다"
- 실행 동사 패턴: ~하기로, ~준비, ~작성, ~제출, ~완료, ~진행, ~검토, ~분석
- 담당자 패턴: "OO님", "OO이/가", "OO은/는", "OO께서"
- 기한 표현: "다음주", "이번주", "~까지", "~일까지", "~월까지"
---
@ -77,10 +87,16 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
"agenda_title": "안건 제목",
"summary_short": "짧은 요약 (20자 이내)",
"summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2",
"decisions": ["결정사항1", "결정사항2"],
"pending": ["보류사항"],
"todos": [
{{
"title": "Todo 제목"
"title": "인플루언서 리스트 작성",
"assignee": "김대리"
}},
{{
"title": "캠페인 콘텐츠 기획안 초안 작성",
"assignee": "박과장"
}}
]
}}
@ -97,11 +113,13 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
3. **완전성**: 모든 필드를 빠짐없이 작성
4. **구조화**: 안건별로 명확히 분리
5. **결정사항 추출**:
- 회의 전체 결정사항(decisions) 모든 안건의 결정사항을 포함
- 안건별 summary에도 결정사항을 포함하여 사용자가 수정 가능하도록 작성
- 회의 전체 결정사항(decisions): 모든 안건의 결정사항을 포함 (TEXT 형식)
- 안건별 결정사항(agenda_summaries[].decisions): 안건의 결정사항을 배열로 추출
- 결정사항이 명확하게 언급된 경우에만 포함
6. **summary 작성**:
- summary_short: AI가 자동 생성한 1 요약 (사용자 수정 불가)
- summary: 논의사항과 결정사항을 포함한 전체 요약 (사용자 수정 가능)
- summary: 논의사항과 결정사항 모두 포함 (사용자 수정 가능)
- decisions: summary의 결정사항 부분을 배열로 별도 추출 (대시보드 표시용)
7. **Todo 추출**: 제목만 추출 (담당자나 마감일 없어도 )
8. **JSON만 출력**: 추가 설명 없이 JSON만 반환

View File

@ -96,6 +96,7 @@ class TranscriptService:
agenda_title=agenda_data.get("agenda_title", ""),
summary_short=agenda_data.get("summary_short", ""),
summary=agenda_data.get("summary", ""),
decisions=agenda_data.get("decisions", []),
pending=agenda_data.get("pending", []),
todos=todos
)

View File

@ -65,6 +65,10 @@ spec:
key: eventhub-connection-string
- name: NOTIFICATION_SERVICE_URL
value: "http://notification-service:8082"
- name: AI_SERVICE_URL
value: "http://ai-service:8087"
- name: AI_SERVICE_TIMEOUT
value: "60000"
resources:
requests:
cpu: 256m

View File

@ -13,11 +13,14 @@ import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
@ -45,7 +48,9 @@ public class EndMeetingService implements EndMeetingUseCase {
private final MinutesSectionJpaRepository minutesSectionRepository;
private final TodoJpaRepository todoRepository;
private final MeetingAnalysisJpaRepository analysisRepository;
private final AgendaSectionJpaRepository agendaSectionRepository;
private final AIServiceClient aiServiceClient;
private final ObjectMapper objectMapper;
/**
* 회의 종료 AI 분석 실행
@ -58,44 +63,73 @@ public class EndMeetingService implements EndMeetingUseCase {
public MeetingEndDTO endMeeting(String meetingId) {
log.info("회의 종료 시작 - meetingId: {}", meetingId);
// 1. 회의 정보 조회
MeetingEntity meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
try {
// 1. 회의 정보 조회
MeetingEntity meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> {
log.error("회의를 찾을 수 없음 - meetingId: {}", meetingId);
return new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId);
});
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
log.info("회의 정보 조회 완료 - title: {}, status: {}", meeting.getTitle(), meeting.getStatus());
if (participantMinutesList.isEmpty()) {
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
if (participantMinutesList.isEmpty()) {
log.error("참석자 회의록이 없음 - meetingId: {}", meetingId);
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
}
log.info("참석자 회의록 조회 완료 - 참석자 수: {}", participantMinutesList.size());
// 3. 회의록의 sections 조회 통합
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
for (MinutesEntity minutes : participantMinutesList) {
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
minutes.getMinutesId()
);
allMinutesSections.addAll(sections);
log.debug("회의록 섹션 조회 - minutesId: {}, userId: {}, 섹션 수: {}",
minutes.getMinutesId(), minutes.getUserId(), sections.size());
}
log.info("전체 회의록 섹션 조회 완료 - 총 섹션 수: {}", allMinutesSections.size());
// 4. AI 통합 분석 요청 데이터 생성
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
log.info("AI 통합 분석 요청 데이터 생성 완료 - participantMinutes 수: {}",
request.getParticipantMinutes().size());
// 5. AI Service 호출
log.info("AI Service 호출 시작...");
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
log.info("AI Service 호출 완료 - 안건 수: {}", aiResponse.getAgendaSummaries().size());
// 6. AI 분석 결과 저장
MeetingAnalysis analysis = saveAnalysisResult(meeting, aiResponse);
// 7. Todo 생성 저장
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
// 8. 회의 종료 처리
meeting.end();
meetingRepository.save(meeting);
log.info("회의 상태 업데이트 완료 - status: {}", meeting.getStatus());
// 9. 응답 DTO 생성
MeetingEndDTO result = createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
log.info("회의 종료 처리 완료 - meetingId: {}, 안건 수: {}, Todo 수: {}",
meetingId, analysis.getAgendaAnalyses().size(), todos.size());
return result;
} catch (Exception e) {
log.error("회의 종료 처리 중 오류 발생 - meetingId: {}, 에러: {}",
meetingId, e.getMessage(), e);
throw e;
}
// 3. 회의록의 sections 조회 통합
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
for (MinutesEntity minutes : participantMinutesList) {
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
minutes.getMinutesId()
);
allMinutesSections.addAll(sections);
}
// 4. AI 통합 분석 요청 데이터 생성
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
// 5. AI Service 호출
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
// 5. AI 분석 결과 저장
MeetingAnalysis analysis = saveAnalysisResult(meeting, aiResponse);
// 6. Todo 생성 저장
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
// 6. 회의 종료 처리
meeting.end();
meetingRepository.save(meeting);
// 7. 응답 DTO 생성
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
}
/**
@ -141,7 +175,7 @@ public class EndMeetingService implements EndMeetingUseCase {
.title(summary.getAgendaTitle())
.aiSummaryShort(summary.getSummaryShort())
.discussion(summary.getSummary() != null ? summary.getSummary() : "")
.decisions(List.of())
.decisions(summary.getDecisions() != null ? summary.getDecisions() : List.of())
.pending(summary.getPending() != null ? summary.getPending() : List.of())
.extractedTodos(summary.getTodos() != null
? summary.getTodos().stream()
@ -168,18 +202,61 @@ public class EndMeetingService implements EndMeetingUseCase {
MeetingAnalysisEntity entity = MeetingAnalysisEntity.fromDomain(analysis);
analysisRepository.save(entity);
// AgendaSection 저장 (안건별 회의록)
saveAgendaSections(meeting.getMeetingId(), aiResponse);
log.info("AI 분석 결과 저장 완료 - analysisId: {}", analysis.getAnalysisId());
return analysis;
}
/**
* AgendaSection 저장
*/
private void saveAgendaSections(String meetingId, ConsolidateResponse aiResponse) {
int agendaNumber = 1;
for (var summary : aiResponse.getAgendaSummaries()) {
try {
// pending items와 todos를 JSON 문자열로 변환
String pendingItemsJson = summary.getPending() != null && !summary.getPending().isEmpty()
? objectMapper.writeValueAsString(summary.getPending())
: null;
String todosJson = summary.getTodos() != null && !summary.getTodos().isEmpty()
? objectMapper.writeValueAsString(summary.getTodos())
: null;
AgendaSectionEntity agendaSection = AgendaSectionEntity.builder()
.id(UUID.randomUUID().toString())
.minutesId(meetingId) // AI 통합 회의록 ID로 사용
.meetingId(meetingId)
.agendaNumber(agendaNumber++)
.agendaTitle(summary.getAgendaTitle())
.aiSummaryShort(summary.getSummaryShort())
.summary(summary.getSummary())
.pendingItems(pendingItemsJson)
.todos(todosJson)
.build();
agendaSectionRepository.save(agendaSection);
log.debug("AgendaSection 저장 완료 - agendaTitle: {}", summary.getAgendaTitle());
} catch (Exception e) {
log.error("AgendaSection 저장 실패 - agendaTitle: {}", summary.getAgendaTitle(), e);
}
}
log.info("AgendaSection 저장 완료 - meetingId: {}, count: {}", meetingId, aiResponse.getAgendaSummaries().size());
}
/**
* Todo 생성 저장
*/
private List<TodoEntity> createAndSaveTodos(MeetingEntity meeting, ConsolidateResponse aiResponse, MeetingAnalysis analysis) {
List<TodoEntity> todos = aiResponse.getAgendaSummaries().stream()
.<TodoEntity>flatMap(agenda -> {
String agendaId = findAgendaIdByTitle(analysis, agenda.getAgendaTitle());
// agendaId는 향후 Todo와 안건 매핑에 사용될 있음 (현재는 사용하지 않음)
List<ExtractedTodoDTO> todoList = agenda.getTodos() != null ? agenda.getTodos() : List.of();
return todoList.stream()
.<TodoEntity>map(todo -> TodoEntity.builder()

View File

@ -67,7 +67,7 @@ public class MeetingMemoService implements SaveMeetingMemoUseCase {
// 4. 기존 메모 섹션 조회 또는 새로운 섹션 생성
MinutesSection memoSection = minutesSectionReader.findFirstByMinutesIdAndType(
minutes.getMinutesId(),
"AI_MEMO"
"MEMO"
).orElseGet(() -> createNewMemoSection(minutes.getMinutesId()));
// 5. 메모 내용 업데이트 (기존 내용에 추가)
@ -81,7 +81,7 @@ public class MeetingMemoService implements SaveMeetingMemoUseCase {
MinutesSection updatedSection = MinutesSection.builder()
.sectionId(memoSection.getSectionId())
.minutesId(memoSection.getMinutesId())
.type("AI_MEMO")
.type("MEMO")
.title("회의 메모")
.content(updatedContent)
.order(memoSection.getOrder())
@ -105,7 +105,7 @@ public class MeetingMemoService implements SaveMeetingMemoUseCase {
return MinutesSection.builder()
.sectionId(generateSectionId())
.minutesId(minutesId)
.type("AI_MEMO")
.type("MEMO")
.title("회의 메모")
.content("")
.order(maxOrder + 1)

View File

@ -43,11 +43,10 @@ public class AIServiceClient {
* @return 통합 요약 응답
*/
public ConsolidateResponse consolidateMinutes(ConsolidateRequest request) {
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
String url = aiServiceUrl + "/api/transcripts/consolidate";
log.info("AI Service 호출 시작 - URL: {}, meetingId: {}", url, request.getMeetingId());
try {
String url = aiServiceUrl + "/api/transcripts/consolidate";
// HTTP 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@ -55,6 +54,9 @@ public class AIServiceClient {
// HTTP 요청 생성
HttpEntity<ConsolidateRequest> httpEntity = new HttpEntity<>(request, headers);
log.debug("AI Service 요청 데이터 - participantMinutes 수: {}",
request.getParticipantMinutes() != null ? request.getParticipantMinutes().size() : 0);
// API 호출
ResponseEntity<ConsolidateResponse> response = restTemplate.postForEntity(
url,
@ -65,15 +67,30 @@ public class AIServiceClient {
ConsolidateResponse result = response.getBody();
if (result == null) {
log.error("AI Service 응답이 비어있습니다 - HTTP Status: {}", response.getStatusCode());
throw new RuntimeException("AI Service 응답이 비어있습니다");
}
log.info("AI Service 응답 수신 완료 - 안건 수: {}", result.getAgendaSummaries().size());
log.info("AI Service 응답 수신 완료 - 안건 수: {}, HTTP Status: {}",
result.getAgendaSummaries() != null ? result.getAgendaSummaries().size() : 0,
response.getStatusCode());
return result;
} catch (org.springframework.web.client.ResourceAccessException e) {
log.error("AI Service 연결 실패 - URL: {}, 에러: {}", url, e.getMessage());
throw new RuntimeException("AI 서비스에 연결할 수 없습니다. 서비스가 실행 중인지 확인해주세요.", e);
} catch (org.springframework.web.client.HttpClientErrorException e) {
log.error("AI Service 클라이언트 오류 - HTTP Status: {}, 응답: {}",
e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("AI 서비스 요청이 거부되었습니다: " + e.getMessage(), e);
} catch (org.springframework.web.client.HttpServerErrorException e) {
log.error("AI Service 서버 오류 - HTTP Status: {}, 응답: {}",
e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("AI 서비스에서 오류가 발생했습니다: " + e.getMessage(), e);
} catch (Exception e) {
log.error("AI Service 호출 실패: {}", e.getMessage(), e);
log.error("AI Service 호출 중 예상치 못한 오류 - 타입: {}, 메시지: {}",
e.getClass().getSimpleName(), e.getMessage(), e);
throw new RuntimeException("AI 회의록 통합 처리 중 오류가 발생했습니다: " + e.getMessage(), e);
}
}

View File

@ -37,10 +37,15 @@ public class AgendaSummaryDTO {
/**
* 안건별 회의록 요약
* 사용자가 입력한 회의록 내용을 요약한 결과
* 사용자가 입력한 회의록 내용을 요약한 결과 (논의사항 + 결정사항)
*/
private String summary;
/**
* 안건별 결정사항 배열 (대시보드 표시용)
*/
private List<String> decisions;
/**
* 보류 사항
*/

View File

@ -0,0 +1,17 @@
-- =====================================================
-- Check agenda_sections data for meeting-test
-- =====================================================
SELECT
id,
meeting_id,
agenda_number,
agenda_title,
ai_summary_short,
LENGTH(summary) as summary_length,
pending_items,
todos,
created_at
FROM agenda_sections
WHERE meeting_id = 'meeting-test'
ORDER BY agenda_number;

View File

@ -0,0 +1,26 @@
-- =====================================================
-- Cleanup Test Data for meeting-test
-- =====================================================
-- 1. agenda_sections 삭제 (외래키 제약이 있을 수 있으므로 먼저)
DELETE FROM agenda_sections WHERE meeting_id = 'meeting-test';
-- 2. todos 삭제
DELETE FROM todos WHERE meeting_id = 'meeting-test';
-- 3. meeting_analysis 관련 삭제
DELETE FROM meeting_keywords WHERE analysis_id IN (SELECT analysis_id FROM meeting_analysis WHERE meeting_id = 'meeting-test');
DELETE FROM meeting_analysis WHERE meeting_id = 'meeting-test';
-- 4. minutes_sections 삭제
DELETE FROM minutes_sections WHERE minutes_id IN (SELECT minutes_id FROM minutes WHERE meeting_id = 'meeting-test');
-- 5. minutes 삭제
DELETE FROM minutes WHERE meeting_id = 'meeting-test';
-- 6. 확인
SELECT 'Cleanup completed' AS status;
SELECT COUNT(*) as remaining_minutes FROM minutes WHERE meeting_id = 'meeting-test';
SELECT COUNT(*) as remaining_sections FROM minutes_sections WHERE minutes_id IN (SELECT minutes_id FROM minutes WHERE meeting_id = 'meeting-test');
SELECT COUNT(*) as remaining_agenda FROM agenda_sections WHERE meeting_id = 'meeting-test';
SELECT COUNT(*) as remaining_todos FROM todos WHERE meeting_id = 'meeting-test';

View File

@ -0,0 +1,238 @@
-- =====================================================
-- Meeting Test Data for AI Service Testing (UPDATED with user_id)
-- =====================================================
-- 1. meeting-test 데이터가 있는지 확인
SELECT * FROM meetings WHERE meeting_id = 'meeting-test';
-- 2. 기존 meeting-test 관련 데이터 정리 (있다면)
DELETE FROM minutes_sections WHERE minutes_id IN (SELECT minutes_id FROM minutes WHERE meeting_id = 'meeting-test');
DELETE FROM minutes WHERE meeting_id = 'meeting-test';
-- 3. 참석자별 회의록(minutes) 생성 - user1
INSERT INTO minutes (
minutes_id,
meeting_id,
user_id,
title,
status,
version,
created_by,
created_at,
updated_at
) VALUES (
'minutes-test-user1',
'meeting-test',
'user1',
'Q4 마케팅 전략 회의 - user1 작성',
'DRAFT',
1,
'user1',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 4. user1 회의록 섹션 - MEMO
INSERT INTO minutes_sections (
id,
section_id,
minutes_id,
type,
title,
content,
"order",
verified,
locked,
created_at,
updated_at
) VALUES
(
'section-user1-1',
'section-user1-1',
'minutes-test-user1',
'MEMO',
'회의 안건',
'1. Q4 마케팅 캠페인 기획
2.
3.
[ ]
. , 5 . 11 12 .
, . .
[ ]
1. +
2. : 5
3. : 11 ~ 12 ',
1,
false,
false,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 8. 참석자별 회의록(minutes) 생성 - user2
INSERT INTO minutes (
minutes_id,
meeting_id,
user_id,
title,
status,
version,
created_by,
created_at,
updated_at
) VALUES (
'minutes-test-user2',
'meeting-test',
'user2',
'Q4 마케팅 전략 회의 - user2 작성',
'DRAFT',
1,
'user2',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 9. user2 회의록 섹션 - MEMO
INSERT INTO minutes_sections (
id,
section_id,
minutes_id,
type,
title,
content,
"order",
verified,
locked,
created_at,
updated_at
) VALUES
(
'section-user2-1',
'section-user2-1',
'minutes-test-user2',
'MEMO',
'회의 목표',
'Q4 마케팅 전략 수립 및 예산 확정
[ ]
Q4 . . 20-30, 3, 2 .
, .
[ ]
1. ,
2. : 20-30
3. : 3, 2',
1,
false,
false,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 참석자별 회의록(minutes) 생성 - user3
INSERT INTO minutes (
minutes_id,
meeting_id,
user_id,
title,
status,
version,
created_by,
created_at,
updated_at
) VALUES (
'minutes-test-user3',
'meeting-test',
'user3',
'Q4 마케팅 전략 회의 - user3 작성',
'DRAFT',
1,
'user3',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- 14. user3 회의록 섹션 - MEMO
INSERT INTO minutes_sections (
id,
section_id,
minutes_id,
type,
title,
content,
"order",
verified,
locked,
created_at,
updated_at
) VALUES
(
'section-user3-1',
'section-user3-1',
'minutes-test-user3',
'MEMO',
'안건',
'1. Q4 마케팅 목표 설정
2.
3. KPI
[ ]
Q4 . 5 , , , KPI로 . ROI 150%
. KPI 11 .',
1,
false,
false,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
/*
-- 16. user3 회의록 섹션 - DECISION
INSERT INTO minutes_sections (
id,
section_id,
minutes_id,
type,
title,
content,
"order",
verified,
locked,
created_at,
updated_at
) VALUES
(
'section-user3-3',
'section-user3-3',
'minutes-test-user3',
'DECISION',
'결정 사항',
'1. 목표: 브랜드 인지도 상승 + 매출 증대\n2. 예산: 총 5천만원 (디지털 광고 + 인플루언서)\n3. KPI: 도달률, 참여율, 전환율, ROI 150% 이상',
3,
false,
false,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
*/
-- 18. 데이터 확인
SELECT 'Minutes 데이터:' AS info;
SELECT * FROM minutes WHERE meeting_id = 'meeting-test';
SELECT 'Minutes Sections 데이터:' AS info;
SELECT ms.section_id, ms.minutes_id, m.created_by, m.user_id, ms.type, ms.title,
LENGTH(ms.content) as content_length, ms."order"
FROM minutes_sections ms
JOIN minutes m ON ms.minutes_id = m.minutes_id
WHERE m.meeting_id = 'meeting-test'
ORDER BY m.created_by, ms."order";