diff --git a/package-lock.json b/package-lock.json
index 7d3dd3c..bc3952a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-table": "^8.21.3",
- "axios": "^1.11.0",
+ "axios": "^1.12.1",
"dayjs": "^1.11.18",
"react": "^18.3.1",
"react-big-calendar": "^1.19.4",
@@ -2448,9 +2448,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz",
+ "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -4960,20 +4960,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yaml": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
- "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 8fd8bd9..4c8bdc6 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-table": "^8.21.3",
- "axios": "^1.11.0",
+ "axios": "^1.12.1",
"dayjs": "^1.11.18",
"react": "^18.3.1",
"react-big-calendar": "^1.19.4",
diff --git a/src/api/meetingAPI.js b/src/api/meetingAPI.js
index 5d943c3..ef0bd94 100644
--- a/src/api/meetingAPI.js
+++ b/src/api/meetingAPI.js
@@ -1,7 +1,7 @@
import api from './client';
// 회의록 생성
-export const createMeeting = async (projectId, meetingData) => {
+export const createMeeting = async (projectId, meetingData, audioFile = null) => {
try {
// FormData 생성 - meetingData를 JSON 문자열로 전송
const formData = new FormData();
@@ -14,6 +14,11 @@ export const createMeeting = async (projectId, meetingData) => {
participantIds: meetingData.participantIds
}));
+ // 오디오 파일이 있으면 추가
+ if (audioFile) {
+ formData.append('audio', audioFile, 'recording.wav');
+ }
+
const response = await api.post(`/meeting/${projectId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
@@ -54,7 +59,7 @@ export const getMeetingDetail = async (projectId, meetingId) => {
};
// 회의록 수정
-export const updateMeeting = async (projectId, meetingId, meetingData) => {
+export const updateMeeting = async (projectId, meetingId, meetingData, audioFile = null) => {
try {
// FormData 생성 - meetingData를 JSON 문자열로 전송
const formData = new FormData();
@@ -67,6 +72,11 @@ export const updateMeeting = async (projectId, meetingId, meetingData) => {
participantIds: meetingData.participantIds
}));
+ // 오디오 파일이 있으면 추가
+ if (audioFile) {
+ formData.append('audio', audioFile, 'recording.wav');
+ }
+
const response = await api.put(`/meeting/${projectId}/${meetingId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
@@ -89,3 +99,109 @@ export const deleteMeeting = async (projectId, meetingId) => {
throw error;
}
};
+
+// 파일 다운로드
+export const downloadMeetingReport = async (projectId, meetingId) => {
+ try {
+ const response = await api.get(`/meeting/${projectId}/${meetingId}/report/download`, {
+ responseType: 'blob',
+ });
+
+ // Content-Disposition 헤더에서 파일명 추출
+ const contentDisposition = response.headers['content-disposition'];
+ let fileName = 'meeting_report.docx'; // 기본 파일명
+
+ if (contentDisposition) {
+ console.log('Content-Disposition:', contentDisposition); // 디버깅용
+
+ // 다양한 Content-Disposition 형식 지원
+ let fileNameMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
+
+ if (fileNameMatch && fileNameMatch[1]) {
+ // RFC 5987 형식 (filename*=UTF-8''encoded-filename)
+ try {
+ fileName = decodeURIComponent(fileNameMatch[1]);
+ } catch {
+ // e 변수 제거
+ console.warn('UTF-8 디코딩 실패, 원본 사용:', fileNameMatch[1]);
+ fileName = fileNameMatch[1];
+ }
+ } else {
+ // 일반 형식 (filename="filename")
+ fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
+ if (fileNameMatch && fileNameMatch[1]) {
+ fileName = fileNameMatch[1];
+ }
+ }
+ }
+
+ console.log('추출된 파일명:', fileName); // 디버깅용
+
+ // Blob 객체 생성
+ const blob = new Blob([response.data], {
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ });
+
+ // 파일 다운로드 실행
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+
+ // 정리
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ return { success: true, fileName };
+ } catch (error) {
+ console.error('회의록 다운로드 실패:', error);
+ throw error;
+ }
+};
+
+
+// 이메일 전송
+export const sendMeetingEmail = async (projectId, meetingId, emailData, attachments = []) => {
+ try {
+ const formData = new FormData();
+
+ // emailData를 JSON 문자열로 변환하여 추가
+ formData.append('emailData', JSON.stringify({
+ to: emailData.to,
+ subject: emailData.subject,
+ content: emailData.content
+ }));
+
+ // 첨부파일 추가 - index 매개변수 제거
+ if (attachments && attachments.length > 0) {
+ attachments.forEach((file) => {
+ formData.append('attachments', file);
+ });
+ }
+
+ const response = await api.post(`/meeting/${projectId}/${meetingId}/email`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return response.data;
+ } catch (error) {
+ console.error('이메일 전송 실패:', error);
+ throw error;
+ }
+};
+
+
+// 회의록 작성 프로젝트 참여 인원 호출
+export const getProjectParticipants = async (projectId) => {
+ try {
+ const response = await api.get(`/meeting/${projectId}/participant`);
+ return response.data;
+ } catch (error) {
+ console.error('프로젝트 팀 인원 조회 실패:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/components/common/AISummaryPanel.jsx b/src/components/common/AISummaryPanel.jsx
index 813e5b0..511c8d7 100644
--- a/src/components/common/AISummaryPanel.jsx
+++ b/src/components/common/AISummaryPanel.jsx
@@ -5,22 +5,24 @@ function AISummaryPanel({
title = "AI 정리",
className = "bg-gray-100 rounded-lg p-6 border border-gray-200",
}) {
- const defaultSummary = `임시 예시 텍스트:
-
-📝 주요 안건
-• 고객 마케팅을 위한 앱 개선 방안 논의
-• 신규 기능 개발 일정 조율
-• 팀 간 협업 프로세스 개선
-
-✅ 결정 사항
-• UI/UX 개선안 3월 말까지 완료
-• 백엔드 API 연동 4월 초 시작
-• 주간 스탠드업 미팅 화요일 오전 10시로 변경
+ // summary가 없거나 빈 객체인 경우 기본값 설정
+ if (!summary || typeof summary !== 'object') {
+ return (
+
+
+
+
{title}
+
+
+ AI 요약 정보가 없습니다.
+
+
+
+ );
+ }
-📋 액션 아이템
-• 박서호: 프론트엔드 컴포넌트 설계 (3/25까지)
-• 이지민: 디자인 시스템 업데이트 (3/28까지)
-• 최우식: 요구사항 문서 작성 (3/30까지)`;
+ // 서버에서 받은 데이터 구조에 맞게 처리
+ const { mainTopics = [], decisions = [], priorities = [], recommends = [] } = summary;
return (
@@ -29,14 +31,88 @@ function AISummaryPanel({
{title}
-
-
- {summary || defaultSummary}
-
+
+ {/* 주요 안건 */}
+ {mainTopics && mainTopics.length > 0 && (
+
+
+ 📝 주요 안건
+
+
+ {mainTopics.map((topic, index) => (
+
+
+ {topic}
+
+ ))}
+
+
+ )}
+
+ {/* 결정 사항 */}
+ {decisions && decisions.length > 0 && (
+
+
+ ✅ 결정 사항
+
+
+ {decisions.map((decision, index) => (
+
+
+ {decision}
+
+ ))}
+
+
+ )}
+
+ {/* 액션 아이템 */}
+ {priorities && priorities.length > 0 && (
+
+
+ 📋 우선 사항
+
+
+ {priorities.map((priority, index) => (
+
+
+ {priority}
+
+ ))}
+
+
+ )}
+
+ {/* 추천 아이템 */}
+ {recommends && recommends.length > 0 && (
+
+
+ 👍 추천 업무
+
+
+ {recommends.map((recommand, index) => (
+
+
+ {recommand}
+
+ ))}
+
+
+ )}
+
+ {/* 모든 데이터가 없는 경우 */}
+ {(!mainTopics || mainTopics.length === 0) &&
+ (!decisions || decisions.length === 0) &&
+ (!priorities || priorities.length === 0) &&
+ (!recommends || recommends.length === 0) && (
+
+ AI 요약 정보가 없습니다.
+
+ )}
);
}
-export default AISummaryPanel;
+export default AISummaryPanel;
\ No newline at end of file
diff --git a/src/components/meeting/EmailSendModal.jsx b/src/components/meeting/EmailSendModal.jsx
new file mode 100644
index 0000000..6acf163
--- /dev/null
+++ b/src/components/meeting/EmailSendModal.jsx
@@ -0,0 +1,235 @@
+// src/components/meeting/EmailSendModal.jsx (새 파일 생성)
+import { useState } from 'react';
+import { Close, AttachFile, Send } from '@mui/icons-material';
+import { sendMeetingEmail } from '../../api/meetingAPI';
+import { useToast } from '../../hooks/useToast';
+
+function EmailSendModal({ isOpen, onClose, projectId, meetingId, meetingTitle }) {
+ const { showSuccess, showError } = useToast();
+
+ const [formData, setFormData] = useState({
+ to: '',
+ subject: `회의록: ${meetingTitle || '제목 없음'}`,
+ content: ''
+ });
+ const [attachments, setAttachments] = useState([]);
+ const [isSending, setIsSending] = useState(false);
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleFileChange = (e) => {
+ const files = Array.from(e.target.files);
+ setAttachments(prev => [...prev, ...files]);
+ };
+
+ const removeAttachment = (index) => {
+ setAttachments(prev => prev.filter((_, i) => i !== index));
+ };
+
+ // src/components/meeting/EmailSendModal.jsx의 handleSubmit 함수 수정
+const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.to.trim()) {
+ showError('수신자 이메일 주소를 입력해주세요.');
+ return;
+ }
+
+ if (!formData.subject.trim()) {
+ showError('이메일 제목을 입력해주세요.');
+ return;
+ }
+
+ if (!formData.content.trim()) {
+ showError('이메일 내용을 입력해주세요.');
+ return;
+ }
+
+ // 이메일 주소 유효성 검사 개선
+ const toEmails = formData.to.split(',').map(email => email.trim()).filter(email => email);
+
+ // 각 이메일 주소 유효성 검사
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const invalidEmails = toEmails.filter(email => !emailRegex.test(email));
+
+ if (invalidEmails.length > 0) {
+ showError(`유효하지 않은 이메일 주소가 있습니다: ${invalidEmails.join(', ')}`);
+ return;
+ }
+
+ setIsSending(true);
+ try {
+ await sendMeetingEmail(projectId, meetingId, {
+ to: toEmails,
+ subject: formData.subject,
+ content: formData.content
+ }, attachments);
+
+ showSuccess('이메일이 성공적으로 전송되었습니다.');
+ onClose();
+
+ // 폼 초기화
+ setFormData({
+ to: '',
+ subject: `회의록: ${meetingTitle || '제목 없음'}`,
+ content: ''
+ });
+ setAttachments([]);
+ } catch (error) {
+ console.error('이메일 전송 실패:', error);
+ if (error.response?.status === 400) {
+ showError('잘못된 요청입니다. 입력 정보를 확인해주세요.');
+ } else if (error.response?.status === 500) {
+ showError('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
+ } else {
+ showError('이메일 전송 중 오류가 발생했습니다.');
+ }
+ } finally {
+ setIsSending(false);
+ }
+};
+
+ if (!isOpen) return null;
+
+ return (
+
+ );
+}
+
+export default EmailSendModal;
\ No newline at end of file
diff --git a/src/components/meeting/Meeting.jsx b/src/components/meeting/Meeting.jsx
index 6d6f99d..b31d4aa 100644
--- a/src/components/meeting/Meeting.jsx
+++ b/src/components/meeting/Meeting.jsx
@@ -8,10 +8,13 @@ import { LoadingSpinner, ErrorFallback } from "../common/loading/LoadingComponen
import useMeetingStore from "../../store/meetingStore";
import { useToast } from "../../hooks/useToast";
-function Meeting() {
+function Meeting({ projectId }) {
const [searchTerm, setSearchTerm] = useState("");
const [filterType, setFilterType] = useState("all");
+ console.log('Meeting에서 받은 projectId:', projectId);
+ console.log('projectId 타입:', typeof projectId);
+
// Zustand store 사용
const {
meetings,
@@ -35,8 +38,10 @@ function Meeting() {
// 컴포넌트 마운트 시 회의록 목록 조회
useEffect(() => {
- fetchMeetings(1); // 프로젝트 ID 1로 고정
- }, [fetchMeetings]);
+ if (projectId) {
+ fetchMeetings(projectId, 1, 10);
+ }
+ }, [projectId, fetchMeetings]);
// 필터링된 회의 목록
const filteredMeetings = meetings.filter((meeting) => {
@@ -55,7 +60,7 @@ function Meeting() {
// 삭제 확인 함수
const handleDeleteConfirm = async (meeting) => {
try {
- await deleteMeeting(meeting.id);
+ await deleteMeeting(meeting.id, projectId);
showSuccess("회의록이 성공적으로 삭제되었습니다.");
} catch (error) {
console.error('삭제 실패:', error);
@@ -67,7 +72,7 @@ function Meeting() {
const handleMeetingAction = (action, meeting) => {
switch (action) {
case "view":
- selectMeeting(meeting);
+ selectMeeting(meeting, projectId);
break;
case "edit":
showEdit(meeting);
@@ -80,7 +85,7 @@ function Meeting() {
console.log('handleSaveMeeting 호출됨, API 응답:', apiResponse);
// 회의록 생성 후 목록 새로고침
- await refreshAfterCreate(1);
+ await refreshAfterCreate(projectId);
} catch (error) {
console.error('handleSaveMeeting 에러:', error);
@@ -93,7 +98,7 @@ function Meeting() {
console.log('handleUpdateMeeting 호출됨, API 응답:', apiResponse);
// 회의록 수정 후 목록 새로고침
- await fetchMeetings(1, pagination.page, pagination.size);
+ await fetchMeetings(projectId, pagination.page, pagination.size);
showList();
} catch (error) {
@@ -117,6 +122,7 @@ function Meeting() {
) : currentView === 'edit' ? (
) : currentView === 'detail' ? (
fetchMeetings(1)}
+ onRetry={() => fetchMeetings(projectId)}
/>
) : (
<>
@@ -195,7 +202,7 @@ function Meeting() {
{!loading && pagination.totalPages > 1 && (
changePage(pagination.page - 1)}
+ onClick={() => changePage(pagination.page - 1,projectId)}
disabled={!pagination.hasPrevious}
className="px-3 py-1 rounded border disabled:opacity-50"
>
@@ -205,7 +212,7 @@ function Meeting() {
{pagination.page} / {pagination.totalPages}
changePage(pagination.page + 1)}
+ onClick={() => changePage(pagination.page + 1, projectId)}
disabled={!pagination.hasNext}
className="px-3 py-1 rounded border disabled:opacity-50"
>
diff --git a/src/components/meeting/MeetingCreate.jsx b/src/components/meeting/MeetingCreate.jsx
index 458e901..9cda746 100644
--- a/src/components/meeting/MeetingCreate.jsx
+++ b/src/components/meeting/MeetingCreate.jsx
@@ -1,12 +1,13 @@
-import { useState, useRef } from "react";
+import { useState, useRef, useEffect } from "react";
import { ArrowBack, Mic, Stop, PlayArrow, Pause } from "@mui/icons-material";
import { employeesData } from "../../data/employees";
import Dropdown from '../common/Dropdown';
import { LoadingSpinner } from '../common/loading/LoadingComponents';
import { useCreateMeeting, useUpdateMeeting } from '../../hooks/useMeetingQueries';
import { useToast } from '../../hooks/useToast';
+import { getProjectParticipants } from '../../api/meetingAPI';
-function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
+function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false, projectId }) {
const [meetingData, setMeetingData] = useState({
title: meeting?.title || "",
place: meeting?.location || meeting?.place || "",
@@ -16,12 +17,19 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
progressDate: meeting?.progressDate || new Date().toISOString().slice(0, -1) + '+09:00',
});
+ console.log('MeetingCreate에서 받은 projectId:', projectId);
+ console.log('projectId 타입:', typeof projectId);
+
const [isRecording, setIsRecording] = useState(false);
const [recordingTime, setRecordingTime] = useState(0);
const [audioBlob, setAudioBlob] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [showParticipantDropdown, setShowParticipantDropdown] = useState(false);
+ // 팀 인원 데이터 상태 추가
+ const [teamMembers, setTeamMembers] = useState([]);
+ const [isLoadingMembers, setIsLoadingMembers] = useState(false);
+
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const intervalRef = useRef(null);
@@ -44,6 +52,38 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
{ value: 'RETROSPECTIVE', label: 'Sprint Retrospective' }
];
+ // 팀 인원 조회 함수 추가
+ const fetchTeamMembers = async () => {
+ setIsLoadingMembers(true);
+ try {
+ const response = await getProjectParticipants(projectId);
+
+ if (response.statusCode === 200) {
+ // 서버 응답 데이터를 컴포넌트에서 사용할 수 있는 형태로 변환
+ const members = response.data.map(member => ({
+ id: member.memberId || member.id,
+ name: member.memberName || member.name,
+ email: member.email,
+ department: member.department,
+ position: member.position
+ }));
+ setTeamMembers(members);
+ }
+ } catch (error) {
+ console.error('팀 인원 조회 실패:', error);
+ showError('팀 인원 정보를 불러오는데 실패했습니다.');
+ // 실패 시 기본 데이터 사용
+ setTeamMembers(employeesData);
+ } finally {
+ setIsLoadingMembers(false);
+ }
+ };
+
+ // 컴포넌트 마운트 시 팀 인원 조회
+ useEffect(() => {
+ fetchTeamMembers();
+ }, []);
+
// 입력값 변경 핸들러
const handleInputChange = (field, value) => {
setMeetingData((prev) => ({
@@ -52,13 +92,13 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
}));
};
- // 참석자 선택/해제 - ID 기반으로 수정
- const toggleParticipant = (employee) => {
+ // 참석자 선택/해제 - 팀 인원 데이터 사용
+ const toggleParticipant = (member) => {
setMeetingData((prev) => ({
...prev,
- participants: prev.participants.includes(employee.id)
- ? prev.participants.filter((id) => id !== employee.id)
- : [...prev.participants, employee.id],
+ participants: prev.participants.includes(member.id)
+ ? prev.participants.filter((id) => id !== member.id)
+ : [...prev.participants, member.id],
}));
};
@@ -152,27 +192,29 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
participantIds: meetingData.participants
};
- const projectId = 1; // 현재는 1번 프로젝트만 있다고 가정
-
try {
let result;
if (isEditing && meeting?.id) {
- // 수정 모드
+ // 수정 모드 - 오디오 파일 전송
result = await updateMutation.mutateAsync({
projectId,
meetingId: meeting.id,
- meetingData: requestData
+ meetingData: requestData,
+ audioFile: audioBlob // 오디오 파일 추가
});
showSuccess("회의록이 성공적으로 수정되었습니다.");
} else {
- // 생성 모드
+ // 생성 모드 - 오디오 파일 전송
result = await createMutation.mutateAsync({
projectId,
- meetingData: requestData
+ meetingData: requestData,
+ audioFile: audioBlob // 오디오 파일 추가
});
showSuccess("회의록이 성공적으로 저장되었습니다.");
}
+ console.log('API 응답:', result); // 응답 확인용 로그
+ console.log('전송된 오디오 파일:', audioBlob); // 오디오 파일 확인용 로그
console.log('API 응답:', result); // 응답 확인용 로그
// 성공 시 콜백 호출
@@ -309,48 +351,68 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
{showParticipantDropdown && (
- {employeesData.map((employee) => (
-
toggleParticipant(employee)}
- >
-
{}}
- className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
- />
-
-
- {employee.name}
-
-
- {employee.department} · {employee.position}
-
-
+ {isLoadingMembers ? (
+
+
- ))}
+ ) : (
+
+ {teamMembers.map((member) => (
+
toggleParticipant(member)}
+ className={`w-full px-4 py-2 text-left hover:bg-gray-50 flex items-center justify-between ${
+ meetingData.participants.includes(member.id)
+ ? 'bg-blue-50 text-blue-700'
+ : 'text-gray-700'
+ }`}
+ >
+
+ {member.name}
+ {member.department && (
+ {member.department}
+ )}
+ {member.position && (
+ {member.position}
+ )}
+
+ {meetingData.participants.includes(member.id) && (
+
+
+
+ )}
+
+ ))}
+
+ )}
)}
- {/* 선택된 참석자 표시 - 이름으로 표시하되 ID 기반으로 관리 */}
+ {/* 선택된 참석자 표시 - 팀 인원 데이터 사용 */}
{meetingData.participants.length > 0 && (
{meetingData.participants.map((participantId) => {
- const participant = employeesData.find(emp => emp.id === participantId);
+ const participant = teamMembers.find(member => member.id === participantId);
return participant ? (
{participant.name}
toggleParticipant(participant)}
- className="ml-1 hover:text-blue-600"
+ className="ml-2 text-blue-600 hover:text-blue-800"
>
×
@@ -453,4 +515,4 @@ function MeetingCreate({ onBack, onSave, meeting = null, isEditing = false }) {
);
}
-export default MeetingCreate;
+export default MeetingCreate;
\ No newline at end of file
diff --git a/src/components/meeting/MeetingDetail.jsx b/src/components/meeting/MeetingDetail.jsx
index e65dd03..07ecac2 100644
--- a/src/components/meeting/MeetingDetail.jsx
+++ b/src/components/meeting/MeetingDetail.jsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
import {
ArrowBack,
CalendarToday,
@@ -7,17 +8,26 @@ import {
Email,
Article,
} from "@mui/icons-material";
+import { downloadMeetingReport } from "../../api/meetingAPI";
+
import BadgeComponent from "../common/BadgeComponent";
import ParticipantList from "../common/ParticipantList";
import MeetingMetadata from "../common/MeetingMetadata";
import AudioPlayer from "../common/AudioPlayer";
import AISummaryPanel from "../common/AISummaryPanel";
import { useToast } from "../../hooks/useToast";
+import EmailSendModal from "./EmailSendModal";
function MeetingDetail({ meeting, onBack, onEdit, onDelete }) {
// Toast hook
const { showError } = useToast();
+ // 다운로드 상태 추가
+ const [isDownloading, setIsDownloading] = useState(false);
+
+ // 이메일 전송 모달 상태 추가
+ const [isEmailModalOpen, setIsEmailModalOpen] = useState(false);
+
if (!meeting) {
return (
@@ -34,6 +44,42 @@ function MeetingDetail({ meeting, onBack, onEdit, onDelete }) {
);
}
+ // 이메일 전송 핸들러 함수 추가
+ const handleEmailSend = () => {
+ if (!meeting?.projectId || !meeting?.id) {
+ showError("프로젝트 ID 또는 회의 ID가 없습니다.");
+ return;
+ }
+ setIsEmailModalOpen(true);
+ };
+
+ // 다운로드 핸들러 함수 추가 (handleDelete 함수 아래에 추가)
+ const handleDownloadReport = async () => {
+ if (!meeting?.projectId || !meeting?.id) {
+ showError("프로젝트 ID 또는 회의 ID가 없습니다.");
+ return;
+ }
+
+ setIsDownloading(true);
+ try {
+ const result = await downloadMeetingReport(meeting.projectId, meeting.id);
+ // 성공 메시지는 부모 컴포넌트에서 처리하거나 여기서 처리
+ console.log(`${result.fileName} 다운로드가 완료되었습니다.`);
+ } catch (error) {
+ console.error('다운로드 중 오류가 발생했습니다:', error);
+ if (error.response?.status === 404) {
+ showError("회의록을 찾을 수 없습니다.");
+ } else if (error.response?.status === 500) {
+ showError("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
+ } else {
+ showError("다운로드 중 오류가 발생했습니다.");
+ }
+ } finally {
+ setIsDownloading(false);
+ }
+ };
+
+
// 날짜 포맷팅
const formatDateTime = (dateTimeStr) => {
const date = new Date(dateTimeStr);
@@ -137,12 +183,15 @@ function MeetingDetail({ meeting, onBack, onEdit, onDelete }) {
-
{}}
- className="px-4 py-2 text-black hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2"
+
- AI 리포트 생성
+ {isDownloading ? '다운로드 중...' : 'AI 리포트 생성'}
{
@@ -150,9 +199,14 @@ function MeetingDetail({ meeting, onBack, onEdit, onDelete }) {
}}
className="px-4 py-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors flex items-center gap-2"
>
-
- 이메일 전송
+
+
+ 이메일 전송
+
@@ -192,10 +246,19 @@ function MeetingDetail({ meeting, onBack, onEdit, onDelete }) {
{/* 우측 영역 - AI 정리 창 */}
+
+ {/* 이메일 전송 모달 */}
+ setIsEmailModalOpen(false)}
+ projectId={meeting?.projectId}
+ meetingId={meeting?.id}
+ meetingTitle={meeting?.title}
+ />
);
}
diff --git a/src/hooks/useMeetingQueries.js b/src/hooks/useMeetingQueries.js
index 7e1b491..c2bb0f3 100644
--- a/src/hooks/useMeetingQueries.js
+++ b/src/hooks/useMeetingQueries.js
@@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient, useSuspenseQuery } from '@tansta
import { getMeetings, deleteMeeting as deleteMeetingAPI, getMeetingDetail, createMeeting, updateMeeting } from '../api/meetingAPI';
// 회의록 목록 조회
-export function useMeetings(projectId = 1, page = 1, size = 10) {
+export function useMeetings(projectId, page = 1, size = 10) {
return useQuery({
queryKey: ['meetings', projectId, page, size],
queryFn: () => getMeetings(projectId, page, size),
@@ -22,7 +22,8 @@ export function useMeetings(projectId = 1, page = 1, size = 10) {
participants: [],
participantCount: meeting.attendeeCount,
description: meeting.content,
- createdAt: meeting.progressTime
+ createdAt: meeting.progressTime,
+ summary : meeting.aiSummary
})),
pagination: {
page: response.data.page,
@@ -40,7 +41,7 @@ export function useMeetings(projectId = 1, page = 1, size = 10) {
}
// Suspense와 함께 사용할 회의록 목록
-export function useSuspenseMeetings(projectId = 1, page = 1, size = 10) {
+export function useSuspenseMeetings(projectId , page = 1, size = 10) {
return useSuspenseQuery({
queryKey: ['meetings', projectId, page, size],
queryFn: () => getMeetings(projectId, page, size),
@@ -111,6 +112,7 @@ export function useMeetingDetail(projectId, meetingId, enabled = true) {
description: response.data.content,
memo: response.data.content,
createdAt: response.data.progressDate,
+ summary : response.data.aiSummary,
};
}
throw new Error('Failed to fetch meeting detail');
@@ -180,7 +182,7 @@ export function useCreateMeeting() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: ({ projectId, meetingData }) => createMeeting(projectId, meetingData),
+ mutationFn: ({ projectId, meetingData, audioFile }) => createMeeting(projectId, meetingData, audioFile),
onSuccess: (data, variables) => {
// 성공 시 관련 쿼리들 무효화
queryClient.invalidateQueries({ queryKey: ['meetings', variables.projectId] });
diff --git a/src/pages/ProjectDetail.jsx b/src/pages/ProjectDetail.jsx
index 14fbbc3..412c8a9 100644
--- a/src/pages/ProjectDetail.jsx
+++ b/src/pages/ProjectDetail.jsx
@@ -21,6 +21,11 @@ import SettingsTab from "../components/setting/Settings";
function ProjectDetail() {
const { id } = useParams();
+
+ // 디버깅용 로그 추가
+ console.log('ProjectDetail에서 받은 id:', id);
+ console.log('id 타입:', typeof id);
+
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("kanban");
@@ -97,7 +102,7 @@ function ProjectDetail() {
case "timeline":
return ;
case "meeting":
- return ;
+ return ;
case "team":
return ;
case "settings":
diff --git a/src/store/meetingStore.js b/src/store/meetingStore.js
index 136bb53..3f3754b 100644
--- a/src/store/meetingStore.js
+++ b/src/store/meetingStore.js
@@ -18,7 +18,7 @@ const useMeetingStore = create((set, get) => ({
},
// API에서 회의록 목록 조회
- fetchMeetings: async (projectId = 1, page = 1, size = 10) => {
+ fetchMeetings: async (projectId , page = 1, size = 10) => {
set({ loading: true, error: null });
try {
const response = await getMeetings(projectId, page, size);
@@ -34,7 +34,8 @@ const useMeetingStore = create((set, get) => ({
participants: [],
participantCount: meeting.attendeeCount,
description: meeting.content,
- createdAt: meeting.progressTime
+ createdAt: meeting.progressTime,
+ summary : meeting.aiSummary
}));
set({
@@ -57,40 +58,42 @@ const useMeetingStore = create((set, get) => ({
},
// 회의록 상세 조회
- fetchMeetingDetail: async (meetingId, projectId = 1, listMeeting = null) => {
- set({ loading: true, error: null });
- try {
- const response = await getMeetingDetail(projectId, meetingId);
-
- if (response.statusCode === 200) {
- const meetingDetail = {
- id: meetingId,
- title: response.data.title,
- organizer: response.data.writerName,
- type: listMeeting?.type || 'MEETING',
- scheduledDate: response.data.progressDate,
- location: response.data.place,
- participants: response.data.participants,
- participantCount: response.data.participants?.length || 0,
- content: response.data.content,
- description: response.data.content,
- memo: response.data.content,
- createdAt: response.data.progressDate,
- };
+ fetchMeetingDetail: async (meetingId, projectId, listMeeting = null) => {
+ set({ loading: true, error: null });
+ try {
+ const response = await getMeetingDetail(projectId, meetingId);
+
+ if (response.statusCode === 200) {
+ const meetingDetail = {
+ id: meetingId,
+ projectId: projectId, // projectId 추가
+ title: response.data.title,
+ organizer: response.data.writerName,
+ type: listMeeting?.type || 'MEETING',
+ scheduledDate: response.data.progressDate,
+ location: response.data.place,
+ participants: response.data.participants,
+ participantCount: response.data.participants?.length || 0,
+ content: response.data.content,
+ description: response.data.content,
+ memo: response.data.content,
+ createdAt: response.data.progressDate,
+ summary : response.data.aiSummary
+ };
- set({
- selectedMeeting: meetingDetail,
- currentView: 'detail',
- loading: false
- });
- }
- } catch (error) {
- console.error('회의록 상세 조회 실패:', error);
- set({ error: error.message, loading: false });
+ set({
+ selectedMeeting: meetingDetail,
+ currentView: 'detail',
+ loading: false
+ });
}
- },
+ } catch (error) {
+ console.error('회의록 상세 조회 실패:', error);
+ set({ error: error.message, loading: false });
+ }
+},
- selectMeeting: async (meeting, projectId = 1) => {
+ selectMeeting: async (meeting, projectId ) => {
// 목록에서 선택할 때는 상세 정보를 가져와야 함
// 목록 정보의 타입을 보존하기 위해 전달
await get().fetchMeetingDetail(meeting.id, projectId, meeting);
@@ -111,14 +114,14 @@ const useMeetingStore = create((set, get) => ({
}),
// 회의록 생성 후 목록 새로고침
- refreshAfterCreate: async (projectId = 1) => {
+ refreshAfterCreate: async (projectId) => {
const { fetchMeetings, pagination } = get();
await fetchMeetings(projectId, pagination.page, pagination.size);
set({ currentView: 'list' });
},
// 회의록 삭제
- deleteMeeting: async (meetingId, projectId = 1) => {
+ deleteMeeting: async (meetingId, projectId) => {
try {
await deleteMeetingAPI(projectId, meetingId);
// 삭제 후 목록 새로고침
@@ -133,7 +136,7 @@ const useMeetingStore = create((set, get) => ({
},
// 페이지 변경
- changePage: async (newPage, projectId = 1) => {
+ changePage: async (newPage, projectId ) => {
const { pagination, fetchMeetings } = get();
await fetchMeetings(projectId, newPage, pagination.size);
}