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="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-auto rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
<p class="mt-2"> <p class="mt-2 fw-bold">
{{ user.name }} {{ user.name }}
</p> </p>
<div class="row g-0"> <div class="row g-0">
<div class="col-6 pe-1"> <div class="col-6 pe-1">
<p>출근시간</p> <p class="mb-1">출근시간</p>
<button class="btn btn-outline-primary border-3 w-100 py-0"> <button class="btn btn-outline-primary border-3 w-100 py-0 h-px-50" :class="workTime ? 'p-0' : ''" @click="setWorkTime">
<i 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>
</button> </button>
</div> </div>
<div class="col-6 ps-1"> <div class="col-6 ps-1">
<p>퇴근시간</p> <p class="mb-1">퇴근시간</p>
<button class="btn btn-outline-secondary border-3 w-100 py-0"> <button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50">
<i class='bx bxs-door-open fs-2'></i> <i class='bx bxs-door-open fs-2'></i>
</button> </button>
</div> </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 }} {{ 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> </div>
</div> </div>
@ -50,26 +64,12 @@
</div> </div>
<center-modal :display="isModalVisible" @close="isModalVisible = $event"> <center-modal :display="isModalVisible" @close="isModalVisible = $event">
<template #title> Add Event </template> <template #title> 상세보기 </template>
<template #body> <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>
<template #footer> <template #footer>
<button @click="addEvent">추가</button> <BackBtn @click="closeModal" />
</template> </template>
</center-modal> </center-modal>
</template> </template>
@ -81,38 +81,123 @@ import interactionPlugin from '@fullcalendar/interaction';
import CenterModal from '@c/modal/CenterModal.vue'; import CenterModal from '@c/modal/CenterModal.vue';
import { inject, onMounted, reactive, ref, watch } from 'vue'; import { inject, onMounted, reactive, ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
import { isEmpty } from '@/common/utils';
import FormInput from '../input/FormInput.vue';
import 'flatpickr/dist/flatpickr.min.css'; import 'flatpickr/dist/flatpickr.min.css';
import '@/assets/css/app-calendar.css'; 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';
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const user = ref({}); const user = ref({});
const project = ref({}); const project = ref({});
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
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 isModalVisible = ref(false);
const eventAlert = ref(false);
const eventDateAlert = ref(false);
const eventTitle = ref('');
const eventDate = ref(''); const eventDate = ref('');
const selectedDate = ref(null);
const workTime = ref(null);
const selectedProject = ref(null);
const checkedInProject = ref(null);
// const draggedProject = ref(null);
const handleDateSelect = (selectedDates) => {
if (selectedDates.length > 0) { //
// YYYY-MM-DD const dragStart = (event, project) => {
const selectedDate = dayjs(selectedDates[0]).format('YYYY-MM-DD'); draggedProject.value = project;
eventDate.value = selectedDate; //
showModal(); // 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')); const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event'));
// //
calendarEvents.value = [...existingEvents, ...holidayEvents]; calendarEvents.value = [...existingEvents, ...holidayEvents];
//
if (workTime.value && checkedInProject.value) {
setTimeout(addProfileToCalendar, 100); //
}
} catch (error) { } catch (error) {
console.error('공휴일 정보 로딩 실패:', error); console.error('공휴일 정보 로딩 실패:', error);
} }
@ -160,72 +250,108 @@ const moveCalendar = async (value = 0) => {
await fetchData(); await fetchData();
}; };
// //
const showModal = () => { const isSelectableDate = (date) => {
isModalVisible.value = true; 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 = () => { let todayElement = null;
// const handleDateClick = (info) => {
if (!checkEvent()) { if (isSelectableDate(info.date)) {
// const isToday = dayjs(info.date).isSame(dayjs(), 'day');
calendarEvents.value.push({
title: eventTitle.value, if (isToday) {
start: eventDate.value, //
backgroundColor: '#4CAF50' // todayElement = info.dayEl;
}); todayElement.classList.remove('fc-day-today');
closeModal(); // } else if (todayElement) {
//
todayElement.classList.add('fc-day-today');
todayElement = null;
}
eventDate.value = dayjs(info.date).format('YYYY-MM-DD');
showModal();
} }
}; };
// // todayElement
const checkEvent = () => { document.addEventListener('click', (event) => {
// if (todayElement && !event.target.closest('.fc-daygrid-day')) {
eventAlert.value = isEmpty(eventTitle.value); todayElement.classList.add('fc-day-today');
eventDateAlert.value = isEmpty(eventDate.value); todayElement = null;
// true ( ) }
return eventAlert.value || eventDateAlert.value; }, 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({ const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin], // plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth', // () initialView: 'dayGridMonth',
headerToolbar: { // headerToolbar: {
left: 'today', // : left: 'today',
center: 'title', // : () center: 'title',
right: 'prev,next', // : / 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: { customButtons: {
prev: { prev: {
text: 'PREV', // text: 'PREV',
click: () => moveCalendar(1), // click: () => moveCalendar(1),
}, },
today: { today: {
text: 'TODAY', // text: 'TODAY',
click: () => moveCalendar(3), // click: () => moveCalendar(3),
}, },
next: { next: {
text: 'NEXT', // text: 'NEXT',
click: () => moveCalendar(2), // click: () => moveCalendar(2),
}, },
}, },
}); });
@ -235,13 +361,44 @@ watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => {
await fetchData(); 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 () => { 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();
project.value = projectStore.projectList; project.value = projectStore.projectList;
//
const storedProject = projectStore.getSelectedProject();
if (storedProject) {
selectedProject.value = storedProject;
}
//
setTimeout(() => {
// ( )
if (workTime.value && checkedInProject.value) {
addProfileToCalendar();
}
}, 500);
}); });
</script> </script>

View File

@ -8,6 +8,12 @@
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse"> <div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row align-items-center ms-auto"> <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="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button> -->
<!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> --> <!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
@ -234,36 +240,79 @@
<script setup> <script setup>
import { useAuthStore } from '@s/useAuthStore'; import { useAuthStore } from '@s/useAuthStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useThemeStore } from '@s/darkmode'; import { useThemeStore } from '@s/darkmode';
import { onMounted, ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
const user = ref(null);
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const baseUrl = import.meta.env.VITE_SERVER; const baseUrl = import.meta.env.VITE_SERVER;
const authStore = useAuthStore(); const authStore = useAuthStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const router = useRouter(); 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 () => { onMounted(async () => {
if (isDarkMode) { // if (isDarkMode) {
switchToDarkMode(); // switchToDarkMode();
} else { // } else {
switchToLightMode(); // switchToLightMode();
} // }
await userStore.userInfo(); await userStore.userInfo();
user.value = userStore.user; user.value = userStore.user;
await getMemberProjects();
});
watch(() => projectStore.selectedProject, (newProject) => {
if (newProject) {
selectedProject.value = newProject.PROJCTSEQ; // select
}
}); });
const handleLogout = async () => { const handleLogout = async () => {
await authStore.logout(); await authStore.logout();
router.push('/login'); router.push('/login');
}; };
</script> </script>
<style scoped> <style scoped>
</style> </style>

View File

@ -6,11 +6,12 @@
설명 : 프로젝트 목록 설명 : 프로젝트 목록
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
export const useProjectStore = defineStore('project', () => { export const useProjectStore = defineStore('project', () => {
const projectList = ref([]); const projectList = ref([]);
const selectedProject = ref(null);
const getProjectList = async (searchText = '', selectedYear = '') => { const getProjectList = async (searchText = '', selectedYear = '') => {
const res = await $api.get('project/select', { const res = await $api.get('project/select', {
@ -22,6 +23,36 @@ export const useProjectStore = defineStore('project', () => {
projectList.value = res.data.data.projectList; 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
};
}); });