작업계획서대로 진행
This commit is contained in:
@@ -27,6 +27,9 @@
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><i class="bi bi-folder me-2"></i>프로젝트별 실적/계획</strong>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" @click="openMaintenanceModal">
|
||||
<i class="bi bi-tools me-1"></i>유지보수 불러오기
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" @click="showAiModal = true">
|
||||
<i class="bi bi-robot me-1"></i>AI 자동채우기
|
||||
</button>
|
||||
@@ -447,6 +450,83 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop fade show" v-if="showAiModal" @click="closeAiModal"></div>
|
||||
|
||||
<!-- 유지보수 불러오기 모달 -->
|
||||
<div class="modal fade" :class="{ show: showMaintenanceModal }" :style="{ display: showMaintenanceModal ? 'block' : 'none' }">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-tools me-2"></i>유지보수 업무 불러오기</h5>
|
||||
<button type="button" class="btn-close" @click="showMaintenanceModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Step 1: 프로젝트 선택 -->
|
||||
<div v-if="maintenanceStep === 'select'">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">프로젝트 선택</label>
|
||||
<select class="form-select" v-model="maintenanceProjectId" @change="loadMaintenanceTasks">
|
||||
<option value="">선택하세요</option>
|
||||
<option v-for="p in allProjects" :key="p.projectId" :value="p.projectId">{{ p.projectName }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="isLoadingMaintenance" class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm"></div> 유지보수 업무 조회 중...
|
||||
</div>
|
||||
<div v-else-if="maintenanceTasks.length === 0 && maintenanceProjectId" class="alert alert-info">
|
||||
해당 주차에 완료된 유지보수 업무가 없습니다.
|
||||
</div>
|
||||
<div v-else-if="maintenanceTasks.length > 0">
|
||||
<div class="mb-2 text-muted small">{{ form.weekStartDate }} ~ {{ form.weekEndDate }} 완료 업무</div>
|
||||
<div class="list-group">
|
||||
<label v-for="t in maintenanceTasks" :key="t.taskId" class="list-group-item list-group-item-action">
|
||||
<div class="d-flex align-items-start">
|
||||
<input type="checkbox" class="form-check-input me-2 mt-1" v-model="t.selected" />
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">{{ t.requestTitle }}</div>
|
||||
<small class="text-muted">
|
||||
[{{ getTypeLabel(t.taskType) }}] {{ t.requesterName || '-' }}
|
||||
<span v-if="t.resolutionContent"> → {{ truncate(t.resolutionContent, 30) }}</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step 2: AI 변환 결과 -->
|
||||
<div v-if="maintenanceStep === 'convert'">
|
||||
<div class="mb-2 text-muted small">AI가 생성한 실적 문장 (수정 가능)</div>
|
||||
<div v-for="(g, idx) in generatedTasks" :key="idx" class="mb-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{{ getTypeLabel(g.taskType) }}</span>
|
||||
<input type="text" class="form-control" v-model="g.description" />
|
||||
<button class="btn btn-outline-danger" type="button" @click="generatedTasks.splice(idx, 1)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showMaintenanceModal = false">취소</button>
|
||||
<template v-if="maintenanceStep === 'select'">
|
||||
<button type="button" class="btn btn-primary" @click="convertMaintenance"
|
||||
:disabled="!selectedMaintenanceCount || isConverting">
|
||||
<span v-if="isConverting"><span class="spinner-border spinner-border-sm me-1"></span></span>
|
||||
AI 실적 변환 ({{ selectedMaintenanceCount }}건)
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="maintenanceStep = 'select'">이전</button>
|
||||
<button type="button" class="btn btn-primary" @click="applyMaintenanceTasks" :disabled="generatedTasks.length === 0">
|
||||
<i class="bi bi-check-lg me-1"></i>실적 추가
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop fade show" v-if="showMaintenanceModal"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -508,6 +588,17 @@ const aiParsedResult = ref<{
|
||||
remarkDescription: string | null
|
||||
} | null>(null)
|
||||
|
||||
// 유지보수 불러오기 모달
|
||||
const showMaintenanceModal = ref(false)
|
||||
const maintenanceStep = ref<'select' | 'convert'>('select')
|
||||
const maintenanceProjectId = ref('')
|
||||
const maintenanceTasks = ref<any[]>([])
|
||||
const isLoadingMaintenance = ref(false)
|
||||
const isConverting = ref(false)
|
||||
const generatedTasks = ref<{ description: string; taskType: string; sourceTaskIds: number[] }[]>([])
|
||||
|
||||
const selectedMaintenanceCount = computed(() => maintenanceTasks.value.filter(t => t.selected).length)
|
||||
|
||||
const form = ref({
|
||||
reportYear: new Date().getFullYear(),
|
||||
reportWeek: 1,
|
||||
@@ -922,6 +1013,94 @@ function closeAiModal() {
|
||||
aiParsedResult.value = null
|
||||
}
|
||||
|
||||
// 유지보수 불러오기 함수들
|
||||
function openMaintenanceModal() {
|
||||
showMaintenanceModal.value = true
|
||||
maintenanceStep.value = 'select'
|
||||
maintenanceProjectId.value = ''
|
||||
maintenanceTasks.value = []
|
||||
generatedTasks.value = []
|
||||
}
|
||||
|
||||
async function loadMaintenanceTasks() {
|
||||
if (!maintenanceProjectId.value) {
|
||||
maintenanceTasks.value = []
|
||||
return
|
||||
}
|
||||
isLoadingMaintenance.value = true
|
||||
try {
|
||||
const res = await $fetch<{ tasks: any[] }>('/api/maintenance/report/available', {
|
||||
query: {
|
||||
projectId: maintenanceProjectId.value,
|
||||
weekStartDate: form.value.weekStartDate,
|
||||
weekEndDate: form.value.weekEndDate
|
||||
}
|
||||
})
|
||||
maintenanceTasks.value = (res.tasks || []).map(t => ({ ...t, selected: true }))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
maintenanceTasks.value = []
|
||||
} finally {
|
||||
isLoadingMaintenance.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function convertMaintenance() {
|
||||
const selected = maintenanceTasks.value.filter(t => t.selected)
|
||||
if (selected.length === 0) return
|
||||
|
||||
isConverting.value = true
|
||||
try {
|
||||
const res = await $fetch<{ generatedTasks: any[] }>('/api/maintenance/report/generate-text', {
|
||||
method: 'POST',
|
||||
body: { tasks: selected }
|
||||
})
|
||||
generatedTasks.value = res.generatedTasks || []
|
||||
maintenanceStep.value = 'convert'
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// 실패 시 기본 변환
|
||||
generatedTasks.value = selected.map(t => ({
|
||||
description: `[${getTypeLabel(t.taskType)}] ${t.requestTitle}`,
|
||||
taskType: t.taskType,
|
||||
sourceTaskIds: [t.taskId]
|
||||
}))
|
||||
maintenanceStep.value = 'convert'
|
||||
} finally {
|
||||
isConverting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyMaintenanceTasks() {
|
||||
const projectId = Number(maintenanceProjectId.value)
|
||||
const proj = allProjects.value.find(p => p.projectId === projectId)
|
||||
|
||||
for (const g of generatedTasks.value) {
|
||||
form.value.tasks.push({
|
||||
projectId,
|
||||
projectCode: proj?.projectCode || '',
|
||||
projectName: proj?.projectName || '',
|
||||
taskType: 'WORK',
|
||||
description: g.description,
|
||||
hours: 0,
|
||||
isCompleted: true
|
||||
})
|
||||
}
|
||||
|
||||
showMaintenanceModal.value = false
|
||||
toast.success(`${generatedTasks.value.length}건의 실적이 추가되었습니다.`)
|
||||
}
|
||||
|
||||
function getTypeLabel(type: string): string {
|
||||
const labels: Record<string, string> = { bug: '버그수정', feature: '기능개선', inquiry: '문의대응', other: '기타' }
|
||||
return labels[type] || '기타'
|
||||
}
|
||||
|
||||
function truncate(s: string, len: number): string {
|
||||
if (!s) return ''
|
||||
return s.length > len ? s.substring(0, len) + '...' : s
|
||||
}
|
||||
|
||||
function handleAiDrop(e: DragEvent) {
|
||||
isDragging.value = false
|
||||
const files = e.dataTransfer?.files
|
||||
|
||||
Reference in New Issue
Block a user