diff --git a/ai-python/app/config.py b/ai-python/app/config.py index b7224dd..4648e55 100644 --- a/ai-python/app/config.py +++ b/ai-python/app/config.py @@ -15,7 +15,7 @@ class Settings(BaseSettings): # Claude API claude_api_key: str = "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" claude_model: str = "claude-sonnet-4-5-20250929" - claude_max_tokens: int = 8192 # 4096 → 8192 증가 (더 많은 제안사항 생성 가능) + claude_max_tokens: int = 25000 # 회의록 통합을 위해 25000으로 증가 claude_temperature: float = 0.7 # Redis diff --git a/ai-python/app/prompts/consolidate_prompt.py b/ai-python/app/prompts/consolidate_prompt.py index c0e9529..07009c3 100644 --- a/ai-python/app/prompts/consolidate_prompt.py +++ b/ai-python/app/prompts/consolidate_prompt.py @@ -19,7 +19,9 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s f"{i+1}. {agenda}" for i, agenda in enumerate(agendas) ]) - prompt = f"""당신은 회의록 작성 전문가입니다. 여러 참석자가 작성한 회의록을 통합하여 정확하고 체계적인 회의록을 생성해주세요. + prompt = f"""당신은 회의록 작성 전문가이며 JSON 생성 전문가입니다. 여러 참석자가 작성한 회의록을 통합하여 정확하고 체계적인 회의록을 생성해주세요. + +**매우 중요**: 응답은 반드시 유효한 JSON 형식이어야 합니다. 문자열 내의 모든 특수문자를 올바르게 이스케이프해야 합니다. # 입력 데이터 @@ -40,7 +42,11 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s 3. **회의 전체 결정사항 (decisions)**: - 회의 전체에서 최종 결정된 사항들을 TEXT 형식으로 정리 - 안건별 결정사항을 모두 포함하여 회의록 수정 페이지에서 사용자가 확인 및 수정할 수 있도록 작성 - - 형식: "**안건1 결정사항:**\n- 결정1\n- 결정2\n\n**안건2 결정사항:**\n- 결정3" + - 형식: "안건1 결정사항:\n- 결정1\n- 결정2\n\n안건2 결정사항:\n- 결정3" + - **JSON 이스케이프 필수**: + * 큰따옴표(")는 반드시 제거하거나 작은따옴표(')로 대체 + * 줄바꿈은 \\n으로 이스케이프 + * 역슬래시(\\)는 \\\\로 이스케이프 4. **안건별 요약 (agenda_summaries)**: 회의 내용을 분석하여 안건별로 구조화: @@ -51,8 +57,9 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s - **summary_short**: AI가 생성한 1줄 요약 (20자 이내, 사용자 수정 불가) - **summary**: 안건별 회의록 요약 (논의사항과 결정사항 모두 포함) * 회의록 수정 페이지에서 사용자가 수정할 수 있는 입력 필드 - * 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2" + * 형식: "논의 사항:\n- 논의내용1\n- 논의내용2\n\n결정 사항:\n- 결정1\n- 결정2" * 사용자가 자유롭게 편집할 수 있도록 구조화된 텍스트로 작성 + * **JSON 이스케이프 필수**: 큰따옴표(")는 제거하거나 작은따옴표(')로 대체, 줄바꿈은 \\n으로 표현 - **decisions**: 안건별 결정사항 배열 (대시보드 표시용, summary의 결정사항 부분을 배열로 추출) * 형식: ["결정사항1", "결정사항2", "결정사항3"] * 회의에서 최종 결정된 사항만 포함 @@ -71,22 +78,28 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s # 출력 형식 -반드시 아래 JSON 형식으로만 응답하세요. 다른 텍스트는 포함하지 마세요. +**매우 중요 - JSON 생성 규칙**: +1. JSON 외의 다른 텍스트는 절대 포함하지 마세요 +2. 문자열 내부의 큰따옴표(")는 반드시 작은따옴표(')로 대체하세요 +3. 문자열 내부의 줄바꿈은 반드시 \\n으로 이스케이프하세요 +4. 역슬래시(\\)는 \\\\로 이스케이프하세요 +5. 모든 문자열이 끝까지 올바르게 닫혀 있는지 확인하세요 +6. 각 문자열 값이 유효한 JSON 문자열인지 검증하세요 ```json {{ "keywords": ["키워드1", "키워드2", "키워드3"], "statistics": {{ - "agendas_count": 숫자, - "todos_count": 숫자 + "agendas_count": 2, + "todos_count": 3 }}, - "decisions": "**안건1 결정사항:**\\n- 결정1\\n- 결정2\\n\\n**안건2 결정사항:**\\n- 결정3", + "decisions": "안건1 결정사항:\\n- 결정1\\n- 결정2\\n\\n안건2 결정사항:\\n- 결정3", "agenda_summaries": [ {{ "agenda_number": 1, "agenda_title": "안건 제목", - "summary_short": "짧은 요약 (20자 이내)", - "summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2", + "summary_short": "짧은 요약", + "summary": "논의 사항:\\n- 논의내용1\\n- 논의내용2\\n\\n결정 사항:\\n- 결정1\\n- 결정2", "decisions": ["결정사항1", "결정사항2"], "pending": ["보류사항"], "todos": [ @@ -113,20 +126,27 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s 3. **완전성**: 모든 필드를 빠짐없이 작성 4. **구조화**: 안건별로 명확히 분리 5. **결정사항 추출**: - - 회의 전체 결정사항(decisions): 모든 안건의 결정사항을 포함 (TEXT 형식) + - 회의 전체 결정사항(decisions): 모든 안건의 결정사항을 포함 (TEXT 형식, \\n 이스케이프 필수) - 안건별 결정사항(agenda_summaries[].decisions): 각 안건의 결정사항을 배열로 추출 - 결정사항이 명확하게 언급된 경우에만 포함 6. **summary 작성**: - - summary_short: AI가 자동 생성한 1줄 요약 (사용자 수정 불가) - - summary: 논의사항과 결정사항 모두 포함 (사용자 수정 가능) + - summary_short: AI가 자동 생성한 1줄 요약 (사용자 수정 불가, 특수문자 이스케이프) + - summary: 논의사항과 결정사항 모두 포함 (사용자 수정 가능, \\n 이스케이프 필수) - decisions: summary의 결정사항 부분을 배열로 별도 추출 (대시보드 표시용) 7. **Todo 추출**: - 제목 필수, 담당자는 언급된 경우에만 추출 - 자연스러운 표현에서 추출: "김대리가 ~하기로 함" → title: "~", assignee: "김대리" - 담당자가 없으면 assignee: "" (빈 문자열) -8. **JSON만 출력**: 추가 설명 없이 JSON만 반환 +8. **JSON 형식 엄수 - 가장 중요**: + - 추가 설명, 주석, 서문 없이 JSON만 반환 + - 문자열 내 큰따옴표(")는 작은따옴표(')로 대체 + - 문자열 내 줄바꿈은 \\n으로 이스케이프 + - 역슬래시는 \\\\로 이스케이프 + - 모든 문자열을 올바르게 닫기 + - 생성 후 유효한 JSON인지 자체 검증 -이제 위 회의록들을 분석하여 통합 요약을 JSON 형식으로 생성해주세요. +**최종 지시**: 위 회의록들을 분석하여 **유효한 JSON 형식으로만** 통합 요약을 생성해주세요. +JSON 파싱 오류가 발생하지 않도록 모든 특수문자를 올바르게 이스케이프하세요. """ return prompt diff --git a/ai-python/app/services/claude_service.py b/ai-python/app/services/claude_service.py index 00a2802..0cf2276 100644 --- a/ai-python/app/services/claude_service.py +++ b/ai-python/app/services/claude_service.py @@ -43,7 +43,7 @@ class ClaudeService: ] # API 호출 - logger.info(f"Claude API 호출 시작 - Model: {self.model}") + logger.info(f"Claude API 호출 시작 - Model: {self.model}, Max Tokens: {self.max_tokens}") if system_prompt: response = self.client.messages.create( @@ -63,7 +63,12 @@ class ClaudeService: # 응답 텍스트 추출 response_text = response.content[0].text - logger.info(f"Claude API 응답 수신 완료 - Tokens: {response.usage.input_tokens + response.usage.output_tokens}") + logger.info( + f"Claude API 응답 수신 완료 - " + f"Input Tokens: {response.usage.input_tokens}, " + f"Output Tokens: {response.usage.output_tokens}, " + f"Stop Reason: {response.stop_reason}" + ) # JSON 파싱 # ```json ... ``` 블록 제거 @@ -72,13 +77,113 @@ class ClaudeService: elif "```" in response_text: response_text = response_text.split("```")[1].split("```")[0].strip() + # JSON 파싱 전 전처리: 제어 문자 및 문제 문자 정리 + import re + # 탭 문자를 공백으로 변환 + response_text = response_text.replace('\t', ' ') + # 연속된 공백을 하나로 축소 (JSON 문자열 내부는 제외) + # response_text = re.sub(r'\s+', ' ', response_text) + result = json.loads(response_text) return result except json.JSONDecodeError as e: logger.error(f"JSON 파싱 실패: {e}") - logger.error(f"응답 텍스트: {response_text[:500]}...") + logger.error(f"응답 텍스트 전체 길이: {len(response_text)}") + logger.error(f"응답 텍스트 (처음 1000자): {response_text[:1000]}") + logger.error(f"응답 텍스트 (마지막 1000자): {response_text[-1000:]}") + + # 전체 응답을 파일로 저장하여 디버깅 + import os + from datetime import datetime + debug_dir = "/Users/jominseo/HGZero/ai-python/logs/debug" + os.makedirs(debug_dir, exist_ok=True) + debug_file = f"{debug_dir}/claude_response_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + with open(debug_file, 'w', encoding='utf-8') as f: + f.write(response_text) + logger.error(f"전체 응답을 파일로 저장: {debug_file}") + + # JSON 파싱 재시도 전략 + + # 방법 1: 이스케이프되지 않은 개행 문자 처리 + try: + import re + # JSON 문자열 내부의 이스케이프되지 않은 개행을 찾아 \\n으로 변환 + # 패턴: 큰따옴표 내부에서 "\"로 시작하지 않는 개행 + def fix_unescaped_newlines(text): + # 간단한 접근: 모든 실제 개행을 \\n으로 변환 + # 단, JSON 구조의 개행 (객체/배열 사이)은 유지 + in_string = False + escape_next = False + result = [] + + for char in text: + if escape_next: + result.append(char) + escape_next = False + continue + + if char == '\\': + escape_next = True + result.append(char) + continue + + if char == '"': + in_string = not in_string + result.append(char) + continue + + if char == '\n': + if in_string: + # 문자열 내부의 개행은 \\n으로 변환 + result.append('\\n') + else: + # JSON 구조의 개행은 유지 + result.append(char) + else: + result.append(char) + + return ''.join(result) + + fixed_text = fix_unescaped_newlines(response_text) + result = json.loads(fixed_text) + logger.info("JSON 파싱 재시도 성공 (개행 문자 수정)") + return result + except Exception as e1: + logger.warning(f"개행 문자 수정 실패: {e1}") + + # 방법 2: strict=False 옵션으로 파싱 + try: + result = json.loads(response_text, strict=False) + logger.info("JSON 파싱 재시도 성공 (strict=False)") + return result + except Exception as e2: + logger.warning(f"strict=False 파싱 실패: {e2}") + + # 방법 3: 마지막 닫는 괄호까지만 파싱 시도 + try: + last_brace_idx = response_text.rfind('}') + if last_brace_idx > 0: + truncated_text = response_text[:last_brace_idx+1] + # 개행 수정도 적용 + truncated_text = fix_unescaped_newlines(truncated_text) + result = json.loads(truncated_text, strict=False) + logger.info("JSON 파싱 재시도 성공 (잘린 부분 복구)") + return result + except Exception as e3: + logger.warning(f"잘린 부분 복구 실패: {e3}") + + # 방법 4: 정규식으로 문제 문자 제거 후 재시도 + try: + # 제어 문자 제거 (줄바꿈, 탭 제외) + cleaned = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', response_text) + result = json.loads(cleaned, strict=False) + logger.info("JSON 파싱 재시도 성공 (제어 문자 제거)") + return result + except Exception as e4: + logger.warning(f"제어 문자 제거 실패: {e4}") + raise ValueError(f"Claude API 응답을 JSON으로 파싱할 수 없습니다: {str(e)}") except Exception as e: diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/EndMeetingService.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/EndMeetingService.java index 47b968e..ab0839c 100644 --- a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/EndMeetingService.java +++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/EndMeetingService.java @@ -31,6 +31,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -106,13 +107,19 @@ public class EndMeetingService implements EndMeetingUseCase { ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request); log.info("AI Service 호출 완료 - 안건 수: {}", aiResponse.getAgendaSummaries().size()); - // 6. AI 분석 결과 저장 - MeetingAnalysis analysis = saveAnalysisResult(meeting, aiResponse); + // 6. 통합 회의록 생성 또는 조회 + MinutesEntity consolidatedMinutes = getOrCreateConsolidatedMinutes(meeting); - // 7. Todo 생성 및 저장 + // 7. 통합 회의록에 전체 결정사항 저장 + saveConsolidatedDecisions(consolidatedMinutes, aiResponse.getDecisions()); + + // 8. AI 분석 결과 저장 + MeetingAnalysis analysis = saveAnalysisResult(meeting, consolidatedMinutes, aiResponse); + + // 9. Todo 생성 및 저장 List todos = createAndSaveTodos(meeting, aiResponse, analysis); - // 8. 회의 종료 처리 + // 10. 회의 종료 처리 meeting.end(); meetingRepository.save(meeting); log.info("회의 상태 업데이트 완료 - status: {}", meeting.getStatus()); @@ -164,10 +171,54 @@ public class EndMeetingService implements EndMeetingUseCase { .build(); } + /** + * 통합 회의록 생성 또는 조회 + * userId가 NULL인 회의록 = AI 통합 회의록 + */ + private MinutesEntity getOrCreateConsolidatedMinutes(MeetingEntity meeting) { + // userId가 NULL인 회의록 찾기 (AI 통합 회의록) + Optional existing = minutesRepository + .findByMeetingIdAndUserIdIsNull(meeting.getMeetingId()); + + if (existing.isPresent()) { + log.debug("기존 통합 회의록 사용 - minutesId: {}", existing.get().getMinutesId()); + return existing.get(); + } + + // 없으면 새로 생성 + MinutesEntity consolidatedMinutes = MinutesEntity.builder() + .minutesId(UUID.randomUUID().toString()) + .meetingId(meeting.getMeetingId()) + .userId(null) // NULL = AI 통합 회의록 + .title(meeting.getTitle() + " - AI 통합 회의록") + .status("FINALIZED") + .version(1) + .createdBy("AI") + .build(); + + MinutesEntity saved = minutesRepository.save(consolidatedMinutes); + log.info("통합 회의록 생성 완료 - minutesId: {}", saved.getMinutesId()); + return saved; + } + + /** + * 통합 회의록에 전체 결정사항 저장 + */ + private void saveConsolidatedDecisions(MinutesEntity minutes, String decisions) { + if (decisions != null && !decisions.trim().isEmpty()) { + minutes.updateDecisions(decisions); + minutesRepository.save(minutes); + log.info("Minutes에 전체 결정사항 저장 완료 - minutesId: {}, 길이: {}", + minutes.getMinutesId(), decisions.length()); + } else { + log.warn("저장할 결정사항이 없음 - minutesId: {}", minutes.getMinutesId()); + } + } + /** * AI 분석 결과 저장 */ - private MeetingAnalysis saveAnalysisResult(MeetingEntity meeting, ConsolidateResponse aiResponse) { + private MeetingAnalysis saveAnalysisResult(MeetingEntity meeting, MinutesEntity consolidatedMinutes, ConsolidateResponse aiResponse) { // AgendaAnalysis 리스트 생성 List agendaAnalyses = aiResponse.getAgendaSummaries().stream() .map(summary -> MeetingAnalysis.AgendaAnalysis.builder() @@ -203,7 +254,7 @@ public class EndMeetingService implements EndMeetingUseCase { analysisRepository.save(entity); // AgendaSection 저장 (안건별 회의록) - saveAgendaSections(meeting.getMeetingId(), aiResponse); + saveAgendaSections(meeting.getMeetingId(), consolidatedMinutes.getMinutesId(), aiResponse); log.info("AI 분석 결과 저장 완료 - analysisId: {}", analysis.getAnalysisId()); @@ -213,7 +264,7 @@ public class EndMeetingService implements EndMeetingUseCase { /** * AgendaSection 저장 */ - private void saveAgendaSections(String meetingId, ConsolidateResponse aiResponse) { + private void saveAgendaSections(String meetingId, String minutesId, ConsolidateResponse aiResponse) { int agendaNumber = 1; for (var summary : aiResponse.getAgendaSummaries()) { @@ -229,7 +280,7 @@ public class EndMeetingService implements EndMeetingUseCase { AgendaSectionEntity agendaSection = AgendaSectionEntity.builder() .id(UUID.randomUUID().toString()) - .minutesId(meetingId) // AI 통합 회의록 ID로 사용 + .minutesId(minutesId) // 통합 회의록 ID .meetingId(meetingId) .agendaNumber(agendaNumber++) .agendaTitle(summary.getAgendaTitle()) @@ -247,7 +298,8 @@ public class EndMeetingService implements EndMeetingUseCase { } } - log.info("AgendaSection 저장 완료 - meetingId: {}, count: {}", meetingId, aiResponse.getAgendaSummaries().size()); + log.info("AgendaSection 저장 완료 - meetingId: {}, minutesId: {}, count: {}", + meetingId, minutesId, aiResponse.getAgendaSummaries().size()); } /** diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MinutesEntity.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MinutesEntity.java index ee61ac5..394d937 100644 --- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MinutesEntity.java +++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MinutesEntity.java @@ -57,6 +57,13 @@ public class MinutesEntity extends BaseTimeEntity { @Column(name = "finalized_at") private LocalDateTime finalizedAt; + /** + * 결정사항 업데이트 + */ + public void updateDecisions(String decisions) { + this.decisions = decisions; + } + public Minutes toDomain() { return Minutes.builder() .minutesId(this.minutesId) diff --git a/meeting/src/main/resources/application.yml b/meeting/src/main/resources/application.yml index 660a68a..be778ce 100644 --- a/meeting/src/main/resources/application.yml +++ b/meeting/src/main/resources/application.yml @@ -156,4 +156,4 @@ azure: ai: service: url: ${AI_SERVICE_URL:http://localhost:8087} - timeout: ${AI_SERVICE_TIMEOUT:30000} + timeout: ${AI_SERVICE_TIMEOUT:60000} diff --git a/meeting/src/test/resources/test-data-ai-minutes.sql b/meeting/src/test/resources/test-data-ai-minutes.sql new file mode 100644 index 0000000..a788c2c --- /dev/null +++ b/meeting/src/test/resources/test-data-ai-minutes.sql @@ -0,0 +1,274 @@ +-- ======================================== +-- AI 회의록 요약 테스트 데이터 +-- ======================================== +-- 목적: Minutes.decisions 및 AgendaSection 저장 검증 +-- 회의: 2025년 신제품 런칭 전략 회의 +-- 참석자: 3명 (마케팅팀장, 개발팀장, 디자인팀장) +-- 안건: 3개 (타겟 고객 설정, 핵심 기능 정의, 런칭 일정) + +-- ======================================== +-- 1. 회의 생성 +-- ======================================== +INSERT INTO meetings ( + meeting_id, + title, + purpose, + description, + location, + scheduled_at, + end_time, + started_at, + ended_at, + status, + organizer_id, + template_id, + created_at, + updated_at +) VALUES ( + 'ai_test_meeting', + '2025년 신제품 런칭 전략 회의', + '신제품 출시를 위한 전략 수립 및 일정 협의', + '타겟 고객층 정의, 핵심 기능 결정, 출시 일정 확정', + '본사 4층 회의실', + '2025-01-15 14:00:00', + '2025-01-15 16:00:00', + '2025-01-15 14:05:00', + NULL, -- 아직 종료 안됨 + 'IN_PROGRESS', + 'user_organizer', + 'template_general', + NOW(), + NOW() +); + +-- ======================================== +-- 2. 참석자별 회의록 생성 (user_id NOT NULL) +-- ======================================== + +-- 2-1. 마케팅팀장 회의록 +INSERT INTO minutes ( + minutes_id, + meeting_id, + user_id, + title, + status, + version, + created_by, + created_at, + updated_at +) VALUES ( + 'ai_test_minutes_marketing', + 'ai_test_meeting', + 'user_marketing', + '2025년 신제품 런칭 전략 회의 - 마케팅팀장', + 'DRAFT', + 1, + 'user_marketing_head', + NOW(), + NOW() +); + +-- 2-2. 개발팀장 회의록 +INSERT INTO minutes ( + minutes_id, + meeting_id, + user_id, + title, + status, + version, + created_by, + created_at, + updated_at +) VALUES ( + 'ai_test_minutes_dev', + 'ai_test_meeting', + 'user_dev', + '2025년 신제품 런칭 전략 회의 - 개발팀장', + 'DRAFT', + 1, + 'user_dev', + NOW(), + NOW() +); + +-- 2-3. 디자인팀장 회의록 +INSERT INTO minutes ( + minutes_id, + meeting_id, + user_id, + title, + status, + version, + created_by, + created_at, + updated_at +) VALUES ( + 'ai_test_minutes_design', + 'ai_test_meeting', + 'user_design', + '2025년 신제품 런칭 전략 회의 - 디자인팀장', + 'DRAFT', + 1, + 'user_design', + NOW(), + NOW() +); + +-- ======================================== +-- 3. 마케팅팀장 회의록 섹션 (MEMO 타입) +-- ======================================== + +INSERT INTO minutes_sections ( + id, + minutes_id, + type, + title, + content, + "order", + verified, + locked, + locked_by +) VALUES ( + 'ai_test_section_marketing', + 'ai_test_minutes_marketing', + 'MEMO', + '마케팅팀장 메모', + '【안건 1: 타겟 고객 설정】 +- 주요 타겟: 20-30대 직장인, 특히 재택근무자와 1인 가구 +- 김마케팅팀장 의견: 최근 시장조사 결과 20대 후반~30대 초반 직장인들의 스마트홈 제품 관심도가 가장 높았습니다 +- 부타겟: 40대 맞벌이 부부 +- 결정사항: 1차 타겟을 20-30대 직장인으로 확정, SNS 마케팅 집중 + +【안건 2: 핵심 기능 정의】 +- 음성인식 AI 비서 기능은 필수로 포함하기로 결정 +- 김마케팅팀장: 경쟁사 제품 대비 차별화를 위해 멀티 디바이스 연동 기능이 핵심입니다 +- 스마트폰 앱 연동은 1차 출시에 포함 +- 보류: IoT 센서 연동은 2차 업데이트로 연기 (기술 검증 필요) +- 담당: 이개발팀장이 AI 음성인식 기술 파트너 컨택 예정 + +【안건 3: 런칭 일정】 +- 목표 출시일: 2025년 6월 1일 +- 마케팅 캠페인은 5월 초부터 티저 광고 시작 +- 김마케팅팀장: 5월 첫째 주에 인플루언서 협업 콘텐츠 제작 필요 +- 결정: 박디자인팀장이 3월 말까지 프로토타입 디자인 완료 +- 할일: 김마케팅팀장이 2월 중 타겟 인플루언서 리스트 작성', + 1, + FALSE, + FALSE, + NULL +); + +-- ======================================== +-- 4. 개발팀장 회의록 섹션 (MEMO 타입) +-- ======================================== + +INSERT INTO minutes_sections ( + id, + minutes_id, + type, + title, + content, + "order", + verified, + locked, + locked_by +) VALUES ( + 'ai_test_section_dev', + 'ai_test_minutes_dev', + 'MEMO', + '개발팀장 메모', + '【안건 1: 타겟 고객 설정】 +- 이개발팀장: 타겟 연령층이 20-30대라면 모바일 앱 UX가 정말 중요합니다 +- 20-30대 직장인 타겟 동의 +- 기술적으로 이 연령층은 새로운 기술 수용도가 높아서 AI 기능 적극 활용 예상 + +【안건 2: 핵심 기능 정의】 +- 음성인식 AI: OpenAI Whisper 또는 Google Speech API 검토 중 +- 이개발팀장: 멀티 디바이스 연동을 위해서는 클라우드 기반 아키텍처 필수입니다 +- 결정: 음성인식 AI는 한국어 특화 모델 사용하기로 확정 +- 보류사항: IoT 센서는 기술 검증 후 2차 업데이트 (전력 소비 이슈) +- 할일: 이개발팀장이 1월 말까지 AI 음성인식 기술 파트너 미팅 잡기 +- 할일: 최개발자가 2월 중 클라우드 인프라 설계서 작성 + +【안건 3: 런칭 일정】 +- 6월 1일 출시 목표는 타이트하지만 가능할 것으로 판단 +- 개발 일정: 2월 프로토타입, 3월 알파, 4월 베타, 5월 최종 QA +- 이개발팀장: 베타 테스트는 최소 3주 필요, 5월 초에 시작해야 합니다 +- 결정: 개발팀은 4월 중순까지 베타 버전 완성 +- 보류: 다국어 지원은 일단 한국어만, 영어는 추후 검토', + 1, + FALSE, + FALSE, + NULL +); + +-- ======================================== +-- 5. 디자인팀장 회의록 섹션 (MEMO 타입) +-- ======================================== + +INSERT INTO minutes_sections ( + id, + minutes_id, + type, + title, + content, + "order", + verified, + locked, + locked_by +) VALUES ( + 'ai_test_section_design', + 'ai_test_minutes_design', + 'MEMO', + '디자인팀장 메모', + '【안건 1: 타겟 고객 설정】 +- 20-30대 타겟에 동의, 디자인 방향도 MZ세대 취향 고려 필요 +- 박디자인팀장: "미니멀하고 감각적인 디자인으로 차별화해야 합니다" +- 레퍼런스: 애플 HomePod, 구글 Nest Hub +- 색상은 화이트, 그레이, 블랙 3종으로 결정 + +【안건 2: 핵심 기능 정의】 +- 음성인식 피드백 UI가 중요 - LED 링 또는 디스플레이 활용 +- 박디자인팀장: "사용자가 AI가 듣고 있다는 걸 직관적으로 알 수 있어야 합니다" +- 멀티 디바이스 연동 시 화면 전환 애니메이션 필요 +- 결정: 터치 인터페이스와 음성 인터페이스 병행 +- 할일: 김디자이너가 2월 말까지 UI/UX 목업 완성 + +【안건 3: 런칭 일정】 +- 6월 출시 위해 3월 말 프로토타입 디자인 완료 확약 +- 디자인 일정: 2월 컨셉 확정, 3월 프로토타입, 4월 최종 디자인 +- 박디자인팀장: "패키지 디자인도 프리미엄 느낌으로 가야 합니다" +- 결정: 제품 패키지는 친환경 소재 사용 +- 할일: 박디자인팀장이 3월 말까지 프로토타입 디자인 완료 +- 할일: 박디자인팀장이 4월 중 패키지 디자인 최종안 제출', + 1, + FALSE, + FALSE, + NULL +); + +-- ======================================== +-- 검증 쿼리 +-- ======================================== + +-- 회의 확인 +-- SELECT * FROM meetings WHERE meeting_id = 'ai_test_meeting_001'; + +-- 참석자별 회의록 확인 +-- SELECT minutes_id, user_id, title FROM minutes WHERE meeting_id = 'ai_test_meeting_001'; + +-- 회의록 섹션 확인 +-- SELECT id, minutes_id, title, LEFT(content, 100) as content_preview +-- FROM minutes_sections +-- WHERE minutes_id LIKE 'ai_test_minutes_%'; + +-- ======================================== +-- 테스트 실행 가이드 +-- ======================================== +-- 1. 이 SQL 실행하여 테스트 데이터 생성 +-- 2. POST /api/meetings/ai_test_meeting_001/end 호출 +-- 3. 검증: +-- - minutes 테이블에 userId=NULL인 통합 회의록 생성 확인 +-- - minutes.decisions 필드에 전체 결정사항 저장 확인 +-- - agenda_sections 테이블에 3개 안건 저장 확인 +-- - agenda_sections.summary에 논의+결정 내용 저장 확인