Files
weeklyreport/frontend/admin/bulk-import.vue
2026-01-04 21:31:45 +09:00

330 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<AppHeader />
<div class="container-fluid py-4">
<h4 class="mb-4">
<i class="bi bi-file-earmark-arrow-up me-2"></i>주간보고 일괄등록
</h4>
<!-- Step 1: 텍스트 입력 -->
<div class="card mb-4" v-if="step === 1">
<div class="card-header">
<strong>1단계:</strong> 주간보고 내용 붙여넣기
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">직원들의 주간보고 내용을 붙여넣으세요</label>
<textarea
class="form-control font-monospace"
v-model="rawText"
rows="15"
placeholder="예시:
홍길동 (hong@turbosoft.co.kr)
- PIMS 고도화: API 개발 완료
- 차주: 테스트 진행
- 이슈: 서버 메모리 부족
김철수 (kim@turbosoft.co.kr)
- I-PIMS 유지보수: 버그수정 3건
- 휴가: 1/10(금) 연차"
></textarea>
</div>
<div class="d-flex justify-content-end">
<button
class="btn btn-primary"
@click="parseReport"
:disabled="isParsing || !rawText.trim()"
>
<span v-if="isParsing" class="spinner-border spinner-border-sm me-1"></span>
<i v-else class="bi bi-robot me-1"></i>
AI 분석
</button>
</div>
</div>
</div>
<!-- Step 2: 분석 결과 확인 -->
<div v-if="step === 2">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><strong>2단계:</strong> 분석 결과 확인 수정</span>
<button class="btn btn-sm btn-outline-secondary" @click="step = 1">
<i class="bi bi-arrow-left me-1"></i>다시 입력
</button>
</div>
<div class="card-body">
<!-- 주차 정보 -->
<div class="row mb-4">
<div class="col-md-2">
<label class="form-label">연도</label>
<input type="number" class="form-control" v-model="parsedData.reportYear" />
</div>
<div class="col-md-2">
<label class="form-label">주차</label>
<input type="number" class="form-control" v-model="parsedData.reportWeek" />
</div>
<div class="col-md-3">
<label class="form-label">시작일</label>
<input type="date" class="form-control" v-model="parsedData.weekStartDate" />
</div>
<div class="col-md-3">
<label class="form-label">종료일</label>
<input type="date" class="form-control" v-model="parsedData.weekEndDate" />
</div>
</div>
<hr />
<!-- 직원별 보고서 -->
<div v-for="(report, rIdx) in parsedData.reports" :key="rIdx" class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center"
:class="report.isEmployeeMatched ? 'bg-light' : 'bg-warning bg-opacity-25'">
<div>
<span v-if="report.isEmployeeMatched" class="badge bg-success me-2">매칭됨</span>
<span v-else class="badge bg-warning text-dark me-2">매칭필요</span>
<strong>{{ report.employeeName }}</strong>
<small class="text-muted ms-2">{{ report.employeeEmail || '이메일 없음' }}</small>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" v-model="report.enabled" :id="'chk-'+rIdx" />
<label class="form-check-label" :for="'chk-'+rIdx">등록</label>
</div>
</div>
<div class="card-body" v-if="report.enabled">
<!-- 직원 선택 -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">직원 선택 <span class="text-danger">*</span></label>
<select class="form-select" v-model="report.matchedEmployeeId"
:class="{'is-invalid': !report.matchedEmployeeId}">
<option :value="null">-- 선택 --</option>
<option v-for="emp in employees" :key="emp.employeeId" :value="emp.employeeId">
{{ emp.employeeName }} ({{ emp.employeeEmail }})
</option>
</select>
</div>
</div>
<!-- 프로젝트별 실적 -->
<div v-for="(proj, pIdx) in report.projects" :key="pIdx" class="border rounded p-3 mb-2">
<div class="row mb-2">
<div class="col-md-6">
<label class="form-label">프로젝트</label>
<div class="input-group">
<select class="form-select" v-model="proj.matchedProjectId">
<option :value="null"> 신규 생성</option>
<option v-for="p in projects" :key="p.projectId" :value="p.projectId">
{{ p.projectCode }} - {{ p.projectName }}
</option>
</select>
</div>
<input
v-if="!proj.matchedProjectId"
type="text"
class="form-control mt-2"
v-model="proj.projectName"
placeholder="신규 프로젝트명"
/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">금주 실적</label>
<textarea class="form-control" v-model="proj.workDescription" rows="2"></textarea>
</div>
<div class="col-md-6">
<label class="form-label">차주 계획</label>
<textarea class="form-control" v-model="proj.planDescription" rows="2"></textarea>
</div>
</div>
</div>
<!-- 공통사항 -->
<div class="row mt-3">
<div class="col-md-4">
<label class="form-label">이슈/리스크</label>
<textarea class="form-control" v-model="report.issueDescription" rows="2"></textarea>
</div>
<div class="col-md-4">
<label class="form-label">휴가일정</label>
<textarea class="form-control" v-model="report.vacationDescription" rows="2"></textarea>
</div>
<div class="col-md-4">
<label class="form-label">기타사항</label>
<textarea class="form-control" v-model="report.remarkDescription" rows="2"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button class="btn btn-outline-secondary" @click="step = 1">취소</button>
<button
class="btn btn-primary"
@click="bulkRegister"
:disabled="isRegistering || !canRegister"
>
<span v-if="isRegistering" class="spinner-border spinner-border-sm me-1"></span>
<i v-else class="bi bi-check-lg me-1"></i>
일괄 등록 ({{ enabledCount }})
</button>
</div>
</div>
<!-- Step 3: 결과 -->
<div class="card" v-if="step === 3">
<div class="card-header bg-success text-white">
<i class="bi bi-check-circle me-2"></i>등록 완료
</div>
<div class="card-body">
<p class="mb-3">
<strong>{{ registerResult.totalCount }}</strong>
<strong class="text-success">{{ registerResult.successCount }}</strong> 등록 완료
</p>
<ul class="list-group">
<li v-for="(r, idx) in registerResult.results" :key="idx"
class="list-group-item d-flex justify-content-between align-items-center">
<span>
<i :class="r.success ? 'bi bi-check-circle text-success' : 'bi bi-x-circle text-danger'" class="me-2"></i>
{{ r.employeeName }}
</span>
<span v-if="r.success" class="badge" :class="r.isUpdate ? 'bg-warning' : 'bg-success'">
{{ r.isUpdate ? '덮어쓰기' : '신규등록' }}
</span>
<span v-else class="text-danger small">{{ r.error }}</span>
</li>
</ul>
<div class="mt-4">
<button class="btn btn-primary" @click="reset">
<i class="bi bi-plus-lg me-1"></i>추가 등록
</button>
<NuxtLink to="/report/weekly" class="btn btn-outline-secondary ms-2">
주간보고 목록
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { fetchCurrentUser } = useAuth()
const router = useRouter()
const step = ref(1)
const rawText = ref('')
const isParsing = ref(false)
const isRegistering = ref(false)
const parsedData = ref<any>({
reportYear: new Date().getFullYear(),
reportWeek: 1,
weekStartDate: '',
weekEndDate: '',
reports: []
})
const employees = ref<any[]>([])
const projects = ref<any[]>([])
const registerResult = ref<any>({})
const enabledCount = computed(() =>
parsedData.value.reports?.filter((r: any) => r.enabled).length || 0
)
const canRegister = computed(() => {
const enabledReports = parsedData.value.reports?.filter((r: any) => r.enabled) || []
return enabledReports.length > 0 && enabledReports.every((r: any) => r.matchedEmployeeId)
})
onMounted(async () => {
const user = await fetchCurrentUser()
if (!user) {
router.push('/login')
return
}
// 관리자 체크
if (user.employeeEmail !== 'coziny@gmail.com') {
alert('관리자만 접근할 수 있습니다.')
router.push('/')
return
}
})
async function parseReport() {
isParsing.value = true
try {
const res = await $fetch<any>('/api/admin/parse-report', {
method: 'POST',
body: { rawText: rawText.value }
})
parsedData.value = res.parsed
parsedData.value.reports.forEach((r: any) => r.enabled = true)
employees.value = res.employees
projects.value = res.projects
step.value = 2
} catch (e: any) {
alert(e.data?.message || e.message || 'AI 분석에 실패했습니다.')
} finally {
isParsing.value = false
}
}
async function bulkRegister() {
isRegistering.value = true
try {
const enabledReports = parsedData.value.reports
.filter((r: any) => r.enabled)
.map((r: any) => ({
employeeId: r.matchedEmployeeId,
projects: r.projects.map((p: any) => ({
projectId: p.matchedProjectId,
projectName: p.projectName,
workDescription: p.workDescription,
planDescription: p.planDescription
})),
issueDescription: r.issueDescription,
vacationDescription: r.vacationDescription,
remarkDescription: r.remarkDescription
}))
const res = await $fetch<any>('/api/admin/bulk-register', {
method: 'POST',
body: {
reportYear: parsedData.value.reportYear,
reportWeek: parsedData.value.reportWeek,
weekStartDate: parsedData.value.weekStartDate,
weekEndDate: parsedData.value.weekEndDate,
reports: enabledReports
}
})
registerResult.value = res
step.value = 3
} catch (e: any) {
alert(e.data?.message || e.message || '등록에 실패했습니다.')
} finally {
isRegistering.value = false
}
}
function reset() {
step.value = 1
rawText.value = ''
parsedData.value = {
reportYear: new Date().getFullYear(),
reportWeek: 1,
weekStartDate: '',
weekEndDate: '',
reports: []
}
registerResult.value = {}
}
</script>