projects 테이블에 project_code 컬럼이 없고 job_no가 올바른 컬럼명. 백엔드 SQL에서는 pr.job_no AS project_code alias 사용, 프론트 드롭다운에서는 p.job_no로 직접 참조. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
224 lines
7.7 KiB
JavaScript
224 lines
7.7 KiB
JavaScript
// models/meetingModel.js
|
|
const { getDb } = require('../dbPool');
|
|
|
|
const MeetingModel = {
|
|
// === 회의록 ===
|
|
async getAll(filters = {}) {
|
|
const db = await getDb();
|
|
let sql = `
|
|
SELECT m.*,
|
|
su.name AS created_by_name,
|
|
(SELECT COUNT(*) FROM meeting_attendees WHERE meeting_id = m.meeting_id) AS attendee_count,
|
|
(SELECT COUNT(*) FROM meeting_agenda_items WHERE meeting_id = m.meeting_id) AS agenda_count,
|
|
(SELECT COUNT(*) FROM meeting_agenda_items
|
|
WHERE meeting_id = m.meeting_id AND status IN ('open','in_progress')) AS open_action_count
|
|
FROM meeting_minutes m
|
|
LEFT JOIN sso_users su ON m.created_by = su.user_id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
if (filters.year && filters.month) {
|
|
sql += ' AND YEAR(m.meeting_date) = ? AND MONTH(m.meeting_date) = ?';
|
|
params.push(filters.year, filters.month);
|
|
} else if (filters.year) {
|
|
sql += ' AND YEAR(m.meeting_date) = ?';
|
|
params.push(filters.year);
|
|
}
|
|
if (filters.search) {
|
|
sql += ' AND (m.title LIKE ? OR m.summary LIKE ?)';
|
|
params.push(`%${filters.search}%`, `%${filters.search}%`);
|
|
}
|
|
sql += ' ORDER BY m.meeting_date DESC, m.created_at DESC';
|
|
const [rows] = await db.query(sql, params);
|
|
return rows;
|
|
},
|
|
|
|
async getById(meetingId) {
|
|
const db = await getDb();
|
|
// 회의 기본정보
|
|
const [meetings] = await db.query(`
|
|
SELECT m.*, su.name AS created_by_name
|
|
FROM meeting_minutes m
|
|
LEFT JOIN sso_users su ON m.created_by = su.user_id
|
|
WHERE m.meeting_id = ?
|
|
`, [meetingId]);
|
|
if (meetings.length === 0) return null;
|
|
const meeting = meetings[0];
|
|
|
|
// 참석자
|
|
const [attendees] = await db.query(`
|
|
SELECT ma.id, ma.user_id, su.name, su.username, su.department
|
|
FROM meeting_attendees ma
|
|
JOIN sso_users su ON ma.user_id = su.user_id
|
|
WHERE ma.meeting_id = ?
|
|
ORDER BY su.name
|
|
`, [meetingId]);
|
|
meeting.attendees = attendees;
|
|
|
|
// 안건
|
|
const [items] = await db.query(`
|
|
SELECT ai.*, pr.project_name, pr.job_no AS project_code,
|
|
ms.milestone_name, ms.milestone_date,
|
|
su.name AS responsible_name
|
|
FROM meeting_agenda_items ai
|
|
LEFT JOIN projects pr ON ai.project_id = pr.project_id
|
|
LEFT JOIN schedule_milestones ms ON ai.milestone_id = ms.milestone_id
|
|
LEFT JOIN sso_users su ON ai.responsible_user_id = su.user_id
|
|
WHERE ai.meeting_id = ?
|
|
ORDER BY ai.display_order, ai.item_id
|
|
`, [meetingId]);
|
|
meeting.items = items;
|
|
|
|
return meeting;
|
|
},
|
|
|
|
async create(data) {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO meeting_minutes (meeting_date, meeting_time, title, location, summary, status, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[data.meeting_date, data.meeting_time || null, data.title,
|
|
data.location || null, data.summary || null, 'draft', data.created_by]
|
|
);
|
|
const meetingId = result.insertId;
|
|
|
|
// 참석자 추가
|
|
if (data.attendees && data.attendees.length > 0) {
|
|
const values = data.attendees.map(userId => [meetingId, userId]);
|
|
await db.query('INSERT INTO meeting_attendees (meeting_id, user_id) VALUES ?', [values]);
|
|
}
|
|
|
|
return meetingId;
|
|
},
|
|
|
|
async update(meetingId, data) {
|
|
const db = await getDb();
|
|
const fields = [];
|
|
const params = [];
|
|
const allowed = ['meeting_date', 'meeting_time', 'title', 'location', 'summary'];
|
|
for (const key of allowed) {
|
|
if (data[key] !== undefined) { fields.push(`${key} = ?`); params.push(data[key]); }
|
|
}
|
|
if (fields.length > 0) {
|
|
fields.push('updated_at = NOW()');
|
|
params.push(meetingId);
|
|
await db.query(`UPDATE meeting_minutes SET ${fields.join(', ')} WHERE meeting_id = ?`, params);
|
|
}
|
|
|
|
// 참석자 재설정
|
|
if (data.attendees !== undefined) {
|
|
await db.query('DELETE FROM meeting_attendees WHERE meeting_id = ?', [meetingId]);
|
|
if (data.attendees.length > 0) {
|
|
const values = data.attendees.map(userId => [meetingId, userId]);
|
|
await db.query('INSERT INTO meeting_attendees (meeting_id, user_id) VALUES ?', [values]);
|
|
}
|
|
}
|
|
},
|
|
|
|
async publish(meetingId) {
|
|
const db = await getDb();
|
|
await db.query(
|
|
"UPDATE meeting_minutes SET status = 'published', updated_at = NOW() WHERE meeting_id = ?",
|
|
[meetingId]
|
|
);
|
|
},
|
|
|
|
async unpublish(meetingId) {
|
|
const db = await getDb();
|
|
await db.query(
|
|
"UPDATE meeting_minutes SET status = 'draft', updated_at = NOW() WHERE meeting_id = ?",
|
|
[meetingId]
|
|
);
|
|
},
|
|
|
|
async delete(meetingId) {
|
|
const db = await getDb();
|
|
await db.query('DELETE FROM meeting_minutes WHERE meeting_id = ?', [meetingId]);
|
|
},
|
|
|
|
// === 안건 ===
|
|
async addItem(meetingId, data) {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO meeting_agenda_items
|
|
(meeting_id, project_id, milestone_id, item_type, content, decision, action_required,
|
|
responsible_user_id, due_date, status, display_order)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[meetingId, data.project_id || null, data.milestone_id || null,
|
|
data.item_type || 'other', data.content, data.decision || null,
|
|
data.action_required || null, data.responsible_user_id || null,
|
|
data.due_date || null, data.status || 'open', data.display_order || 0]
|
|
);
|
|
return result.insertId;
|
|
},
|
|
|
|
async updateItem(itemId, data) {
|
|
const db = await getDb();
|
|
const fields = [];
|
|
const params = [];
|
|
const allowed = ['project_id', 'milestone_id', 'item_type', 'content', 'decision',
|
|
'action_required', 'responsible_user_id', 'due_date', 'status', 'display_order'];
|
|
for (const key of allowed) {
|
|
if (data[key] !== undefined) { fields.push(`${key} = ?`); params.push(data[key]); }
|
|
}
|
|
if (fields.length === 0) return;
|
|
fields.push('updated_at = NOW()');
|
|
params.push(itemId);
|
|
await db.query(`UPDATE meeting_agenda_items SET ${fields.join(', ')} WHERE item_id = ?`, params);
|
|
},
|
|
|
|
async deleteItem(itemId) {
|
|
const db = await getDb();
|
|
await db.query('DELETE FROM meeting_agenda_items WHERE item_id = ?', [itemId]);
|
|
},
|
|
|
|
async updateItemStatus(itemId, status) {
|
|
const db = await getDb();
|
|
await db.query(
|
|
'UPDATE meeting_agenda_items SET status = ?, updated_at = NOW() WHERE item_id = ?',
|
|
[status, itemId]
|
|
);
|
|
},
|
|
|
|
// === 미완료 조치사항 ===
|
|
async getActionItems(filters = {}) {
|
|
const db = await getDb();
|
|
let sql = `
|
|
SELECT ai.*, m.title AS meeting_title, m.meeting_date,
|
|
pr.project_name, pr.job_no AS project_code,
|
|
su.name AS responsible_name
|
|
FROM meeting_agenda_items ai
|
|
JOIN meeting_minutes m ON ai.meeting_id = m.meeting_id
|
|
LEFT JOIN projects pr ON ai.project_id = pr.project_id
|
|
LEFT JOIN sso_users su ON ai.responsible_user_id = su.user_id
|
|
WHERE ai.item_type IN ('action_item', 'issue', 'decision')
|
|
`;
|
|
const params = [];
|
|
if (filters.status) {
|
|
sql += ' AND ai.status = ?';
|
|
params.push(filters.status);
|
|
} else {
|
|
sql += " AND ai.status IN ('open', 'in_progress')";
|
|
}
|
|
if (filters.responsible_user_id) {
|
|
sql += ' AND ai.responsible_user_id = ?';
|
|
params.push(filters.responsible_user_id);
|
|
}
|
|
sql += ' ORDER BY ai.due_date ASC, m.meeting_date DESC';
|
|
const [rows] = await db.query(sql, params);
|
|
return rows;
|
|
},
|
|
|
|
// 회의록 상태 조회 (published 체크용)
|
|
async getStatus(meetingId) {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
'SELECT status FROM meeting_minutes WHERE meeting_id = ?',
|
|
[meetingId]
|
|
);
|
|
return rows.length > 0 ? rows[0].status : null;
|
|
}
|
|
};
|
|
|
|
module.exports = MeetingModel;
|