기능구현중

This commit is contained in:
2026-01-11 14:41:41 +09:00
parent 0205e8d437
commit 17852cc5dc
7 changed files with 434 additions and 115 deletions

View File

@@ -0,0 +1,87 @@
import { queryOne } from '../../../utils/db'
import { requireAuth, getCurrentUser } from '../../../utils/session'
import { getValidGoogleToken } from '../../../utils/google-token'
/**
* 구글 그룹 메시지 목록 조회
* GET /api/google-group/[id]/messages
*/
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
const groupId = parseInt(getRouterParam(event, 'id') || '0')
const queryParams = getQuery(event)
if (!groupId) {
throw createError({ statusCode: 400, message: '그룹 ID가 필요합니다.' })
}
// 그룹 정보 조회
const group = await queryOne<any>(`
SELECT group_email, group_name FROM wr_google_group WHERE group_id = $1
`, [groupId])
if (!group) {
throw createError({ statusCode: 404, message: '그룹을 찾을 수 없습니다.' })
}
// Google 토큰 확인
const accessToken = await getValidGoogleToken(user.employeeId)
if (!accessToken) {
throw createError({
statusCode: 401,
message: 'Google 계정 연결이 필요합니다.'
})
}
const maxResults = parseInt(queryParams.limit as string) || 20
const pageToken = queryParams.pageToken as string || ''
try {
// Gmail API로 그룹 메일 검색
const searchQuery = encodeURIComponent(`from:${group.group_email} OR to:${group.group_email}`)
let url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${searchQuery}&maxResults=${maxResults}`
if (pageToken) url += `&pageToken=${pageToken}`
const listRes = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
})
if (!listRes.ok) {
throw createError({ statusCode: 500, message: 'Gmail API 오류' })
}
const listData = await listRes.json()
const messages: any[] = []
// 각 메시지 상세 정보 조회 (최대 10개)
for (const msg of (listData.messages || []).slice(0, 10)) {
const detailRes = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
)
if (detailRes.ok) {
const detail = await detailRes.json()
const headers = detail.payload?.headers || []
messages.push({
messageId: msg.id,
threadId: msg.threadId,
subject: headers.find((h: any) => h.name === 'Subject')?.value || '(제목 없음)',
from: headers.find((h: any) => h.name === 'From')?.value || '',
date: headers.find((h: any) => h.name === 'Date')?.value || '',
snippet: detail.snippet || ''
})
}
}
return {
group: { groupId, groupEmail: group.group_email, groupName: group.group_name },
messages,
nextPageToken: listData.nextPageToken || null
}
} catch (e: any) {
throw createError({ statusCode: 500, message: e.message || '메시지 조회 실패' })
}
})

View File

@@ -0,0 +1,23 @@
import { execute, insertReturning } from '../../utils/db'
import { requireAuth } from '../../utils/session'
/**
* 구글 그룹 등록
* POST /api/google-group/create
*/
export default defineEventHandler(async (event) => {
await requireAuth(event)
const body = await readBody(event)
if (!body.groupEmail || !body.groupName) {
throw createError({ statusCode: 400, message: '그룹 이메일과 이름은 필수입니다.' })
}
const result = await insertReturning(`
INSERT INTO wr_google_group (group_email, group_name, description)
VALUES ($1, $2, $3)
RETURNING group_id
`, [body.groupEmail.toLowerCase().trim(), body.groupName.trim(), body.description || null])
return { success: true, groupId: result.group_id, message: '그룹이 등록되었습니다.' }
})

View File

@@ -1,102 +1,28 @@
import { queryOne } from '../../utils/db'
import { query } 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)
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])
const groups = await query(`
SELECT group_id, group_email, group_name, description, is_active, created_at
FROM wr_google_group
WHERE is_active = true
ORDER BY group_name
`)
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: '구글 그룹 목록을 가져오는데 실패했습니다.'
})
return {
groups: groups.map(g => ({
groupId: g.group_id,
groupEmail: g.group_email,
groupName: g.group_name,
description: g.description,
isActive: g.is_active,
createdAt: g.created_at
}))
}
})
/**
* 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('토큰 갱신 실패')
}

View File

@@ -0,0 +1,100 @@
import { query, queryOne, insertReturning } from '../../../../utils/db'
import { requireAuth } from '../../../../utils/session'
import { getValidGoogleToken } from '../../../../utils/google-token'
/**
* 주간보고 그룹 공유 (Gmail 발송)
* POST /api/report/weekly/[id]/share
*/
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
const reportId = parseInt(getRouterParam(event, 'id') || '0')
const body = await readBody(event)
if (!reportId) {
throw createError({ statusCode: 400, message: '보고서 ID가 필요합니다.' })
}
const groupIds = body.groupIds as number[]
if (!groupIds?.length) {
throw createError({ statusCode: 400, message: '공유할 그룹을 선택해주세요.' })
}
// 보고서 조회
const report = await queryOne<any>(`
SELECT r.*, e.employee_name, e.employee_email,
p.project_name, p.project_code
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: '보고서를 찾을 수 없습니다.' })
}
// Google 토큰 확인
const accessToken = await getValidGoogleToken(user.employeeId)
if (!accessToken) {
throw createError({ statusCode: 401, message: 'Google 계정 연결이 필요합니다.' })
}
// 선택된 그룹 조회
const groups = await query<any>(`
SELECT group_id, group_email, group_name
FROM wr_google_group WHERE group_id = ANY($1) AND is_active = true
`, [groupIds])
if (!groups.length) {
throw createError({ statusCode: 400, message: '유효한 그룹이 없습니다.' })
}
// 이메일 제목 및 본문 생성
const weekInfo = `${report.report_year}${report.report_week}주차`
const subject = `[주간보고] ${report.project_name || '개인'} - ${weekInfo} (${report.employee_name})`
const emailBody = buildEmailBody(report)
// 각 그룹에 발송
const results: any[] = []
for (const group of groups) {
try {
const rawEmail = createRawEmail({
to: group.group_email, subject, body: emailBody, from: user.employeeEmail
})
const sendRes = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ raw: rawEmail })
})
if (sendRes.ok) {
results.push({ groupId: group.group_id, groupName: group.group_name, success: true })
} else {
const err = await sendRes.json()
results.push({ groupId: group.group_id, groupName: group.group_name, success: false, error: err.error?.message })
}
} catch (e: any) {
results.push({ groupId: group.group_id, groupName: group.group_name, success: false, error: e.message })
}
}
return { success: results.some(r => r.success), message: `${results.filter(r => r.success).length}/${groups.length}개 그룹에 공유됨`, results }
})
function buildEmailBody(report: any): string {
return `<html><body style="font-family:sans-serif;line-height:1.6">
<h2>📋 주간보고 - ${report.report_year}${report.report_week}주차</h2>
<p><b>작성자:</b> ${report.employee_name} | <b>프로젝트:</b> ${report.project_name || '개인'}</p>
<hr><h3>📌 금주 실적</h3><div style="background:#f5f5f5;padding:15px;border-radius:5px">${(report.this_week_work || '').replace(/\n/g, '<br>')}</div>
<h3>📅 차주 계획</h3><div style="background:#f5f5f5;padding:15px;border-radius:5px">${(report.next_week_plan || '').replace(/\n/g, '<br>')}</div>
${report.issues ? `<h3>⚠️ 이슈</h3><div style="background:#fff3cd;padding:15px;border-radius:5px">${report.issues.replace(/\n/g, '<br>')}</div>` : ''}
<hr><p style="color:#666;font-size:12px">주간업무보고 시스템에서 발송</p></body></html>`
}
function createRawEmail(opts: { to: string; subject: string; body: string; from: string }): string {
const email = [`From: ${opts.from}`, `To: ${opts.to}`, `Subject: =?UTF-8?B?${Buffer.from(opts.subject).toString('base64')}?=`, 'MIME-Version: 1.0', 'Content-Type: text/html; charset=UTF-8', '', opts.body].join('\r\n')
return Buffer.from(email).toString('base64url')
}