출퇴근

This commit is contained in:
yoon 2025-03-12 23:00:52 +09:00
parent 65e620d579
commit ccad3596c7
3 changed files with 165 additions and 78 deletions

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y pb-0">
<div class="card app-calendar-wrapper"> <div class="card app-calendar-wrapper">
<div class="row g-0"> <div class="row g-0">
<div class="col-3 border-end text-center"> <div class="col-3 border-end text-center">
<div class="card-body pb-0"> <div class="card-body pb-0">
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-auto rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/> <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"> <p class="mt-2 fw-bold">
{{ user.name }} {{ user.name }}
</p> </p>
@ -12,33 +12,39 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-6 pe-1"> <div class="col-6 pe-1">
<p class="mb-1">출근시간</p> <p class="mb-1">출근시간</p>
<button class="btn btn-outline-primary border-3 w-100 py-0 h-px-50" :class="workTime ? 'p-0' : ''" @click="setWorkTime"> <button class="btn border-3 w-100 py-0 h-px-50" :class="workTime ? 'p-0 btn-primary pe-none' : 'btn-outline-primary'" @click="setWorkTime" >
<i v-if="!workTime" class="bx bx-run fs-2"></i> <i v-if="!workTime" class="bx bx-run fs-2"></i>
<span v-if="workTime" class="ql-size-12px ">{{ workTime }}</span> <span v-if="workTime" class="ql-size-12px">{{ workTime }}</span>
</button> </button>
</div> </div>
<div class="col-6 ps-1"> <div class="col-6 ps-1">
<p class="mb-1">퇴근시간</p> <p class="mb-1">퇴근시간</p>
<button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50"> <button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50" @click="setLeaveTime">
<i class='bx bxs-door-open fs-2'></i> <i v-if="!leaveTime" class='bx bxs-door-open fs-2'></i>
<span v-if="leaveTime" class="ql-size-12px">{{ leaveTime }}</span>
</button> </button>
</div> </div>
<div v-for="post in project" :key="post.PROJCTSEQ" <div v-for="post in project" :key="post.PROJCTSEQ"
class="border border-2 mt-3" class="border border-2 mt-3 card p-2"
:style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`" :style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`"
@dragover="allowDrop($event)" @dragover="allowDrop($event)"
@drop="handleDrop($event, post)"> @drop="handleDrop($event, post)">
{{ post.PROJCTNAM }} <p class="mb-1">
<div v-if="checkedInProject && checkedInProject.PROJCTSEQ === post.PROJCTSEQ && user && workTime" class="mt-2"> {{ post.PROJCTNAM }}
<img :src="`${baseUrl}upload/img/profile/${user.profile}`" </p>
alt="User Profile" <div class="row gx-2">
class="rounded-circle" <div v-for="commuter in commuters.filter(c => c.PROJCTNAM === post.PROJCTNAM)" :key="commuter.COMMUTCMT" class="col-4">
width="50" <div class="ratio ratio-1x1">
draggable="true" <img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
@dragstart="dragStart($event, post)" alt="User Profile"
@error="$event.target.src = '/img/icons/icon.png'"> class="rounded-circle"
:draggable="isCurrentUser(commuter)"
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
@error="$event.target.src = '/img/icons/icon.png'">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -63,15 +69,7 @@
</div> </div>
</div> </div>
<center-modal :display="isModalVisible" @close="isModalVisible = $event">
<template #title> 상세보기 </template>
<template #body>
</template>
<template #footer>
<BackBtn @click="closeModal" />
</template>
</center-modal>
</template> </template>
<script setup> <script setup>
@ -86,7 +84,6 @@ import '@/assets/css/app-calendar.css';
import { fetchHolidays } from '@c/calendar/holiday'; import { fetchHolidays } from '@c/calendar/holiday';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore'; import { useProjectStore } from '@/stores/useProjectStore';
import BackBtn from '@c/button/BackBtn.vue';
import { useToastStore } from '@/stores/toastStore'; import { useToastStore } from '@/stores/toastStore';
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
@ -99,15 +96,23 @@ const toastStore = useToastStore();
const dayjs = inject('dayjs'); const dayjs = inject('dayjs');
const fullCalendarRef = ref(null); const fullCalendarRef = ref(null);
const calendarEvents = ref([]); const calendarEvents = ref([]);
const isModalVisible = ref(false);
const eventDate = ref(''); const eventDate = ref('');
const workTime = ref(null); const workTime = ref(null);
const leaveTime = ref(null);
const selectedProject = ref(null); const selectedProject = ref(null);
const checkedInProject = ref(null); const checkedInProject = ref(null);
const draggedProject = ref(null); const draggedProject = ref(null);
const commuters = ref([]);
//
const isCurrentUser = (commuter) => {
return user.value && commuter && commuter.MEMBERSEQ === user.value.id;
};
// //
const dragStart = (event, project) => { const dragStart = (event, project) => {
draggedProject.value = project; draggedProject.value = project;
@ -130,6 +135,19 @@ const handleDrop = (event, targetProject) => {
return; return;
} }
// select
if (!selectedProject.value) {
$api.patch('project/updateYon', {
projctSeq: targetProject.PROJCTSEQ,
memberSeq: user.value.id,
projctYon: '1'
}).then(res => {
if (res.status === 200) {
projectStore.getProjectList();
}
});
}
// //
checkedInProject.value = targetProject; checkedInProject.value = targetProject;
projectStore.setSelectedProject(targetProject); projectStore.setSelectedProject(targetProject);
@ -137,40 +155,87 @@ const handleDrop = (event, targetProject) => {
// select // select
selectedProject.value = targetProject.PROJCTSEQ; selectedProject.value = targetProject.PROJCTSEQ;
// $api.patch('commuters/update', {
addProfileToCalendar(); projctSeq: targetProject.PROJCTSEQ,
memberSeq: user.value.id,
}).then(res => {
if (res.status === 200) {
toastStore.onToast('출근 프로젝트가 변경 되었습니다.', 's');
todaysCommuter();
loadCommuters();
}
});
}; };
//
const todayCommuterInfo = async () => {
if (!user.value || !user.value.id) return;
const res = await $api.get(`commuters/today/${user.value.id}`);
if (res.status === 200 ) {
const commuterInfo = res.data.data[0];
if (commuterInfo) {
workTime.value = commuterInfo.COMMUTCMT;
leaveTime.value = commuterInfo.COMMUTLVE;
}
}
};
//
const todaysCommuter = async () => {
const res = await $api.get(`commuters/todays`);
if (res.status === 200 ) {
commuters.value = res.data.data;
}
};
// //
const setWorkTime = () => { const setWorkTime = () => {
// //
if (workTime.value) return; if (workTime.value) return;
const now = new Date();
workTime.value = now.toLocaleTimeString('ko-KR', { hour12: false });
// //
const currentProject = projectStore.selectedProject || projectStore.getSelectedProject(); const currentProject = projectStore.selectedProject || projectStore.getSelectedProject();
if (currentProject) { if (currentProject) {
checkedInProject.value = currentProject; checkedInProject.value = currentProject;
//
addProfileToCalendar();
} }
return;
$api.post('commuters/insert', { $api.post('commuters/insert', {
memberSeq: user.value.id, memberSeq: user.value.id,
projctSeq: checkedInProject.value.PROJCTSEQ, projctSeq: checkedInProject.value.PROJCTSEQ,
commutCmt: workTime.value,
commutLvt: null, commutLvt: null,
commutArr: null, commutArr: null,
}).then(res => { }).then(res => {
if (res.status === 200) { if (res.status === 200) {
toastStore.onToast('출근 등록되었습니다.', 's'); toastStore.onToast('출근 완료.', 's');
} }
}); });
}; };
//
const setLeaveTime = () => {
$api.patch('commuters/updateLve', {
memberSeq: user.value.id,
commutLve: leaveTime.value || null,
}).then(res => {
if (res.status === 200) {
if (leaveTime.value) {
toastStore.onToast('퇴근 시간 초기화', 'e');
} else {
toastStore.onToast('퇴근 완료', 's');
}
todayCommuterInfo();
}
});
}
// //
const addProfileToCalendar = () => { const addProfileToCalendar = () => {
const calendarApi = fullCalendarRef.value?.getApi(); const calendarApi = fullCalendarRef.value?.getApi();
@ -184,10 +249,6 @@ const addProfileToCalendar = () => {
const dayFrame = todayCell.querySelector('.fc-daygrid-day-events'); const dayFrame = todayCell.querySelector('.fc-daygrid-day-events');
if (dayFrame) { if (dayFrame) {
const existingProfileImg = dayFrame.querySelector('.profile-img');
if (existingProfileImg) {
dayFrame.removeChild(existingProfileImg);
}
const profileImg = document.createElement('img'); const profileImg = document.createElement('img');
profileImg.src = `${baseUrl}upload/img/profile/${user.value.profile}`; profileImg.src = `${baseUrl}upload/img/profile/${user.value.profile}`;
@ -225,10 +286,9 @@ const fetchData = async () => {
// //
calendarEvents.value = [...existingEvents, ...holidayEvents]; calendarEvents.value = [...existingEvents, ...holidayEvents];
// //
if (workTime.value && checkedInProject.value) { await loadCommuters();
setTimeout(addProfileToCalendar, 100); //
}
} catch (error) { } catch (error) {
console.error('공휴일 정보 로딩 실패:', error); console.error('공휴일 정보 로딩 실패:', error);
} }
@ -246,14 +306,12 @@ const moveCalendar = async (value = 0) => {
calendarApi.today(); // calendarApi.today(); //
} }
//
await fetchData(); await fetchData();
}; };
// //
const isSelectableDate = (date) => { const isSelectableDate = (date) => {
const checkDate = dayjs(date); const checkDate = dayjs(date);
const today = dayjs().startOf('day');
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6; const isWeekend = checkDate.day() === 0 || checkDate.day() === 6;
// //
const isHoliday = calendarEvents.value.some(event => const isHoliday = calendarEvents.value.some(event =>
@ -261,7 +319,7 @@ const isSelectableDate = (date) => {
dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD') dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD')
); );
return !checkDate.isBefore(today) && !isWeekend && !isHoliday; return !isWeekend && !isHoliday;
}; };
@ -282,7 +340,6 @@ const handleDateClick = (info) => {
} }
eventDate.value = dayjs(info.date).format('YYYY-MM-DD'); eventDate.value = dayjs(info.date).format('YYYY-MM-DD');
showModal();
} }
}; };
@ -297,17 +354,63 @@ document.addEventListener('click', (event) => {
// //
const getCellClassNames = (arg) => { const getCellClassNames = (arg) => {
const cellDate = dayjs(arg.date); const cellDate = dayjs(arg.date);
const today = dayjs().startOf('day');
const classes = []; const classes = [];
// (, , ) // (, , )
if (!isSelectableDate(cellDate) || cellDate.isBefore(today)) { if (!isSelectableDate(cellDate)) {
classes.push('fc-day-sat-sun'); classes.push('fc-day-sat-sun');
} }
return classes; 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) {
const commuters = res.data.data;
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
img.remove();
});
commuters.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) {
//
const profileImg = document.createElement('img');
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
profileImg.className = 'rounded-circle w-px-20 h-px-20 ms-1 position-relative z-5';
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
dayEvents.appendChild(profileImg);
}
}
});
}
};
// //
const calendarOptions = reactive({ const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin], plugins: [dayGridPlugin, interactionPlugin],
@ -320,6 +423,8 @@ const calendarOptions = reactive({
locale: 'kr', locale: 'kr',
events: calendarEvents, events: calendarEvents,
eventOrder: 'sortIdx', eventOrder: 'sortIdx',
contentHeight:"auto",
// eventContent: calendarCommuter,
// //
selectable: true, selectable: true,
selectAllow: (selectInfo) => isSelectableDate(selectInfo.start), selectAllow: (selectInfo) => isSelectableDate(selectInfo.start),
@ -331,14 +436,6 @@ const calendarOptions = reactive({
droppable: false, droppable: false,
eventDisplay: 'block', eventDisplay: 'block',
//
datesSet: () => {
// ( )
if (workTime.value && checkedInProject.value) {
setTimeout(addProfileToCalendar, 100);
}
},
// //
customButtons: { customButtons: {
prev: { prev: {
@ -361,44 +458,30 @@ watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => {
await fetchData(); await fetchData();
}); });
//
const showModal = () => {
isModalVisible.value = true;
};
//
const closeModal = () => {
isModalVisible.value = false;
};
// selectedProject // selectedProject
watch(() => projectStore.selectedProject, (newProject) => { watch(() => projectStore.selectedProject, (newProject) => {
if (newProject) { if (newProject) {
selectedProject.value = newProject.PROJCTSEQ; selectedProject.value = newProject.PROJCTSEQ;
checkedInProject.value = newProject; checkedInProject.value = newProject;
addProfileToCalendar();
} }
}); });
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
await userStore.userInfo(); await userStore.userInfo();
user.value = userStore.user; user.value = userStore.user;
await projectStore.getProjectList(); await projectStore.getProjectList('', '', 'true');
project.value = projectStore.projectList; project.value = projectStore.projectList;
await todayCommuterInfo();
await todaysCommuter();
// //
const storedProject = projectStore.getSelectedProject(); const storedProject = projectStore.getSelectedProject();
if (storedProject) { if (storedProject) {
selectedProject.value = storedProject; selectedProject.value = storedProject;
} }
//
setTimeout(() => {
// ( )
if (workTime.value && checkedInProject.value) {
addProfileToCalendar();
}
}, 500);
}); });
</script> </script>

View File

@ -284,6 +284,8 @@
selectedProject.value = memberProject.value[0].PROJCTSEQ; selectedProject.value = memberProject.value[0].PROJCTSEQ;
updateSelectedProject(); updateSelectedProject();
} }
console.log(memberProject.value);
}; };
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore(); // const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();

View File

@ -13,16 +13,18 @@ export const useProjectStore = defineStore('project', () => {
const projectList = ref([]); const projectList = ref([]);
const selectedProject = ref(null); const selectedProject = ref(null);
const getProjectList = async (searchText = '', selectedYear = '') => { const getProjectList = async (searchText = '', selectedYear = '', excludeEnded = '') => {
const res = await $api.get('project/select', { const res = await $api.get('project/select', {
params: { params: {
searchKeyword: searchText || '', searchKeyword: searchText || '',
category: selectedYear || '', category: selectedYear || '',
excludeEnded: excludeEnded
}, },
}); });
projectList.value = res.data.data.projectList; projectList.value = res.data.data.projectList;
}; };
const setSelectedProject = (project) => { const setSelectedProject = (project) => {
selectedProject.value = { ...project }; selectedProject.value = { ...project };