workMainViewPublish
This commit is contained in:
parent
6613fed587
commit
2be48fe901
BIN
public/img/mainImg/tea-cup-619268.png
Normal file
BIN
public/img/mainImg/tea-cup-619268.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
423
src/components/main/MainCalendar.vue
Normal file
423
src/components/main/MainCalendar.vue
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl flex-grow-1 container-p-y pb-0">
|
||||||
|
<div class="card app-calendar-wrapper">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-3 border-end text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<img
|
||||||
|
v-if="user"
|
||||||
|
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||||
|
alt="Profile Image"
|
||||||
|
class="w-px-50 h-px-50 rounded-circle"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
|
/>
|
||||||
|
<p class="mt-2 fw-bold">
|
||||||
|
{{ user.name }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CommuterBtn
|
||||||
|
:userId="user.id"
|
||||||
|
:checkedInProject="checkedInProject || {}"
|
||||||
|
@workTimeUpdated="handleWorkTimeUpdate"
|
||||||
|
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||||
|
ref="workTimeComponentRef"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommuterProjectList
|
||||||
|
:project="project"
|
||||||
|
:commuters="commuters"
|
||||||
|
:baseUrl="baseUrl"
|
||||||
|
:user="user"
|
||||||
|
:selectedProject="selectedProject"
|
||||||
|
:checkedInProject="checkedInProject"
|
||||||
|
@drop="handleProjectDrop"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col app-calendar-content">
|
||||||
|
<div class="card shadow-none border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<full-calendar
|
||||||
|
ref="fullCalendarRef"
|
||||||
|
:events="calendarEvents"
|
||||||
|
:options="calendarOptions"
|
||||||
|
defaultView="dayGridMonth"
|
||||||
|
class="flatpickr-calendar-only"
|
||||||
|
>
|
||||||
|
</full-calendar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title>
|
||||||
|
{{ eventDate }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedDateCommuters.length > 0">
|
||||||
|
<div v-for="(commuter, index) in selectedDateCommuters" :key="index">
|
||||||
|
<div class="d-flex align-items-center my-2">
|
||||||
|
<img
|
||||||
|
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
|
class="rounded-circle me-2 w-px-50 h-px-50"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="text-white fw-bold rounded py-1 px-3" :style="`background: ${commuter.projctcolor} !important;`">{{
|
||||||
|
commuter.memberName
|
||||||
|
}}</span>
|
||||||
|
|
||||||
|
<div class="ms-auto text-start fw-bold">{{ commuter.COMMUTCMT }} ~ {{ commuter.COMMUTLVE || '00:00:00' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<BackBtn @click="closeModal" />
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FullCalendar from '@fullcalendar/vue3';
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
|
import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
import 'flatpickr/dist/flatpickr.min.css';
|
||||||
|
import '@/assets/css/app-calendar.css';
|
||||||
|
import { fetchHolidays } from '@c/calendar/holiday';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
|
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||||
|
import CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
|
||||||
|
import BackBtn from '@c/button/BackBtn.vue';
|
||||||
|
|
||||||
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
|
const user = ref({});
|
||||||
|
const project = ref({});
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
|
||||||
|
const dayjs = inject('dayjs');
|
||||||
|
const fullCalendarRef = ref(null);
|
||||||
|
const workTimeComponentRef = ref(null);
|
||||||
|
const calendarEvents = ref([]);
|
||||||
|
const eventDate = ref('');
|
||||||
|
|
||||||
|
const selectedProject = ref(null);
|
||||||
|
const checkedInProject = ref(null);
|
||||||
|
|
||||||
|
// 모달 상태
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
|
||||||
|
const commuters = ref([]);
|
||||||
|
const monthlyCommuters = ref([]);
|
||||||
|
|
||||||
|
// 출퇴근 컴포넌트 이벤트 핸들러
|
||||||
|
const handleWorkTimeUpdate = () => {
|
||||||
|
todaysCommuter();
|
||||||
|
loadCommuters();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveTimeUpdate = () => {
|
||||||
|
todaysCommuter();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 드롭 이벤트 핸들러 (ProjectList 컴포넌트에서 전달받음)
|
||||||
|
const handleProjectDrop = ({ event, targetProject }) => {
|
||||||
|
// 드래그된 프로젝트 데이터 가져오기
|
||||||
|
const draggedProjectData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||||
|
|
||||||
|
// 드래그한 프로젝트와 드롭한 프로젝트가 같으면 아무 동작 안 함
|
||||||
|
if (draggedProjectData.PROJCTSEQ === targetProject.PROJCTSEQ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 프로젝트 변경
|
||||||
|
checkedInProject.value = targetProject;
|
||||||
|
projectStore.setSelectedProject(targetProject);
|
||||||
|
|
||||||
|
// select 값도 변경
|
||||||
|
selectedProject.value = targetProject.PROJCTSEQ;
|
||||||
|
|
||||||
|
$api.patch('commuters/update', {
|
||||||
|
projctSeq: targetProject.PROJCTSEQ,
|
||||||
|
memberSeq: user.value.id,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
todaysCommuter();
|
||||||
|
loadCommuters();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 오늘 출근 모든 사용자 조회
|
||||||
|
const todaysCommuter = async () => {
|
||||||
|
const res = await $api.get(`commuters/todays`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
commuters.value = res.data.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 캘린더 데이터 가져오기
|
||||||
|
const fetchData = async () => {
|
||||||
|
// FullCalendar API 인스턴스 가져오기
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
if (!calendarApi) return;
|
||||||
|
|
||||||
|
// 현재 표시된 달력의 연도, 월 추출
|
||||||
|
const date = calendarApi.currentData.viewTitle;
|
||||||
|
const dateArr = date.split(' ');
|
||||||
|
let currentYear = dateArr[0].trim();
|
||||||
|
let currentMonth = dateArr[1].trim();
|
||||||
|
const regex = /\D/g;
|
||||||
|
// 숫자가 아닌 문자 제거 후 정수로 변환
|
||||||
|
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
||||||
|
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 현재 표시 중인 월의 공휴일 정보 가져오기
|
||||||
|
const holidayEvents = await fetchHolidays(currentYear, String(currentMonth).padStart(2, '0'));
|
||||||
|
// 기존 이벤트에서 공휴일 이벤트를 제외한 이벤트만 필터링
|
||||||
|
const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event'));
|
||||||
|
// 필터링된 이벤트와 새로 가져온 공휴일 이벤트 병합
|
||||||
|
calendarEvents.value = [...existingEvents, ...holidayEvents];
|
||||||
|
|
||||||
|
// 출근 정보
|
||||||
|
await loadCommuters();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('공휴일 정보 로딩 실패:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 캘린더 이동 함수 (이전, 다음, 오늘)
|
||||||
|
const moveCalendar = async (value = 0) => {
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
|
||||||
|
if (value === 1) {
|
||||||
|
calendarApi.prev(); // 이전 달로 이동
|
||||||
|
} else if (value === 2) {
|
||||||
|
calendarApi.next(); // 다음 달로 이동
|
||||||
|
} else if (value === 3) {
|
||||||
|
calendarApi.today(); // 오늘 날짜로 이동
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 선택 가능 여부를 확인하는 공통 함수
|
||||||
|
const isSelectableDate = date => {
|
||||||
|
const checkDate = dayjs(date);
|
||||||
|
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6;
|
||||||
|
// 공휴일 체크
|
||||||
|
const isHoliday = calendarEvents.value.some(
|
||||||
|
event =>
|
||||||
|
event.classNames?.includes('holiday-event') && dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !isWeekend && !isHoliday;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 클릭 이벤트 함수
|
||||||
|
let todayElement = null;
|
||||||
|
const handleDateClick = info => {
|
||||||
|
const clickedDate = dayjs(info.date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 클릭한 날짜에 월별 출근 정보가 있는지 확인
|
||||||
|
const dateCommuters = monthlyCommuters.value.filter(commuter => commuter.COMMUTDAY === clickedDate);
|
||||||
|
|
||||||
|
// 출근 기록이 있는 경우에만 모달 열기
|
||||||
|
if (dateCommuters.length > 0) {
|
||||||
|
eventDate.value = clickedDate;
|
||||||
|
isModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelectableDate(info.date)) {
|
||||||
|
const isToday = dayjs(info.date).isSame(dayjs(), 'day');
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
|
||||||
|
todayElement = info.dayEl;
|
||||||
|
todayElement.classList.remove('fc-day-today');
|
||||||
|
} else if (todayElement) {
|
||||||
|
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
|
||||||
|
todayElement.classList.add('fc-day-today');
|
||||||
|
todayElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 바깥 클릭 시 todayElement 클래스 복구
|
||||||
|
document.addEventListener(
|
||||||
|
'click',
|
||||||
|
event => {
|
||||||
|
if (todayElement && !event.target.closest('.fc-daygrid-day')) {
|
||||||
|
todayElement.classList.add('fc-day-today');
|
||||||
|
todayElement = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 날짜 셀 클래스 추가 함수
|
||||||
|
const getCellClassNames = arg => {
|
||||||
|
const cellDate = dayjs(arg.date);
|
||||||
|
const classes = [];
|
||||||
|
|
||||||
|
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
|
||||||
|
if (!isSelectableDate(cellDate)) {
|
||||||
|
classes.push('fc-day-sat-sun');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 현재 달의 모든 출근 정보 조회
|
||||||
|
const loadCommuters = async () => {
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
if (!calendarApi) return;
|
||||||
|
const date = calendarApi.currentData.viewTitle;
|
||||||
|
const dateArr = date.split(' ');
|
||||||
|
let currentYear = dateArr[0].trim();
|
||||||
|
let currentMonth = dateArr[1].trim();
|
||||||
|
const regex = /\D/g;
|
||||||
|
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
||||||
|
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
||||||
|
|
||||||
|
const res = await $api.get('commuters/month', {
|
||||||
|
params: {
|
||||||
|
year: currentYear,
|
||||||
|
month: currentMonth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
// 월별 출근 정보 저장
|
||||||
|
monthlyCommuters.value = res.data.data;
|
||||||
|
|
||||||
|
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
|
||||||
|
img.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
monthlyCommuters.value.forEach(commuter => {
|
||||||
|
const date = commuter.COMMUTDAY;
|
||||||
|
const dateCell =
|
||||||
|
document.querySelector(`.fc-day[data-date="${date}"]`) ||
|
||||||
|
document.querySelector(`.fc-daygrid-day[data-date="${date}"]`);
|
||||||
|
if (dateCell) {
|
||||||
|
const dayEvents = dateCell.querySelector('.fc-daygrid-day-events');
|
||||||
|
if (dayEvents) {
|
||||||
|
dayEvents.classList.add('text-center');
|
||||||
|
// 프로필 이미지 생성
|
||||||
|
const profileImg = document.createElement('img');
|
||||||
|
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
||||||
|
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto';
|
||||||
|
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
|
||||||
|
profileImg.onerror = () => {
|
||||||
|
profileImg.src = '/img/icons/icon.png';
|
||||||
|
};
|
||||||
|
|
||||||
|
dayEvents.appendChild(profileImg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 캘린더 옵션 설정
|
||||||
|
const calendarOptions = reactive({
|
||||||
|
plugins: [dayGridPlugin, interactionPlugin],
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'prev,next',
|
||||||
|
},
|
||||||
|
locale: 'kr',
|
||||||
|
events: calendarEvents,
|
||||||
|
eventOrder: 'sortIdx',
|
||||||
|
contentHeight: 'auto',
|
||||||
|
// eventContent: calendarCommuter,
|
||||||
|
// 날짜 선택 관련 옵션 수정
|
||||||
|
selectable: true,
|
||||||
|
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
|
||||||
|
dateClick: handleDateClick,
|
||||||
|
dayCellClassNames: getCellClassNames,
|
||||||
|
|
||||||
|
// 날짜 클릭 비활성화를 위한 추가 설정
|
||||||
|
unselectAuto: true,
|
||||||
|
droppable: false,
|
||||||
|
eventDisplay: 'block',
|
||||||
|
|
||||||
|
// 커스텀 버튼 정의
|
||||||
|
customButtons: {
|
||||||
|
prev: {
|
||||||
|
text: 'PREV',
|
||||||
|
click: () => moveCalendar(1),
|
||||||
|
},
|
||||||
|
today: {
|
||||||
|
text: 'TODAY',
|
||||||
|
click: () => moveCalendar(3),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
text: 'NEXT',
|
||||||
|
click: () => moveCalendar(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
||||||
|
watch(
|
||||||
|
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
|
||||||
|
async () => {
|
||||||
|
await fetchData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// selectedProject 변경 감지
|
||||||
|
watch(
|
||||||
|
() => projectStore.selectedProject,
|
||||||
|
newProject => {
|
||||||
|
if (newProject) {
|
||||||
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
|
checkedInProject.value = newProject;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 모달 열기
|
||||||
|
const openModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 닫기
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDateCommuters = computed(() => {
|
||||||
|
return monthlyCommuters.value.filter(commuter => commuter.COMMUTDAY === eventDate.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
await userStore.userInfo();
|
||||||
|
user.value = userStore.user;
|
||||||
|
await projectStore.getProjectList('', '', 'true');
|
||||||
|
project.value = projectStore.projectList;
|
||||||
|
|
||||||
|
await todaysCommuter();
|
||||||
|
|
||||||
|
// 저장된 선택 프로젝트 가져오기
|
||||||
|
const storedProject = projectStore.getSelectedProject();
|
||||||
|
if (storedProject) {
|
||||||
|
selectedProject.value = storedProject.PROJCTSEQ;
|
||||||
|
checkedInProject.value = storedProject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user