출퇴근

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>
<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="row g-0">
<div class="col-3 border-end text-center">
<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">
{{ user.name }}
</p>
@ -12,33 +12,39 @@
<div class="row g-0">
<div class="col-6 pe-1">
<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>
<span v-if="workTime" class="ql-size-12px ">{{ workTime }}</span>
<span v-if="workTime" class="ql-size-12px">{{ workTime }}</span>
</button>
</div>
<div class="col-6 ps-1">
<p class="mb-1">퇴근시간</p>
<button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50">
<i class='bx bxs-door-open fs-2'></i>
<button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50" @click="setLeaveTime">
<i v-if="!leaveTime" class='bx bxs-door-open fs-2'></i>
<span v-if="leaveTime" class="ql-size-12px">{{ leaveTime }}</span>
</button>
</div>
<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;`"
@dragover="allowDrop($event)"
@drop="handleDrop($event, post)">
{{ post.PROJCTNAM }}
<div v-if="checkedInProject && checkedInProject.PROJCTSEQ === post.PROJCTSEQ && user && workTime" class="mt-2">
<img :src="`${baseUrl}upload/img/profile/${user.profile}`"
alt="User Profile"
class="rounded-circle"
width="50"
draggable="true"
@dragstart="dragStart($event, post)"
@error="$event.target.src = '/img/icons/icon.png'">
<p class="mb-1">
{{ post.PROJCTNAM }}
</p>
<div class="row gx-2">
<div v-for="commuter in commuters.filter(c => c.PROJCTNAM === post.PROJCTNAM)" :key="commuter.COMMUTCMT" class="col-4">
<div class="ratio ratio-1x1">
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
alt="User Profile"
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>
@ -63,15 +69,7 @@
</div>
</div>
<center-modal :display="isModalVisible" @close="isModalVisible = $event">
<template #title> 상세보기 </template>
<template #body>
</template>
<template #footer>
<BackBtn @click="closeModal" />
</template>
</center-modal>
</template>
<script setup>
@ -86,7 +84,6 @@ 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\/$/, '');
@ -99,15 +96,23 @@ const toastStore = useToastStore();
const dayjs = inject('dayjs');
const fullCalendarRef = ref(null);
const calendarEvents = ref([]);
const isModalVisible = ref(false);
const eventDate = ref('');
const workTime = ref(null);
const leaveTime = ref(null);
const selectedProject = ref(null);
const checkedInProject = 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) => {
draggedProject.value = project;
@ -130,6 +135,19 @@ const handleDrop = (event, targetProject) => {
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;
projectStore.setSelectedProject(targetProject);
@ -137,40 +155,87 @@ const handleDrop = (event, targetProject) => {
// select
selectedProject.value = targetProject.PROJCTSEQ;
//
addProfileToCalendar();
$api.patch('commuters/update', {
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 = () => {
//
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();
}
return;
$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');
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 calendarApi = fullCalendarRef.value?.getApi();
@ -184,10 +249,6 @@ const addProfileToCalendar = () => {
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}`;
@ -225,10 +286,9 @@ const fetchData = async () => {
//
calendarEvents.value = [...existingEvents, ...holidayEvents];
//
if (workTime.value && checkedInProject.value) {
setTimeout(addProfileToCalendar, 100); //
}
//
await loadCommuters();
} catch (error) {
console.error('공휴일 정보 로딩 실패:', error);
}
@ -246,14 +306,12 @@ const moveCalendar = async (value = 0) => {
calendarApi.today(); //
}
//
await fetchData();
};
//
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 =>
@ -261,7 +319,7 @@ const isSelectableDate = (date) => {
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');
showModal();
}
};
@ -297,17 +354,63 @@ document.addEventListener('click', (event) => {
//
const getCellClassNames = (arg) => {
const cellDate = dayjs(arg.date);
const today = dayjs().startOf('day');
const classes = [];
// (, , )
if (!isSelectableDate(cellDate) || cellDate.isBefore(today)) {
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) {
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({
plugins: [dayGridPlugin, interactionPlugin],
@ -320,6 +423,8 @@ const calendarOptions = reactive({
locale: 'kr',
events: calendarEvents,
eventOrder: 'sortIdx',
contentHeight:"auto",
// eventContent: calendarCommuter,
//
selectable: true,
selectAllow: (selectInfo) => isSelectableDate(selectInfo.start),
@ -331,14 +436,6 @@ const calendarOptions = reactive({
droppable: false,
eventDisplay: 'block',
//
datesSet: () => {
// ( )
if (workTime.value && checkedInProject.value) {
setTimeout(addProfileToCalendar, 100);
}
},
//
customButtons: {
prev: {
@ -361,44 +458,30 @@ 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();
}
});
onMounted(async () => {
await fetchData();
await userStore.userInfo();
user.value = userStore.user;
await projectStore.getProjectList();
await projectStore.getProjectList('', '', 'true');
project.value = projectStore.projectList;
await todayCommuterInfo();
await todaysCommuter();
//
const storedProject = projectStore.getSelectedProject();
if (storedProject) {
selectedProject.value = storedProject;
}
//
setTimeout(() => {
// ( )
if (workTime.value && checkedInProject.value) {
addProfileToCalendar();
}
}, 500);
});
</script>

View File

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

View File

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