출퇴근
This commit is contained in:
parent
10aaae307e
commit
a772a2b4e6
129
src/components/commuters/CommuterBtn.vue
Normal file
129
src/components/commuters/CommuterBtn.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-6 pe-1">
|
||||||
|
<p class="mb-1">출근시간</p>
|
||||||
|
<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>
|
||||||
|
</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"
|
||||||
|
@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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, onMounted, watch } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
import { useToastStore } from '@/stores/toastStore';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
userId: {
|
||||||
|
type: [Number],
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
checkedInProject: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated']);
|
||||||
|
|
||||||
|
const workTime = ref(null);
|
||||||
|
const leaveTime = ref(null);
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
|
// 오늘 사용자의 출근 정보 조회
|
||||||
|
const todayCommuterInfo = async () => {
|
||||||
|
if (!props.userId) return;
|
||||||
|
|
||||||
|
const res = await $api.get(`commuters/today/${props.userId}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
const commuterInfo = res.data.data[0];
|
||||||
|
|
||||||
|
if (commuterInfo) {
|
||||||
|
workTime.value = commuterInfo.COMMUTCMT;
|
||||||
|
leaveTime.value = commuterInfo.COMMUTLVE;
|
||||||
|
|
||||||
|
// 부모 컴포넌트에 상태 업데이트 알림
|
||||||
|
emit('workTimeUpdated', workTime.value);
|
||||||
|
emit('leaveTimeUpdated', leaveTime.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 출근 시간
|
||||||
|
const setWorkTime = () => {
|
||||||
|
// 이미 출근 시간이 설정된 경우 중복 실행 방지
|
||||||
|
if (workTime.value) return;
|
||||||
|
|
||||||
|
$api.post('commuters/insert', {
|
||||||
|
memberSeq: props.userId,
|
||||||
|
projctSeq: props.checkedInProject.PROJCTSEQ,
|
||||||
|
commutLvt: null,
|
||||||
|
commutArr: null,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
toastStore.onToast('출근 완료.', 's');
|
||||||
|
todayCommuterInfo();
|
||||||
|
// 부모 컴포넌트에 업데이트 이벤트 발생
|
||||||
|
emit('workTimeUpdated', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 퇴근
|
||||||
|
const setLeaveTime = () => {
|
||||||
|
$api.patch('commuters/updateLve', {
|
||||||
|
memberSeq: props.userId,
|
||||||
|
commutLve: leaveTime.value || null,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (leaveTime.value) {
|
||||||
|
toastStore.onToast('퇴근 시간 초기화', 'e');
|
||||||
|
} else {
|
||||||
|
toastStore.onToast('퇴근 완료', 's');
|
||||||
|
}
|
||||||
|
todayCommuterInfo();
|
||||||
|
// 부모 컴포넌트에 업데이트 이벤트 발생
|
||||||
|
emit('leaveTimeUpdated', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// props 변경 감지
|
||||||
|
watch(() => props.userId, async () => {
|
||||||
|
if (props.userId) {
|
||||||
|
await todayCommuterInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.checkedInProject, () => {
|
||||||
|
// 프로젝트가 변경되면 필요한 처리 수행
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await todayCommuterInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 외부에서 접근할 메서드 노출
|
||||||
|
defineExpose({
|
||||||
|
todayCommuterInfo,
|
||||||
|
workTime,
|
||||||
|
leaveTime
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -3,57 +3,35 @@
|
|||||||
<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">
|
||||||
<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'"/>
|
<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>
|
||||||
|
|
||||||
<div class="row g-0">
|
<CommuterBtn
|
||||||
<div class="col-6 pe-1">
|
:userId="user.id"
|
||||||
<p class="mb-1">출근시간</p>
|
:checkedInProject="checkedInProject || {}"
|
||||||
<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" >
|
@workTimeUpdated="handleWorkTimeUpdate"
|
||||||
<i v-if="!workTime" class="bx bx-run fs-2"></i>
|
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||||
<span v-if="workTime" class="ql-size-12px">{{ workTime }}</span>
|
ref="workTimeComponentRef"
|
||||||
</button>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6 ps-1">
|
<CommuterProjectList
|
||||||
<p class="mb-1">퇴근시간</p>
|
:project="project"
|
||||||
<button class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50" @click="setLeaveTime">
|
:commuters="commuters"
|
||||||
<i v-if="!leaveTime" class='bx bxs-door-open fs-2'></i>
|
:baseUrl="baseUrl"
|
||||||
<span v-if="leaveTime" class="ql-size-12px">{{ leaveTime }}</span>
|
:user="user"
|
||||||
</button>
|
:selectedProject="selectedProject"
|
||||||
</div>
|
:checkedInProject="checkedInProject"
|
||||||
|
@drop="handleProjectDrop"
|
||||||
<div v-for="post in project" :key="post.PROJCTSEQ"
|
/>
|
||||||
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)">
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col app-calendar-content">
|
<div class="col app-calendar-content">
|
||||||
<div class="card shadow-none border-0">
|
<div class="card shadow-none border-0">
|
||||||
<div class="card-body pb-0">
|
<div class="card-body">
|
||||||
<full-calendar
|
<full-calendar
|
||||||
ref="fullCalendarRef"
|
ref="fullCalendarRef"
|
||||||
:events="calendarEvents"
|
:events="calendarEvents"
|
||||||
@ -69,7 +47,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title>
|
||||||
|
{{ eventDate }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedDateCommuters.length > 0">
|
||||||
|
<div v-for="(commuter, index) in selectedDateCommuters" :key="index">
|
||||||
|
<div class="d-flex align-items-center my-2">
|
||||||
|
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
|
class="rounded-circle me-2 w-px-50 h-px-50"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'">
|
||||||
|
|
||||||
|
<span class="text-white fw-bold rounded py-1 px-3" :style="`background: ${commuter.projctcolor} !important;`">{{ commuter.memberName }}</span>
|
||||||
|
|
||||||
|
<div class="ms-auto text-start fw-bold">
|
||||||
|
{{ commuter.COMMUTCMT }} ~ {{ commuter.COMMUTLVE || "00:00:00" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<BackBtn @click="closeModal" />
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -77,7 +79,7 @@ import FullCalendar from '@fullcalendar/vue3';
|
|||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
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 { computed, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
import 'flatpickr/dist/flatpickr.min.css';
|
import 'flatpickr/dist/flatpickr.min.css';
|
||||||
import '@/assets/css/app-calendar.css';
|
import '@/assets/css/app-calendar.css';
|
||||||
@ -85,6 +87,9 @@ 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 { useToastStore } from '@/stores/toastStore';
|
import { useToastStore } from '@/stores/toastStore';
|
||||||
|
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||||
|
import CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
|
||||||
|
import BackBtn from '@c/button/BackBtn.vue';
|
||||||
|
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
const user = ref({});
|
const user = ref({});
|
||||||
@ -95,43 +100,37 @@ const toastStore = useToastStore();
|
|||||||
|
|
||||||
const dayjs = inject('dayjs');
|
const dayjs = inject('dayjs');
|
||||||
const fullCalendarRef = ref(null);
|
const fullCalendarRef = ref(null);
|
||||||
|
const workTimeComponentRef = ref(null);
|
||||||
const calendarEvents = ref([]);
|
const calendarEvents = ref([]);
|
||||||
const eventDate = ref('');
|
const eventDate = ref('');
|
||||||
|
|
||||||
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 isModalOpen = ref(false);
|
||||||
|
|
||||||
const commuters = ref([]);
|
const commuters = ref([]);
|
||||||
|
const monthlyCommuters = ref([]);
|
||||||
|
|
||||||
|
|
||||||
// 현재 사용자 확인
|
// 출퇴근 컴포넌트 이벤트 핸들러
|
||||||
const isCurrentUser = (commuter) => {
|
const handleWorkTimeUpdate = () => {
|
||||||
return user.value && commuter && commuter.MEMBERSEQ === user.value.id;
|
todaysCommuter();
|
||||||
|
loadCommuters();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 드래그 시작 이벤트 핸들러
|
const handleLeaveTimeUpdate = () => {
|
||||||
const dragStart = (event, project) => {
|
todaysCommuter();
|
||||||
draggedProject.value = project;
|
|
||||||
// 드래그 데이터 설정
|
|
||||||
event.dataTransfer.setData('application/json', JSON.stringify(project));
|
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 드래그 오버 드롭 허용
|
// 프로젝트 드롭 이벤트 핸들러 (ProjectList 컴포넌트에서 전달받음)
|
||||||
const allowDrop = (event) => {
|
const handleProjectDrop = ({ event, targetProject }) => {
|
||||||
event.preventDefault();
|
// 드래그된 프로젝트 데이터 가져오기
|
||||||
};
|
const draggedProjectData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||||
|
|
||||||
// 드롭
|
|
||||||
const handleDrop = (event, targetProject) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// 드래그한 프로젝트와 드롭한 프로젝트가 같으면 아무 동작 안 함
|
// 드래그한 프로젝트와 드롭한 프로젝트가 같으면 아무 동작 안 함
|
||||||
if (draggedProject.value.PROJCTSEQ === targetProject.PROJCTSEQ) {
|
if (draggedProjectData.PROJCTSEQ === targetProject.PROJCTSEQ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,23 +167,6 @@ const handleDrop = (event, targetProject) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 오늘 사용자의 출근 정보 조회
|
|
||||||
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 todaysCommuter = async () => {
|
||||||
const res = await $api.get(`commuters/todays`);
|
const res = await $api.get(`commuters/todays`);
|
||||||
@ -193,75 +175,6 @@ const todaysCommuter = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 출근 시간
|
|
||||||
const setWorkTime = () => {
|
|
||||||
// 이미 출근 시간이 설정된 경우 중복 실행 방지
|
|
||||||
if (workTime.value) return;
|
|
||||||
|
|
||||||
// 현재 선택된 프로젝트 가져오기
|
|
||||||
const currentProject = projectStore.selectedProject || projectStore.getSelectedProject();
|
|
||||||
if (currentProject) {
|
|
||||||
checkedInProject.value = currentProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
$api.post('commuters/insert', {
|
|
||||||
memberSeq: user.value.id,
|
|
||||||
projctSeq: checkedInProject.value.PROJCTSEQ,
|
|
||||||
commutLvt: null,
|
|
||||||
commutArr: null,
|
|
||||||
}).then(res => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
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();
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 캘린더 데이터 가져오기
|
// 캘린더 데이터 가져오기
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// FullCalendar API 인스턴스 가져오기
|
// FullCalendar API 인스턴스 가져오기
|
||||||
@ -326,20 +239,31 @@ const isSelectableDate = (date) => {
|
|||||||
// 날짜 클릭 이벤트 함수
|
// 날짜 클릭 이벤트 함수
|
||||||
let todayElement = null;
|
let todayElement = null;
|
||||||
const handleDateClick = (info) => {
|
const handleDateClick = (info) => {
|
||||||
|
const clickedDate = dayjs(info.date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 클릭한 날짜에 월별 출근 정보가 있는지 확인
|
||||||
|
const dateCommuters = monthlyCommuters.value.filter(commuter =>
|
||||||
|
commuter.COMMUTDAY === clickedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
// 출근 기록이 있는 경우에만 모달 열기
|
||||||
|
if (dateCommuters.length > 0) {
|
||||||
|
eventDate.value = clickedDate;
|
||||||
|
isModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelectableDate(info.date)) {
|
if (isSelectableDate(info.date)) {
|
||||||
const isToday = dayjs(info.date).isSame(dayjs(), 'day');
|
const isToday = dayjs(info.date).isSame(dayjs(), 'day');
|
||||||
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
|
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
|
||||||
todayElement = info.dayEl;
|
todayElement = info.dayEl;
|
||||||
todayElement.classList.remove('fc-day-today');
|
todayElement.classList.remove('fc-day-today');
|
||||||
} else if (todayElement) {
|
} else if (todayElement) {
|
||||||
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
|
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
|
||||||
todayElement.classList.add('fc-day-today');
|
todayElement.classList.add('fc-day-today');
|
||||||
todayElement = null;
|
todayElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventDate.value = dayjs(info.date).format('YYYY-MM-DD');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -364,6 +288,7 @@ const getCellClassNames = (arg) => {
|
|||||||
return classes;
|
return classes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 현재 달의 모든 출근 정보 조회
|
||||||
const loadCommuters = async () => {
|
const loadCommuters = async () => {
|
||||||
const calendarApi = fullCalendarRef.value?.getApi();
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
if (!calendarApi) return;
|
if (!calendarApi) return;
|
||||||
@ -374,6 +299,7 @@ const loadCommuters = async () => {
|
|||||||
const regex = /\D/g;
|
const regex = /\D/g;
|
||||||
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
||||||
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
||||||
|
|
||||||
const res = await $api.get('commuters/month', {
|
const res = await $api.get('commuters/month', {
|
||||||
params: {
|
params: {
|
||||||
year: currentYear,
|
year: currentYear,
|
||||||
@ -381,13 +307,14 @@ const loadCommuters = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const commuters = res.data.data;
|
// 월별 출근 정보 저장
|
||||||
|
monthlyCommuters.value = res.data.data;
|
||||||
|
|
||||||
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
|
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
|
||||||
img.remove();
|
img.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
commuters.forEach(commuter => {
|
monthlyCommuters.value.forEach(commuter => {
|
||||||
const date = commuter.COMMUTDAY;
|
const date = commuter.COMMUTDAY;
|
||||||
const dateCell = document.querySelector(`.fc-day[data-date="${date}"]`) ||
|
const dateCell = document.querySelector(`.fc-day[data-date="${date}"]`) ||
|
||||||
document.querySelector(`.fc-daygrid-day[data-date="${date}"]`);
|
document.querySelector(`.fc-daygrid-day[data-date="${date}"]`);
|
||||||
@ -395,7 +322,6 @@ const loadCommuters = async () => {
|
|||||||
if (dateCell) {
|
if (dateCell) {
|
||||||
const dayEvents = dateCell.querySelector('.fc-daygrid-day-events');
|
const dayEvents = dateCell.querySelector('.fc-daygrid-day-events');
|
||||||
if (dayEvents) {
|
if (dayEvents) {
|
||||||
|
|
||||||
// 프로필 이미지 생성
|
// 프로필 이미지 생성
|
||||||
const profileImg = document.createElement('img');
|
const profileImg = document.createElement('img');
|
||||||
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
||||||
@ -467,6 +393,22 @@ watch(() => projectStore.selectedProject, (newProject) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 모달 열기
|
||||||
|
const openModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 닫기
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDateCommuters = computed(() => {
|
||||||
|
return monthlyCommuters.value.filter(commuter =>
|
||||||
|
commuter.COMMUTDAY === eventDate.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchData();
|
await fetchData();
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
@ -474,14 +416,13 @@ onMounted(async () => {
|
|||||||
await projectStore.getProjectList('', '', 'true');
|
await projectStore.getProjectList('', '', 'true');
|
||||||
project.value = projectStore.projectList;
|
project.value = projectStore.projectList;
|
||||||
|
|
||||||
await todayCommuterInfo();
|
|
||||||
await todaysCommuter();
|
await todaysCommuter();
|
||||||
|
|
||||||
// 저장된 선택 프로젝트 가져오기
|
// 저장된 선택 프로젝트 가져오기
|
||||||
const storedProject = projectStore.getSelectedProject();
|
const storedProject = projectStore.getSelectedProject();
|
||||||
if (storedProject) {
|
if (storedProject) {
|
||||||
selectedProject.value = storedProject;
|
selectedProject.value = storedProject.PROJCTSEQ;
|
||||||
|
checkedInProject.value = storedProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
82
src/components/commuters/CommuterProjectList.vue
Normal file
82
src/components/commuters/CommuterProjectList.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div style="max-height: 590px; overflow-y: auto;">
|
||||||
|
<div v-for="post in project" :key="post.PROJCTSEQ"
|
||||||
|
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)">
|
||||||
|
<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"
|
||||||
|
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
||||||
|
:draggable="isCurrentUser(commuter)"
|
||||||
|
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
commuters: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
selectedProject: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
checkedInProject: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['drop', 'update:selectedProject', 'update:checkedInProject']);
|
||||||
|
|
||||||
|
// 현재 사용자 확인
|
||||||
|
const isCurrentUser = (commuter) => {
|
||||||
|
return props.user && commuter && commuter.MEMBERSEQ === props.user.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드래그 시작 이벤트 핸들러
|
||||||
|
const dragStart = (event, project) => {
|
||||||
|
// 드래그 데이터 설정
|
||||||
|
event.dataTransfer.setData('application/json', JSON.stringify(project));
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드래그 오버 드롭 허용
|
||||||
|
const allowDrop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드롭
|
||||||
|
const handleDrop = (event, targetProject) => {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('drop', { event, targetProject });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -177,10 +177,12 @@ import ArrInput from '@c/input/ArrInput.vue';
|
|||||||
import { useToastStore } from '@s/toastStore';
|
import { useToastStore } from '@s/toastStore';
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import commonApi from '@/common/commonApi';
|
import commonApi from '@/common/commonApi';
|
||||||
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
|
|
||||||
// 스토어
|
// 스토어
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -448,7 +450,8 @@ const handleDelete = () => {
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||||
location.reload()
|
projectStore.getProjectList();
|
||||||
|
projectStore.getMemberProjects();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@ -247,6 +247,8 @@
|
|||||||
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||||
closeCreateModal();
|
closeCreateModal();
|
||||||
getProjectList();
|
getProjectList();
|
||||||
|
projectStore.getMemberProjects();
|
||||||
|
formReset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<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">
|
<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">
|
<option v-for="item in projectStore.memberProjectList" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
||||||
{{ item.PROJCTNAM }}
|
{{ item.PROJCTNAM }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@ -254,14 +254,13 @@
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
const memberProject = ref({});
|
|
||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
|
|
||||||
// 프로젝트 선택 변경 시 스토어에 저장
|
// 프로젝트 선택 변경 시 스토어에 저장
|
||||||
const updateSelectedProject = () => {
|
const updateSelectedProject = () => {
|
||||||
if (!selectedProject.value) return;
|
if (!selectedProject.value) return;
|
||||||
|
|
||||||
const selected = memberProject.value.find(
|
const selected = projectStore.memberProjectList.find(
|
||||||
project => project.PROJCTSEQ === selectedProject.value
|
project => project.PROJCTSEQ === selectedProject.value
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -270,23 +269,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(memberProject.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||||
|
|
||||||
@ -300,16 +282,26 @@
|
|||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
user.value = userStore.user;
|
user.value = userStore.user;
|
||||||
|
|
||||||
await getMemberProjects();
|
await projectStore.getMemberProjects();
|
||||||
|
|
||||||
|
// memberProjectList가 로드된 후 selectedProject 업데이트
|
||||||
|
if (projectStore.selectedProject) {
|
||||||
|
selectedProject.value = projectStore.selectedProject.PROJCTSEQ;
|
||||||
|
} else if (projectStore.memberProjectList.length > 0) {
|
||||||
|
selectedProject.value = projectStore.memberProjectList[0].PROJCTSEQ;
|
||||||
|
projectStore.setSelectedProject(projectStore.memberProjectList[0]);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// projectStore.selectedProject 변경 감지
|
||||||
watch(() => projectStore.selectedProject, (newProject) => {
|
watch(() => projectStore.selectedProject, (newProject) => {
|
||||||
if (newProject) {
|
if (newProject) {
|
||||||
selectedProject.value = newProject.PROJCTSEQ; // select 값 강제 변경
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await authStore.logout();
|
await authStore.logout();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
|
|||||||
@ -8,11 +8,15 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
|
||||||
export const useProjectStore = defineStore('project', () => {
|
export const useProjectStore = defineStore('project', () => {
|
||||||
const projectList = ref([]);
|
const projectList = ref([]);
|
||||||
|
const memberProjectList = ref([]);
|
||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
|
||||||
|
// 전체 프로젝트 가져오기
|
||||||
const getProjectList = async (searchText = '', selectedYear = '', excludeEnded = '') => {
|
const getProjectList = async (searchText = '', selectedYear = '', excludeEnded = '') => {
|
||||||
const res = await $api.get('project/select', {
|
const res = await $api.get('project/select', {
|
||||||
params: {
|
params: {
|
||||||
@ -24,12 +28,25 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
projectList.value = res.data.data.projectList;
|
projectList.value = res.data.data.projectList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 사용자가 속한 프로젝트 목록 가져오기
|
||||||
|
const getMemberProjects = async () => {
|
||||||
|
if (!userStore.user) return; // 로그인한 사용자 확인
|
||||||
|
|
||||||
|
const res = await $api.get(`project/${userStore.user.id}`);
|
||||||
|
memberProjectList.value = res.data.data;
|
||||||
|
|
||||||
|
// 로그인 직후 자동으로 프로젝트 선택 (watch와 별개로 직접 처리)
|
||||||
|
if (memberProjectList.value.length > 0 && !selectedProject.value) {
|
||||||
|
setSelectedProject(memberProjectList.value[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setSelectedProject = (project) => {
|
const setSelectedProject = (project) => {
|
||||||
selectedProject.value = { ...project };
|
selectedProject.value = project ? { ...project } : null;
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
localStorage.setItem('selectedProject', JSON.stringify(project));
|
localStorage.setItem('selectedProject', JSON.stringify(project));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('selectedProject');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,15 +62,38 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
|
|
||||||
// 프로젝트 리스트가 변경될 때 자동으로 반응
|
// 프로젝트 리스트가 변경될 때 자동으로 반응
|
||||||
watch(projectList, (newList) => {
|
watch(projectList, (newList) => {
|
||||||
|
|
||||||
if (!selectedProject.value && newList.length > 0) {
|
if (!selectedProject.value && newList.length > 0) {
|
||||||
setSelectedProject(newList[0]);
|
setSelectedProject(newList[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(memberProjectList, (newList) => {
|
||||||
|
if (newList.length > 0) {
|
||||||
|
// 현재 선택된 프로젝트가 없거나 목록에 없는 경우 첫 번째 항목 선택
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
setSelectedProject(newList[0]);
|
||||||
|
} else {
|
||||||
|
// 선택된 프로젝트가 있는 경우 목록에 있는지 확인
|
||||||
|
const exists = newList.some(project =>
|
||||||
|
project.PROJCTSEQ === selectedProject.value.PROJCTSEQ);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
setSelectedProject(newList[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 목록이 비어있으면 선택된 프로젝트를 null로 설정
|
||||||
|
setSelectedProject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectList,
|
projectList,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
getProjectList,
|
getProjectList,
|
||||||
|
memberProjectList,
|
||||||
|
getMemberProjects,
|
||||||
setSelectedProject,
|
setSelectedProject,
|
||||||
getSelectedProject
|
getSelectedProject
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,5 +3,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CommuteCalendar from '@c/commuters/CommuteCalendar.vue';
|
import CommuteCalendar from '@c/commuters/CommuterCalendar.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user