Merge remote-tracking branch 'origin/main' into wordDict
This commit is contained in:
commit
fc59cc4e5a
1
.env.dev
1
.env.dev
@ -3,3 +3,4 @@ VITE_DOMAIN = http://localhost:5173/
|
|||||||
# VITE_FILE_URL = http://localhost:10325/ms/
|
# VITE_FILE_URL = http://localhost:10325/ms/
|
||||||
# VITE_API_URL = http://localhost:10325/api/
|
# VITE_API_URL = http://localhost:10325/api/
|
||||||
VITE_API_URL = http://localhost:10325/test/
|
VITE_API_URL = http://localhost:10325/test/
|
||||||
|
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492
|
||||||
18
package-lock.json
generated
18
package-lock.json
generated
@ -30,7 +30,8 @@
|
|||||||
"vite-plugin-mkcert": "^1.17.6",
|
"vite-plugin-mkcert": "^1.17.6",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-flatpickr-component": "^11.0.5",
|
"vue-flatpickr-component": "^11.0.5",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"vue3-kakao-maps": "^2.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
@ -2544,6 +2545,12 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kakao.maps.d.ts": {
|
||||||
|
"version": "0.1.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/kakao.maps.d.ts/-/kakao.maps.d.ts-0.1.40.tgz",
|
||||||
|
"integrity": "sha512-nX69MB1ok04epe3OqS+/tEeWBbU31GSQbvDPJmQRRltzzqn6t4jBsO5v1nzalUjCKzwcH2CptOc767NZ7Hbu3g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -4142,6 +4149,15 @@
|
|||||||
"version": "6.6.4",
|
"version": "6.6.4",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vue3-kakao-maps": {
|
||||||
|
"version": "2.3.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue3-kakao-maps/-/vue3-kakao-maps-2.3.10.tgz",
|
||||||
|
"integrity": "sha512-vzzxvdw1ZtyID4clA/LFg++Qf1uTq49ksnL3ejqbp/rB+WVRrulSQ0ja+L929VFehbQY628lJPsp9KwxC6VrNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kakao.maps.d.ts": "^0.1.39"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@ -33,7 +33,8 @@
|
|||||||
"vite-plugin-mkcert": "^1.17.6",
|
"vite-plugin-mkcert": "^1.17.6",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-flatpickr-component": "^11.0.5",
|
"vue-flatpickr-component": "^11.0.5",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"vue3-kakao-maps": "^2.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
|
|||||||
@ -44,13 +44,56 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grayscaleImg {
|
.grayscaleImg {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scrollbar 안보이게 */
|
||||||
|
.scrollbar-none {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* project list */
|
||||||
|
|
||||||
|
.map {
|
||||||
|
top: -160px;
|
||||||
|
left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0% {
|
||||||
|
color: #ffcc00;
|
||||||
}
|
}
|
||||||
|
50% {
|
||||||
|
color: red;
|
||||||
/* scrollbar 안보이게 */
|
|
||||||
.scrollbar-none {
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
}
|
||||||
|
100% {
|
||||||
|
color: #ffcc00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bxs-map {
|
||||||
|
animation: sparkle 1s infinite; /* 1초마다 반복 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
opacity: 1;
|
||||||
|
padding: 0.635rem;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(161, 172, 184, 0.4);
|
||||||
|
transition: all 0.23s ease 0.1s;
|
||||||
|
transform: translate(23px, -25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
outline: none;
|
||||||
|
transform: translate(20px, -20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* project list end */
|
||||||
13
src/common/formattedDate.js
Normal file
13
src/common/formattedDate.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** 날짜 포맷1 (YYYY-MM-DD HH:MM) */
|
||||||
|
export const formattedDate = (dateString) => {
|
||||||
|
if (!dateString) return "날짜 없음";
|
||||||
|
const dateObj = new Date(dateString);
|
||||||
|
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 날짜 포맷2 (YYYY-MM-DD) */
|
||||||
|
export const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return "날짜 없음";
|
||||||
|
const dateObj = new Date(dateString);
|
||||||
|
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
@ -26,25 +26,57 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 주소 -->
|
<!-- 주소 -->
|
||||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||||
<i class="bx bxs-map"></i>
|
<div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
|
||||||
<div class="ms-2">주소</div>
|
<i
|
||||||
<div class="ms-12">{{ address }}</div>
|
class="bx bxs-map cursor-pointer"
|
||||||
|
ref="mapIconRef"
|
||||||
|
></i>
|
||||||
|
<div class="ms-2">주소</div>
|
||||||
|
</div>
|
||||||
|
<div class="ms-12 position-relative">
|
||||||
|
{{ address }} {{ addressdtail }}
|
||||||
|
<!-- 팝오버 -->
|
||||||
|
<div
|
||||||
|
v-if="isPopoverVisible"
|
||||||
|
class="position-absolute w-100 map text-end"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-1">
|
||||||
|
<KakaoMap
|
||||||
|
v-if="coordinates"
|
||||||
|
:lat="coordinates.lat"
|
||||||
|
:lng="coordinates.lng"
|
||||||
|
class="w-100 h-px-200"
|
||||||
|
>
|
||||||
|
<KakaoMapMarker
|
||||||
|
:lat="coordinates.lat"
|
||||||
|
:lng="coordinates.lng"
|
||||||
|
/>
|
||||||
|
</KakaoMap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctCol} !important;`" @click.stop="openModal">log</button>
|
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctCol} !important;`" @click.stop="openModal">log</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CenterModal :display="isModalOpen" @close="closeModal" >
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
<template #title> Log </template>
|
<template #title> Log </template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="border border-3 rounded p-5 ms-4 mt-2" v-if="logData">
|
<div class="ms-4 mt-2 border p-3" v-if="logData">
|
||||||
<p class="mb-1">{{ logData.createDate }}</p>
|
<p class="mb-1">{{ logData.createDate }}</p>
|
||||||
<strong class="">[{{ logData.creator }}] 프로젝트 등록</strong>
|
<strong>[{{ logData.creator }}] 프로젝트 등록</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border border-3 rounded p-5 ms-4 mt-2" v-if="logData?.updateDate">
|
<div class="log-item" v-if="logData?.updateDate">
|
||||||
<p class="mb-1">{{ logData.updateDate }}</p>
|
<div class="ms-4 mt-2 border p-3">
|
||||||
<strong>[{{ logData.updater }}] 프로젝트 수정</strong>
|
<p class="mb-1">{{ logData.updateDate }}</p>
|
||||||
|
<strong>[{{ logData.updater }}] 프로젝트 수정</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@ -55,10 +87,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, ref } from 'vue';
|
import { defineProps, onMounted, ref } from 'vue';
|
||||||
import UserList from '@c/user/UserList.vue';
|
import UserList from '@c/user/UserList.vue';
|
||||||
import CenterModal from '@c/modal/CenterModal.vue';
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
import { KakaoMap, KakaoMapMarker } from 'vue3-kakao-maps';
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -83,6 +116,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
addressdtail: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
projctSeq: {
|
projctSeq: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false
|
required: false
|
||||||
@ -98,13 +135,13 @@ defineEmits(['click']);
|
|||||||
const isModalOpen = ref(false);
|
const isModalOpen = ref(false);
|
||||||
const logData = ref(null);
|
const logData = ref(null);
|
||||||
|
|
||||||
|
const isPopoverVisible = ref(false);
|
||||||
|
const mapIconRef = ref(null);
|
||||||
|
const coordinates = ref(null);
|
||||||
|
|
||||||
const fetchLogData = async () => {
|
const fetchLogData = async () => {
|
||||||
try {
|
const response = await $api.get(`project/log/${props.projctSeq}`);
|
||||||
const response = await $api.get(`project/log/${props.projctSeq}`);
|
logData.value = response.data.data.length > 0 ? response.data.data[0] : {};
|
||||||
logData.value = response.data.data.length > 0 ? response.data.data[0] : {};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('로그 정보 조회 실패:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openModal = async () => {
|
const openModal = async () => {
|
||||||
@ -116,5 +153,32 @@ const closeModal = () => {
|
|||||||
isModalOpen.value = false;
|
isModalOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 주소를 좌표로 변환하는 함수
|
||||||
|
const convertAddressToCoordinates = () => {
|
||||||
|
const geocoder = new window.kakao.maps.services.Geocoder();
|
||||||
|
|
||||||
|
geocoder.addressSearch(props.address, (result, status) => {
|
||||||
|
if (status === window.kakao.maps.services.Status.OK) {
|
||||||
|
coordinates.value = {
|
||||||
|
lat: parseFloat(result[0].y),
|
||||||
|
lng: parseFloat(result[0].x)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 기본 좌표 설정 (본사)
|
||||||
|
coordinates.value = {
|
||||||
|
lat: 37.2108651707078,
|
||||||
|
lng: 127.089445559923
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 주소 변환
|
||||||
|
onMounted(() => {
|
||||||
|
convertAddressToCoordinates();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, computed } from "vue";
|
import { defineProps, defineEmits, computed } from "vue";
|
||||||
|
import { formatDate } from '@/common/formattedDate.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isOpen: Boolean,
|
isOpen: Boolean,
|
||||||
@ -130,14 +131,6 @@ const mergedVacations = computed(() => {
|
|||||||
return all;
|
return all;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 날짜 포맷 (YYYY-MM-DD) */
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return "";
|
|
||||||
// 만약 dateString이 "YYYY-MM-DD" 형식이라면 그대로 반환
|
|
||||||
// 혹은 "YYYY-MM-DD..." 라면 앞 10자만 잘라 반환
|
|
||||||
return dateString.substring(0, 10);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/** 모달 닫기 */
|
/** 모달 닫기 */
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<!-- 프로젝트 목록 -->
|
<!-- 프로젝트 목록 -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div v-if="projectList.length === 0" class="text-center">
|
<div v-if="projectList.length === 0" class="text-center">
|
||||||
<p class="text-muted mt-4">게시물이 없습니다.</p>
|
<p class="text-muted mt-4">등록된 프로젝트가 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="post in projectList" :key="post.PROJCTSEQ" @click="openEditModal(post)" class="cursor-pointer">
|
<div v-for="post in projectList" :key="post.PROJCTSEQ" @click="openEditModal(post)" class="cursor-pointer">
|
||||||
@ -17,7 +17,8 @@
|
|||||||
:description="post.PROJCTDES"
|
:description="post.PROJCTDES"
|
||||||
:strdate="post.PROJCTSTR"
|
:strdate="post.PROJCTSTR"
|
||||||
:enddate="post.PROJCTEND"
|
:enddate="post.PROJCTEND"
|
||||||
:address="post.PROJCTARR + ' ' + post.PROJCTDTL"
|
:address="post.PROJCTARR"
|
||||||
|
:addressdtail="post.PROJCTDTL"
|
||||||
:projctSeq="post.PROJCTSEQ"
|
:projctSeq="post.PROJCTSEQ"
|
||||||
:projctCol="post.projctcolor"
|
:projctCol="post.projctcolor"
|
||||||
/>
|
/>
|
||||||
@ -258,6 +259,7 @@ const handleAddressUpdate = addressData => {
|
|||||||
postcode.value = addressData.postcode;
|
postcode.value = addressData.postcode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 프로젝트 등록
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
nameAlert.value = name.value.trim() === '';
|
nameAlert.value = name.value.trim() === '';
|
||||||
addressAlert.value = address.value.trim() === '';
|
addressAlert.value = address.value.trim() === '';
|
||||||
@ -330,6 +332,7 @@ const updateAddress = (addressData) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 프로젝트 수정
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
if (!hasChanges.value) {
|
if (!hasChanges.value) {
|
||||||
toastStore.onToast('변경된 내용이 없습니다.', 'e');
|
toastStore.onToast('변경된 내용이 없습니다.', 'e');
|
||||||
@ -345,7 +348,7 @@ const handleUpdate = () => {
|
|||||||
projctZip: selectedProject.value.PROJCTZIP,
|
projctZip: selectedProject.value.PROJCTZIP,
|
||||||
projctStr: selectedProject.value.PROJCTSTR,
|
projctStr: selectedProject.value.PROJCTSTR,
|
||||||
projctEnd: selectedProject.value.PROJCTEND || null,
|
projctEnd: selectedProject.value.PROJCTEND || null,
|
||||||
projctDes: selectedProject.value.PROJCTDES,
|
projctDes: selectedProject.value.PROJCTDES || null,
|
||||||
projctUmb: user.value.name,
|
projctUmb: user.value.name,
|
||||||
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value
|
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
|
<UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
|
||||||
|
|
||||||
@ -12,7 +13,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-grid gap-2 mt-7 mb-5">
|
<div class="d-grid gap-2 mt-7 mb-5">
|
||||||
<button type="submit" @click="handleSubmit" class="btn btn-primary">로그인</button>
|
<button type="submit" class="btn btn-primary">로그인</button>
|
||||||
<p v-if="errorMessage" class="invalid-feedback d-block mb-0">{{ errorMessage }}</p>
|
<p v-if="errorMessage" class="invalid-feedback d-block mb-0">{{ errorMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -25,6 +26,7 @@
|
|||||||
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink>
|
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card-body d-flex justify-content-center">
|
<div class="card-body d-flex justify-content-center m-n5">
|
||||||
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
|
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
|
||||||
<li
|
<li
|
||||||
v-for="(user, index) in sortedUserList"
|
v-for="(user, index) in sortedUserList"
|
||||||
|
|||||||
@ -152,7 +152,7 @@
|
|||||||
<!-- User -->
|
<!-- User -->
|
||||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||||
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
|
||||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-px-40 rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@ -6,10 +6,15 @@ import router from '@/router'
|
|||||||
import dayjs from '@p/dayjs'
|
import dayjs from '@p/dayjs'
|
||||||
import ToastModal from '@c/modal/ToastModal.vue';
|
import ToastModal from '@c/modal/ToastModal.vue';
|
||||||
import common from '@/common/common.js'
|
import common from '@/common/common.js'
|
||||||
|
import { useKakao } from 'vue3-kakao-maps/@utils'
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPersist)
|
pinia.use(piniaPersist)
|
||||||
|
|
||||||
|
const kakaoApiKey = import.meta.env.VITE_KAKAO_MAP_KEY;
|
||||||
|
|
||||||
|
useKakao(kakaoApiKey, ['services'])
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
|
|||||||
@ -4,35 +4,32 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<!-- Sidebar: 사이드바 영역 -->
|
<!-- Sidebar: 사이드바 영역 -->
|
||||||
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
|
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
|
||||||
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
|
|
||||||
<VacationModal
|
|
||||||
v-if="isModalOpen"
|
|
||||||
:isOpen="isModalOpen"
|
|
||||||
:myVacations="filteredMyVacations"
|
|
||||||
:receivedVacations="filteredReceivedVacations"
|
|
||||||
:userColors="userColors"
|
|
||||||
@close="isModalOpen = false"
|
|
||||||
/>
|
|
||||||
<VacationGrantModal
|
|
||||||
v-if="isGrantModalOpen"
|
|
||||||
:isOpen="isGrantModalOpen"
|
|
||||||
:targetUser="selectedUser"
|
|
||||||
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
|
|
||||||
@close="isGrantModalOpen = false"
|
|
||||||
@updateVacation="fetchRemainingVacation"
|
|
||||||
/>
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<!-- 사원 프로필 리스트 -->
|
<div class="sidebar-actions text-center my-3">
|
||||||
|
<HalfDayButtons
|
||||||
|
@toggleHalfDay="toggleHalfDay"
|
||||||
|
@addVacationRequests="saveVacationChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ProfileList
|
<ProfileList
|
||||||
@profileClick="handleProfileClick"
|
@profileClick="handleProfileClick"
|
||||||
:remainingVacationData="remainingVacationData"
|
:remainingVacationData="remainingVacationData"
|
||||||
/>
|
/>
|
||||||
</div>
|
<VacationModal
|
||||||
<div class="sidebar-actions text-center my-3">
|
v-if="isModalOpen"
|
||||||
<!-- 액션 버튼 -->
|
:isOpen="isModalOpen"
|
||||||
<HalfDayButtons
|
:myVacations="filteredMyVacations"
|
||||||
@toggleHalfDay="toggleHalfDay"
|
:receivedVacations="filteredReceivedVacations"
|
||||||
@addVacationRequests="saveVacationChanges"
|
:userColors="userColors"
|
||||||
|
@close="isModalOpen = false"
|
||||||
|
/>
|
||||||
|
<VacationGrantModal
|
||||||
|
v-if="isGrantModalOpen"
|
||||||
|
:isOpen="isGrantModalOpen"
|
||||||
|
:targetUser="selectedUser"
|
||||||
|
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
|
||||||
|
@close="isGrantModalOpen = false"
|
||||||
|
@updateVacation="fetchRemainingVacation"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,12 +37,18 @@
|
|||||||
<!-- Main Content: 캘린더 영역 -->
|
<!-- Main Content: 캘린더 영역 -->
|
||||||
<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 pb-0" style="position: relative;">
|
||||||
<full-calendar
|
<full-calendar
|
||||||
ref="fullCalendarRef"
|
ref="fullCalendarRef"
|
||||||
:options="calendarOptions"
|
:options="calendarOptions"
|
||||||
class="flatpickr-calendar-only"
|
class="flatpickr-calendar-only"
|
||||||
/>
|
/>
|
||||||
|
<!-- 숨겨진 데이트피커 인풋 -->
|
||||||
|
<input
|
||||||
|
ref="calendarDatepicker"
|
||||||
|
type="text"
|
||||||
|
style="display: none; position: absolute;"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,14 +57,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
|
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
|
||||||
import axios from "@api";
|
import axios from "@api";
|
||||||
import FullCalendar from "@fullcalendar/vue3";
|
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";
|
||||||
|
// Flatpickr 및 MonthSelect 플러그인 임포트
|
||||||
|
import flatpickr from "flatpickr";
|
||||||
|
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
|
||||||
import "flatpickr/dist/flatpickr.min.css";
|
import "flatpickr/dist/flatpickr.min.css";
|
||||||
|
import "flatpickr/dist/plugins/monthSelect/style.css";
|
||||||
|
|
||||||
import "@/assets/css/app-calendar.css";
|
import "@/assets/css/app-calendar.css";
|
||||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||||
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
|
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
|
||||||
@ -76,8 +83,8 @@
|
|||||||
const userListStore = useUserStore();
|
const userListStore = useUserStore();
|
||||||
const userList = ref([]);
|
const userList = ref([]);
|
||||||
const userColors = ref({});
|
const userColors = ref({});
|
||||||
const myVacations = ref([]); // 전체 "사용한 연차" 목록
|
const myVacations = ref([]); // 로그인한 사원의 휴가
|
||||||
const receivedVacations = ref([]); // 전체 "받은 연차" 목록
|
const receivedVacations = ref([]);
|
||||||
const isModalOpen = ref(false);
|
const isModalOpen = ref(false);
|
||||||
const remainingVacationData = ref({});
|
const remainingVacationData = ref({});
|
||||||
|
|
||||||
@ -95,6 +102,10 @@
|
|||||||
const holidayDates = ref(new Set());
|
const holidayDates = ref(new Set());
|
||||||
const fetchedEvents = ref([]);
|
const fetchedEvents = ref([]);
|
||||||
|
|
||||||
|
// 데이트피커 인풋 ref
|
||||||
|
const calendarDatepicker = ref(null);
|
||||||
|
let fpInstance = null;
|
||||||
|
|
||||||
const calendarOptions = reactive({
|
const calendarOptions = reactive({
|
||||||
plugins: [dayGridPlugin, interactionPlugin],
|
plugins: [dayGridPlugin, interactionPlugin],
|
||||||
initialView: "dayGridMonth",
|
initialView: "dayGridMonth",
|
||||||
@ -113,24 +124,64 @@
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
await fetchRemainingVacation();
|
await fetchRemainingVacation();
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
await fetchVacationHistory(currentYear);
|
||||||
|
|
||||||
|
// Flatpickr 초기화 (달 선택 모드)
|
||||||
|
fpInstance = flatpickr(calendarDatepicker.value, {
|
||||||
|
dateFormat: "Y-m",
|
||||||
|
plugins: [
|
||||||
|
new monthSelectPlugin({
|
||||||
|
shorthand: true,
|
||||||
|
dateFormat: "Y-m",
|
||||||
|
altFormat: "F Y"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
onChange: function(selectedDatesArr, dateStr) {
|
||||||
|
// 선택한 달의 첫날로 달력을 이동
|
||||||
|
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
|
||||||
|
const [year, month] = dateStr.split("-");
|
||||||
|
lastRemainingYear.value = parseInt(year, 10);
|
||||||
|
lastRemainingMonth.value = month;
|
||||||
|
loadCalendarData(lastRemainingYear.value, lastRemainingMonth.value);
|
||||||
|
},
|
||||||
|
onClose: function() {
|
||||||
|
calendarDatepicker.value.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// FullCalendar 헤더 제목(.fc-toolbar-title) 클릭 시 데이트피커 열기
|
||||||
|
nextTick(() => {
|
||||||
|
const titleEl = document.querySelector('.fc-toolbar-title');
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.style.cursor = 'pointer';
|
||||||
|
titleEl.addEventListener('click', () => {
|
||||||
|
fpInstance.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// lastRemainingYear 값이 변경될 때마다 해당 연도의 연차 내역을 재조회
|
// 연차 내역 API (초기 호출용)
|
||||||
watch(lastRemainingYear, async (newYear, oldYear) => {
|
async function fetchVacationHistory(year) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`vacation/history?year=${newYear}`);
|
const response = await axios.get(`vacation/history?year=${year}`);
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
myVacations.value = response.data.data.usedVacations || [];
|
myVacations.value = response.data.data.usedVacations || [];
|
||||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||||
} else {
|
} else {
|
||||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
||||||
myVacations.value = [];
|
myVacations.value = [];
|
||||||
receivedVacations.value = [];
|
receivedVacations.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
watch(lastRemainingYear, async (newYear, oldYear) => {
|
||||||
|
await fetchVacationHistory(newYear);
|
||||||
|
});
|
||||||
|
|
||||||
const fetchRemainingVacation = async () => {
|
const fetchRemainingVacation = async () => {
|
||||||
try {
|
try {
|
||||||
@ -146,43 +197,23 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 프로필 클릭 시 연차 내역 가져오기
|
const handleProfileClick = async (user) => {
|
||||||
// 프로필 클릭 시 연차 내역 가져오기
|
try {
|
||||||
const handleProfileClick = async (user) => {
|
// 열린 모달이 있으면 모두 닫음 후 새 모달 열기
|
||||||
try {
|
|
||||||
// 이미 모달이 열려있다면 토글하여 닫음
|
|
||||||
if (isModalOpen.value) {
|
|
||||||
isModalOpen.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (user.MEMBERSEQ === userStore.user.id) {
|
|
||||||
const year = new Date().getFullYear(); // 현재 연도
|
|
||||||
// 연도 파라미터를 전달하여 전체 연도의 연차 내역을 조회
|
|
||||||
const response = await axios.get(`vacation/history?year=${year}`);
|
|
||||||
if (response.status === 200 && response.data) {
|
|
||||||
myVacations.value = response.data.data.usedVacations || [];
|
|
||||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
|
||||||
isModalOpen.value = true;
|
|
||||||
// 모달을 열 때 기준 연도 갱신
|
|
||||||
lastRemainingYear.value = year;
|
|
||||||
isGrantModalOpen.value = false;
|
|
||||||
} else {
|
|
||||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 이미 모달이 열려있다면 토글하여 닫음
|
|
||||||
if (isGrantModalOpen.value) {
|
|
||||||
isGrantModalOpen.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedUser.value = user;
|
|
||||||
isGrantModalOpen.value = true;
|
|
||||||
isModalOpen.value = false;
|
isModalOpen.value = false;
|
||||||
|
isGrantModalOpen.value = false;
|
||||||
|
if (user.MEMBERSEQ === userStore.user.id) {
|
||||||
|
const displayedYear = lastRemainingYear.value;
|
||||||
|
await fetchVacationHistory(displayedYear);
|
||||||
|
isModalOpen.value = true;
|
||||||
|
} else {
|
||||||
|
selectedUser.value = user;
|
||||||
|
isGrantModalOpen.value = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUserList = async () => {
|
const fetchUserList = async () => {
|
||||||
try {
|
try {
|
||||||
@ -221,28 +252,21 @@ const handleProfileClick = async (user) => {
|
|||||||
return vacationCodeMap.value[typeCode] || "기타";
|
return vacationCodeMap.value[typeCode] || "기타";
|
||||||
};
|
};
|
||||||
|
|
||||||
// computed: lastRemainingYear과과 일치하는 항목만 필터링
|
const filteredMyVacations = computed(() => {
|
||||||
const filteredMyVacations = computed(() => {
|
return myVacations.value.filter(vac => {
|
||||||
const filtered = myVacations.value.filter(vac => {
|
const dateStr = vac.date;
|
||||||
// vac.date가 없으면 vac.LOCVACUDT를 사용하도록 함
|
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||||
const dateStr = vac.date || vac.LOCVACUDT;
|
return year === String(lastRemainingYear.value);
|
||||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
});
|
||||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
|
||||||
return year === String(lastRemainingYear.value);
|
|
||||||
});
|
});
|
||||||
console.log("filteredMyVacations:", filtered);
|
|
||||||
return filtered;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredReceivedVacations = computed(() => {
|
const filteredReceivedVacations = computed(() => {
|
||||||
return receivedVacations.value.filter(vac => {
|
return receivedVacations.value.filter(vac => {
|
||||||
const dateStr = vac.date || vac.LOCVACUDT;
|
const dateStr = vac.date;
|
||||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
return dateStr && year === String(lastRemainingYear.value);
|
||||||
return dateStr && year === String(lastRemainingYear.value);
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function updateCalendarEvents() {
|
function updateCalendarEvents() {
|
||||||
const selectedEvents = Array.from(selectedDates.value)
|
const selectedEvents = Array.from(selectedDates.value)
|
||||||
@ -255,12 +279,16 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
display: "background",
|
display: "background",
|
||||||
classNames: [getVacationTypeClass(type), "selected-event"]
|
classNames: [getVacationTypeClass(type), "selected-event"]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filteredFetchedEvents = fetchedEvents.value.filter(event => {
|
const filteredFetchedEvents = fetchedEvents.value.filter(event => {
|
||||||
if (event.saved) {
|
if (event.saved && selectedDates.value.get(event.start) === "delete") {
|
||||||
return selectedDates.value.get(event.start) !== "delete";
|
if (event.memberSeq === userStore.user.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
calendarEvents.value = [...filteredFetchedEvents, ...selectedEvents];
|
calendarEvents.value = [...filteredFetchedEvents, ...selectedEvents];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +302,7 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
const clickedDateStr = info.dateStr;
|
const clickedDateStr = info.dateStr;
|
||||||
const clickedDate = info.date;
|
const clickedDate = info.date;
|
||||||
const todayStr = new Date().toISOString().split("T")[0];
|
const todayStr = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
clickedDate.getDay() === 0 ||
|
clickedDate.getDay() === 0 ||
|
||||||
clickedDate.getDay() === 6 ||
|
clickedDate.getDay() === 6 ||
|
||||||
@ -282,22 +311,30 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isMyVacation = myVacations.value.some(vac => {
|
||||||
|
const vacDate = vac.date ? String(vac.date).substring(0, 10) : "";
|
||||||
|
return vacDate === clickedDateStr && !vac.receiverId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMyVacation) {
|
||||||
|
if (selectedDates.value.get(clickedDateStr) === "delete") {
|
||||||
|
selectedDates.value.delete(clickedDateStr);
|
||||||
|
} else {
|
||||||
|
selectedDates.value.set(clickedDateStr, "delete");
|
||||||
|
}
|
||||||
|
updateCalendarEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedDates.value.has(clickedDateStr)) {
|
if (selectedDates.value.has(clickedDateStr)) {
|
||||||
selectedDates.value.delete(clickedDateStr);
|
selectedDates.value.delete(clickedDateStr);
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const unsentVacation = myVacations.value.find(
|
const type = halfDayType.value
|
||||||
(vac) => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(clickedDateStr) && !vac.LOCVACRMM
|
? (halfDayType.value === "AM" ? "700101" : "700102")
|
||||||
);
|
: "700103";
|
||||||
if (unsentVacation) {
|
selectedDates.value.set(clickedDateStr, type);
|
||||||
selectedDates.value.set(clickedDateStr, "delete");
|
|
||||||
} else {
|
|
||||||
const type = halfDayType.value
|
|
||||||
? (halfDayType.value === "AM" ? "700101" : "700102")
|
|
||||||
: "700103";
|
|
||||||
selectedDates.value.set(clickedDateStr, type);
|
|
||||||
}
|
|
||||||
halfDayType.value = null;
|
halfDayType.value = null;
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
}
|
}
|
||||||
@ -307,61 +344,59 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchVacationData(year, month) {
|
async function fetchVacationData(year, month) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`vacation/list/${year}/${month}`);
|
const response = await axios.get(`vacation/list/${year}/${month}`);
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const vacationList = response.data;
|
const vacationList = response.data;
|
||||||
// 모달이 열려 있더라도 전달받은 연도가 기존 lastRemainingYear 다르면 업데이트
|
if (lastRemainingYear.value !== year) {
|
||||||
if (lastRemainingYear.value !== year) {
|
myVacations.value = vacationList.filter(
|
||||||
myVacations.value = vacationList.filter(
|
(vac) => vac.MEMBERSEQ === userStore.user.id
|
||||||
(vac) => vac.MEMBERSEQ === userStore.user.id
|
);
|
||||||
);
|
lastRemainingYear.value = year;
|
||||||
lastRemainingYear.value = year;
|
}
|
||||||
// modalMonth는 그대로 유지 (월은 모달 업데이트 조건에서 제외)
|
const events = vacationList
|
||||||
|
.filter((vac) => !vac.LOCVACRMM)
|
||||||
|
.map((vac) => {
|
||||||
|
let dateStr = vac.LOCVACUDT ? vac.LOCVACUDT.split("T")[0] : "";
|
||||||
|
let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
|
||||||
|
return {
|
||||||
|
title: getVacationType(vac.LOCVACTYP),
|
||||||
|
start: dateStr,
|
||||||
|
backgroundColor,
|
||||||
|
classNames: [getVacationTypeClass(vac.LOCVACTYP)],
|
||||||
|
saved: true,
|
||||||
|
memberSeq: vac.MEMBERSEQ,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((event) => event.start);
|
||||||
|
return events;
|
||||||
|
} else {
|
||||||
|
console.warn("📌 휴가 데이터를 불러오지 못함");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
// 캘린더 이벤트 매핑
|
} catch (error) {
|
||||||
const events = vacationList
|
console.error("Error fetching vacation data:", error);
|
||||||
.filter((vac) => !vac.LOCVACRMM)
|
|
||||||
.map((vac) => {
|
|
||||||
let dateStr = vac.LOCVACUDT ? vac.LOCVACUDT.split("T")[0] : "";
|
|
||||||
let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
|
|
||||||
return {
|
|
||||||
title: getVacationType(vac.LOCVACTYP),
|
|
||||||
start: dateStr,
|
|
||||||
backgroundColor,
|
|
||||||
classNames: [getVacationTypeClass(vac.LOCVACTYP)],
|
|
||||||
saved: true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((event) => event.start);
|
|
||||||
return events;
|
|
||||||
} else {
|
|
||||||
console.warn("📌 휴가 데이터를 불러오지 못함");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching vacation data:", error);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function saveVacationChanges() {
|
async function saveVacationChanges() {
|
||||||
const selectedDatesArray = Array.from(selectedDates.value);
|
const selectedDatesArray = Array.from(selectedDates.value);
|
||||||
const vacationsToAdd = selectedDatesArray
|
const vacationsToAdd = selectedDatesArray
|
||||||
.filter(([date, type]) => type !== "delete")
|
.filter(([date, type]) => type !== "delete")
|
||||||
.filter(([date, type]) =>
|
.filter(([date, type]) =>
|
||||||
!myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date)) ||
|
!myVacations.value.some(vac => vac.date && vac.date.startsWith(date)) ||
|
||||||
myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date) && vac.LOCVACRMM)
|
myVacations.value.some(vac => vac.date && vac.date.startsWith(date) && vac.receiverId)
|
||||||
)
|
)
|
||||||
.map(([date, type]) => ({ date, type }));
|
.map(([date, type]) => ({ date, type }));
|
||||||
const vacationsToDelete = myVacations.value
|
const vacationsToDelete = myVacations.value
|
||||||
.filter(vac => {
|
.filter(vac => {
|
||||||
if (!vac.LOCVACUDT) return false;
|
if (!vac.date) return false;
|
||||||
const date = vac.LOCVACUDT.split("T")[0];
|
const date = vac.date.split("T")[0];
|
||||||
return selectedDates.value.get(date) === "delete" && !vac.LOCVACRMM;
|
return selectedDates.value.get(date) === "delete" && !vac.receiverId;
|
||||||
})
|
})
|
||||||
.map(vac => {
|
.map(vac => {
|
||||||
const id = vac.LOCVACSEQ;
|
const id = vac.id;
|
||||||
return typeof id === "number" ? Number(id) : id;
|
return typeof id === "number" ? Number(id) : id;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@ -372,6 +407,9 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
if (response.data && response.data.status === "OK") {
|
if (response.data && response.data.status === "OK") {
|
||||||
alert("✅ 휴가 변경 사항이 저장되었습니다.");
|
alert("✅ 휴가 변경 사항이 저장되었습니다.");
|
||||||
await fetchRemainingVacation();
|
await fetchRemainingVacation();
|
||||||
|
if (isModalOpen.value) {
|
||||||
|
await fetchVacationHistory(lastRemainingYear.value);
|
||||||
|
}
|
||||||
const currentDate = fullCalendarRef.value.getApi().getDate();
|
const currentDate = fullCalendarRef.value.getApi().getDate();
|
||||||
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
|
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
|
||||||
selectedDates.value.clear();
|
selectedDates.value.clear();
|
||||||
@ -416,10 +454,16 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const year = today.getFullYear();
|
const year = today.getFullYear();
|
||||||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||||
|
await fetchVacationData(year, month);
|
||||||
await loadCalendarData(year, month);
|
await loadCalendarData(year, month);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 스타일 정의 */
|
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
|
||||||
|
.fc-toolbar-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 데이트피커 인풋은 Flatpickr에서 동적으로 스타일 적용됨 */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user