Merge branch 'main' into login
This commit is contained in:
commit
8427dce4cc
2137
package-lock.json
generated
2137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -48,7 +48,7 @@
|
||||
<div ref="editor"></div>
|
||||
|
||||
<!-- Alert 메시지 표시 -->
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div>
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -57,25 +57,32 @@ import Quill from 'quill';
|
||||
import 'quill/dist/quill.snow.css';
|
||||
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
isAlert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const editor = ref(null);
|
||||
const font = ref('nanum-gothic');
|
||||
const fontSize = ref('16px');
|
||||
|
||||
const editor = ref(null); // 에디터 DOM 참조
|
||||
const font = ref('nanum-gothic'); // 기본 폰트
|
||||
const fontSize = ref('16px'); // 기본 폰트 크기
|
||||
const emit = defineEmits(['update:data']);
|
||||
|
||||
onMounted(() => {
|
||||
// 툴바에서 선택할 수 있는 폰트 목록 설정
|
||||
const Font = Quill.import('formats/font');
|
||||
Font.whitelist = ['nanum-gothic', 'd2coding', 'consolas', 'serif', 'monospace'];
|
||||
Quill.register(Font, true);
|
||||
|
||||
// 툴바에서 선택할 수 있는 폰트 크기 목록 설정
|
||||
const Size = Quill.import('attributors/style/size');
|
||||
Size.whitelist = ['12px', '14px', '16px', '18px', '24px', '32px', '48px'];
|
||||
Quill.register(Size, true);
|
||||
|
||||
// Quill 에디터 인스턴스 생성
|
||||
const quillInstance = new Quill(editor.value, {
|
||||
theme: 'snow',
|
||||
placeholder: '내용을 입력해주세요...',
|
||||
@ -86,74 +93,84 @@ onMounted(() => {
|
||||
syntax: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 폰트와 폰트 크기 설정
|
||||
quillInstance.format('font', font.value);
|
||||
quillInstance.format('size', fontSize.value);
|
||||
|
||||
// 텍스트가 변경될 때마다 부모 컴포넌트로 변경된 내용 전달
|
||||
quillInstance.on('text-change', () => {
|
||||
const delta = quillInstance.getContents(); // Get Delta format
|
||||
const delta = quillInstance.getContents(); // Delta 포맷으로 내용 가져오기
|
||||
emit('update:data', delta);
|
||||
});
|
||||
|
||||
// 폰트나 폰트 크기가 변경될 때 에디터 스타일 업데이트
|
||||
watch([font, fontSize], () => {
|
||||
quillInstance.format('font', font.value);
|
||||
quillInstance.format('size', fontSize.value);
|
||||
});
|
||||
|
||||
// Handle image upload
|
||||
let imageUrls = new Set();
|
||||
// 이미지 업로드 기능 처리
|
||||
let imageUrls = new Set(); // 업로드된 이미지 URL을 추적
|
||||
quillInstance.getModule('toolbar').addHandler('image', () => {
|
||||
selectLocalImage();
|
||||
selectLocalImage(); // 이미지 버튼 클릭 시 로컬 이미지 선택
|
||||
});
|
||||
|
||||
// 에디터의 텍스트가 변경될 때마다 이미지 처리
|
||||
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
||||
// Emit Delta when content changes
|
||||
emit('update:data', quillInstance.getContents());
|
||||
delta.ops.forEach(op => {
|
||||
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
|
||||
const imageUrl = op.insert.image;
|
||||
imageUrls.add(imageUrl);
|
||||
const imageUrl = op.insert.image; // 이미지 URL 추출
|
||||
imageUrls.add(imageUrl); // URL 추가
|
||||
} else if (op.delete) {
|
||||
checkForDeletedImages();
|
||||
checkForDeletedImages(); // 삭제된 이미지 확인
|
||||
}
|
||||
});
|
||||
});
|
||||
// 로컬 이미지 파일 선택
|
||||
async function selectLocalImage() {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
input.click();
|
||||
input.click(); // 파일 선택 다이얼로그 열기
|
||||
input.onchange = () => {
|
||||
const file = input.files[0];
|
||||
if (file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 이미지 서버에 업로드 후 URL 받기
|
||||
uploadImageToServer(formData).then(serverImageUrl => {
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const fullImageUrl = `${baseUrl}${serverImageUrl.replace(/\\/g, '/')}`;
|
||||
|
||||
const range = quillInstance.getSelection();
|
||||
quillInstance.insertEmbed(range.index, 'image', fullImageUrl);
|
||||
quillInstance.insertEmbed(range.index, 'image', fullImageUrl); // 선택된 위치에 이미지 삽입
|
||||
|
||||
imageUrls.add(fullImageUrl);
|
||||
imageUrls.add(fullImageUrl); // 이미지 URL 추가
|
||||
}).catch(e => {
|
||||
toastStore.onToast('잠시후 다시 시도해주세요.', 'e');
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
// 이미지 서버 업로드
|
||||
async function uploadImageToServer(formData) {
|
||||
try {
|
||||
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
|
||||
const imageUrl = response.data.data;
|
||||
return imageUrl;
|
||||
return imageUrl; // 서버에서 받은 이미지 URL 반환
|
||||
} catch (error) {
|
||||
toastStore.onToast('잠시후 다시 시도해주세요.', 'e');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 삭제된 이미지 확인
|
||||
function checkForDeletedImages() {
|
||||
const editorImages = document.querySelectorAll('#editor img');
|
||||
const currentImages = new Set(Array.from(editorImages).map(img => img.src));
|
||||
const currentImages = new Set(Array.from(editorImages).map(img => img.src)); // 현재 에디터에 있는 이미지들
|
||||
|
||||
imageUrls.forEach(url => {
|
||||
if (!currentImages.has(url)) {
|
||||
|
||||
@ -1,48 +1,53 @@
|
||||
<template>
|
||||
<!-- 컴포넌트 사용 ex)
|
||||
|
||||
<UserList @user-list-update="handleUserListUpdate" />
|
||||
|
||||
-->
|
||||
<ul class="list-unstyled users-list d-flex align-items-center">
|
||||
<li
|
||||
v-for="(user, index) in userList"
|
||||
:key="index"
|
||||
class="avatar pull-up"
|
||||
:class="{ disabled: user.disabled }"
|
||||
@click="toggleDisable(index)"
|
||||
data-bs-toggle="tooltip"
|
||||
data-popup="tooltip-custom"
|
||||
data-bs-placement="top"
|
||||
:aria-label="user.MEMBERSEQ"
|
||||
:data-bs-original-title="user.MEMBERSEQ"
|
||||
>
|
||||
<img
|
||||
class="rounded-circle"
|
||||
:src="`http://localhost:10325/upload/img/profile/${user.MEMBERPRF}`"
|
||||
alt="profile"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled users-list d-flex align-items-center">
|
||||
<li
|
||||
v-for="(user, index) in userList"
|
||||
:key="index"
|
||||
class="avatar pull-up"
|
||||
:class="{ disabled: user.disabled }"
|
||||
@click="toggleDisable(index)"
|
||||
data-bs-toggle="tooltip"
|
||||
data-popup="tooltip-custom"
|
||||
data-bs-placement="top"
|
||||
:aria-label="user.MEMBERSEQ"
|
||||
:data-bs-original-title="getTooltipTitle(user)"
|
||||
>
|
||||
<img
|
||||
class="rounded-circle user-avatar"
|
||||
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
||||
alt="user"
|
||||
:style="{ borderColor: user.usercolor}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, nextTick } from 'vue';
|
||||
import { useUserStore } from '@s/userList';
|
||||
import $api from '@api';
|
||||
|
||||
const emit = defineEmits();
|
||||
const userStore = useUserStore();
|
||||
const userList = ref([]);
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
// 사용자 목록 호출
|
||||
onMounted(async () => {
|
||||
await userStore.fetchUserList();
|
||||
userList.value = userStore.userList;
|
||||
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach((tooltip) => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 클릭 시 활성화/비활성화
|
||||
const toggleDisable = (index) => {
|
||||
const user = userList.value[index];
|
||||
const user = userList.value[index];
|
||||
if (user) {
|
||||
user.disabled = !user.disabled;
|
||||
emitUserListUpdate();
|
||||
@ -51,20 +56,30 @@ const user = userList.value[index];
|
||||
|
||||
// emit
|
||||
const emitUserListUpdate = () => {
|
||||
const activeUsers = userList.value.filter(user => !user.disabled);
|
||||
const disabledUsers = userList.value.filter(user => user.disabled);
|
||||
const activeUsers = userList.value.filter((user) => !user.disabled);
|
||||
const disabledUsers = userList.value.filter((user) => user.disabled);
|
||||
|
||||
emit('user-list-update', { activeUsers, disabledUsers });
|
||||
};
|
||||
|
||||
const getTooltipTitle = (user) => {
|
||||
return user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* disabled 클래스를 적용할 때 사용자의 이미지를 흐리게 */
|
||||
.avatar.disabled {
|
||||
opacity: 0.5; /* 흐리게 만들기 */
|
||||
opacity: 1.0; /* 흐리게 만들기 */
|
||||
}
|
||||
|
||||
/* 비활성화된 상태에서 이미지를 회색으로 변환 */
|
||||
.avatar.disabled img {
|
||||
filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
/* 동그란 테두리 설정 */
|
||||
.user-avatar {
|
||||
border: 3px solid; /* 테두리 */
|
||||
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -11,7 +11,12 @@
|
||||
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
||||
<div class="d-flex flex-wrap align-items-center mb-50">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<img :src="getProfileImage(item.author.profileImage)" alt="최초 작성자" class="rounded-circle">
|
||||
<img
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.author.profileImage)"
|
||||
alt="최초 작성자"
|
||||
:style="{ borderColor: item.author.color}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p>
|
||||
@ -21,7 +26,12 @@
|
||||
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
||||
<div class="d-flex flex-wrap align-items-center mb-50">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<img :src="getProfileImage(item.lastEditor.profileImage)" alt="최근 작성자" class="rounded-circle">
|
||||
<img
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||
alt="최근 작성자"
|
||||
:style="{ borderColor: item.lastEditor.color}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p>
|
||||
@ -33,18 +43,27 @@
|
||||
|
||||
<script setup>
|
||||
import EditBtn from '@/components/button/EditBtn.vue';
|
||||
import $api from '@api';
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
// Props
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
// 날짜
|
||||
const formatDate = (dateString) => new Date(dateString).toLocaleString();
|
||||
// 이미지
|
||||
const getProfileImage = (imagePath) =>
|
||||
imagePath ? `/img/avatars/${imagePath}` : '/img/avatars/default-Profile.jpg';
|
||||
</script>
|
||||
imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 동그란 테두리 설정 */
|
||||
.user-avatar {
|
||||
border: 3px solid; /* 테두리 */
|
||||
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -11,11 +11,23 @@ import axios from "@api";
|
||||
export const useUserStore = defineStore("userStore", {
|
||||
state: () => ({
|
||||
userList: [],
|
||||
userInfo: {}
|
||||
}),
|
||||
actions: {
|
||||
async fetchUserList() {
|
||||
const response = await axios.get('user/allUserList');
|
||||
this.userList = response.data.data;
|
||||
console.log('response',response)
|
||||
this.userList = response.data.data.allUserList;
|
||||
this.userInfo = response.data.data.user;
|
||||
|
||||
if (this.userInfo) {
|
||||
const index = this.userList.findIndex(user => user.MEMBERSEQ === this.userInfo.id);
|
||||
if (index !== -1) {
|
||||
const [user] = this.userList.splice(index, 1);
|
||||
this.userList.unshift(user);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div class="vacation-management">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="save-button-container">
|
||||
<button class="btn btn-success" @click="addVacationRequests">✔ 저장</button>
|
||||
</div>
|
||||
<div class="container flex-grow-1">
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="card-body">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
@ -17,13 +14,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="half-day-buttons">
|
||||
<button class="btn btn-info" :class="{ active: halfDayType === 'AM' }" @click="toggleHalfDay('AM')">
|
||||
오전반차
|
||||
<button
|
||||
class="btn btn-info"
|
||||
:class="{ active: halfDayType === 'AM' }"
|
||||
@click="toggleHalfDay('AM')"
|
||||
>
|
||||
<i class="bi bi-sun"></i>
|
||||
</button>
|
||||
<button class="btn btn-warning" :class="{ active: halfDayType === 'PM' }" @click="toggleHalfDay('PM')">
|
||||
오후반차
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
:class="{ active: halfDayType === 'PM' }"
|
||||
@click="toggleHalfDay('PM')"
|
||||
>
|
||||
<i class="bi bi-moon"></i>
|
||||
</button>
|
||||
<div class="save-button-container">
|
||||
<button class="btn btn-success" @click="addVacationRequests">
|
||||
✔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,207 +42,336 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
import "@/assets/css/app-calendar.css";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import axios from "@api";
|
||||
<script setup>
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
import "@/assets/css/app-calendar.css";
|
||||
import { reactive, ref, onMounted, nextTick } from "vue";
|
||||
import axios from "@api";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
const fullCalendarRef = ref(null);
|
||||
const calendarEvents = ref([]); // FullCalendar의 이벤트 리스트
|
||||
const selectedDates = ref(new Map());
|
||||
const halfDayType = ref(null);
|
||||
const employeeId = ref(1);
|
||||
// FullCalendar 관련 참조 및 데이터
|
||||
const fullCalendarRef = ref(null);
|
||||
const calendarEvents = ref([]); // 최종적으로 FullCalendar에 표시할 이벤트 (API 이벤트 + 선택 이벤트)
|
||||
const fetchedEvents = ref([]); // API에서 불러온 이벤트 (휴가, 공휴일)
|
||||
const selectedDates = ref(new Map()); // 사용자가 클릭한 날짜 및 타입
|
||||
const halfDayType = ref(null);
|
||||
const employeeId = ref(1);
|
||||
|
||||
const calendarOptions = reactive({
|
||||
// 공휴일 날짜(YYYY-MM-DD 형식)를 저장 (클릭 불가 처리용)
|
||||
const holidayDates = ref(new Set());
|
||||
|
||||
// FullCalendar 옵션 객체 (events에 calendarEvents를 지정)
|
||||
const calendarOptions = reactive({
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
headerToolbar: {
|
||||
left: "today",
|
||||
center: "title",
|
||||
right: "prev,next",
|
||||
left: "today",
|
||||
center: "title",
|
||||
right: "prev,next",
|
||||
},
|
||||
locale: "ko",
|
||||
selectable: true,
|
||||
selectable: false,
|
||||
dateClick: handleDateClick,
|
||||
events: calendarEvents, // 이벤트 리스트를 직접 반영
|
||||
});
|
||||
datesSet: handleMonthChange,
|
||||
events: calendarEvents,
|
||||
});
|
||||
|
||||
/**
|
||||
* 날짜 클릭 이벤트
|
||||
*/
|
||||
function handleDateClick(info) {
|
||||
const date = info.dateStr;
|
||||
if (!selectedDates.value.has(date)) {
|
||||
const type = halfDayType.value ? (halfDayType.value === "AM" ? "D" : "N") : "F";
|
||||
selectedDates.value.set(date, type);
|
||||
/**
|
||||
* API 이벤트(fetchedEvents)와 사용자가 선택한 날짜(selectedDates)를 병합하여
|
||||
* calendarEvents를 업데이트하는 함수
|
||||
* - 선택 이벤트는 display: "background" 옵션을 사용하여 배경으로 표시
|
||||
* - 선택된 타입에 따라 클래스(selected-am, selected-pm, selected-full)를 부여함
|
||||
*/
|
||||
function updateCalendarEvents() {
|
||||
const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => {
|
||||
let className = "";
|
||||
let title = "";
|
||||
if (type === "D") {
|
||||
className = "selected-am"; // 오전: 왼쪽 절반
|
||||
title = "오전반차 (선택)";
|
||||
} else if (type === "N") {
|
||||
className = "selected-pm"; // 오후: 오른쪽 절반
|
||||
title = "오후반차 (선택)";
|
||||
} else {
|
||||
className = "selected-full"; // 전체 영역
|
||||
title = "연차 (선택)";
|
||||
}
|
||||
return {
|
||||
title,
|
||||
start: date,
|
||||
backgroundColor: "rgba(0, 128, 0, 0.3)",
|
||||
display: "background",
|
||||
classNames: [className],
|
||||
};
|
||||
});
|
||||
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 클릭 이벤트
|
||||
* - 주말(토, 일)과 공휴일은 클릭되지 않음
|
||||
* - 클릭 시 해당 날짜를 selectedDates에 추가 또는 제거한 후 updateCalendarEvents() 호출
|
||||
*/
|
||||
function handleDateClick(info) {
|
||||
const clickedDateStr = info.dateStr;
|
||||
const clickedDate = info.date;
|
||||
|
||||
// 주말 (토:6, 일:0)은 클릭 무시
|
||||
if (clickedDate.getDay() === 0 || clickedDate.getDay() === 6) {
|
||||
return;
|
||||
}
|
||||
// 공휴일이면 클릭 무시
|
||||
if (holidayDates.value.has(clickedDateStr)) {
|
||||
return;
|
||||
}
|
||||
if (!selectedDates.value.has(clickedDateStr)) {
|
||||
const type = halfDayType.value
|
||||
? halfDayType.value === "AM"
|
||||
? "D"
|
||||
: "N"
|
||||
: "F";
|
||||
selectedDates.value.set(clickedDateStr, type);
|
||||
} else {
|
||||
selectedDates.value.delete(date);
|
||||
selectedDates.value.delete(clickedDateStr);
|
||||
}
|
||||
halfDayType.value = null;
|
||||
}
|
||||
updateCalendarEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 오전/오후 반차 선택
|
||||
*/
|
||||
function toggleHalfDay(type) {
|
||||
/**
|
||||
* 오전/오후 반차 버튼 토글
|
||||
*/
|
||||
function toggleHalfDay(type) {
|
||||
halfDayType.value = halfDayType.value === type ? null : type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 백엔드에서 휴가 데이터를 가져와 FullCalendar에 반영
|
||||
*/
|
||||
async function fetchVacationData() {
|
||||
/**
|
||||
* 백엔드에서 휴가 데이터를 가져와 이벤트로 변환
|
||||
*/
|
||||
async function fetchVacationData(year, month) {
|
||||
try {
|
||||
const response = await axios.get("vacation/list");
|
||||
|
||||
if (response.data.status === "OK") {
|
||||
const vacationList = response.data.data;
|
||||
console.log("📌 백엔드 응답 데이터:", vacationList);
|
||||
|
||||
if (!Array.isArray(vacationList)) {
|
||||
throw new Error("vacationList is not an array.");
|
||||
console.log(`📌 휴가 데이터 요청: ${year}-${month}`);
|
||||
const response = await axios.get(`vacation/list/${year}/${month}`);
|
||||
if (response.status == 200) {
|
||||
const vacationList = response.data;
|
||||
console.log("📌 백엔드 응답 데이터:", vacationList);
|
||||
const events = vacationList
|
||||
.map((vac) => {
|
||||
let dateStr = vac.LOCVACUDT.split("T")[0];
|
||||
let className = "fc-daygrid-event";
|
||||
let backgroundColor = getColorByEmployeeId(vac.MEMBERSEQ);
|
||||
let title = "연차";
|
||||
if (vac.LOCVACTYP === "D") {
|
||||
title = "오전반차";
|
||||
className += " half-day-am";
|
||||
} else if (vac.LOCVACTYP === "N") {
|
||||
title = "오후반차";
|
||||
className += " half-day-pm";
|
||||
} else if (vac.LOCVACTYP === "F") {
|
||||
title = "연차";
|
||||
className += " full-day";
|
||||
}
|
||||
|
||||
// 이벤트 리스트 변환
|
||||
const events = vacationList.map((vac) => {
|
||||
let dateStr = vac.LOCVACUDT.split("T")[0];
|
||||
let className = "fc-daygrid-event";
|
||||
|
||||
let title = "연차";
|
||||
if (vac.LOCVACTYP === "D") {
|
||||
title = "오전반차";
|
||||
className += " half-day-am";
|
||||
} else if (vac.LOCVACTYP === "N") {
|
||||
title = "오후반차";
|
||||
className += " half-day-pm";
|
||||
} else if (vac.LOCVACTYP === "F") {
|
||||
title = "연차";
|
||||
className += " full-day";
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
start: dateStr,
|
||||
backgroundColor: getColorByEmployeeId(vac.MEMBERSEQ),
|
||||
classNames: [className],
|
||||
};
|
||||
}).filter((event) => event !== null);
|
||||
|
||||
console.log("📌 변환된 이벤트:", events);
|
||||
calendarEvents.value = events; // FullCalendar 이벤트 업데이트
|
||||
}
|
||||
return {
|
||||
title,
|
||||
start: dateStr,
|
||||
backgroundColor,
|
||||
classNames: [className],
|
||||
};
|
||||
})
|
||||
.filter((event) => event !== null);
|
||||
return events;
|
||||
} else {
|
||||
console.warn("📌 휴가 데이터를 불러오지 못함");
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching vacation data:", error);
|
||||
console.error("Error fetching vacation data:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 요청 추가
|
||||
*/
|
||||
async function addVacationRequests() {
|
||||
if (selectedDates.value.size === 0) {
|
||||
alert("휴가를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
|
||||
date,
|
||||
type,
|
||||
employeeId: employeeId.value,
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await axios.post("vacation", vacationRequests);
|
||||
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("휴가가 저장되었습니다.");
|
||||
fetchVacationData(); // 휴가 저장 후 데이터 다시 불러오기
|
||||
selectedDates.value.clear();
|
||||
} else {
|
||||
alert("휴가 저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("휴가 저장에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 ID별 색상 반환
|
||||
*/
|
||||
function getColorByEmployeeId(employeeId) {
|
||||
/**
|
||||
* 사원 ID별 색상 반환
|
||||
*/
|
||||
function getColorByEmployeeId(employeeId) {
|
||||
const colors = ["#ade3ff", "#ffade3", "#ade3ad", "#ffadad"];
|
||||
return colors[employeeId % colors.length];
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터 불러오기 호출
|
||||
onMounted(() => {
|
||||
fetchVacationData();
|
||||
});
|
||||
</script>
|
||||
/**
|
||||
* 휴가 요청 추가 (선택된 날짜를 백엔드로 전송)
|
||||
*/
|
||||
async function addVacationRequests() {
|
||||
if (selectedDates.value.size === 0) {
|
||||
alert("휴가를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
|
||||
date,
|
||||
type,
|
||||
employeeId: employeeId.value,
|
||||
}));
|
||||
try {
|
||||
const response = await axios.post("vacation", vacationRequests);
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("휴가가 저장되었습니다.");
|
||||
// 저장 후 현재 달 데이터 다시 불러오기
|
||||
const currentDate = fullCalendarRef.value.getApi().getDate();
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
loadCalendarData(year, month);
|
||||
selectedDates.value.clear();
|
||||
updateCalendarEvents();
|
||||
} else {
|
||||
alert("휴가 저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("휴가 저장에 실패했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
.vacation-management {
|
||||
padding: 20px;
|
||||
}
|
||||
/**
|
||||
* 공휴일 데이터 요청 및 이벤트 변환
|
||||
*/
|
||||
async function fetchHolidays(year, month) {
|
||||
try {
|
||||
console.log(`📌 공휴일 요청: ${year}-${month}`);
|
||||
const response = await axios.get(`vacation/${year}/${month}`);
|
||||
console.log("📌 공휴일 API 응답:", response.data);
|
||||
const holidayEvents = response.data.map((holiday) => ({
|
||||
title: holiday.name,
|
||||
start: holiday.date, // "YYYY-MM-DD" 형식
|
||||
backgroundColor: "#ff6666",
|
||||
classNames: ["holiday-event"],
|
||||
}));
|
||||
return holidayEvents;
|
||||
} catch (error) {
|
||||
console.error("공휴일 정보를 불러오지 못했습니다.", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/* 버튼 스타일 */
|
||||
.half-day-buttons {
|
||||
/**
|
||||
* 달력 월 변경 시 호출 (FullCalendar의 datesSet 옵션)
|
||||
*/
|
||||
function handleMonthChange(viewInfo) {
|
||||
const currentDate = viewInfo.view.currentStart;
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
console.log(`📌 월 변경 감지: ${year}-${month}`);
|
||||
loadCalendarData(year, month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청)
|
||||
*/
|
||||
async function loadCalendarData(year, month) {
|
||||
console.log(`📌 ${year}-${month} 데이터 로드 시작`);
|
||||
fetchedEvents.value = [];
|
||||
const [vacationEvents, holidayEvents] = await Promise.all([
|
||||
fetchVacationData(year, month),
|
||||
fetchHolidays(year, month),
|
||||
]);
|
||||
console.log("📌 변환된 휴가 이벤트:", vacationEvents);
|
||||
console.log("📌 변환된 공휴일 이벤트:", holidayEvents);
|
||||
// 클릭 불가 처리를 위해 공휴일 날짜 Set 업데이트
|
||||
holidayDates.value = new Set(holidayEvents.map((event) => event.start));
|
||||
fetchedEvents.value = [...vacationEvents, ...holidayEvents];
|
||||
updateCalendarEvents();
|
||||
await nextTick();
|
||||
fullCalendarRef.value.getApi().refetchEvents();
|
||||
console.log("📌 FullCalendar 데이터 업데이트 완료");
|
||||
}
|
||||
|
||||
// 컴포넌트 마운트 시 현재 달의 데이터 로드
|
||||
onMounted(() => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||
console.log(`📌 초기 로드: ${year}-${month}`);
|
||||
loadCalendarData(year, month);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 버튼 스타일 */
|
||||
.half-day-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.half-day-buttons .btn.active {
|
||||
}
|
||||
.half-day-buttons .btn.active {
|
||||
border: 2px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
/* FullCalendar 이벤트 스타일 */
|
||||
.fc-daygrid-event {
|
||||
/* 날짜 칸 높이 고정 */
|
||||
.fc-daygrid-day-frame {
|
||||
min-height: 80px !important;
|
||||
max-height: 120px !important;
|
||||
overflow: hidden !important;
|
||||
padding-top: 25px !important;
|
||||
}
|
||||
|
||||
/* 날짜 칸 내부의 이벤트 목록 */
|
||||
.fc-daygrid-day-events {
|
||||
max-height: 100px !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.fc-daygrid-event {
|
||||
position: absolute !important;
|
||||
height: 20px !important; /* 실선 두께 */
|
||||
width: 90% !important;
|
||||
left: 5% !important;
|
||||
height: 20px !important;
|
||||
width: 100% !important;
|
||||
left: 0 !important;
|
||||
margin: 2px 0 !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 2px !important;
|
||||
background-color: inherit !important;
|
||||
border: none !important; /* 기본 FullCalendar 테두리 제거 */
|
||||
}
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 여러 이벤트가 같은 날짜에 있을 때 정렬 */
|
||||
.fc-daygrid-event-harness {
|
||||
/* 여러 이벤트가 같은 날짜에 있을 때 정렬 */
|
||||
.fc-daygrid-event-harness {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 3px;
|
||||
}
|
||||
width: 100%;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
/* 오전반차(왼쪽부터 중앙까지) */
|
||||
.fc-daygrid-event.half-day-am {
|
||||
/* 기본 휴가/반차 이벤트 (API에서 받은 이벤트) */
|
||||
.fc-daygrid-event.half-day-am {
|
||||
width: 45% !important;
|
||||
left: 0% !important;
|
||||
background-color: #ffdd57 !important; /* 노란색 */
|
||||
}
|
||||
|
||||
/* 오후반차(오른쪽부터 중앙까지) */
|
||||
.fc-daygrid-event.half-day-pm {
|
||||
left: 0 !important;
|
||||
}
|
||||
.fc-daygrid-event.half-day-pm {
|
||||
width: 45% !important;
|
||||
left: auto !important;
|
||||
right: 0% !important;
|
||||
background-color: #57a5ff !important; /* 파란색 */
|
||||
}
|
||||
|
||||
/* 연차 (전체 너비) */
|
||||
.fc-daygrid-event.full-day {
|
||||
right: 0 !important;
|
||||
}
|
||||
.fc-daygrid-event.full-day {
|
||||
width: 100% !important;
|
||||
left: 0 !important;
|
||||
background-color: #ff85a2 !important; /* 연한 빨강 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 요일별 날짜 칸 스타일 */
|
||||
.fc-day-sun .fc-daygrid-day-number,
|
||||
.fc-col-header-cell:first-child .fc-col-header-cell-cushion {
|
||||
color: #ff4500 !important;
|
||||
}
|
||||
.fc-day-sat .fc-daygrid-day-number,
|
||||
.fc-col-header-cell:last-child .fc-col-header-cell-cushion {
|
||||
color: #324fde !important;
|
||||
}
|
||||
.fc-daygrid-day-number {
|
||||
position: absolute !important;
|
||||
top: 0px !important;
|
||||
left: 5px !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user