기능구현중
This commit is contained in:
102
backend/api/google-group/list.get.ts
Normal file
102
backend/api/google-group/list.get.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { queryOne } from '../../utils/db'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 구글 그룹 목록 조회
|
||||
* GET /api/google-group/list
|
||||
*
|
||||
* Cloud Identity Groups API 사용
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireAuth(event)
|
||||
|
||||
// 사용자의 Google 토큰 조회
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT google_access_token, google_refresh_token, google_token_expires_at
|
||||
FROM wr_employee_info WHERE employee_id = $1
|
||||
`, [session.employeeId])
|
||||
|
||||
if (!employee?.google_access_token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Google 계정이 연결되지 않았습니다. 먼저 Google 로그인을 해주세요.'
|
||||
})
|
||||
}
|
||||
|
||||
let accessToken = employee.google_access_token
|
||||
|
||||
// 토큰 만료 확인 및 갱신
|
||||
if (employee.google_token_expires_at && new Date(employee.google_token_expires_at) < new Date()) {
|
||||
accessToken = await refreshGoogleToken(session.employeeId, employee.google_refresh_token)
|
||||
}
|
||||
|
||||
try {
|
||||
// Cloud Identity Groups API로 그룹 목록 조회
|
||||
const response = await $fetch<any>('https://cloudidentity.googleapis.com/v1/groups:search', {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||||
query: {
|
||||
query: 'parent == "customers/my_customer" && "cloudidentity.googleapis.com/groups.discussion_forum" in labels',
|
||||
pageSize: 100
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
groups: (response.groups || []).map((g: any) => ({
|
||||
groupId: g.name?.split('/')[1],
|
||||
groupKey: g.groupKey?.id,
|
||||
displayName: g.displayName,
|
||||
description: g.description,
|
||||
email: g.groupKey?.id
|
||||
}))
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Google Groups API error:', e)
|
||||
|
||||
// 권한 없음 에러
|
||||
if (e.status === 403) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '구글 그룹 접근 권한이 없습니다. 관리자에게 문의하세요.'
|
||||
})
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '구글 그룹 목록을 가져오는데 실패했습니다.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Google 토큰 갱신
|
||||
*/
|
||||
async function refreshGoogleToken(employeeId: number, refreshToken: string): Promise<string> {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const response = await $fetch<any>('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: config.googleClientId,
|
||||
client_secret: config.googleClientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
}).toString(),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
})
|
||||
|
||||
if (response.access_token) {
|
||||
// DB 업데이트
|
||||
const { execute } = await import('../../utils/db')
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET google_access_token = $1,
|
||||
google_token_expires_at = NOW() + INTERVAL '${response.expires_in} seconds'
|
||||
WHERE employee_id = $2
|
||||
`, [response.access_token, employeeId])
|
||||
|
||||
return response.access_token
|
||||
}
|
||||
|
||||
throw new Error('토큰 갱신 실패')
|
||||
}
|
||||
139
backend/api/google-group/messages.get.ts
Normal file
139
backend/api/google-group/messages.get.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { queryOne, execute } from '../../utils/db'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 구글 그룹 메시지 조회
|
||||
* GET /api/google-group/messages
|
||||
*
|
||||
* Gmail API로 그룹 이메일 조회
|
||||
*
|
||||
* Query params:
|
||||
* - groupEmail: 그룹 이메일 주소 (예: dev-team@company.com)
|
||||
* - maxResults: 최대 결과 수 (기본 20)
|
||||
* - after: 이 날짜 이후 메시지 (YYYY-MM-DD)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireAuth(event)
|
||||
const query = getQuery(event)
|
||||
|
||||
const groupEmail = query.groupEmail as string
|
||||
const maxResults = parseInt(query.maxResults as string) || 20
|
||||
const after = query.after as string
|
||||
|
||||
if (!groupEmail) {
|
||||
throw createError({ statusCode: 400, message: '그룹 이메일이 필요합니다.' })
|
||||
}
|
||||
|
||||
// 사용자의 Google 토큰 조회
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT google_access_token, google_refresh_token, google_token_expires_at
|
||||
FROM wr_employee_info WHERE employee_id = $1
|
||||
`, [session.employeeId])
|
||||
|
||||
if (!employee?.google_access_token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Google 계정이 연결되지 않았습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
let accessToken = employee.google_access_token
|
||||
|
||||
// 토큰 만료 확인 및 갱신
|
||||
if (employee.google_token_expires_at && new Date(employee.google_token_expires_at) < new Date()) {
|
||||
accessToken = await refreshGoogleToken(session.employeeId, employee.google_refresh_token)
|
||||
}
|
||||
|
||||
try {
|
||||
// Gmail API로 그룹 메일 검색
|
||||
let searchQuery = `list:${groupEmail}`
|
||||
if (after) {
|
||||
searchQuery += ` after:${after}`
|
||||
}
|
||||
|
||||
const listResponse = await $fetch<any>('https://gmail.googleapis.com/gmail/v1/users/me/messages', {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||||
query: {
|
||||
q: searchQuery,
|
||||
maxResults: maxResults
|
||||
}
|
||||
})
|
||||
|
||||
if (!listResponse.messages || listResponse.messages.length === 0) {
|
||||
return { messages: [], total: 0 }
|
||||
}
|
||||
|
||||
// 각 메시지의 상세 정보 조회
|
||||
const messages = await Promise.all(
|
||||
listResponse.messages.slice(0, maxResults).map(async (msg: any) => {
|
||||
const detail = await $fetch<any>(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}`, {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||||
query: { format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] }
|
||||
})
|
||||
|
||||
const headers = detail.payload?.headers || []
|
||||
const getHeader = (name: string) => headers.find((h: any) => h.name === name)?.value || ''
|
||||
|
||||
return {
|
||||
id: msg.id,
|
||||
threadId: msg.threadId,
|
||||
subject: getHeader('Subject'),
|
||||
from: getHeader('From'),
|
||||
to: getHeader('To'),
|
||||
date: getHeader('Date'),
|
||||
snippet: detail.snippet
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
messages,
|
||||
total: listResponse.resultSizeEstimate || messages.length
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Gmail API error:', e)
|
||||
|
||||
if (e.status === 403) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Gmail 접근 권한이 없습니다. Google 로그인 시 권한을 허용해주세요.'
|
||||
})
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '그룹 메시지를 가져오는데 실패했습니다.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Google 토큰 갱신
|
||||
*/
|
||||
async function refreshGoogleToken(employeeId: number, refreshToken: string): Promise<string> {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const response = await $fetch<any>('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: config.googleClientId,
|
||||
client_secret: config.googleClientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
}).toString(),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
})
|
||||
|
||||
if (response.access_token) {
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET google_access_token = $1,
|
||||
google_token_expires_at = NOW() + INTERVAL '${response.expires_in} seconds'
|
||||
WHERE employee_id = $2
|
||||
`, [response.access_token, employeeId])
|
||||
|
||||
return response.access_token
|
||||
}
|
||||
|
||||
throw new Error('토큰 갱신 실패')
|
||||
}
|
||||
207
backend/api/google-group/share-report.post.ts
Normal file
207
backend/api/google-group/share-report.post.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { queryOne, execute, query } from '../../utils/db'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 주간보고를 구글 그룹에 공유 (이메일 전송)
|
||||
* POST /api/google-group/share-report
|
||||
*
|
||||
* Gmail API로 그룹에 이메일 전송
|
||||
*
|
||||
* Body:
|
||||
* - reportId: 주간보고 ID
|
||||
* - groupEmail: 그룹 이메일 주소
|
||||
* - subject?: 이메일 제목 (기본값 자동 생성)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireAuth(event)
|
||||
const body = await readBody(event)
|
||||
|
||||
const { reportId, groupEmail, subject } = body
|
||||
|
||||
if (!reportId || !groupEmail) {
|
||||
throw createError({ statusCode: 400, message: '보고서 ID와 그룹 이메일이 필요합니다.' })
|
||||
}
|
||||
|
||||
// 주간보고 조회
|
||||
const report = await queryOne<any>(`
|
||||
SELECT r.*, e.employee_name, e.employee_email, p.project_name
|
||||
FROM wr_weekly_report r
|
||||
JOIN wr_employee_info e ON r.employee_id = e.employee_id
|
||||
LEFT JOIN wr_project_info p ON r.project_id = p.project_id
|
||||
WHERE r.report_id = $1
|
||||
`, [reportId])
|
||||
|
||||
if (!report) {
|
||||
throw createError({ statusCode: 404, message: '주간보고를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 권한 확인 (본인 보고서만)
|
||||
if (report.employee_id !== session.employeeId) {
|
||||
throw createError({ statusCode: 403, message: '본인의 주간보고만 공유할 수 있습니다.' })
|
||||
}
|
||||
|
||||
// 사용자의 Google 토큰 조회
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT google_access_token, google_refresh_token, google_token_expires_at, employee_email
|
||||
FROM wr_employee_info WHERE employee_id = $1
|
||||
`, [session.employeeId])
|
||||
|
||||
if (!employee?.google_access_token) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Google 계정이 연결되지 않았습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
let accessToken = employee.google_access_token
|
||||
|
||||
// 토큰 만료 확인 및 갱신
|
||||
if (employee.google_token_expires_at && new Date(employee.google_token_expires_at) < new Date()) {
|
||||
accessToken = await refreshGoogleToken(session.employeeId, employee.google_refresh_token)
|
||||
}
|
||||
|
||||
// 이메일 내용 생성
|
||||
const emailSubject = subject || `[주간보고] ${report.project_name || '개인'} - ${report.report_week}주차 (${report.employee_name})`
|
||||
const emailBody = generateReportEmailBody(report)
|
||||
|
||||
// RFC 2822 형식의 이메일 메시지 생성
|
||||
const emailLines = [
|
||||
`From: ${employee.employee_email}`,
|
||||
`To: ${groupEmail}`,
|
||||
`Subject: =?UTF-8?B?${Buffer.from(emailSubject).toString('base64')}?=`,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'',
|
||||
emailBody
|
||||
]
|
||||
|
||||
const rawEmail = Buffer.from(emailLines.join('\r\n'))
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
try {
|
||||
// Gmail API로 이메일 전송
|
||||
const response = await $fetch<any>('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: { raw: rawEmail }
|
||||
})
|
||||
|
||||
// 공유 이력 저장
|
||||
await execute(`
|
||||
INSERT INTO wr_report_share_log (report_id, shared_to, shared_type, shared_by, message_id)
|
||||
VALUES ($1, $2, 'GOOGLE_GROUP', $3, $4)
|
||||
`, [reportId, groupEmail, session.employeeId, response.id])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${groupEmail}로 주간보고가 공유되었습니다.`,
|
||||
messageId: response.id
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Gmail send error:', e)
|
||||
|
||||
if (e.status === 403) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Gmail 발송 권한이 없습니다. Google 로그인 시 권한을 허용해주세요.'
|
||||
})
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '이메일 발송에 실패했습니다.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 주간보고 이메일 본문 생성
|
||||
*/
|
||||
function generateReportEmailBody(report: any): string {
|
||||
const weekRange = `${report.week_start_date?.split('T')[0] || ''} ~ ${report.week_end_date?.split('T')[0] || ''}`
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family: 'Malgun Gothic', sans-serif; padding: 20px; max-width: 800px;">
|
||||
<h2 style="color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px;">
|
||||
📋 주간업무보고
|
||||
</h2>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td style="padding: 8px; background: #f8f9fa; width: 120px;"><strong>작성자</strong></td>
|
||||
<td style="padding: 8px;">${report.employee_name}</td>
|
||||
<td style="padding: 8px; background: #f8f9fa; width: 120px;"><strong>프로젝트</strong></td>
|
||||
<td style="padding: 8px;">${report.project_name || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; background: #f8f9fa;"><strong>보고 주차</strong></td>
|
||||
<td style="padding: 8px;">${report.report_year}년 ${report.report_week}주차</td>
|
||||
<td style="padding: 8px; background: #f8f9fa;"><strong>기간</strong></td>
|
||||
<td style="padding: 8px;">${weekRange}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 style="color: #28a745;">✅ 금주 실적</h3>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; white-space: pre-wrap;">${report.this_week_work || '(내용 없음)'}</div>
|
||||
|
||||
<h3 style="color: #007bff;">📅 차주 계획</h3>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; white-space: pre-wrap;">${report.next_week_plan || '(내용 없음)'}</div>
|
||||
|
||||
${report.issues ? `
|
||||
<h3 style="color: #dc3545;">⚠️ 이슈사항</h3>
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin-bottom: 20px; white-space: pre-wrap;">${report.issues}</div>
|
||||
` : ''}
|
||||
|
||||
${report.remarks ? `
|
||||
<h3 style="color: #6c757d;">📝 비고</h3>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; white-space: pre-wrap;">${report.remarks}</div>
|
||||
` : ''}
|
||||
|
||||
<hr style="margin-top: 30px; border: none; border-top: 1px solid #ddd;">
|
||||
<p style="color: #6c757d; font-size: 12px;">
|
||||
이 메일은 주간업무보고 시스템에서 자동 발송되었습니다.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Google 토큰 갱신
|
||||
*/
|
||||
async function refreshGoogleToken(employeeId: number, refreshToken: string): Promise<string> {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const response = await $fetch<any>('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: config.googleClientId,
|
||||
client_secret: config.googleClientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
}).toString(),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
})
|
||||
|
||||
if (response.access_token) {
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET google_access_token = $1,
|
||||
google_token_expires_at = NOW() + INTERVAL '${response.expires_in} seconds'
|
||||
WHERE employee_id = $2
|
||||
`, [response.access_token, employeeId])
|
||||
|
||||
return response.access_token
|
||||
}
|
||||
|
||||
throw new Error('토큰 갱신 실패')
|
||||
}
|
||||
Reference in New Issue
Block a user