Files
weeklyreport/frontend/report/weekly/write.vue
2026-01-04 20:58:47 +09:00

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>