기능구현중

This commit is contained in:
2026-01-11 17:01:01 +09:00
parent 375d5bf91a
commit 954ba21211
148 changed files with 2276 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import { query, execute } from '../../../../utils/db'
import { requireAuth } from '../../../../utils/session'
const ADMIN_EMAIL = 'coziny@gmail.com'
/**
* 주간보고 삭제
* DELETE /api/report/weekly/[id]/delete
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
const reportId = getRouterParam(event, 'id')
if (!reportId) {
throw createError({ statusCode: 400, message: '보고서 ID가 필요합니다.' })
}
// 현재 사용자 정보 조회
const currentUser = await query<any>(`
SELECT employee_email FROM wr_employee_info WHERE employee_id = $1
`, [userId])
const isAdmin = currentUser[0]?.employee_email === ADMIN_EMAIL
// 보고서 정보 조회
const report = await query<any>(`
SELECT report_id, author_id FROM wr_weekly_report WHERE report_id = $1
`, [reportId])
if (!report[0]) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
// 권한 체크: 본인 또는 관리자만 삭제 가능
if (report[0].author_id !== userId && !isAdmin) {
throw createError({ statusCode: 403, message: '삭제 권한이 없습니다.' })
}
// 프로젝트 실적 먼저 삭제
await execute(`DELETE FROM wr_weekly_report_project WHERE report_id = $1`, [reportId])
// 주간보고 삭제
await execute(`DELETE FROM wr_weekly_report WHERE report_id = $1`, [reportId])
return {
success: true,
message: '주간보고가 삭제되었습니다.'
}
})

View File

@@ -0,0 +1,136 @@
import { query, queryOne } from '../../../../utils/db'
import { requireAuth } from '../../../../utils/session'
/**
* 주간보고 상세 조회
* GET /api/report/weekly/[id]/detail
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
const reportId = getRouterParam(event, 'id')
// 마스터 조회
const report = await queryOne<any>(`
SELECT
r.*,
e.employee_name as author_name,
e.employee_email as author_email
FROM wr_weekly_report r
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE r.report_id = $1
`, [reportId])
if (!report) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
// 같은 주차의 이전/다음 보고서 조회
const prevReport = await queryOne<any>(`
SELECT r.report_id, e.employee_name
FROM wr_weekly_report r
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE r.report_year = $1 AND r.report_week = $2 AND r.report_id < $3
ORDER BY r.report_id DESC
LIMIT 1
`, [report.report_year, report.report_week, reportId])
const nextReport = await queryOne<any>(`
SELECT r.report_id, e.employee_name
FROM wr_weekly_report r
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE r.report_year = $1 AND r.report_week = $2 AND r.report_id > $3
ORDER BY r.report_id ASC
LIMIT 1
`, [report.report_year, report.report_week, reportId])
// Task 조회
const tasks = await query<any>(`
SELECT
t.task_id,
t.project_id,
p.project_code,
p.project_name,
t.task_type,
t.task_description,
t.task_hours,
t.is_completed
FROM wr_weekly_report_task t
JOIN wr_project_info p ON t.project_id = p.project_id
WHERE t.report_id = $1
ORDER BY t.project_id, t.task_type, t.task_id
`, [reportId])
// 프로젝트별로 그룹핑
const projectMap = new Map<number, any>()
for (const task of tasks) {
if (!projectMap.has(task.project_id)) {
projectMap.set(task.project_id, {
projectId: task.project_id,
projectCode: task.project_code,
projectName: task.project_name,
workTasks: [],
planTasks: []
})
}
const proj = projectMap.get(task.project_id)
const taskItem = {
taskId: task.task_id,
description: task.task_description,
hours: parseFloat(task.task_hours) || 0,
isCompleted: task.is_completed
}
if (task.task_type === 'WORK') {
proj.workTasks.push(taskItem)
} else {
proj.planTasks.push(taskItem)
}
}
return {
report: {
reportId: report.report_id,
authorId: report.author_id,
authorName: report.author_name,
authorEmail: report.author_email,
reportYear: report.report_year,
reportWeek: report.report_week,
weekStartDate: formatDateOnly(report.week_start_date),
weekEndDate: formatDateOnly(report.week_end_date),
issueDescription: report.issue_description,
vacationDescription: report.vacation_description,
remarkDescription: report.remark_description,
reportStatus: report.report_status,
submittedAt: report.submitted_at,
createdAt: report.created_at,
updatedAt: report.updated_at,
aiReview: report.ai_review,
aiReviewAt: report.ai_review_at
},
prevReport: prevReport ? { reportId: prevReport.report_id, authorName: prevReport.employee_name } : null,
nextReport: nextReport ? { reportId: nextReport.report_id, authorName: nextReport.employee_name } : null,
projects: Array.from(projectMap.values()),
tasks: tasks.map((t: any) => ({
taskId: t.task_id,
projectId: t.project_id,
projectCode: t.project_code,
projectName: t.project_name,
taskType: t.task_type,
taskDescription: t.task_description,
taskHours: parseFloat(t.task_hours) || 0,
isCompleted: t.is_completed
}))
}
})
// 날짜를 YYYY-MM-DD 형식으로 변환 (타임존 보정)
function formatDateOnly(date: Date | string | null): string {
if (!date) return ''
const d = new Date(date)
const kstOffset = 9 * 60 * 60 * 1000
const kstDate = new Date(d.getTime() + kstOffset)
return kstDate.toISOString().split('T')[0]
}

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')
}

View File

@@ -0,0 +1,45 @@
import { execute, queryOne } from '../../../../utils/db'
import { getClientIp } from '../../../../utils/ip'
import { getCurrentUserEmail } from '../../../../utils/user'
import { requireAuth } from '../../../../utils/session'
/**
* 주간보고 제출
* POST /api/report/weekly/[id]/submit
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
const reportId = getRouterParam(event, 'id')
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
// 보고서 조회 및 권한 확인
const report = await queryOne<any>(`
SELECT * FROM wr_weekly_report WHERE report_id = $1
`, [reportId])
if (!report) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
if (report.author_id !== userId) {
throw createError({ statusCode: 403, message: '본인의 보고서만 제출할 수 있습니다.' })
}
if (report.report_status !== 'DRAFT') {
throw createError({ statusCode: 400, message: '이미 제출된 보고서입니다.' })
}
await execute(`
UPDATE wr_weekly_report SET
report_status = 'SUBMITTED',
submitted_at = NOW(),
updated_at = NOW(),
updated_ip = $1,
updated_email = $2
WHERE report_id = $3
`, [clientIp, userEmail, reportId])
return { success: true }
})

View File

@@ -0,0 +1,104 @@
import { query, execute, queryOne } from '../../../../utils/db'
import { requireAuth } from '../../../../utils/session'
const ADMIN_EMAIL = 'coziny@gmail.com'
/**
* 주간보고 수정
* PUT /api/report/weekly/[id]/update
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
const reportId = getRouterParam(event, 'id')
const clientIp = getHeader(event, 'x-forwarded-for') || 'unknown'
const user = await queryOne<any>(`SELECT employee_email FROM wr_employee_info WHERE employee_id = $1`, [userId])
const userEmail = user?.employee_email || ''
const isAdmin = userEmail === ADMIN_EMAIL
// 보고서 조회 및 권한 체크
const report = await queryOne<any>(`
SELECT report_id, author_id, report_status FROM wr_weekly_report WHERE report_id = $1
`, [reportId])
if (!report) {
throw createError({ statusCode: 404, message: '보고서를 찾을 수 없습니다.' })
}
// 관리자가 아니면 본인 보고서만 수정 가능
if (!isAdmin && report.author_id !== userId) {
throw createError({ statusCode: 403, message: '본인의 보고서만 수정할 수 있습니다.' })
}
// 취합완료된 보고서는 수정 불가 (관리자도)
if (report.report_status === 'AGGREGATED') {
throw createError({ statusCode: 400, message: '취합완료된 보고서는 수정할 수 없습니다.' })
}
const body = await readBody<{
reportYear?: number
reportWeek?: number
weekStartDate?: string
weekEndDate?: string
tasks: {
projectId: number
taskType: 'WORK' | 'PLAN'
taskDescription: string
taskHours: number
isCompleted?: boolean
}[]
issueDescription?: string
vacationDescription?: string
remarkDescription?: string
}>(event)
if (!body.tasks || body.tasks.length === 0) {
throw createError({ statusCode: 400, message: '최소 1개 이상의 Task가 필요합니다.' })
}
// 마스터 수정
await execute(`
UPDATE wr_weekly_report SET
report_year = COALESCE($1, report_year),
report_week = COALESCE($2, report_week),
week_start_date = COALESCE($3, week_start_date),
week_end_date = COALESCE($4, week_end_date),
issue_description = $5,
vacation_description = $6,
remark_description = $7,
updated_at = NOW(),
updated_ip = $8,
updated_email = $9
WHERE report_id = $10
`, [
body.reportYear || null,
body.reportWeek || null,
body.weekStartDate || null,
body.weekEndDate || null,
body.issueDescription || null,
body.vacationDescription || null,
body.remarkDescription || null,
clientIp, userEmail, reportId
])
// 기존 Task 삭제 후 재등록
await execute(`DELETE FROM wr_weekly_report_task WHERE report_id = $1`, [reportId])
for (const task of body.tasks) {
await execute(`
INSERT INTO wr_weekly_report_task (
report_id, project_id, task_type, task_description, task_hours, is_completed,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $7, $8)
`, [
reportId, task.projectId, task.taskType, task.taskDescription, task.taskHours || 0,
task.taskType === 'WORK' ? (task.isCompleted !== false) : null,
clientIp, userEmail
])
}
return {
success: true,
message: '주간보고가 수정되었습니다.'
}
})

View File

@@ -0,0 +1,131 @@
import { defineEventHandler, getQuery, createError } from 'h3'
import { query } from '../../../utils/db'
const ADMIN_EMAIL = 'admin@turbosoft.co.kr'
export default defineEventHandler(async (event) => {
const userEmail = event.node.req.headers['x-user-email'] as string
if (!userEmail) {
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
}
// 관리자만 취합 가능
if (userEmail !== ADMIN_EMAIL) {
throw createError({ statusCode: 403, message: '관리자만 취합할 수 있습니다.' })
}
const { year, week, projectIds } = getQuery(event)
if (!year || !week) {
throw createError({ statusCode: 400, message: '연도와 주차를 지정해주세요.' })
}
// 프로젝트 ID 파싱
let projectIdList: number[] = []
if (projectIds) {
projectIdList = String(projectIds).split(',').map(Number).filter(n => !isNaN(n))
}
// 해당 주차의 모든 프로젝트별 Task 조회
let projectFilter = ''
const params: any[] = [Number(year), Number(week)]
if (projectIdList.length > 0) {
projectFilter = `AND t.project_id = ANY($3)`
params.push(projectIdList)
}
const tasks = await query(`
SELECT
t.task_id,
t.project_id,
p.project_code,
p.project_name,
t.task_type,
t.task_description,
t.task_hours,
t.is_completed,
r.report_id,
r.author_id,
e.employee_name as author_name
FROM wr_weekly_report r
JOIN wr_weekly_report_task t ON r.report_id = t.report_id
JOIN wr_project_info p ON t.project_id = p.project_id
JOIN wr_employee_info e ON r.author_id = e.employee_id
WHERE r.report_year = $1 AND r.report_week = $2
${projectFilter}
ORDER BY p.project_name, t.task_type, e.employee_name
`, params)
// 프로젝트별로 그룹핑
const projectMap = new Map<number, {
projectId: number
projectCode: string
projectName: string
workTasks: any[]
planTasks: any[]
totalWorkHours: number
totalPlanHours: number
}>()
for (const task of tasks) {
if (!projectMap.has(task.project_id)) {
projectMap.set(task.project_id, {
projectId: task.project_id,
projectCode: task.project_code,
projectName: task.project_name,
workTasks: [],
planTasks: [],
totalWorkHours: 0,
totalPlanHours: 0
})
}
const proj = projectMap.get(task.project_id)!
const taskItem = {
taskId: task.task_id,
description: task.task_description,
hours: parseFloat(task.task_hours) || 0,
isCompleted: task.is_completed,
authorName: task.author_name
}
if (task.task_type === 'WORK') {
proj.workTasks.push(taskItem)
proj.totalWorkHours += taskItem.hours
} else {
proj.planTasks.push(taskItem)
proj.totalPlanHours += taskItem.hours
}
}
// 해당 주차에 보고서가 있는 모든 프로젝트 목록
const allProjects = await query(`
SELECT DISTINCT p.project_id, p.project_code, p.project_name
FROM wr_weekly_report r
JOIN wr_weekly_report_task t ON r.report_id = t.report_id
JOIN wr_project_info p ON t.project_id = p.project_id
WHERE r.report_year = $1 AND r.report_week = $2
ORDER BY p.project_name
`, [Number(year), Number(week)])
// 해당 주차 보고서 수
const reportCount = await query(`
SELECT COUNT(DISTINCT report_id) as cnt
FROM wr_weekly_report
WHERE report_year = $1 AND report_week = $2
`, [Number(year), Number(week)])
return {
year: Number(year),
week: Number(week),
reportCount: parseInt(reportCount[0]?.cnt || '0'),
availableProjects: allProjects.map((p: any) => ({
projectId: p.project_id,
projectCode: p.project_code,
projectName: p.project_name
})),
projects: Array.from(projectMap.values())
}
})

View File

@@ -0,0 +1,87 @@
import { query, execute, queryOne } from '../../../utils/db'
import { requireAuth } from '../../../utils/session'
/**
* 주간보고 작성
* POST /api/report/weekly/create
*/
export default defineEventHandler(async (event) => {
// 세션 기반 인증 사용 (레거시 쿠키 대신)
const userId = await requireAuth(event)
const clientIp = getHeader(event, 'x-forwarded-for') || 'unknown'
const user = await queryOne<any>(`SELECT employee_email FROM wr_employee_info WHERE employee_id = $1`, [userId])
const userEmail = user?.employee_email || ''
const body = await readBody<{
reportYear: number
reportWeek: number
weekStartDate: string
weekEndDate: string
tasks: {
projectId: number
taskType: 'WORK' | 'PLAN'
taskDescription: string
taskHours: number
isCompleted?: boolean
}[]
issueDescription?: string
vacationDescription?: string
remarkDescription?: string
}>(event)
// 필수값 체크
if (!body.reportYear || !body.reportWeek || !body.weekStartDate || !body.weekEndDate) {
throw createError({ statusCode: 400, message: '주차 정보가 필요합니다.' })
}
if (!body.tasks || body.tasks.length === 0) {
throw createError({ statusCode: 400, message: '최소 1개 이상의 Task가 필요합니다.' })
}
// 중복 체크
const existing = await queryOne<any>(`
SELECT report_id FROM wr_weekly_report
WHERE author_id = $1 AND report_year = $2 AND report_week = $3
`, [userId, body.reportYear, body.reportWeek])
if (existing) {
throw createError({ statusCode: 409, message: '해당 주차에 이미 작성된 보고서가 있습니다.' })
}
// 마스터 등록
const result = await queryOne<any>(`
INSERT INTO wr_weekly_report (
author_id, report_year, report_week, week_start_date, week_end_date,
issue_description, vacation_description, remark_description,
report_status, created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'DRAFT', $9, $10, $9, $10)
RETURNING report_id
`, [
userId, body.reportYear, body.reportWeek, body.weekStartDate, body.weekEndDate,
body.issueDescription || null, body.vacationDescription || null, body.remarkDescription || null,
clientIp, userEmail
])
const reportId = result.report_id
// Task 등록
for (const task of body.tasks) {
await execute(`
INSERT INTO wr_weekly_report_task (
report_id, project_id, task_type, task_description, task_hours, is_completed,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $7, $8)
`, [
reportId, task.projectId, task.taskType, task.taskDescription, task.taskHours || 0,
task.taskType === 'WORK' ? (task.isCompleted !== false) : null,
clientIp, userEmail
])
}
return {
success: true,
reportId,
message: '주간보고가 작성되었습니다.'
}
})

View File

@@ -0,0 +1,44 @@
import { query } from '../../../utils/db'
import { getWeekInfo, formatWeekString } from '../../../utils/week-calc'
import { requireAuth } from '../../../utils/session'
/**
* 이번 주 보고서 현황 조회
* GET /api/report/weekly/current-week
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
const weekInfo = getWeekInfo()
// 이번 주 내 보고서 목록
const reports = await query(`
SELECT r.*, p.project_name, p.project_code
FROM wr_weekly_report_detail r
JOIN wr_project_info p ON r.project_id = p.project_id
WHERE r.author_id = $1 AND r.report_year = $2 AND r.report_week = $3
ORDER BY p.project_name
`, [userId, weekInfo.year, weekInfo.week])
return {
weekInfo: {
year: weekInfo.year,
week: weekInfo.week,
weekString: formatWeekString(weekInfo.year, weekInfo.week),
startDate: weekInfo.startDateStr,
endDate: weekInfo.endDateStr
},
reports: reports.map((r: any) => ({
reportId: r.report_id,
projectId: r.project_id,
projectName: r.project_name,
projectCode: r.project_code,
reportStatus: r.report_status,
workDescription: r.work_description,
planDescription: r.plan_description,
issueDescription: r.issue_description,
workHours: r.work_hours,
updatedAt: r.updated_at
}))
}
})

View File

@@ -0,0 +1,171 @@
import { query } from '../../../utils/db'
import { requireAuth } from '../../../utils/session'
const ADMIN_EMAIL = 'coziny@gmail.com'
/**
* 주간보고 목록 조회 (필터링 지원)
* GET /api/report/weekly/list
*
* Query params:
* - authorId: 작성자 ID
* - projectId: 프로젝트 ID
* - year: 연도
* - weekFrom: 시작 주차
* - weekTo: 종료 주차
* - startDate: 시작일 (YYYY-MM-DD)
* - endDate: 종료일 (YYYY-MM-DD)
* - status: 상태 (DRAFT, SUBMITTED, AGGREGATED)
* - viewAll: 전체 조회 (관리자만)
* - limit: 조회 개수 (기본 100)
*/
export default defineEventHandler(async (event) => {
// 세션 기반 인증 사용
const userId = await requireAuth(event)
// 현재 사용자 정보 조회 (관리자 여부 확인)
const currentUser = await query<any>(`
SELECT employee_email FROM wr_employee_info WHERE employee_id = $1
`, [userId])
const isAdmin = currentUser[0]?.employee_email === ADMIN_EMAIL
const q = getQuery(event)
const limit = parseInt(q.limit as string) || 100
// 필터 조건 구성
const conditions: string[] = []
const params: any[] = []
let paramIndex = 1
// 작성자 필터 (선택된 경우에만 적용)
if (q.authorId) {
conditions.push(`r.author_id = $${paramIndex++}`)
params.push(q.authorId)
}
// 프로젝트 필터
if (q.projectId) {
conditions.push(`EXISTS (
SELECT 1 FROM wr_weekly_report_project wrp
WHERE wrp.report_id = r.report_id AND wrp.project_id = $${paramIndex++}
)`)
params.push(q.projectId)
}
// 연도 필터
if (q.year) {
conditions.push(`r.report_year = $${paramIndex++}`)
params.push(q.year)
}
// 주차 필터 (단일)
if (q.week) {
conditions.push(`r.report_week = $${paramIndex++}`)
params.push(q.week)
}
// 특정 주차 이전 필터 (beforeYear, beforeWeek)
if (q.beforeYear && q.beforeWeek) {
conditions.push(`(r.report_year < $${paramIndex} OR (r.report_year = $${paramIndex} AND r.report_week < $${paramIndex + 1}))`)
params.push(q.beforeYear)
paramIndex++
params.push(q.beforeWeek)
paramIndex++
}
// 주차 범위 필터
if (q.weekFrom) {
conditions.push(`r.report_week >= $${paramIndex++}`)
params.push(q.weekFrom)
}
if (q.weekTo) {
conditions.push(`r.report_week <= $${paramIndex++}`)
params.push(q.weekTo)
}
// 날짜 범위 필터
if (q.startDate) {
conditions.push(`r.week_start_date >= $${paramIndex++}`)
params.push(q.startDate)
}
if (q.endDate) {
conditions.push(`r.week_end_date <= $${paramIndex++}`)
params.push(q.endDate)
}
// 상태 필터
if (q.status) {
conditions.push(`r.report_status = $${paramIndex++}`)
params.push(q.status)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
params.push(limit)
const reports = await query<any>(`
SELECT
r.report_id,
r.author_id,
e.employee_name as author_name,
e.employee_email as author_email,
r.report_year,
r.report_week,
r.week_start_date,
r.week_end_date,
r.issue_description,
r.vacation_description,
r.report_status,
r.submitted_at,
r.created_at,
r.updated_at,
r.ai_review,
(SELECT COUNT(DISTINCT project_id) FROM wr_weekly_report_task WHERE report_id = r.report_id) as project_count,
(SELECT string_agg(DISTINCT p.project_name, ', ')
FROM wr_weekly_report_task t
JOIN wr_project_info p ON t.project_id = p.project_id
WHERE t.report_id = r.report_id) as project_names,
(SELECT COALESCE(SUM(task_hours), 0) FROM wr_weekly_report_task WHERE report_id = r.report_id AND task_type = 'WORK') as total_work_hours,
(SELECT COALESCE(SUM(task_hours), 0) FROM wr_weekly_report_task WHERE report_id = r.report_id AND task_type = 'PLAN') as total_plan_hours
FROM wr_weekly_report r
JOIN wr_employee_info e ON r.author_id = e.employee_id
${whereClause}
ORDER BY r.report_year DESC, r.report_week DESC, e.employee_name
LIMIT $${paramIndex}
`, params)
return {
isAdmin,
reports: reports.map((r: any) => ({
reportId: r.report_id,
authorId: r.author_id,
authorName: r.author_name,
authorEmail: r.author_email,
reportYear: r.report_year,
reportWeek: r.report_week,
weekStartDate: formatDateOnly(r.week_start_date),
weekEndDate: formatDateOnly(r.week_end_date),
issueDescription: r.issue_description,
vacationDescription: r.vacation_description,
reportStatus: r.report_status,
submittedAt: r.submitted_at,
createdAt: r.created_at,
updatedAt: r.updated_at,
aiReview: r.ai_review,
projectCount: parseInt(r.project_count),
projectNames: r.project_names,
totalWorkHours: parseFloat(r.total_work_hours) || 0,
totalPlanHours: parseFloat(r.total_plan_hours) || 0
}))
}
})
// 날짜를 YYYY-MM-DD 형식으로 변환 (타임존 보정)
function formatDateOnly(date: Date | string | null): string {
if (!date) return ''
const d = new Date(date)
// 한국 시간 기준으로 날짜만 추출
const kstOffset = 9 * 60 * 60 * 1000
const kstDate = new Date(d.getTime() + kstOffset)
return kstDate.toISOString().split('T')[0]
}