추가
This commit is contained in:
71
backend/utils/db.ts
Normal file
71
backend/utils/db.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import pg from 'pg'
|
||||
|
||||
const { Pool } = pg
|
||||
|
||||
let pool: pg.Pool | null = null
|
||||
|
||||
/**
|
||||
* PostgreSQL 연결 풀 가져오기
|
||||
*/
|
||||
export function getPool(): pg.Pool {
|
||||
if (!pool) {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const poolConfig = {
|
||||
host: config.dbHost,
|
||||
port: parseInt(config.dbPort as string),
|
||||
database: config.dbName,
|
||||
user: config.dbUser,
|
||||
password: config.dbPassword,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}
|
||||
|
||||
console.log(`[DB] Connecting to ${poolConfig.host}:${poolConfig.port}/${poolConfig.database}`)
|
||||
|
||||
pool = new Pool(poolConfig)
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('[DB] Unexpected pool error:', err)
|
||||
})
|
||||
|
||||
console.log('[DB] PostgreSQL pool created')
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 실행
|
||||
*/
|
||||
export async function query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const pool = getPool()
|
||||
const result = await pool.query(sql, params)
|
||||
return result.rows as T[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 행 조회
|
||||
*/
|
||||
export async function queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
const rows = await query<T>(sql, params)
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT/UPDATE/DELETE 실행
|
||||
*/
|
||||
export async function execute(sql: string, params?: any[]): Promise<number> {
|
||||
const pool = getPool()
|
||||
const result = await pool.query(sql, params)
|
||||
return result.rowCount || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT 후 반환
|
||||
*/
|
||||
export async function insertReturning<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
const pool = getPool()
|
||||
const result = await pool.query(sql, params)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
99
backend/utils/report-scheduler.ts
Normal file
99
backend/utils/report-scheduler.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { query, execute, insertReturning } from './db'
|
||||
import { getLastWeekInfo, formatDate } from './week-calc'
|
||||
|
||||
let isRunning = false
|
||||
|
||||
/**
|
||||
* 주간보고 취합 실행
|
||||
*/
|
||||
export async function aggregateWeeklyReports(targetYear?: number, targetWeek?: number) {
|
||||
const weekInfo = targetYear && targetWeek
|
||||
? { year: targetYear, week: targetWeek }
|
||||
: getLastWeekInfo()
|
||||
|
||||
console.log(`[Aggregator] 취합 시작: ${weekInfo.year}-W${weekInfo.week}`)
|
||||
|
||||
// 해당 주차에 제출된 보고서가 있는 프로젝트 조회
|
||||
const projects = await query<any>(`
|
||||
SELECT DISTINCT project_id
|
||||
FROM wr_weekly_report_detail
|
||||
WHERE report_year = $1 AND report_week = $2 AND report_status = 'SUBMITTED'
|
||||
`, [weekInfo.year, weekInfo.week])
|
||||
|
||||
let aggregatedCount = 0
|
||||
|
||||
for (const { project_id } of projects) {
|
||||
// 해당 프로젝트의 제출된 보고서들
|
||||
const reports = await query<any>(`
|
||||
SELECT report_id, work_hours
|
||||
FROM wr_weekly_report_detail
|
||||
WHERE project_id = $1 AND report_year = $2 AND report_week = $3
|
||||
AND report_status = 'SUBMITTED'
|
||||
`, [project_id, weekInfo.year, weekInfo.week])
|
||||
|
||||
const reportIds = reports.map((r: any) => r.report_id)
|
||||
const totalHours = reports.reduce((sum: number, r: any) => sum + (parseFloat(r.work_hours) || 0), 0)
|
||||
|
||||
// 주차 날짜 계산
|
||||
const jan4 = new Date(weekInfo.year, 0, 4)
|
||||
const firstMonday = new Date(jan4)
|
||||
firstMonday.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7))
|
||||
const targetMonday = new Date(firstMonday)
|
||||
targetMonday.setDate(firstMonday.getDate() + (weekInfo.week - 1) * 7)
|
||||
const targetSunday = new Date(targetMonday)
|
||||
targetSunday.setDate(targetMonday.getDate() + 6)
|
||||
|
||||
// UPSERT 취합 보고서
|
||||
await execute(`
|
||||
INSERT INTO wr_aggregated_report_summary (
|
||||
project_id, report_year, report_week,
|
||||
week_start_date, week_end_date,
|
||||
report_ids, member_count, total_work_hours
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (project_id, report_year, report_week)
|
||||
DO UPDATE SET
|
||||
report_ids = $6,
|
||||
member_count = $7,
|
||||
total_work_hours = $8,
|
||||
aggregated_at = NOW(),
|
||||
updated_at = NOW()
|
||||
`, [
|
||||
project_id,
|
||||
weekInfo.year,
|
||||
weekInfo.week,
|
||||
formatDate(targetMonday),
|
||||
formatDate(targetSunday),
|
||||
reportIds,
|
||||
reportIds.length,
|
||||
totalHours || null
|
||||
])
|
||||
|
||||
// 개별 보고서 상태 변경
|
||||
await execute(`
|
||||
UPDATE wr_weekly_report_detail SET
|
||||
report_status = 'AGGREGATED',
|
||||
updated_at = NOW()
|
||||
WHERE report_id = ANY($1)
|
||||
`, [reportIds])
|
||||
|
||||
aggregatedCount++
|
||||
console.log(`[Aggregator] 프로젝트 ${project_id}: ${reportIds.length}건 취합`)
|
||||
}
|
||||
|
||||
console.log(`[Aggregator] 취합 완료: ${aggregatedCount}개 프로젝트`)
|
||||
|
||||
return {
|
||||
year: weekInfo.year,
|
||||
week: weekInfo.week,
|
||||
projectCount: aggregatedCount
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러 상태
|
||||
*/
|
||||
export function getSchedulerStatus() {
|
||||
return {
|
||||
isRunning
|
||||
}
|
||||
}
|
||||
95
backend/utils/week-calc.ts
Normal file
95
backend/utils/week-calc.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* ISO 8601 주차 계산 유틸리티
|
||||
*/
|
||||
|
||||
export interface WeekInfo {
|
||||
year: number
|
||||
week: number
|
||||
startDate: Date // 월요일
|
||||
endDate: Date // 일요일
|
||||
startDateStr: string // YYYY-MM-DD
|
||||
endDateStr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 YYYY-MM-DD 문자열로 변환
|
||||
*/
|
||||
export function formatDate(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 ISO 주차 정보 반환
|
||||
*/
|
||||
export function getWeekInfo(date: Date = new Date()): WeekInfo {
|
||||
const target = new Date(date)
|
||||
target.setHours(0, 0, 0, 0)
|
||||
|
||||
// 목요일 기준으로 연도 판단 (ISO 규칙)
|
||||
const thursday = new Date(target)
|
||||
thursday.setDate(target.getDate() - ((target.getDay() + 6) % 7) + 3)
|
||||
|
||||
const year = thursday.getFullYear()
|
||||
const firstThursday = new Date(year, 0, 4)
|
||||
firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3)
|
||||
|
||||
const week = Math.ceil((thursday.getTime() - firstThursday.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1
|
||||
|
||||
// 해당 주의 월요일
|
||||
const monday = new Date(target)
|
||||
monday.setDate(target.getDate() - ((target.getDay() + 6) % 7))
|
||||
|
||||
// 해당 주의 일요일
|
||||
const sunday = new Date(monday)
|
||||
sunday.setDate(monday.getDate() + 6)
|
||||
|
||||
return {
|
||||
year,
|
||||
week,
|
||||
startDate: monday,
|
||||
endDate: sunday,
|
||||
startDateStr: formatDate(monday),
|
||||
endDateStr: formatDate(sunday)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 주차 문자열 반환 (예: "2026-W01")
|
||||
*/
|
||||
export function formatWeekString(year: number, week: number): string {
|
||||
return `${year}-W${week.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 지난 주 정보
|
||||
*/
|
||||
export function getLastWeekInfo(): WeekInfo {
|
||||
const lastWeek = new Date()
|
||||
lastWeek.setDate(lastWeek.getDate() - 7)
|
||||
return getWeekInfo(lastWeek)
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 연도/주차의 날짜 범위 반환
|
||||
*/
|
||||
export function getWeekDates(year: number, week: number): { startDate: Date, endDate: Date } {
|
||||
// 해당 연도의 1월 4일 (1주차에 반드시 포함)
|
||||
const jan4 = new Date(year, 0, 4)
|
||||
|
||||
// 1월 4일이 포함된 주의 월요일
|
||||
const firstMonday = new Date(jan4)
|
||||
firstMonday.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7))
|
||||
|
||||
// 원하는 주차의 월요일
|
||||
const targetMonday = new Date(firstMonday)
|
||||
targetMonday.setDate(firstMonday.getDate() + (week - 1) * 7)
|
||||
|
||||
// 일요일
|
||||
const targetSunday = new Date(targetMonday)
|
||||
targetSunday.setDate(targetMonday.getDate() + 6)
|
||||
|
||||
return { startDate: targetMonday, endDate: targetSunday }
|
||||
}
|
||||
Reference in New Issue
Block a user