226 lines
7.9 KiB
Vue
226 lines
7.9 KiB
Vue
<template>
|
|
<div>
|
|
<AppHeader />
|
|
|
|
<div class="container py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h4 class="mb-0">
|
|
<i class="bi bi-journal-plus me-2"></i>주간보고 작성
|
|
</h4>
|
|
<span class="text-muted">{{ weekInfo.weekString }} ({{ weekInfo.startDateStr }} ~ {{ weekInfo.endDateStr }})</span>
|
|
</div>
|
|
|
|
<form @submit.prevent="handleSubmit">
|
|
<!-- 프로젝트별 실적 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong><i class="bi bi-folder me-2"></i>프로젝트별 실적</strong>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" @click="showProjectModal = true">
|
|
<i class="bi bi-plus"></i> 프로젝트 추가
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div v-if="form.projects.length === 0" class="text-center text-muted py-4">
|
|
<i class="bi bi-inbox display-4"></i>
|
|
<p class="mt-2 mb-0">프로젝트를 추가해주세요.</p>
|
|
</div>
|
|
|
|
<div v-for="(proj, idx) in form.projects" :key="idx" class="border rounded p-3 mb-3">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<strong>{{ proj.projectName }}</strong>
|
|
<small class="text-muted ms-2">({{ proj.projectCode }})</small>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger" @click="removeProject(idx)">
|
|
<i class="bi bi-x"></i> 삭제
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">금주 실적</label>
|
|
<textarea class="form-control" rows="3" v-model="proj.workDescription"
|
|
placeholder="이번 주에 수행한 업무를 작성해주세요."></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">차주 계획</label>
|
|
<textarea class="form-control" rows="3" v-model="proj.planDescription"
|
|
placeholder="다음 주에 수행할 업무를 작성해주세요."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 공통 사항 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<strong><i class="bi bi-chat-left-text me-2"></i>공통 사항</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">이슈/리스크</label>
|
|
<textarea class="form-control" rows="3" v-model="form.issueDescription"
|
|
placeholder="진행 중 발생한 이슈나 리스크를 작성해주세요."></textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">휴가일정</label>
|
|
<textarea class="form-control" rows="2" v-model="form.vacationDescription"
|
|
placeholder="예: 1/6(월) 연차, 1/8(수) 오후 반차"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">기타사항</label>
|
|
<textarea class="form-control" rows="2" v-model="form.remarkDescription"
|
|
placeholder="기타 전달사항이 있으면 작성해주세요."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="d-flex justify-content-end gap-2">
|
|
<NuxtLink to="/report/weekly" class="btn btn-secondary">취소</NuxtLink>
|
|
<button type="submit" class="btn btn-primary" :disabled="isSubmitting || form.projects.length === 0">
|
|
<span v-if="isSubmitting" class="spinner-border spinner-border-sm me-1"></span>
|
|
임시저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 프로젝트 선택 모달 -->
|
|
<div class="modal fade" :class="{ show: showProjectModal }" :style="{ display: showProjectModal ? 'block' : 'none' }" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">프로젝트 선택</h5>
|
|
<button type="button" class="btn-close" @click="showProjectModal = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div v-if="isLoadingProjects" class="text-center py-4">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>로딩 중...
|
|
</div>
|
|
<div v-else-if="availableProjects.length === 0" class="text-center text-muted py-4">
|
|
추가할 수 있는 프로젝트가 없습니다.
|
|
</div>
|
|
<div v-else class="list-group">
|
|
<button type="button" class="list-group-item list-group-item-action"
|
|
v-for="p in availableProjects" :key="p.projectId"
|
|
@click="addProject(p)">
|
|
<strong>{{ p.projectName }}</strong>
|
|
<small class="text-muted ms-2">({{ p.projectCode }})</small>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop fade show" v-if="showProjectModal" @click="showProjectModal = false"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { fetchCurrentUser } = useAuth()
|
|
const { getCurrentWeekInfo } = useWeekCalc()
|
|
const router = useRouter()
|
|
|
|
const weekInfo = getCurrentWeekInfo()
|
|
|
|
interface ProjectItem {
|
|
projectId: number
|
|
projectCode: string
|
|
projectName: string
|
|
workDescription: string
|
|
planDescription: string
|
|
}
|
|
|
|
const form = ref({
|
|
projects: [] as ProjectItem[],
|
|
issueDescription: '',
|
|
vacationDescription: '',
|
|
remarkDescription: ''
|
|
})
|
|
|
|
const allProjects = ref<any[]>([])
|
|
const isLoadingProjects = ref(false)
|
|
const isSubmitting = ref(false)
|
|
const showProjectModal = ref(false)
|
|
|
|
// 아직 추가하지 않은 프로젝트만
|
|
const availableProjects = computed(() => {
|
|
const addedIds = form.value.projects.map(p => p.projectId)
|
|
return allProjects.value.filter(p => !addedIds.includes(p.projectId))
|
|
})
|
|
|
|
onMounted(async () => {
|
|
const user = await fetchCurrentUser()
|
|
if (!user) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
loadProjects()
|
|
})
|
|
|
|
async function loadProjects() {
|
|
isLoadingProjects.value = true
|
|
try {
|
|
const res = await $fetch<any>('/api/project/list')
|
|
allProjects.value = res.projects || []
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
isLoadingProjects.value = false
|
|
}
|
|
}
|
|
|
|
function addProject(p: any) {
|
|
form.value.projects.push({
|
|
projectId: p.projectId,
|
|
projectCode: p.projectCode,
|
|
projectName: p.projectName,
|
|
workDescription: '',
|
|
planDescription: ''
|
|
})
|
|
showProjectModal.value = false
|
|
}
|
|
|
|
function removeProject(idx: number) {
|
|
form.value.projects.splice(idx, 1)
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (form.value.projects.length === 0) {
|
|
alert('최소 1개 이상의 프로젝트를 추가해주세요.')
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
try {
|
|
const res = await $fetch<any>('/api/report/weekly/create', {
|
|
method: 'POST',
|
|
body: {
|
|
reportYear: weekInfo.year,
|
|
reportWeek: weekInfo.week,
|
|
projects: form.value.projects.map(p => ({
|
|
projectId: p.projectId,
|
|
workDescription: p.workDescription,
|
|
planDescription: p.planDescription
|
|
})),
|
|
issueDescription: form.value.issueDescription,
|
|
vacationDescription: form.value.vacationDescription,
|
|
remarkDescription: form.value.remarkDescription
|
|
}
|
|
})
|
|
|
|
alert('저장되었습니다.')
|
|
router.push(`/report/weekly/${res.reportId}`)
|
|
} catch (e: any) {
|
|
alert(e.data?.message || '저장에 실패했습니다.')
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.modal.show {
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
</style>
|