From 1384ae571dbaef28e777e1d0dad21a0dd92efe8c Mon Sep 17 00:00:00 2001 From: yoon Date: Mon, 10 Mar 2025 14:25:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=9C=EA=B7=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/commuters/CommuteCalendar.vue | 327 ++++++++++++++----- src/layouts/TheTop.vue | 67 +++- src/stores/useProjectStore.js | 35 +- 3 files changed, 333 insertions(+), 96 deletions(-) diff --git a/src/components/commuters/CommuteCalendar.vue b/src/components/commuters/CommuteCalendar.vue index 9898eaa..688a129 100644 --- a/src/components/commuters/CommuteCalendar.vue +++ b/src/components/commuters/CommuteCalendar.vue @@ -5,27 +5,41 @@
Profile Image -

+

{{ user.name }}

-

출근시간

-
-

퇴근시간

-
-
+
{{ post.PROJCTNAM }} +
+ User Profile +
@@ -50,26 +64,12 @@
- + @@ -81,38 +81,123 @@ import interactionPlugin from '@fullcalendar/interaction'; import CenterModal from '@c/modal/CenterModal.vue'; import { inject, onMounted, reactive, ref, watch } from 'vue'; import $api from '@api'; -import { isEmpty } from '@/common/utils'; -import FormInput from '../input/FormInput.vue'; 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 BackBtn from '@c/button/BackBtn.vue'; +import { useToastStore } from '@/stores/toastStore'; const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const user = ref({}); const project = ref({}); const userStore = useUserInfoStore(); const projectStore = useProjectStore(); +const toastStore = useToastStore(); const dayjs = inject('dayjs'); const fullCalendarRef = ref(null); const calendarEvents = ref([]); const isModalVisible = ref(false); -const eventAlert = ref(false); -const eventDateAlert = ref(false); -const eventTitle = ref(''); const eventDate = ref(''); -const selectedDate = ref(null); +const workTime = ref(null); +const selectedProject = ref(null); +const checkedInProject = ref(null); -// 날짜 선택 핸들러 -const handleDateSelect = (selectedDates) => { - if (selectedDates.length > 0) { - // 선택된 첫 번째 날짜를 YYYY-MM-DD 형식으로 변환 - const selectedDate = dayjs(selectedDates[0]).format('YYYY-MM-DD'); - eventDate.value = selectedDate; - showModal(); // 모달 표시 +const draggedProject = ref(null); + +// 드래그 시작 이벤트 핸들러 +const dragStart = (event, project) => { + draggedProject.value = project; + // 드래그 데이터 설정 + event.dataTransfer.setData('application/json', JSON.stringify(project)); + event.dataTransfer.effectAllowed = 'copy'; +}; + +// 드래그 오버 드롭 허용 +const allowDrop = (event) => { + event.preventDefault(); +}; + +// 드롭 +const handleDrop = (event, targetProject) => { + event.preventDefault(); + + // 드래그한 프로젝트와 드롭한 프로젝트가 같으면 아무 동작 안 함 + if (draggedProject.value.PROJCTSEQ === targetProject.PROJCTSEQ) { + return; + } + + // 선택된 프로젝트 변경 + checkedInProject.value = targetProject; + projectStore.setSelectedProject(targetProject); + + // select 값도 변경 + selectedProject.value = targetProject.PROJCTSEQ; + + // 프로필 테두리 색상 업데이트 + addProfileToCalendar(); +}; + +// 출근 시간 +const setWorkTime = () => { + // 이미 출근 시간이 설정된 경우 중복 실행 방지 + if (workTime.value) return; + + const now = new Date(); + workTime.value = now.toLocaleTimeString('ko-KR', { hour12: false }); + + // 현재 선택된 프로젝트 가져오기 + const currentProject = projectStore.selectedProject || projectStore.getSelectedProject(); + if (currentProject) { + checkedInProject.value = currentProject; + + // 캘린더에 오늘 날짜에 프로필 이미지 추가하기 + addProfileToCalendar(); + } + + $api.post('commuters/insert', { + memberSeq: user.value.id, + projctSeq: checkedInProject.value.PROJCTSEQ, + commutCmt: workTime.value, + commutLvt: null, + commutArr: null, + }).then(res => { + if (res.status === 200) { + toastStore.onToast('출근 등록되었습니다.', 's'); + } + }); +}; + +// 캘린더의 오늘 날짜에 프로필 이미지 추가 +const addProfileToCalendar = () => { + const calendarApi = fullCalendarRef.value?.getApi(); + if (!calendarApi || !user.value) return; + + // 오늘 날짜 셀 찾기 + const today = dayjs().format('YYYY-MM-DD'); + const todayCell = document.querySelector(`.fc-day[data-date="${today}"]`) || document.querySelector(`.fc-daygrid-day[data-date="${today}"]`); + + if (todayCell) { + const dayFrame = todayCell.querySelector('.fc-daygrid-day-events'); + + if (dayFrame) { + const existingProfileImg = dayFrame.querySelector('.profile-img'); + if (existingProfileImg) { + dayFrame.removeChild(existingProfileImg); + } + + const profileImg = document.createElement('img'); + profileImg.src = `${baseUrl}upload/img/profile/${user.value.profile}`; + profileImg.className = 'profile-img rounded-circle w-px-20 h-px-20'; + profileImg.style.border = `2px solid ${checkedInProject.value.projctcolor}`; + + profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; }; + + dayFrame.appendChild(profileImg); + } } }; @@ -139,6 +224,11 @@ const fetchData = async () => { const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event')); // 필터링된 이벤트와 새로 가져온 공휴일 이벤트 병합 calendarEvents.value = [...existingEvents, ...holidayEvents]; + + // 출근 시간이 있으면 프로필 이미지 다시 추가 + if (workTime.value && checkedInProject.value) { + setTimeout(addProfileToCalendar, 100); // 달력이 렌더링된 후 실행 + } } catch (error) { console.error('공휴일 정보 로딩 실패:', error); } @@ -160,72 +250,108 @@ const moveCalendar = async (value = 0) => { await fetchData(); }; -// 모달 표시 함수 -const showModal = () => { - isModalVisible.value = true; +// 날짜 선택 가능 여부를 확인하는 공통 함수 +const isSelectableDate = (date) => { + const checkDate = dayjs(date); + const today = dayjs().startOf('day'); + 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 !checkDate.isBefore(today) && !isWeekend && !isHoliday; }; -// 모달 닫기 함수 -const closeModal = () => { - isModalVisible.value = false; - // 입력 필드 초기화 - eventTitle.value = ''; - eventDate.value = ''; -}; -// 이벤트 추가 함수 -const addEvent = () => { - // 이벤트 유효성 검사 - if (!checkEvent()) { - // 유효성 검사 통과 시 이벤트 추가 - calendarEvents.value.push({ - title: eventTitle.value, - start: eventDate.value, - backgroundColor: '#4CAF50' // 일반 이벤트 색상 - }); - closeModal(); // 모달 닫기 +// 날짜 클릭 이벤트 함수 +let todayElement = null; +const handleDateClick = (info) => { + 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; + } + + eventDate.value = dayjs(info.date).format('YYYY-MM-DD'); + showModal(); } }; -// 이벤트 유효성 검사 함수 -const checkEvent = () => { - // 제목과 날짜가 비어있는지 확인 - eventAlert.value = isEmpty(eventTitle.value); - eventDateAlert.value = isEmpty(eventDate.value); - // 하나라도 비어있으면 true 반환 (유효성 검사 실패) - return eventAlert.value || eventDateAlert.value; +// 바깥 클릭 시 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 today = dayjs().startOf('day'); + const classes = []; + + // 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가 + if (!isSelectableDate(cellDate) || cellDate.isBefore(today)) { + classes.push('fc-day-sat-sun'); + } + + return classes; }; // 캘린더 옵션 설정 const calendarOptions = reactive({ - plugins: [dayGridPlugin, interactionPlugin], // 사용할 플러그인 - initialView: 'dayGridMonth', // 초기 뷰 (월간) - headerToolbar: { // 상단 툴바 구성 - left: 'today', // 왼쪽: 오늘 버튼 - center: 'title', // 중앙: 제목(연월) - right: 'prev,next', // 오른쪽: 이전/다음 버튼 + plugins: [dayGridPlugin, interactionPlugin], + initialView: 'dayGridMonth', + headerToolbar: { + left: 'today', + center: 'title', + right: 'prev,next', + }, + locale: 'kr', + events: calendarEvents, + eventOrder: 'sortIdx', + // 날짜 선택 관련 옵션 수정 + selectable: true, + selectAllow: (selectInfo) => isSelectableDate(selectInfo.start), + dateClick: handleDateClick, + dayCellClassNames: getCellClassNames, + + // 날짜 클릭 비활성화를 위한 추가 설정 + unselectAuto: true, + droppable: false, + eventDisplay: 'block', + + // 캘린더 렌더링 완료 이벤트 + datesSet: () => { + // 캘린더가 렌더링 된 후 프로필 이미지 다시 추가 (월 변경 시) + if (workTime.value && checkedInProject.value) { + setTimeout(addProfileToCalendar, 100); + } }, - locale: 'kr', // 한국어 지역화 - events: calendarEvents, // 표시할 이벤트 데이터 - eventOrder: 'sortIdx', // 이벤트 정렬 기준 - selectable: true, // 날짜 선택 가능 여부 - dateClick: handleDateSelect, // 날짜 클릭 이벤트 핸들러 - droppable: false, // 드래그 앤 드롭 비활성화 - eventDisplay: 'block', // 이벤트 표시 방식 // 커스텀 버튼 정의 customButtons: { prev: { - text: 'PREV', // 이전 버튼 텍스트 - click: () => moveCalendar(1), // 클릭 시 이전 달로 이동 + text: 'PREV', + click: () => moveCalendar(1), }, today: { - text: 'TODAY', // 오늘 버튼 텍스트 - click: () => moveCalendar(3), // 클릭 시 오늘로 이동 + text: 'TODAY', + click: () => moveCalendar(3), }, next: { - text: 'NEXT', // 다음 버튼 텍스트 - click: () => moveCalendar(2), // 클릭 시 다음 달로 이동 + text: 'NEXT', + click: () => moveCalendar(2), }, }, }); @@ -235,13 +361,44 @@ watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => { await fetchData(); }); +// 모달 표시 함수 +const showModal = () => { + isModalVisible.value = true; +}; + +// 모달 닫기 함수 +const closeModal = () => { + isModalVisible.value = false; +}; + +// selectedProject 변경 감지 +watch(() => projectStore.selectedProject, (newProject) => { + if (newProject) { + selectedProject.value = newProject.PROJCTSEQ; + checkedInProject.value = newProject; + addProfileToCalendar(); + } +}); -console.log(project) onMounted(async () => { await fetchData(); await userStore.userInfo(); user.value = userStore.user; await projectStore.getProjectList(); project.value = projectStore.projectList; + + // 저장된 선택 프로젝트 가져오기 + const storedProject = projectStore.getSelectedProject(); + if (storedProject) { + selectedProject.value = storedProject; + } + + // 캘린더가 완전히 로드된 후 프로필 추가를 위한 지연 설정 + setTimeout(() => { + // 이미 출근 시간이 있는 경우 (페이지 새로고침 등) 프로필 이미지 표시 + if (workTime.value && checkedInProject.value) { + addProfileToCalendar(); + } + }, 500); }); diff --git a/src/layouts/TheTop.vue b/src/layouts/TheTop.vue index 4c1d281..9f1121d 100644 --- a/src/layouts/TheTop.vue +++ b/src/layouts/TheTop.vue @@ -8,6 +8,12 @@