1ㅊㅏ완료
This commit is contained in:
@@ -7,39 +7,112 @@
|
||||
<i class="bi bi-file-earmark-arrow-up me-2"></i>주간보고 일괄등록
|
||||
</h4>
|
||||
|
||||
<!-- Step 1: 텍스트 입력 -->
|
||||
<!-- Step 1: 입력 방식 선택 -->
|
||||
<div class="card mb-4" v-if="step === 1">
|
||||
<div class="card-header">
|
||||
<strong>1단계:</strong> 주간보고 내용 붙여넣기
|
||||
<strong>1단계:</strong> 주간보고 내용 입력
|
||||
<ul class="nav nav-tabs card-header-tabs float-end">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" :class="{ active: inputMode === 'text' }" href="#" @click.prevent="inputMode = 'text'">
|
||||
<i class="bi bi-fonts me-1"></i>텍스트
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" :class="{ active: inputMode === 'image' }" href="#" @click.prevent="inputMode = 'image'">
|
||||
<i class="bi bi-image me-1"></i>이미지
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</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="예시:
|
||||
<!-- 텍스트 입력 모드 -->
|
||||
<div v-if="inputMode === 'text'">
|
||||
<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 개발 완료
|
||||
- 차주: 테스트 진행
|
||||
- PIMS 고도화: API 개발 완료 (8시간), UI 수정 (4시간)
|
||||
- 차주: 테스트 진행 예정 (16시간)
|
||||
- 이슈: 서버 메모리 부족
|
||||
|
||||
김철수 (kim@turbosoft.co.kr)
|
||||
- I-PIMS 유지보수: 버그수정 3건
|
||||
- I-PIMS 유지보수: 버그수정 3건 (12시간)
|
||||
- 휴가: 1/10(금) 연차"
|
||||
></textarea>
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="parseText"
|
||||
: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 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 v-if="inputMode === 'image'">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">카카오톡, 슬랙 등 메신저 캡처 이미지를 업로드하세요</label>
|
||||
<div
|
||||
class="upload-zone p-5 text-center border rounded"
|
||||
:class="{ 'border-primary bg-light': isDragging }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="handleDrop"
|
||||
@click="($refs.fileInput as HTMLInputElement).click()"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
class="d-none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<i class="bi bi-cloud-arrow-up display-4 text-muted"></i>
|
||||
<p class="mt-2 mb-0 text-muted">
|
||||
이미지를 드래그하거나 클릭해서 업로드<br>
|
||||
<small>(최대 10장, PNG/JPG)</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadedImages.length > 0" class="mb-3">
|
||||
<label class="form-label">업로드된 이미지 ({{ uploadedImages.length }}장)</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<div v-for="(img, idx) in uploadedImages" :key="idx" class="position-relative">
|
||||
<img :src="img" class="rounded border" style="width: 120px; height: 120px; object-fit: cover;" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger position-absolute top-0 end-0 rounded-circle"
|
||||
style="transform: translate(30%, -30%); width: 24px; height: 24px; padding: 0;"
|
||||
@click="removeImage(idx)"
|
||||
>
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="parseImages"
|
||||
:disabled="isParsing || uploadedImages.length === 0"
|
||||
>
|
||||
<span v-if="isParsing" class="spinner-border spinner-border-sm me-1"></span>
|
||||
<i v-else class="bi bi-robot me-1"></i>
|
||||
AI 분석 ({{ uploadedImages.length }}장)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,22 +128,32 @@
|
||||
</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 class="row mb-4 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">보고 주차</label>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="changeWeek(-1)">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<span class="input-group-text bg-white" style="min-width: 180px;">
|
||||
<strong>{{ parsedData.reportYear }}년 {{ parsedData.reportWeek }}주차</strong>
|
||||
</span>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="changeWeek(1)">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">주차</label>
|
||||
<input type="number" class="form-control" v-model="parsedData.reportWeek" />
|
||||
<div class="col-auto">
|
||||
<label class="form-label">기간</label>
|
||||
<div class="input-group">
|
||||
<input type="date" class="form-control" v-model="parsedData.weekStartDate" @change="updateWeekFromDate" />
|
||||
<span class="input-group-text">~</span>
|
||||
<input type="date" class="form-control" v-model="parsedData.weekEndDate" readonly />
|
||||
</div>
|
||||
</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 class="col-auto">
|
||||
<button class="btn btn-outline-primary btn-sm" @click="setLastWeek">지난주</button>
|
||||
<button class="btn btn-outline-secondary btn-sm ms-1" @click="setThisWeek">이번주</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,9 +162,10 @@
|
||||
<!-- 직원별 보고서 -->
|
||||
<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'">
|
||||
:class="getHeaderClass(report)">
|
||||
<div>
|
||||
<span v-if="report.isEmployeeMatched" class="badge bg-success me-2">매칭됨</span>
|
||||
<span v-if="report.isEmployeeMatched" class="badge bg-success me-2">기존직원</span>
|
||||
<span v-else-if="report.isNewEmployee" class="badge bg-info 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>
|
||||
@@ -95,47 +179,105 @@
|
||||
<!-- 직원 선택 -->
|
||||
<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}">
|
||||
<label class="form-label">직원</label>
|
||||
<div class="form-check mb-2">
|
||||
<input type="radio" class="form-check-input" :id="'emp-existing-'+rIdx" :value="false" v-model="report.createNewEmployee" />
|
||||
<label class="form-check-label" :for="'emp-existing-'+rIdx">기존 직원 선택</label>
|
||||
</div>
|
||||
<select v-if="!report.createNewEmployee" class="form-select" v-model="report.matchedEmployeeId"
|
||||
:class="{'is-invalid': !report.matchedEmployeeId && !report.createNewEmployee}">
|
||||
<option :value="null">-- 선택 --</option>
|
||||
<option v-for="emp in employees" :key="emp.employeeId" :value="emp.employeeId">
|
||||
{{ emp.employeeName }} ({{ emp.employeeEmail }})
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="form-check mt-2">
|
||||
<input type="radio" class="form-check-input" :id="'emp-new-'+rIdx" :value="true" v-model="report.createNewEmployee" />
|
||||
<label class="form-check-label" :for="'emp-new-'+rIdx">신규 직원 생성</label>
|
||||
</div>
|
||||
<div v-if="report.createNewEmployee" class="mt-2 p-3 bg-light rounded">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">이름 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control form-control-sm" v-model="report.employeeName" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small">이메일 <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control form-control-sm" v-model="report.employeeEmail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트별 실적 -->
|
||||
<div v-for="(proj, pIdx) in report.projects" :key="pIdx" class="border rounded p-3 mb-2">
|
||||
<div class="row mb-2">
|
||||
<!-- 프로젝트별 Task -->
|
||||
<div v-for="(proj, pIdx) in report.projects" :key="pIdx" class="border rounded p-3 mb-3">
|
||||
<div class="row mb-3">
|
||||
<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="신규 프로젝트명"
|
||||
/>
|
||||
<label class="form-label">
|
||||
프로젝트
|
||||
<span v-if="!proj.matchedProjectId" class="badge bg-info ms-1">신규생성</span>
|
||||
</label>
|
||||
<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>
|
||||
<input v-if="!proj.matchedProjectId" type="text" class="form-control mt-2"
|
||||
v-model="proj.projectName" placeholder="신규 프로젝트명" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- 금주 실적 Task -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">금주 실적</label>
|
||||
<textarea class="form-control" v-model="proj.workDescription" rows="2"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0 fw-bold text-primary">금주 실적</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" @click="addParsedTask(report, proj, 'work')">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="(task, tIdx) in proj.workTasks" :key="'work-'+tIdx" class="mb-2">
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<div class="form-check pt-1">
|
||||
<input type="checkbox" class="form-check-input" v-model="task.isCompleted" />
|
||||
</div>
|
||||
<textarea class="form-control form-control-sm auto-resize" v-model="task.description"
|
||||
@input="autoResize($event)" placeholder="작업 내용"></textarea>
|
||||
<div class="text-nowrap">
|
||||
<input type="number" class="form-control form-control-sm text-end mb-1" style="width: 60px;"
|
||||
v-model.number="task.hours" min="0" step="0.5" />
|
||||
<div class="small text-muted text-end">{{ formatHours(task.hours) }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" @click="removeParsedTask(proj.workTasks, tIdx)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 차주 계획 Task -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">차주 계획</label>
|
||||
<textarea class="form-control" v-model="proj.planDescription" rows="2"></textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0 fw-bold text-success">차주 계획</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" @click="addParsedTask(report, proj, 'plan')">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="(task, tIdx) in proj.planTasks" :key="'plan-'+tIdx" class="mb-2">
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<textarea class="form-control form-control-sm auto-resize" v-model="task.description"
|
||||
@input="autoResize($event)" placeholder="계획 내용"></textarea>
|
||||
<div class="text-nowrap">
|
||||
<input type="number" class="form-control form-control-sm text-end mb-1" style="width: 60px;"
|
||||
v-model.number="task.hours" min="0" step="0.5" />
|
||||
<div class="small text-muted text-end">{{ formatHours(task.hours) }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" @click="removeParsedTask(proj.planTasks, tIdx)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,16 +285,16 @@
|
||||
<!-- 공통사항 -->
|
||||
<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>
|
||||
<label class="form-label small">이슈/리스크</label>
|
||||
<textarea class="form-control form-control-sm" 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>
|
||||
<label class="form-label small">휴가일정</label>
|
||||
<textarea class="form-control form-control-sm" 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>
|
||||
<label class="form-label small">기타사항</label>
|
||||
<textarea class="form-control form-control-sm" v-model="report.remarkDescription" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,11 +304,7 @@
|
||||
|
||||
<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"
|
||||
>
|
||||
<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 }}건)
|
||||
@@ -190,9 +328,16 @@
|
||||
<span>
|
||||
<i :class="r.success ? 'bi bi-check-circle text-success' : 'bi bi-x-circle text-danger'" class="me-2"></i>
|
||||
{{ r.employeeName }}
|
||||
<small class="text-muted ms-1">({{ r.employeeEmail }})</small>
|
||||
</span>
|
||||
<span v-if="r.success" class="badge" :class="r.isUpdate ? 'bg-warning' : 'bg-success'">
|
||||
{{ r.isUpdate ? '덮어쓰기' : '신규등록' }}
|
||||
<span v-if="r.success">
|
||||
<span v-if="r.isNewEmployee" class="badge bg-info me-1">직원생성</span>
|
||||
<span v-if="r.newProjects?.length" class="badge bg-secondary me-1">
|
||||
프로젝트 {{ r.newProjects.length }}개 생성
|
||||
</span>
|
||||
<span class="badge" :class="r.isUpdate ? 'bg-warning' : 'bg-success'">
|
||||
{{ r.isUpdate ? '덮어쓰기' : '신규등록' }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="text-danger small">{{ r.error }}</span>
|
||||
</li>
|
||||
@@ -216,7 +361,10 @@ const { fetchCurrentUser } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const step = ref(1)
|
||||
const inputMode = ref<'text' | 'image'>('text')
|
||||
const rawText = ref('')
|
||||
const uploadedImages = ref<string[]>([])
|
||||
const isDragging = ref(false)
|
||||
const isParsing = ref(false)
|
||||
const isRegistering = ref(false)
|
||||
|
||||
@@ -238,7 +386,12 @@ const enabledCount = computed(() =>
|
||||
|
||||
const canRegister = computed(() => {
|
||||
const enabledReports = parsedData.value.reports?.filter((r: any) => r.enabled) || []
|
||||
return enabledReports.length > 0 && enabledReports.every((r: any) => r.matchedEmployeeId)
|
||||
return enabledReports.length > 0 && enabledReports.every((r: any) => {
|
||||
if (r.createNewEmployee) {
|
||||
return r.employeeName && r.employeeEmail
|
||||
}
|
||||
return r.matchedEmployeeId
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -248,7 +401,6 @@ onMounted(async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 관리자 체크
|
||||
if (user.employeeEmail !== 'coziny@gmail.com') {
|
||||
alert('관리자만 접근할 수 있습니다.')
|
||||
router.push('/')
|
||||
@@ -256,19 +408,198 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
async function parseReport() {
|
||||
function getHeaderClass(report: any) {
|
||||
if (report.isEmployeeMatched) return 'bg-light'
|
||||
if (report.isNewEmployee) return 'bg-info bg-opacity-25'
|
||||
return 'bg-warning bg-opacity-25'
|
||||
}
|
||||
|
||||
// 시간 표시 함수
|
||||
function formatHours(hours: number): string {
|
||||
if (!hours || hours <= 0) return '-'
|
||||
const days = Math.floor(hours / 8)
|
||||
const remainHours = hours % 8
|
||||
|
||||
if (days === 0) return `${remainHours}h`
|
||||
if (remainHours === 0) return `${days}일`
|
||||
return `${days}일 ${remainHours}h`
|
||||
}
|
||||
|
||||
// textarea 자동 높이 조절
|
||||
function autoResize(e: Event) {
|
||||
const el = e.target as HTMLTextAreaElement
|
||||
el.style.height = 'auto'
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
function resizeAllTextareas() {
|
||||
nextTick(() => {
|
||||
document.querySelectorAll('.auto-resize').forEach((el) => {
|
||||
const textarea = el as HTMLTextAreaElement
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 이미지 관련 함수들
|
||||
function handleDrop(e: DragEvent) {
|
||||
isDragging.value = false
|
||||
const files = e.dataTransfer?.files
|
||||
if (files) processFiles(files)
|
||||
}
|
||||
|
||||
function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files) processFiles(input.files)
|
||||
}
|
||||
|
||||
function processFiles(files: FileList) {
|
||||
const maxFiles = 10 - uploadedImages.value.length
|
||||
const toProcess = Array.from(files).slice(0, maxFiles)
|
||||
|
||||
toProcess.forEach(file => {
|
||||
if (!file.type.startsWith('image/')) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
uploadedImages.value.push(e.target.result as string)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function removeImage(idx: number) {
|
||||
uploadedImages.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
// Task 추가/삭제
|
||||
function addParsedTask(report: any, proj: any, type: 'work' | 'plan') {
|
||||
const taskArray = type === 'work' ? proj.workTasks : proj.planTasks
|
||||
if (type === 'work') {
|
||||
taskArray.push({ description: '', hours: 0, isCompleted: true })
|
||||
} else {
|
||||
taskArray.push({ description: '', hours: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
function removeParsedTask(taskArray: any[], idx: number) {
|
||||
if (taskArray.length > 0) {
|
||||
taskArray.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 주차 계산 함수들
|
||||
function getMonday(date: Date): Date {
|
||||
const d = new Date(date)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
|
||||
d.setDate(diff)
|
||||
return d
|
||||
}
|
||||
|
||||
function getSunday(monday: Date): Date {
|
||||
const d = new Date(monday)
|
||||
d.setDate(d.getDate() + 6)
|
||||
return d
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
function getWeekNumber(date: Date): { year: number; week: number } {
|
||||
const d = new Date(date)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7)
|
||||
const week1 = new Date(d.getFullYear(), 0, 4)
|
||||
const weekNum = 1 + Math.round(((d.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7)
|
||||
return { year: d.getFullYear(), week: weekNum }
|
||||
}
|
||||
|
||||
function setWeekDates(monday: Date) {
|
||||
const sunday = getSunday(monday)
|
||||
const weekInfo = getWeekNumber(monday)
|
||||
|
||||
parsedData.value.weekStartDate = formatDate(monday)
|
||||
parsedData.value.weekEndDate = formatDate(sunday)
|
||||
parsedData.value.reportYear = weekInfo.year
|
||||
parsedData.value.reportWeek = weekInfo.week
|
||||
}
|
||||
|
||||
function changeWeek(delta: number) {
|
||||
const currentMonday = new Date(parsedData.value.weekStartDate)
|
||||
currentMonday.setDate(currentMonday.getDate() + (delta * 7))
|
||||
setWeekDates(currentMonday)
|
||||
}
|
||||
|
||||
function setLastWeek() {
|
||||
const today = new Date()
|
||||
const lastWeekMonday = getMonday(today)
|
||||
lastWeekMonday.setDate(lastWeekMonday.getDate() - 7)
|
||||
setWeekDates(lastWeekMonday)
|
||||
}
|
||||
|
||||
function setThisWeek() {
|
||||
const today = new Date()
|
||||
const thisWeekMonday = getMonday(today)
|
||||
setWeekDates(thisWeekMonday)
|
||||
}
|
||||
|
||||
function updateWeekFromDate() {
|
||||
const startDate = new Date(parsedData.value.weekStartDate)
|
||||
const monday = getMonday(startDate)
|
||||
setWeekDates(monday)
|
||||
}
|
||||
|
||||
// 분석 결과 처리
|
||||
function handleParseResult(res: any) {
|
||||
parsedData.value = res.parsed
|
||||
parsedData.value.reports.forEach((r: any) => {
|
||||
r.enabled = true
|
||||
r.createNewEmployee = r.isNewEmployee
|
||||
// workTasks, planTasks가 없으면 빈 배열로 초기화
|
||||
r.projects.forEach((p: any) => {
|
||||
p.workTasks = (p.workTasks || []).map((t: any) => ({
|
||||
...t,
|
||||
isCompleted: t.isCompleted !== false
|
||||
}))
|
||||
p.planTasks = p.planTasks || []
|
||||
})
|
||||
})
|
||||
employees.value = res.employees
|
||||
projects.value = res.projects
|
||||
step.value = 2
|
||||
resizeAllTextareas()
|
||||
}
|
||||
|
||||
// 텍스트 분석
|
||||
async function parseText() {
|
||||
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
|
||||
handleParseResult(res)
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || e.message || 'AI 분석에 실패했습니다.')
|
||||
} finally {
|
||||
isParsing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 이미지 분석
|
||||
async function parseImages() {
|
||||
isParsing.value = true
|
||||
try {
|
||||
const res = await $fetch<any>('/api/admin/parse-image', {
|
||||
method: 'POST',
|
||||
body: { images: uploadedImages.value }
|
||||
})
|
||||
handleParseResult(res)
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || e.message || 'AI 분석에 실패했습니다.')
|
||||
} finally {
|
||||
@@ -282,12 +613,14 @@ async function bulkRegister() {
|
||||
const enabledReports = parsedData.value.reports
|
||||
.filter((r: any) => r.enabled)
|
||||
.map((r: any) => ({
|
||||
employeeId: r.matchedEmployeeId,
|
||||
employeeId: r.createNewEmployee ? null : r.matchedEmployeeId,
|
||||
employeeName: r.employeeName,
|
||||
employeeEmail: r.employeeEmail,
|
||||
projects: r.projects.map((p: any) => ({
|
||||
projectId: p.matchedProjectId,
|
||||
projectName: p.projectName,
|
||||
workDescription: p.workDescription,
|
||||
planDescription: p.planDescription
|
||||
workTasks: (p.workTasks || []).filter((t: any) => t.description?.trim()),
|
||||
planTasks: (p.planTasks || []).filter((t: any) => t.description?.trim())
|
||||
})),
|
||||
issueDescription: r.issueDescription,
|
||||
vacationDescription: r.vacationDescription,
|
||||
@@ -317,6 +650,7 @@ async function bulkRegister() {
|
||||
function reset() {
|
||||
step.value = 1
|
||||
rawText.value = ''
|
||||
uploadedImages.value = []
|
||||
parsedData.value = {
|
||||
reportYear: new Date().getFullYear(),
|
||||
reportWeek: 1,
|
||||
@@ -327,3 +661,32 @@ function reset() {
|
||||
registerResult.value = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-zone {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-style: dashed !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: #0d6efd !important;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #0d6efd;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auto-resize {
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
min-height: 38px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user