Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 4 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
120 changes: 118 additions & 2 deletions src/api/meetingAPI.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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',
Expand Down Expand Up @@ -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();
Expand All @@ -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',
Expand All @@ -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;
}
};
116 changes: 96 additions & 20 deletions src/components/common/AISummaryPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="sticky top-6">
<div className={className}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
<div className="text-sm text-gray-500 text-center py-8">
AI 요약 정보가 없습니다.
</div>
</div>
</div>
);
}

📋 액션 아이템
• 박서호: 프론트엔드 컴포넌트 설계 (3/25까지)
• 이지민: 디자인 시스템 업데이트 (3/28까지)
• 최우식: 요구사항 문서 작성 (3/30까지)`;
// 서버에서 받은 데이터 구조에 맞게 처리
const { mainTopics = [], decisions = [], priorities = [], recommends = [] } = summary;

return (
<div className="sticky top-6">
Expand All @@ -29,14 +31,88 @@ function AISummaryPanel({
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>

<div className="space-y-4">
<div className="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">
{summary || defaultSummary}
</div>
<div className="space-y-6">
{/* 주요 안건 */}
{mainTopics && mainTopics.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center">
📝 주요 안건
</h4>
<ul className="space-y-1">
{mainTopics.map((topic, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
{topic}
</li>
))}
</ul>
</div>
)}

{/* 결정 사항 */}
{decisions && decisions.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center">
✅ 결정 사항
</h4>
<ul className="space-y-1">
{decisions.map((decision, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
{decision}
</li>
))}
</ul>
</div>
)}

{/* 액션 아이템 */}
{priorities && priorities.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center">
📋 우선 사항
</h4>
<ul className="space-y-2">
{priorities.map((priority, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="w-1.5 h-1.5 bg-orange-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
<span className="leading-relaxed">{priority}</span>
</li>
))}
</ul>
</div>
)}

{/* 추천 아이템 */}
{recommends && recommends.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center">
👍 추천 업무
</h4>
<ul className="space-y-2">
{recommends.map((recommand, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="w-1.5 h-1.5 bg-orange-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
<span className="leading-relaxed">{recommand}</span>
</li>
))}
</ul>
</div>
)}

{/* 모든 데이터가 없는 경우 */}
{(!mainTopics || mainTopics.length === 0) &&
(!decisions || decisions.length === 0) &&
(!priorities || priorities.length === 0) &&
(!recommends || recommends.length === 0) && (
<div className="text-sm text-gray-500 text-center py-4">
AI 요약 정보가 없습니다.
</div>
)}
</div>
</div>
</div>
);
}

export default AISummaryPanel;
export default AISummaryPanel;
Loading