기능구현중

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,131 @@
import { query } from '../../../utils/db'
import { requireAuth } from '../../../utils/session'
/**
* 프로젝트 커밋 목록 조회
* GET /api/project/[id]/commits
*
* Query params:
* - startDate: 시작일 (YYYY-MM-DD)
* - endDate: 종료일 (YYYY-MM-DD)
* - repoId: 저장소 ID (옵션)
* - authorId: 작성자 ID (옵션)
* - page: 페이지 번호 (기본 1)
* - limit: 페이지당 개수 (기본 50)
*/
export default defineEventHandler(async (event) => {
await requireAuth(event)
const projectId = parseInt(getRouterParam(event, 'id') || '0')
const queryParams = getQuery(event)
if (!projectId) {
throw createError({ statusCode: 400, message: '프로젝트 ID가 필요합니다.' })
}
const startDate = queryParams.startDate as string
const endDate = queryParams.endDate as string
const repoId = queryParams.repoId ? parseInt(queryParams.repoId as string) : null
const authorId = queryParams.authorId ? parseInt(queryParams.authorId as string) : null
const page = parseInt(queryParams.page as string) || 1
const limit = Math.min(parseInt(queryParams.limit as string) || 50, 100)
const offset = (page - 1) * limit
// 조건 빌드
const conditions = ['r.project_id = $1']
const values: any[] = [projectId]
let paramIndex = 2
if (startDate) {
conditions.push(`c.commit_date >= $${paramIndex++}`)
values.push(startDate)
}
if (endDate) {
conditions.push(`c.commit_date <= $${paramIndex++}::date + INTERVAL '1 day'`)
values.push(endDate)
}
if (repoId) {
conditions.push(`c.repo_id = $${paramIndex++}`)
values.push(repoId)
}
if (authorId) {
conditions.push(`c.employee_id = $${paramIndex++}`)
values.push(authorId)
}
const whereClause = conditions.join(' AND ')
// 커밋 목록 조회
const commits = await query(`
SELECT
c.commit_id, c.commit_hash, c.commit_message, c.commit_author, c.commit_email,
c.commit_date, c.employee_id, c.files_changed, c.insertions, c.deletions,
r.repo_id, r.repo_name, r.repo_path,
s.server_type, s.server_name,
e.employee_name, e.display_name
FROM wr_commit_log c
JOIN wr_repository r ON c.repo_id = r.repo_id
JOIN wr_vcs_server s ON r.server_id = s.server_id
LEFT JOIN wr_employee_info e ON c.employee_id = e.employee_id
WHERE ${whereClause}
ORDER BY c.commit_date DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
`, [...values, limit, offset])
// 전체 개수 조회
const countResult = await query(`
SELECT COUNT(*) as total
FROM wr_commit_log c
JOIN wr_repository r ON c.repo_id = r.repo_id
WHERE ${whereClause}
`, values)
const total = parseInt(countResult[0]?.total || '0')
// 통계 (해당 기간)
const statsResult = await query(`
SELECT
COUNT(*) as commit_count,
COALESCE(SUM(c.insertions), 0) as total_insertions,
COALESCE(SUM(c.deletions), 0) as total_deletions,
COUNT(DISTINCT c.employee_id) as author_count
FROM wr_commit_log c
JOIN wr_repository r ON c.repo_id = r.repo_id
WHERE ${whereClause}
`, values)
return {
commits: commits.map(c => ({
commitId: c.commit_id,
commitHash: c.commit_hash,
commitMessage: c.commit_message,
commitAuthor: c.commit_author,
commitEmail: c.commit_email,
commitDate: c.commit_date,
employeeId: c.employee_id,
employeeName: c.display_name || c.employee_name,
filesChanged: c.files_changed,
insertions: c.insertions,
deletions: c.deletions,
repoId: c.repo_id,
repoName: c.repo_name,
repoPath: c.repo_path,
serverType: c.server_type,
serverName: c.server_name
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
},
stats: {
commitCount: parseInt(statsResult[0]?.commit_count || '0'),
totalInsertions: parseInt(statsResult[0]?.total_insertions || '0'),
totalDeletions: parseInt(statsResult[0]?.total_deletions || '0'),
authorCount: parseInt(statsResult[0]?.author_count || '0')
}
}
})

View File

@@ -0,0 +1,31 @@
import { requireAuth } from '../../../../utils/session'
import { syncProjectGitRepositories } from '../../../../utils/git-sync'
import { syncProjectSvnRepositories } from '../../../../utils/svn-sync'
/**
* 프로젝트 커밋 새로고침 (모든 저장소 동기화)
* POST /api/project/[id]/commits/refresh
*/
export default defineEventHandler(async (event) => {
await requireAuth(event)
const projectId = parseInt(getRouterParam(event, 'id') || '0')
if (!projectId) {
throw createError({ statusCode: 400, message: '프로젝트 ID가 필요합니다.' })
}
// Git과 SVN 모두 동기화
const gitResult = await syncProjectGitRepositories(projectId)
const svnResult = await syncProjectSvnRepositories(projectId)
const allResults = [...gitResult.results, ...svnResult.results]
const allSuccess = allResults.every(r => r.success)
return {
success: allSuccess,
message: allSuccess
? `${allResults.length}개 저장소 동기화 완료`
: '일부 저장소 동기화 실패',
results: allResults
}
})

View File

@@ -0,0 +1,50 @@
import { queryOne, query } from '../../../utils/db'
/**
* 프로젝트 상세 조회
* GET /api/project/[id]/detail
*/
export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const project = await queryOne<any>(`
SELECT p.*, b.business_name, b.business_code
FROM wr_project_info p
LEFT JOIN wr_business b ON p.business_id = b.business_id
WHERE p.project_id = $1
`, [projectId])
if (!project) {
throw createError({ statusCode: 404, message: '프로젝트를 찾을 수 없습니다.' })
}
// 현재 PM/PL
const managers = await query(`
SELECT * FROM wr_project_manager_current WHERE project_id = $1
`, [projectId])
const pm = managers.find((m: any) => m.role_type === 'PM')
const pl = managers.find((m: any) => m.role_type === 'PL')
return {
project: {
projectId: project.project_id,
projectCode: project.project_code,
projectName: project.project_name,
projectType: project.project_type || 'SI',
clientName: project.client_name,
projectDescription: project.project_description,
startDate: project.start_date,
endDate: project.end_date,
contractAmount: project.contract_amount,
projectStatus: project.project_status,
businessId: project.business_id,
businessName: project.business_name,
businessCode: project.business_code,
createdAt: project.created_at,
updatedAt: project.updated_at,
currentPm: pm ? { employeeId: pm.employee_id, employeeName: pm.employee_name } : null,
currentPl: pl ? { employeeId: pl.employee_id, employeeName: pl.employee_name } : null
}
}
})

View File

@@ -0,0 +1,58 @@
import { execute, queryOne, insertReturning } from '../../../utils/db'
import { formatDate } from '../../../utils/week-calc'
import { getClientIp } from '../../../utils/ip'
import { getCurrentUserEmail } from '../../../utils/user'
interface AssignManagerBody {
employeeId: number
roleType: 'PM' | 'PL'
startDate?: string
changeReason?: string
}
/**
* PM/PL 지정 (기존 담당자 종료 + 신규 등록)
* POST /api/project/[id]/manager-assign
*/
export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const body = await readBody<AssignManagerBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
if (!body.employeeId || !body.roleType) {
throw createError({ statusCode: 400, message: '담당자와 역할을 선택해주세요.' })
}
if (!['PM', 'PL'].includes(body.roleType)) {
throw createError({ statusCode: 400, message: '역할은 PM 또는 PL만 가능합니다.' })
}
const today = formatDate(new Date())
const startDate = body.startDate || today
// 기존 담당자 종료 (end_date가 NULL인 경우)
await execute(`
UPDATE wr_project_manager_history SET
end_date = $1,
change_reason = COALESCE(change_reason || ' / ', '') || '신규 담당자 지정으로 종료',
updated_at = NOW(),
updated_ip = $2,
updated_email = $3
WHERE project_id = $4 AND role_type = $5 AND end_date IS NULL
`, [startDate, clientIp, userEmail, projectId, body.roleType])
// 신규 담당자 등록
const history = await insertReturning(`
INSERT INTO wr_project_manager_history (
project_id, employee_id, role_type, start_date, change_reason,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $6, $7)
RETURNING *
`, [projectId, body.employeeId, body.roleType, startDate, body.changeReason || null, clientIp, userEmail])
return {
success: true,
historyId: history.history_id
}
})

View File

@@ -0,0 +1,30 @@
import { query } from '../../../utils/db'
/**
* 프로젝트 PM/PL 이력 조회
* GET /api/project/[id]/manager-history
*/
export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const history = await query(`
SELECT h.*, e.employee_name, e.employee_email
FROM wr_project_manager_history h
JOIN wr_employee_info e ON h.employee_id = e.employee_id
WHERE h.project_id = $1
ORDER BY h.role_type, h.start_date DESC
`, [projectId])
return history.map((h: any) => ({
historyId: h.history_id,
projectId: h.project_id,
employeeId: h.employee_id,
employeeName: h.employee_name,
employeeEmail: h.employee_email,
roleType: h.role_type,
startDate: h.start_date,
endDate: h.end_date,
changeReason: h.change_reason,
createdAt: h.created_at
}))
})

View File

@@ -0,0 +1,71 @@
import { execute, queryOne } from '../../../utils/db'
import { getClientIp } from '../../../utils/ip'
import { getCurrentUserEmail } from '../../../utils/user'
interface UpdateProjectBody {
projectName?: string
projectType?: string
clientName?: string
projectDescription?: string
startDate?: string
endDate?: string
contractAmount?: number
projectStatus?: string
businessId?: number | null
}
/**
* 프로젝트 정보 수정
* PUT /api/project/[id]/update
*/
export default defineEventHandler(async (event) => {
const projectId = getRouterParam(event, 'id')
const body = await readBody<UpdateProjectBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
const existing = await queryOne<any>(`
SELECT * FROM wr_project_info WHERE project_id = $1
`, [projectId])
if (!existing) {
throw createError({ statusCode: 404, message: '프로젝트를 찾을 수 없습니다.' })
}
// 프로젝트 유형 검증
if (body.projectType && !['SI', 'SM'].includes(body.projectType)) {
throw createError({ statusCode: 400, message: '프로젝트 유형은 SI 또는 SM이어야 합니다.' })
}
await execute(`
UPDATE wr_project_info SET
project_name = $1,
project_type = $2,
client_name = $3,
project_description = $4,
start_date = $5,
end_date = $6,
contract_amount = $7,
project_status = $8,
business_id = $9,
updated_at = NOW(),
updated_ip = $10,
updated_email = $11
WHERE project_id = $12
`, [
body.projectName ?? existing.project_name,
body.projectType ?? existing.project_type ?? 'SI',
body.clientName ?? existing.client_name,
body.projectDescription ?? existing.project_description,
body.startDate ?? existing.start_date,
body.endDate ?? existing.end_date,
body.contractAmount ?? existing.contract_amount,
body.projectStatus ?? existing.project_status,
body.businessId !== undefined ? body.businessId : existing.business_id,
clientIp,
userEmail,
projectId
])
return { success: true }
})

View File

@@ -0,0 +1,94 @@
import { query, insertReturning } from '../../utils/db'
import { getClientIp } from '../../utils/ip'
import { getCurrentUserEmail } from '../../utils/user'
interface CreateProjectBody {
projectName: string
projectType?: string // SI / SM
clientName?: string
projectDescription?: string
startDate?: string
endDate?: string
contractAmount?: number
businessId?: number | null
}
/**
* 프로젝트 코드 자동 생성 (년도-일련번호)
*/
async function generateProjectCode(): Promise<string> {
const year = new Date().getFullYear()
const prefix = `${year}-`
// 해당 연도의 마지막 코드 조회
const result = await query<{ project_code: string }>(`
SELECT project_code FROM wr_project_info
WHERE project_code LIKE $1
ORDER BY project_code DESC
LIMIT 1
`, [`${prefix}%`])
let nextNum = 1
if (result.length > 0 && result[0].project_code) {
const lastCode = result[0].project_code
const lastNum = parseInt(lastCode.split('-')[1]) || 0
nextNum = lastNum + 1
}
return `${prefix}${String(nextNum).padStart(3, '0')}`
}
/**
* 프로젝트 등록
* POST /api/project/create
*/
export default defineEventHandler(async (event) => {
const body = await readBody<CreateProjectBody>(event)
const clientIp = getClientIp(event)
const userEmail = await getCurrentUserEmail(event)
if (!body.projectName) {
throw createError({ statusCode: 400, message: '프로젝트명을 입력해주세요.' })
}
// 프로젝트 유형 검증
const projectType = body.projectType || 'SI'
if (!['SI', 'SM'].includes(projectType)) {
throw createError({ statusCode: 400, message: '프로젝트 유형은 SI 또는 SM이어야 합니다.' })
}
// 프로젝트 코드 자동 생성
const projectCode = await generateProjectCode()
const project = await insertReturning(`
INSERT INTO wr_project_info (
project_code, project_name, project_type, client_name, project_description,
start_date, end_date, contract_amount, business_id,
created_ip, created_email, updated_ip, updated_email
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $10, $11)
RETURNING *
`, [
projectCode,
body.projectName,
projectType,
body.clientName || null,
body.projectDescription || null,
body.startDate || null,
body.endDate || null,
body.contractAmount || null,
body.businessId || null,
clientIp,
userEmail
])
return {
success: true,
project: {
projectId: project.project_id,
projectCode: project.project_code,
projectName: project.project_name,
projectType: project.project_type,
businessId: project.business_id
}
}
})

View File

@@ -0,0 +1,70 @@
import { query } from '../../utils/db'
/**
* 프로젝트 목록 조회
* GET /api/project/list
*/
export default defineEventHandler(async (event) => {
const queryParams = getQuery(event)
const status = queryParams.status as string || null
const businessId = queryParams.businessId ? Number(queryParams.businessId) : null
let sql = `
SELECT p.*,
b.business_name,
(SELECT employee_name FROM wr_employee_info e
JOIN wr_project_manager_history pm ON e.employee_id = pm.employee_id
WHERE pm.project_id = p.project_id AND pm.role_type = 'PM'
AND (pm.end_date IS NULL OR pm.end_date >= CURRENT_DATE)
LIMIT 1) as pm_name,
(SELECT employee_name FROM wr_employee_info e
JOIN wr_project_manager_history pm ON e.employee_id = pm.employee_id
WHERE pm.project_id = p.project_id AND pm.role_type = 'PL'
AND (pm.end_date IS NULL OR pm.end_date >= CURRENT_DATE)
LIMIT 1) as pl_name
FROM wr_project_info p
LEFT JOIN wr_business b ON p.business_id = b.business_id
`
const conditions: string[] = []
const params: any[] = []
let paramIndex = 1
if (status) {
conditions.push(`p.project_status = $${paramIndex++}`)
params.push(status)
}
if (businessId) {
conditions.push(`p.business_id = $${paramIndex++}`)
params.push(businessId)
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ')
}
sql += ' ORDER BY p.created_at DESC'
const projects = await query(sql, params)
return {
projects: projects.map((p: any) => ({
projectId: p.project_id,
projectCode: p.project_code,
projectName: p.project_name,
projectType: p.project_type || 'SI',
clientName: p.client_name,
projectDescription: p.project_description,
startDate: p.start_date,
endDate: p.end_date,
contractAmount: p.contract_amount,
projectStatus: p.project_status,
businessId: p.business_id,
businessName: p.business_name,
pmName: p.pm_name,
plName: p.pl_name,
createdAt: p.created_at
}))
}
})

View File

@@ -0,0 +1,33 @@
import { query } from '../../utils/db'
import { requireAuth } from '../../utils/session'
/**
* 내가 보고서 작성한 프로젝트 목록
* GET /api/project/my-projects
*/
export default defineEventHandler(async (event) => {
const userId = await requireAuth(event)
// 내가 주간보고를 작성한 프로젝트 + 전체 활성 프로젝트
const projects = await query(`
SELECT DISTINCT p.*,
CASE WHEN t.project_id IS NOT NULL THEN true ELSE false END as has_my_report
FROM wr_project_info p
LEFT JOIN (
SELECT DISTINCT t.project_id
FROM wr_weekly_report_task t
JOIN wr_weekly_report r ON t.report_id = r.report_id
WHERE r.author_id = $1
) t ON p.project_id = t.project_id
WHERE p.project_status = 'ACTIVE'
ORDER BY has_my_report DESC, p.project_name
`, [userId])
return projects.map((p: any) => ({
projectId: p.project_id,
projectCode: p.project_code,
projectName: p.project_name,
clientName: p.client_name,
hasMyReport: p.has_my_report
}))
})