This commit is contained in:
yoon 2025-03-10 14:25:07 +09:00
parent b08e72b813
commit 1384ae571d
3 changed files with 333 additions and 96 deletions

View File

@ -5,27 +5,41 @@
<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'"/>
<p class="mt-2">
<p class="mt-2 fw-bold">
{{ user.name }}
</p>
<div class="row g-0">
<div class="col-6 pe-1">
<p>출근시간</p>
<button class="btn btn-outline-primary border-3 w-100 py-0">
<i class='bx bx-run fs-2'></i>
<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">
<i v-if="!workTime" class="bx bx-run fs-2"></i>
<span v-if="workTime" class="ql-size-12px ">{{ workTime }}</span>
</button>
</div>
<div class="col-6 ps-1">
<p>퇴근시간</p>
<button class="btn btn-outline-secondary border-3 w-100 py-0">
<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>
</div>
<div v-for="post in project" :key="post.PROJCTSEQ" class="border border-2 mt-3" :style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`">
<div v-for="post in project" :key="post.PROJCTSEQ"
class="border border-2 mt-3"
: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'">
</div>
</div>
</div>
</div>
@ -50,26 +64,12 @@
</div>
<center-modal :display="isModalVisible" @close="isModalVisible = $event">
<template #title> Add Event </template>
<template #title> 상세보기 </template>
<template #body>
<FormInput
title="이벤트 제목"
name="event"
:is-essential="true"
:is-alert="eventAlert"
@update:data="eventTitle = $event"
/>
<FormInput
title="이벤트 날짜"
type="date"
name="eventDate"
:is-essential="true"
:is-alert="eventDateAlert"
@update:data="eventDate = $event"
/>
</template>
<template #footer>
<button @click="addEvent">추가</button>
<BackBtn @click="closeModal" />
</template>
</center-modal>
</template>
@ -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);
});
</script>

View File

@ -8,6 +8,12 @@
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row align-items-center ms-auto">
<select class="form-select py-1" id="name" v-model="selectedProject" @change="updateSelectedProject">
<option v-for="item in memberProject" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
{{ item.PROJCTNAM }}
</option>
</select>
<!-- <button class="btn p-1" @click="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button> -->
<!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
@ -234,36 +240,79 @@
<script setup>
import { useAuthStore } from '@s/useAuthStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import { useRouter } from 'vue-router';
import { useThemeStore } from '@s/darkmode';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import $api from '@api';
const user = ref(null);
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const baseUrl = import.meta.env.VITE_SERVER;
const authStore = useAuthStore();
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const router = useRouter();
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
const user = ref(null);
const memberProject = ref({});
const selectedProject = ref(null);
//
const updateSelectedProject = () => {
if (!selectedProject.value) return;
const selected = memberProject.value.find(
project => project.PROJCTSEQ === selectedProject.value
);
if (selected) {
projectStore.setSelectedProject(selected);
}
};
const getMemberProjects = async () => {
const res = await $api.get(`project/${user.value.id}`);
memberProject.value = res.data.data;
projectStore.projectList = memberProject.value; //
//
const storedProject = projectStore.getSelectedProject();
if (storedProject) {
selectedProject.value = storedProject.PROJCTSEQ;
} else if (memberProject.value.length > 0) {
//
selectedProject.value = memberProject.value[0].PROJCTSEQ;
updateSelectedProject();
}
};
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
onMounted(async () => {
if (isDarkMode) {
switchToDarkMode();
} else {
switchToLightMode();
}
// if (isDarkMode) {
// switchToDarkMode();
// } else {
// switchToLightMode();
// }
await userStore.userInfo();
user.value = userStore.user;
await getMemberProjects();
});
watch(() => projectStore.selectedProject, (newProject) => {
if (newProject) {
selectedProject.value = newProject.PROJCTSEQ; // select
}
});
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
</script>
<style scoped>
</style>

View File

@ -6,11 +6,12 @@
설명 : 프로젝트 목록
*/
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { ref, watch } from 'vue';
import $api from '@api';
export const useProjectStore = defineStore('project', () => {
const projectList = ref([]);
const selectedProject = ref(null);
const getProjectList = async (searchText = '', selectedYear = '') => {
const res = await $api.get('project/select', {
@ -22,6 +23,36 @@ export const useProjectStore = defineStore('project', () => {
projectList.value = res.data.data.projectList;
};
const setSelectedProject = (project) => {
selectedProject.value = { ...project };
return { projectList, getProjectList };
if (project) {
localStorage.setItem('selectedProject', JSON.stringify(project));
}
};
const getSelectedProject = () => {
if (!selectedProject.value) {
const storedProject = localStorage.getItem('selectedProject');
if (storedProject) {
selectedProject.value = JSON.parse(storedProject);
}
}
return selectedProject.value;
};
// 프로젝트 리스트가 변경될 때 자동으로 반응
watch(projectList, (newList) => {
if (!selectedProject.value && newList.length > 0) {
setSelectedProject(newList[0]);
}
});
return {
projectList,
selectedProject,
getProjectList,
setSelectedProject,
getSelectedProject
};
});