Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good
All checks were successful
LocalNet_front/pipeline/head This commit looks good
This commit is contained in:
commit
6430d45279
2
.env.dev
2
.env.dev
@ -1,4 +1,4 @@
|
||||
VITE_DOMAIN = https://192.168.0.251:5173/
|
||||
VITE_DOMAIN = https://192.168.0.251:5100/
|
||||
# VITE_LOGIN_URL = http://localhost:10325/ms/
|
||||
VITE_SERVER = https://192.168.0.251:10300/
|
||||
VITE_API_URL = https://192.168.0.251:10300/api/
|
||||
|
||||
BIN
public/img/main-category-img/main-300201.png
Normal file
BIN
public/img/main-category-img/main-300201.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
public/img/main-category-img/main-300202.png
Normal file
BIN
public/img/main-category-img/main-300202.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
public/img/main-category-img/main-300203.png
Normal file
BIN
public/img/main-category-img/main-300203.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
public/img/main-category-img/main-300204.png
Normal file
BIN
public/img/main-category-img/main-300204.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/main-category-img/main-300205.png
Normal file
BIN
public/img/main-category-img/main-300205.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
public/img/main-category-img/main-300206.png
Normal file
BIN
public/img/main-category-img/main-300206.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@ -47,11 +47,11 @@ const common = {
|
||||
*
|
||||
* @param {string} dateStr
|
||||
* @return
|
||||
* 1. Date type 인 경우 예시 '25-02-24 12:02'
|
||||
* 1. Date type 인 경우 예시 '2025-02-24 12:02'
|
||||
* 2. Date type 이 아닌 경우 입력값 리턴
|
||||
*
|
||||
*/
|
||||
dateFormatter(dateStr) {
|
||||
dateFormatter(dateStr, type = null) {
|
||||
const date = new Date(dateStr);
|
||||
const dateCheck = date.getTime();
|
||||
|
||||
@ -59,13 +59,26 @@ const common = {
|
||||
return dateStr;
|
||||
} else {
|
||||
const { year, month, day, hours, minutes } = this.formatDateTime(date);
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
let callback = '';
|
||||
|
||||
if (type == 'YMD') {
|
||||
callback = `${year}-${month}-${day}`;
|
||||
} else if (type == 'MD') {
|
||||
callback = `${month}-${day}`;
|
||||
} else {
|
||||
callback = `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
},
|
||||
|
||||
formatDateTime(date) {
|
||||
const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
||||
formatDateTime(dateObj) {
|
||||
const date = new Date(dateObj);
|
||||
const dateCheck = date.getTime();
|
||||
if (isNaN(dateCheck)) return dateObj;
|
||||
|
||||
const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
||||
return {
|
||||
year: date.getFullYear(),
|
||||
month: zeroFormat(date.getMonth() + 1),
|
||||
@ -76,6 +89,34 @@ const common = {
|
||||
};
|
||||
},
|
||||
|
||||
// 오늘 날짜시간 조회
|
||||
getToday() {
|
||||
const date = new Date();
|
||||
return {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate(),
|
||||
hours: date.getHours(),
|
||||
minutes: date.getMinutes(),
|
||||
seconds: date.getSeconds(),
|
||||
};
|
||||
},
|
||||
|
||||
// 해당 월, 일에 맞는 목록 필터링
|
||||
filterTargetByDate(target, key, month, day) {
|
||||
if (!Array.isArray(target) || target.length === 0) return [];
|
||||
|
||||
return [...target].filter(item => {
|
||||
if (!item[key]) return false;
|
||||
|
||||
const date = new Date(item[key]);
|
||||
const MatchingMonth = date.getMonth() + 1 === parseInt(month, 10);
|
||||
const MatchingDay = date.getDate() === parseInt(day, 10);
|
||||
|
||||
return MatchingMonth && MatchingDay;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 빈값 확인
|
||||
*
|
||||
@ -83,9 +124,17 @@ const common = {
|
||||
* @returns
|
||||
*/
|
||||
isNotEmpty(obj) {
|
||||
if (obj === null || obj === undefined) return false;
|
||||
if (typeof obj === 'string' && obj.trim() === '') return false;
|
||||
if ((Array.isArray(obj) || obj === Object(obj)) && Object.keys(obj).length === 0) return false;
|
||||
if (obj === null || obj === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof obj === 'string' && obj.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((Array.isArray(obj) || obj === Object(obj)) && Object.keys(obj).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
@ -139,6 +188,17 @@ const common = {
|
||||
showImage(event) {
|
||||
return (event.target.style.visibility = 'visible');
|
||||
},
|
||||
|
||||
addHyphenToPhoneNumber(phoneNum) {
|
||||
const phoneNumber = phoneNum;
|
||||
const length = phoneNumber.length;
|
||||
|
||||
if (length >= 9) {
|
||||
return phoneNumber.replace(/[^0-9]/g, '').replace(/^(\d{2,3})(\d{3,4})(\d{4})$/, `$1-$2-$3`);
|
||||
} else {
|
||||
return phoneNum;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
201
src/components/main/EventModal.vue
Normal file
201
src/components/main/EventModal.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="event-modal position-fixed bg-white shadow rounded" :style="modalStyle">
|
||||
<!-- 이벤트 선택 화면 -->
|
||||
<div v-if="!selectedEventType" class="d-flex flex-wrap gap-2 p-2">
|
||||
<div v-for="event in eventTypes" :key="event.code" class="event-icon-wrapper position-relative">
|
||||
<img
|
||||
:src="`${baseUrl}img/main-category-img/main-${event.code}.png`"
|
||||
class="event-icon-select"
|
||||
style="width: 25px; height: 25px; cursor: pointer"
|
||||
@click="handleEventClick(event)"
|
||||
/>
|
||||
<!-- X 표시 수정 -->
|
||||
<span v-if="isEventExists(event.type)" class="cancel-mark"> × </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 입력 폼 화면 -->
|
||||
<div v-else class="p-2" style="min-width: 200px">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-muted">{{ getEventTitle(selectedEventType) }}</small>
|
||||
<button class="btn-close btn-close-sm" style="font-size: 8px" @click="resetForm"></button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm py-1"
|
||||
style="height: 25px; font-size: 12px"
|
||||
placeholder="장소"
|
||||
v-model="eventPlace"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input type="time" class="form-control form-control-sm py-1" style="height: 25px; font-size: 12px" v-model="eventTime" />
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button class="btn btn-primary btn-sm py-1" style="font-size: 12px; height: 25px; line-height: 1" @click="handleSubmit">
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
position: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
selectedDate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dateEvents: {
|
||||
type: Array,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'delete', 'insert']);
|
||||
|
||||
// 폼 관련 상태
|
||||
const selectedEventType = ref(null);
|
||||
const eventPlace = ref('');
|
||||
const eventTime = ref('');
|
||||
|
||||
const eventTypes = [
|
||||
{ type: 'birthdayParty', code: '300203', title: '생일파티' },
|
||||
{ type: 'dinner', code: '300204', title: '회식' },
|
||||
{ type: 'teaTime', code: '300205', title: '티타임' },
|
||||
{ type: 'workshop', code: '300206', title: '워크샵' },
|
||||
];
|
||||
|
||||
const getEventTitle = type => {
|
||||
return eventTypes.find(event => event.type === type)?.title || '';
|
||||
};
|
||||
|
||||
const isEventExists = type => {
|
||||
return props.dateEvents?.some(event => event.type === type);
|
||||
};
|
||||
|
||||
const handleEventClick = event => {
|
||||
if (isEventExists(event.type)) {
|
||||
if (confirm('이벤트를 취소하시겠습니까?')) {
|
||||
emit('delete', {
|
||||
date: props.selectedDate,
|
||||
code: event.code,
|
||||
title: event.title,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
selectedEventType.value = event.code;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!eventPlace.value || !eventTime.value) {
|
||||
alert('장소와 시간을 모두 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('insert', {
|
||||
date: props.selectedDate,
|
||||
code: selectedEventType.value,
|
||||
title: getEventTitle(selectedEventType.value),
|
||||
place: eventPlace.value,
|
||||
time: eventTime.value,
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
selectedEventType.value = null;
|
||||
eventPlace.value = '';
|
||||
eventTime.value = '';
|
||||
};
|
||||
|
||||
// 모달 스타일 계산을 computed로 변경
|
||||
const modalStyle = computed(() => {
|
||||
const modalWidth = 200; // 모달의 예상 너비
|
||||
const modalHeight = 150; // 모달의 예상 높이
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let x = props.position?.x || 0;
|
||||
let y = props.position?.y || 0;
|
||||
|
||||
// 모달이 뷰포트를 벗어나지 않도록 조정
|
||||
if (x + modalWidth > viewportWidth) {
|
||||
x = viewportWidth - modalWidth - 10;
|
||||
}
|
||||
if (x < 0) {
|
||||
x = 10;
|
||||
}
|
||||
if (y + modalHeight > viewportHeight) {
|
||||
y = viewportHeight - modalHeight - 10;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = 10;
|
||||
}
|
||||
|
||||
return {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
zIndex: 1050,
|
||||
maxWidth: '90vw', // 뷰포트 너비의 90%를 넘지 않도록
|
||||
maxHeight: '90vh', // 뷰포트 높이의 90%를 넘지 않도록
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-icon-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.event-icon-select {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.event-icon-select:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.cancel-mark {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.event-modal {
|
||||
min-width: 120px;
|
||||
max-width: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 작은 화면에서의 스타일 */
|
||||
@media (max-width: 576px) {
|
||||
.event-modal {
|
||||
min-width: 100px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
585
src/components/main/MainEventCalendar.vue
Normal file
585
src/components/main/MainEventCalendar.vue
Normal file
@ -0,0 +1,585 @@
|
||||
<template>
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
|
||||
<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'"
|
||||
/>
|
||||
<p class="mt-2 fw-bold">
|
||||
{{ user.name }}
|
||||
</p>
|
||||
|
||||
<CommuterBtn :userId="user.id" :checkedInProject="checkedInProject || {}" ref="workTimeComponentRef" />
|
||||
|
||||
<MainEventList
|
||||
:categoryList="categoryList"
|
||||
:baseUrl="baseUrl"
|
||||
:birthdayList="birthdayList"
|
||||
:vacationList="vacationList"
|
||||
:birthdayPartyList="birthdayPartyList"
|
||||
:dinnerList="dinnerList"
|
||||
:teaTimeList="teaTimeList"
|
||||
:workShopList="workShopList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:events="calendarEvents"
|
||||
:options="calendarOptions"
|
||||
defaultView="dayGridMonth"
|
||||
class="flatpickr-calendar-only"
|
||||
>
|
||||
</full-calendar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventModal
|
||||
v-if="showModal"
|
||||
:position="modalPosition"
|
||||
:selected-date="selectedDate"
|
||||
:base-url="baseUrl"
|
||||
:date-events="currentDateEvents"
|
||||
@select="handleEventSelect"
|
||||
@delete="handleEventDelete"
|
||||
@insert="handleEventInsert"
|
||||
@close="handleCloseModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import { inject, onMounted, reactive, ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
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 CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||
import MainEventList from '@c/main/MainEventList.vue';
|
||||
import EventModal from '@c/main/EventModal.vue';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_DOMAIN;
|
||||
const user = ref({});
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
|
||||
const dayjs = inject('dayjs');
|
||||
const fullCalendarRef = ref(null);
|
||||
const workTimeComponentRef = ref(null);
|
||||
const calendarEvents = ref([]);
|
||||
|
||||
const selectedProject = ref(null);
|
||||
const checkedInProject = ref(null);
|
||||
|
||||
// 이벤트 모달 관련
|
||||
const showModal = ref(false);
|
||||
const modalPosition = ref({ x: 0, y: 0 });
|
||||
const selectedDate = ref('');
|
||||
|
||||
// 공통 함수
|
||||
const $common = inject('common');
|
||||
const toastStore = useToastStore();
|
||||
|
||||
// 롱프레스 관련 변수 추가
|
||||
const pressTimer = ref(null);
|
||||
const longPressDelay = 500; // 0.5초
|
||||
|
||||
// // 출퇴근 컴포넌트 이벤트 핸들러
|
||||
// const handleWorkTimeUpdate = () => {
|
||||
// todaysCommuter();
|
||||
// //loadCommuters();
|
||||
// };
|
||||
|
||||
// const handleLeaveTimeUpdate = () => {
|
||||
// todaysCommuter();
|
||||
// };
|
||||
|
||||
// // 오늘 출근 모든 사용자 조회
|
||||
// const todaysCommuter = async () => {
|
||||
// const res = await $api.get(`commuters/todays`);
|
||||
// if (res.status === 200) {
|
||||
// commuters.value = res.data.data;
|
||||
// }
|
||||
// };
|
||||
|
||||
/************* category ***************/
|
||||
|
||||
// 이벤트 카테고리 데이터 로딩
|
||||
const categoryList = ref([]);
|
||||
const fetchCategoryList = async () => {
|
||||
const { data } = await $api.get('main/category');
|
||||
if (data) categoryList.value = [...data.data.filter(categoryInfo => categoryInfo.CMNCODODR != 0)];
|
||||
};
|
||||
|
||||
/************* init ***************/
|
||||
const monthBirthdayList = ref([]);
|
||||
const monthVacationList = ref([]);
|
||||
const monthBirthdayPartyList = ref([]);
|
||||
const monthDinnerList = ref([]);
|
||||
const monthTeaTimeList = ref([]);
|
||||
const monthWorkShopList = ref([]);
|
||||
|
||||
const birthdayList = ref([]);
|
||||
const vacationList = ref([]);
|
||||
const birthdayPartyList = ref([]);
|
||||
const dinnerList = ref([]);
|
||||
const teaTimeList = ref([]);
|
||||
const workShopList = ref([]);
|
||||
|
||||
const currentDateEvents = ref([]);
|
||||
|
||||
// 생일자, 휴가자, 이벤트 일정 조회
|
||||
const fetchEventList = async param => {
|
||||
const { data } = await $api.get(`main/eventList?${param}`);
|
||||
const res = data?.data;
|
||||
|
||||
// 기존의 공휴일 이벤트는 유지
|
||||
const holidayEvents = calendarEvents.value.filter(event => event.classNames?.includes('holiday-event'));
|
||||
calendarEvents.value = [...holidayEvents];
|
||||
|
||||
// 생일자
|
||||
if (res?.memberBirthdayList?.length) {
|
||||
monthBirthdayList.value = [...res.memberBirthdayList];
|
||||
res.memberBirthdayList.forEach(member => {
|
||||
addEvent($common.dateFormatter(member.MEMBERBTH, 'YMD'), 'birthday', `${member.MEMBERNAM}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 휴가자
|
||||
if (res?.memberVacationList?.length) {
|
||||
monthVacationList.value = [...res.memberVacationList];
|
||||
res.memberVacationList.forEach(member => {
|
||||
addEvent($common.dateFormatter(member.LOCVACUDT, 'YMD'), 'vacation', `${member.MEMBERNAM}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 초기화
|
||||
monthBirthdayPartyList.value = [];
|
||||
monthDinnerList.value = [];
|
||||
monthTeaTimeList.value = [];
|
||||
monthTeaTimeList.value = [];
|
||||
|
||||
if (res?.eventList?.length) {
|
||||
res.eventList.forEach(item => {
|
||||
switch (item.CMNCODVAL) {
|
||||
case 300203:
|
||||
monthBirthdayPartyList.value = [...monthBirthdayPartyList.value, item];
|
||||
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'birthdayParty', '생일파티');
|
||||
break;
|
||||
|
||||
case 300204:
|
||||
monthDinnerList.value = [...monthDinnerList.value, item];
|
||||
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'dinner', '회식');
|
||||
break;
|
||||
|
||||
case 300205:
|
||||
monthTeaTimeList.value = [...monthTeaTimeList.value, item];
|
||||
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'teaTime', '티타임');
|
||||
break;
|
||||
|
||||
case 300206:
|
||||
monthWorkShopList.value = [...monthWorkShopList.value, item];
|
||||
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'workshop', '워크샵');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 달력에 이벤트 데이터 추가
|
||||
const addEvent = (date, type, title) => {
|
||||
// 생일의 경우 달력의 현재 년도로 변경하여 처리
|
||||
if (type === 'birthday') {
|
||||
const calendarApi = fullCalendarRef.value?.getApi();
|
||||
|
||||
if (calendarApi) {
|
||||
const calendarDate = calendarApi.currentData.currentDate;
|
||||
const { year } = $common.formatDateTime(new Date(calendarDate));
|
||||
const birthDate = $common.dateFormatter(date, 'MD');
|
||||
date = `${year}-${birthDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 같은 날짜와 타입의 이벤트가 이미 있는지 확인
|
||||
const existingEvent = calendarEvents.value.find(
|
||||
event => $common.dateFormatter(event.start, 'MD') === $common.dateFormatter(date, 'MD') && event.type == type,
|
||||
);
|
||||
|
||||
// 없는 경우에만 추가
|
||||
if (!existingEvent) {
|
||||
calendarEvents.value.push({
|
||||
start: date,
|
||||
type: type,
|
||||
title: title,
|
||||
classNames: [`${type}-event`],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 해당일 기준 이벤트 리스트 필터링
|
||||
const useFilterEventList = (month, day) => {
|
||||
// 생일자
|
||||
if (monthBirthdayList.value) {
|
||||
birthdayList.value = $common.filterTargetByDate(monthBirthdayList.value, 'MEMBERBTH', month, day);
|
||||
}
|
||||
|
||||
// 휴가자
|
||||
if (monthVacationList.value) {
|
||||
vacationList.value = $common.filterTargetByDate(monthVacationList.value, 'LOCVACUDT', month, day);
|
||||
}
|
||||
|
||||
// 생일파티
|
||||
if (monthBirthdayPartyList.value) {
|
||||
birthdayPartyList.value = $common.filterTargetByDate(monthBirthdayPartyList.value, 'LOCEVTTME', month, day);
|
||||
}
|
||||
|
||||
// 회식
|
||||
if (monthDinnerList.value) {
|
||||
dinnerList.value = $common.filterTargetByDate(monthDinnerList.value, 'LOCEVTTME', month, day);
|
||||
}
|
||||
|
||||
// 티타임
|
||||
if (monthTeaTimeList.value) {
|
||||
teaTimeList.value = $common.filterTargetByDate(monthTeaTimeList.value, 'LOCEVTTME', month, day);
|
||||
}
|
||||
|
||||
// 워크샵
|
||||
if (monthWorkShopList.value) {
|
||||
workShopList.value = $common.filterTargetByDate(monthWorkShopList.value, 'LOCEVTTME', month, day);
|
||||
}
|
||||
};
|
||||
|
||||
// 캘린더 데이터 가져오기
|
||||
const fetchData = async () => {
|
||||
// FullCalendar API 인스턴스 가져오기
|
||||
const calendarApi = fullCalendarRef.value?.getApi();
|
||||
if (!calendarApi) return;
|
||||
|
||||
const date = calendarApi.currentData.currentDate;
|
||||
const { year, month } = $common.formatDateTime(new Date(date));
|
||||
|
||||
try {
|
||||
// 현재 표시 중인 월의 공휴일 정보 가져오기
|
||||
const holidayEvents = await fetchHolidays(year, month);
|
||||
calendarEvents.value = [...holidayEvents]; // 공휴일 정보로 초기화
|
||||
|
||||
// 이벤트 데이터 가져오기
|
||||
const param = new URLSearchParams();
|
||||
param.append('year', year);
|
||||
param.append('month', month);
|
||||
param.append('day', '1'); // 해당 월의 첫날
|
||||
|
||||
await fetchEventList(param);
|
||||
} catch (error) {
|
||||
console.error('공휴일 정보 로딩 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 캘린더 이동 함수 (이전, 다음, 오늘)
|
||||
const moveCalendar = async (value = 0) => {
|
||||
const calendarApi = fullCalendarRef.value?.getApi();
|
||||
|
||||
if (value === 1) {
|
||||
calendarApi.prev(); // 이전 달로 이동
|
||||
} else if (value === 2) {
|
||||
calendarApi.next(); // 다음 달로 이동
|
||||
} else if (value === 3) {
|
||||
calendarApi.today(); // 오늘 날짜로 이동
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
// 날짜 선택 가능 여부를 확인하는 공통 함수
|
||||
const isSelectableDate = date => {
|
||||
const checkDate = dayjs(date);
|
||||
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 !isWeekend && !isHoliday;
|
||||
};
|
||||
|
||||
// 날짜 셀 클래스 추가 함수
|
||||
const getCellClassNames = arg => {
|
||||
const cellDate = dayjs(arg.date);
|
||||
const classes = [];
|
||||
|
||||
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
|
||||
if (!isSelectableDate(cellDate)) {
|
||||
classes.push('fc-day-sat-sun');
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// 날짜 클릭 이벤트 핸들러
|
||||
const handleDateClick = info => {
|
||||
const { month, day } = $common.formatDateTime(new Date(info.dateStr));
|
||||
useFilterEventList(month, day);
|
||||
};
|
||||
|
||||
// 이벤트 모달 핸들러
|
||||
const handleMouseDown = (date, jsEvent) => {
|
||||
if (showModal.value) showModal.value = false;
|
||||
|
||||
// 해당 날짜의 이벤트 필터링
|
||||
const dateEvents = calendarEvents.value.filter(
|
||||
event => $common.dateFormatter(event.start, 'YMD') === $common.dateFormatter(date, 'YMD'),
|
||||
);
|
||||
|
||||
pressTimer.value = setTimeout(() => {
|
||||
modalPosition.value = {
|
||||
x: jsEvent.clientX,
|
||||
y: jsEvent.clientY,
|
||||
};
|
||||
|
||||
selectedDate.value = date;
|
||||
currentDateEvents.value = dateEvents;
|
||||
showModal.value = true;
|
||||
pressTimer.value = null;
|
||||
}, longPressDelay);
|
||||
};
|
||||
|
||||
// 이벤트 모달 외부 클릭 시 닫힘
|
||||
const handleMouseUp = () => {
|
||||
if (pressTimer.value) {
|
||||
clearTimeout(pressTimer.value);
|
||||
pressTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 이벤트 삭제 api
|
||||
const toggleEvent = async (date, code, title) => {
|
||||
const { data } = await $api.post('main/toggleEvent', {
|
||||
date: date,
|
||||
code: code,
|
||||
title: title,
|
||||
});
|
||||
|
||||
if (data?.code === 200) toastStore.onToast(data.message);
|
||||
|
||||
const { year, month, day } = $common.formatDateTime(new Date(date));
|
||||
const param = new URLSearchParams();
|
||||
param.append('year', year);
|
||||
param.append('month', month);
|
||||
param.append('day', day);
|
||||
|
||||
await fetchEventList(param);
|
||||
useFilterEventList(month, year);
|
||||
};
|
||||
|
||||
// 이벤트 추가 api
|
||||
const insertEvent = async (date, code, title, place, time) => {
|
||||
const dateTime = $common.dateFormatter(`${date} ${time}`);
|
||||
const { data } = await $api.post('main/inserEvent', {
|
||||
date: dateTime,
|
||||
code: code,
|
||||
title: title,
|
||||
place: place,
|
||||
});
|
||||
|
||||
if (data?.code === 200) toastStore.onToast(data.message);
|
||||
|
||||
const { year, month, day } = $common.formatDateTime(new Date(date));
|
||||
const param = new URLSearchParams();
|
||||
param.append('year', year);
|
||||
param.append('month', month);
|
||||
param.append('day', day);
|
||||
|
||||
await fetchEventList(param);
|
||||
useFilterEventList(month, year);
|
||||
};
|
||||
|
||||
// 이벤트 선택 핸들러
|
||||
const handleEventSelect = data => {
|
||||
toggleEvent(data.date, data.code, data.title);
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const handleEventInsert = data => {
|
||||
insertEvent(data.date, data.code, data.title, data.place, data.time);
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const handleEventDelete = data => {
|
||||
toggleEvent(data.date, data.code, data.title);
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
// 이벤트 모달 닫기
|
||||
const handleCloseModal = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
// 달력 이벤트 아이콘 표시 함수
|
||||
const handleEventContent = item => {
|
||||
if (!item.event) return null;
|
||||
|
||||
// 공휴일인 경우 텍스트로 표시
|
||||
if (item.event.classNames?.includes('holiday-event')) {
|
||||
return {
|
||||
html: `<div class="holiday-text" style="color: white;">${item.event.title}</div>`,
|
||||
};
|
||||
}
|
||||
|
||||
// 현재 이벤트의 타입만 확인
|
||||
const eventType = item.event.extendedProps.type;
|
||||
if (!eventType) return null;
|
||||
|
||||
let iconCode = '';
|
||||
switch (eventType) {
|
||||
case 'birthday':
|
||||
iconCode = '300201';
|
||||
break;
|
||||
case 'vacation':
|
||||
iconCode = '300202';
|
||||
break;
|
||||
case 'birthdayParty':
|
||||
iconCode = '300203';
|
||||
break;
|
||||
case 'dinner':
|
||||
iconCode = '300204';
|
||||
break;
|
||||
case 'teaTime':
|
||||
iconCode = '300205';
|
||||
break;
|
||||
case 'workshop':
|
||||
iconCode = '300206';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
html: `<img src="${baseUrl}img/main-category-img/main-${iconCode}.png" class="calendar-event-icon" style="width: 20px; height: 20px; margin: 2px;" />`,
|
||||
};
|
||||
};
|
||||
|
||||
// 캘린더 옵션 설정
|
||||
const calendarOptions = reactive({
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'today',
|
||||
center: 'title',
|
||||
right: 'prev,next',
|
||||
},
|
||||
locale: 'kr',
|
||||
events: calendarEvents,
|
||||
eventOrder: 'sortIdx',
|
||||
contentHeight: 'auto',
|
||||
eventContent: handleEventContent,
|
||||
selectable: true,
|
||||
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
|
||||
dateClick: handleDateClick,
|
||||
dayCellDidMount: arg => {
|
||||
const dateCell = arg.el;
|
||||
|
||||
// 마우스 홀드시 이벤트 모달
|
||||
dateCell.addEventListener('mousedown', e => {
|
||||
const date = $common.dateFormatter(arg.date, 'YMD');
|
||||
handleMouseDown(date, e);
|
||||
});
|
||||
dateCell.addEventListener('mouseup', handleMouseUp);
|
||||
dateCell.addEventListener('mouseleave', handleMouseUp);
|
||||
},
|
||||
dayCellClassNames: getCellClassNames,
|
||||
unselectAuto: true,
|
||||
droppable: false,
|
||||
eventDisplay: 'block',
|
||||
customButtons: {
|
||||
prev: {
|
||||
text: 'PREV',
|
||||
click: () => moveCalendar(1),
|
||||
},
|
||||
today: {
|
||||
text: 'TODAY',
|
||||
click: () => moveCalendar(3),
|
||||
},
|
||||
next: {
|
||||
text: 'NEXT',
|
||||
click: () => moveCalendar(2),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
||||
watch(
|
||||
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
|
||||
async () => {
|
||||
await fetchData();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
|
||||
// 저장된 선택 프로젝트 가져오기
|
||||
const storedProject = projectStore.getSelectedProject();
|
||||
if (storedProject) {
|
||||
selectedProject.value = storedProject.PROJCTSEQ;
|
||||
checkedInProject.value = storedProject;
|
||||
}
|
||||
|
||||
// 오늘 기준 데이터 호출
|
||||
const { year, month, day } = $common.getToday();
|
||||
const param = new URLSearchParams();
|
||||
param.append('year', year);
|
||||
param.append('month', month);
|
||||
param.append('day', day);
|
||||
|
||||
// 이벤트 카테고리 호출
|
||||
await fetchCategoryList();
|
||||
await fetchEventList(param);
|
||||
useFilterEventList(month, day);
|
||||
|
||||
// 이벤트모달 외부 클릭 감지
|
||||
// document.addEventListener('click', e => {
|
||||
// if (showModal.value && !e.target.closest('.event-modal') && !e.target.closest('.fc-daygrid-day')) {
|
||||
// showModal.value = false;
|
||||
// }
|
||||
// });
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.fc-h-event {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.event-modal {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.event-icon-select:hover {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/* 이벤트 모달 노출 시 텍스트 선택 방지 */
|
||||
.fc-daygrid-day {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
122
src/components/main/MainEventList.vue
Normal file
122
src/components/main/MainEventList.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<template v-for="category in categoryList" :key="category.CMNCODVAL">
|
||||
<div
|
||||
v-if="
|
||||
(category.CMNCODVAL === 300201 && birthdayList?.length) ||
|
||||
(category.CMNCODVAL === 300202 && vacationList?.length) ||
|
||||
(category.CMNCODVAL === 300203 && birthdayPartyList?.length) ||
|
||||
(category.CMNCODVAL === 300204 && dinnerList?.length) ||
|
||||
(category.CMNCODVAL === 300205 && teaTimeList?.length) ||
|
||||
(category.CMNCODVAL === 300206 && workShopList?.length)
|
||||
"
|
||||
class="border border-2 mt-3 card p-2"
|
||||
>
|
||||
<div class="row g-2 position-relative">
|
||||
<div class="col-3 mx-0 px-0">
|
||||
<div class="ratio ratio-1x1">
|
||||
<img
|
||||
:src="`${baseUrl}img/main-category-img/main-${category.CMNCODVAL}.png`"
|
||||
:alt="`${category.CMNCODNAM}`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9 mx-0 px-0">
|
||||
<template v-if="category.CMNCODVAL === 300201">
|
||||
<MainMemberProfile :members="birthdayList" :baseUrl="baseUrl" />
|
||||
</template>
|
||||
<template v-if="category.CMNCODVAL === 300202">
|
||||
<MainMemberProfile :members="vacationList" :baseUrl="baseUrl" />
|
||||
</template>
|
||||
<template v-if="category.CMNCODVAL === 300203">
|
||||
<div>
|
||||
{{ birthdayPartyList[0].LOCEVTPLC }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $common.dateFormatter(birthdayPartyList[0].LOCEVTTME) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="category.CMNCODVAL === 300204">
|
||||
<div>
|
||||
{{ dinnerList[0].LOCEVTPLC }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $common.dateFormatter(dinnerList[0].LOCEVTTME) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="category.CMNCODVAL === 300205">
|
||||
<div>
|
||||
{{ teaTimeList[0].LOCEVTPLC }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $common.dateFormatter(teaTimeList[0].LOCEVTTME) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="category.CMNCODVAL === 300206">
|
||||
<div>
|
||||
{{ workShopList[0].LOCEVTPLC }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $common.dateFormatter(workShopList[0].LOCEVTTME) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
import MainMemberProfile from '@c/main/MainMemberProfile.vue';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
categoryList: {
|
||||
type: Array,
|
||||
},
|
||||
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,
|
||||
},
|
||||
birthdayList: {
|
||||
type: Array,
|
||||
},
|
||||
vacationList: {
|
||||
type: Array,
|
||||
},
|
||||
birthdayPartyList: {
|
||||
type: Array,
|
||||
},
|
||||
dinnerList: {
|
||||
type: Array,
|
||||
},
|
||||
teaTimeList: {
|
||||
type: Array,
|
||||
},
|
||||
workShopList: {
|
||||
type: Array,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
32
src/components/main/MainMemberProfile.vue
Normal file
32
src/components/main/MainMemberProfile.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="ms-2">
|
||||
<ul class="row gx-1 mb-0 list-inline d-flex align-items-center">
|
||||
<li class="col-4 me-0" v-for="(member, index) in members" :key="index">
|
||||
<div class="ratio ratio-1x1 mb-0 profile-list">
|
||||
<img
|
||||
:src="`${baseUrl}upload/img/profile/${member.MEMBERPRF}`"
|
||||
:style="`border-color: ${member.usercolor} !important;`"
|
||||
alt="User Profile"
|
||||
class="rounded-circle border border-2"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
members: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
63
src/components/main/MemberManagement.vue
Normal file
63
src/components/main/MemberManagement.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div v-if="memberList" class="card mt-2 mb-3 shadow-sm border">
|
||||
<div class="row g-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<h5 class="card-title fw-bold">사원 등록 관리</h5>
|
||||
</div>
|
||||
<div class="g-2 card">
|
||||
<div v-for="member in memberList" :key="member.MEMBERSEQ" class="row card-body">
|
||||
<div class="col-2">
|
||||
<img
|
||||
:src="`upload/img/profile/`"
|
||||
alt="Profile Image"
|
||||
class="img-fluid"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<!-- 날짜 -->
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<div class="">{{ member.MEMBERNAM }}</div>
|
||||
</div>
|
||||
<!-- 참여자 -->
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bxs-envelope"></i>
|
||||
<div class="ms-2">{{ member.MEMBERIDS }}@local-host.co.kr</div>
|
||||
</div>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bxs-phone"></i>
|
||||
<div class="ms-2">{{ $common.addHyphenToPhoneNumber(member.MEMBERTEL) }}</div>
|
||||
</div>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bx-calendar"></i>
|
||||
<div class="ms-2">{{ $common.dateFormatter(member.MEMBERRDT) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="align-items-center">
|
||||
<label class="switch"><input type="checkbox" checked="" /><span class="slider round"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, onMounted, ref } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
const memberList = ref([]);
|
||||
|
||||
const fetchRegisterMemberList = async () => {
|
||||
const { data } = await $api.get('main/registerMemberList');
|
||||
if (data?.data) memberList.value = data.data;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRegisterMemberList();
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user