Merge remote-tracking branch 'origin/main' into wordDict
This commit is contained in:
commit
1e185e81c6
@ -4,8 +4,8 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
let menu, animate
|
||||
;(function () {
|
||||
var menu, animate;
|
||||
(function () {
|
||||
// Initialize menu
|
||||
//-----------------
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>해당 직원에게 부여할 연차 개수를 선택하세요. (남은 개수: {{ availableQuota }}개)</p>
|
||||
<p>선물할 연차 개수를 선택하세요.</p>
|
||||
|
||||
<div class="vacation-control">
|
||||
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
||||
@ -129,7 +129,7 @@
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@ -151,7 +151,7 @@ const closeModal = () => {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@ -222,4 +222,10 @@ const closeModal = () => {
|
||||
color: gray;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 모달 본문 스크롤: 높이가 300px 이상이면 스크롤바 표시 */
|
||||
.modal-body {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="card-body d-flex justify-content-center">
|
||||
<ul class="list-unstyled d-flex align-items-center gap-5 mb-0 mt-2">
|
||||
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
|
||||
<li
|
||||
v-for="(user, index) in sortedUserList"
|
||||
:key="index"
|
||||
@ -78,6 +78,7 @@
|
||||
return myProfile ? [myProfile, ...otherUsers] : userList.value;
|
||||
});
|
||||
|
||||
// 프로필 이미지 URL 반환
|
||||
const getUserProfileImage = (profilePath) => {
|
||||
return profilePath && profilePath.trim()
|
||||
? `${baseUrl}upload/img/profile/${profilePath}`
|
||||
@ -92,14 +93,12 @@
|
||||
event.target.style.visibility = "visible";
|
||||
};
|
||||
|
||||
// 프로필 크기 동적 조정
|
||||
// 프로필 크기 동적 조정: 사이드바 영역에 맞게 조금 더 축소
|
||||
const profileSize = computed(() => {
|
||||
const totalUsers = userList.value.length;
|
||||
|
||||
if (totalUsers <= 7) return "100px"; // 7명 이하
|
||||
if (totalUsers <= 10) return "80px"; // ~10명
|
||||
if (totalUsers <= 20) return "60px"; // ~20명
|
||||
return "40px"; // 20명 이상
|
||||
if (totalUsers <= 10) return "68px"; // ~10명
|
||||
if (totalUsers <= 15) return "50px"; // ~20명
|
||||
return "30px"; // 20명 이상
|
||||
});
|
||||
|
||||
// 개별 유저 스타일 적용
|
||||
@ -123,4 +122,15 @@
|
||||
color: #333;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* ul에 flex-wrap을 적용하여 넘치는 프로필이 다음 줄로 내려가도록 함 */
|
||||
ul {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* li 간 간격 조정 */
|
||||
li {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -138,6 +138,14 @@ const onChange = (newValue) => {
|
||||
|
||||
//용어 등록
|
||||
const saveWord = () => {
|
||||
|
||||
// if(addCategory.value == ''){
|
||||
// addCategoryAlert.value = true;
|
||||
// return;
|
||||
// }else {
|
||||
// addCategoryAlert.value = false;
|
||||
// }
|
||||
|
||||
//validation
|
||||
|
||||
// 용어 체크
|
||||
@ -158,8 +166,7 @@ const saveWord = () => {
|
||||
category: selectedCategory.value,
|
||||
content: content.value,
|
||||
};
|
||||
|
||||
emit('addWord', wordData);
|
||||
emit('addWord', wordData ,addCategory.value );
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<div class="vacation-management">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<ProfileList
|
||||
@profileClick="handleProfileClick"
|
||||
:remainingVacationData="remainingVacationData"
|
||||
/>
|
||||
<div class="card-body w-75 p-3 align-self-center">
|
||||
<!-- 모달에 필터링된 연차 목록 전달 -->
|
||||
<!-- Sidebar: 사이드바 영역 -->
|
||||
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
|
||||
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
|
||||
<VacationModal
|
||||
v-if="isModalOpen"
|
||||
:isOpen="isModalOpen"
|
||||
@ -19,7 +13,6 @@
|
||||
:userColors="userColors"
|
||||
@close="isModalOpen = false"
|
||||
/>
|
||||
|
||||
<VacationGrantModal
|
||||
v-if="isGrantModalOpen"
|
||||
:isOpen="isGrantModalOpen"
|
||||
@ -28,17 +21,32 @@
|
||||
@close="isGrantModalOpen = false"
|
||||
@updateVacation="fetchRemainingVacation"
|
||||
/>
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
class="flatpickr-calendar-only"
|
||||
<div class="sidebar-content">
|
||||
<!-- 사원 프로필 리스트 -->
|
||||
<ProfileList
|
||||
@profileClick="handleProfileClick"
|
||||
:remainingVacationData="remainingVacationData"
|
||||
/>
|
||||
</div>
|
||||
<div class="sidebar-actions text-center my-3">
|
||||
<!-- 액션 버튼 -->
|
||||
<HalfDayButtons
|
||||
@toggleHalfDay="toggleHalfDay"
|
||||
@addVacationRequests="saveVacationChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content: 캘린더 영역 -->
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body pb-0">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
class="flatpickr-calendar-only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,8 +54,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted, nextTick, computed } from "vue";
|
||||
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
|
||||
import axios from "@api";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
@ -71,11 +80,9 @@
|
||||
const receivedVacations = ref([]); // 전체 "받은 연차" 목록
|
||||
const isModalOpen = ref(false);
|
||||
const remainingVacationData = ref({});
|
||||
// 모달을 열 때 기준 연도 (모달에 표시할 연도)
|
||||
const modalYear = ref(new Date().getFullYear());
|
||||
const modalMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
|
||||
const lastRemainingYear = ref(new Date().getFullYear());
|
||||
|
||||
const lastRemainingYear = ref(new Date().getFullYear());
|
||||
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
|
||||
const isGrantModalOpen = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
|
||||
@ -108,6 +115,23 @@
|
||||
await fetchRemainingVacation();
|
||||
});
|
||||
|
||||
// lastRemainingYear 값이 변경될 때마다 해당 연도의 연차 내역을 재조회
|
||||
watch(lastRemainingYear, async (newYear, oldYear) => {
|
||||
try {
|
||||
const response = await axios.get(`vacation/history?year=${newYear}`);
|
||||
if (response.status === 200 && response.data) {
|
||||
myVacations.value = response.data.data.usedVacations || [];
|
||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||
} else {
|
||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
||||
myVacations.value = [];
|
||||
receivedVacations.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchRemainingVacation = async () => {
|
||||
try {
|
||||
const response = await axios.get("vacation/remaining");
|
||||
@ -126,6 +150,11 @@
|
||||
// 프로필 클릭 시 연차 내역 가져오기
|
||||
const handleProfileClick = async (user) => {
|
||||
try {
|
||||
// 이미 모달이 열려있다면 토글하여 닫음
|
||||
if (isModalOpen.value) {
|
||||
isModalOpen.value = false;
|
||||
return;
|
||||
}
|
||||
if (user.MEMBERSEQ === userStore.user.id) {
|
||||
const year = new Date().getFullYear(); // 현재 연도
|
||||
// 연도 파라미터를 전달하여 전체 연도의 연차 내역을 조회
|
||||
@ -135,12 +164,17 @@ const handleProfileClick = async (user) => {
|
||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
||||
isModalOpen.value = true;
|
||||
// 모달을 열 때 기준 연도 갱신
|
||||
modalYear.value = year;
|
||||
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;
|
||||
@ -187,14 +221,14 @@ const handleProfileClick = async (user) => {
|
||||
return vacationCodeMap.value[typeCode] || "기타";
|
||||
};
|
||||
|
||||
// computed: modalYear와 일치하는 항목만 필터링
|
||||
// computed: lastRemainingYear과과 일치하는 항목만 필터링
|
||||
const filteredMyVacations = computed(() => {
|
||||
const filtered = myVacations.value.filter(vac => {
|
||||
// vac.date가 없으면 vac.LOCVACUDT를 사용하도록 함
|
||||
const dateStr = vac.date || vac.LOCVACUDT;
|
||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||
console.log("vacation year:", year, "modalYear:", modalYear.value);
|
||||
return year === String(modalYear.value);
|
||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
||||
return year === String(lastRemainingYear.value);
|
||||
});
|
||||
console.log("filteredMyVacations:", filtered);
|
||||
return filtered;
|
||||
@ -204,8 +238,8 @@ const filteredReceivedVacations = computed(() => {
|
||||
return receivedVacations.value.filter(vac => {
|
||||
const dateStr = vac.date || vac.LOCVACUDT;
|
||||
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
|
||||
console.log("vacation year:", year, "modalYear:", modalYear.value);
|
||||
return dateStr && year === String(modalYear.value);
|
||||
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
|
||||
return dateStr && year === String(lastRemainingYear.value);
|
||||
});
|
||||
});
|
||||
|
||||
@ -277,12 +311,12 @@ const filteredReceivedVacations = computed(() => {
|
||||
const response = await axios.get(`vacation/list/${year}/${month}`);
|
||||
if (response.status === 200) {
|
||||
const vacationList = response.data;
|
||||
// 모달이 열려 있더라도 전달받은 연도가 기존 modalYear와 다르면 업데이트
|
||||
if (modalYear.value !== year) {
|
||||
// 모달이 열려 있더라도 전달받은 연도가 기존 lastRemainingYear 다르면 업데이트
|
||||
if (lastRemainingYear.value !== year) {
|
||||
myVacations.value = vacationList.filter(
|
||||
(vac) => vac.MEMBERSEQ === userStore.user.id
|
||||
);
|
||||
modalYear.value = year;
|
||||
lastRemainingYear.value = year;
|
||||
// modalMonth는 그대로 유지 (월은 모달 업데이트 조건에서 제외)
|
||||
}
|
||||
// 캘린더 이벤트 매핑
|
||||
@ -359,9 +393,10 @@ const filteredReceivedVacations = computed(() => {
|
||||
}
|
||||
|
||||
async function loadCalendarData(year, month) {
|
||||
if (lastRemainingYear.value !== year) {
|
||||
if (lastRemainingYear.value !== year || lastRemainingMonth.value !== month) {
|
||||
await fetchRemainingVacation();
|
||||
lastRemainingYear.value = year;
|
||||
lastRemainingMonth.value = month;
|
||||
}
|
||||
fetchedEvents.value = [];
|
||||
const [vacationEvents, holidayEvents] = await Promise.all([
|
||||
|
||||
@ -177,34 +177,58 @@
|
||||
const addCategory = (data) =>{
|
||||
const lastCategory = cateList.value[cateList.value.length - 1];
|
||||
const newValue = lastCategory ? lastCategory.value + 1 : 600101;
|
||||
axios.post('worddict/insertCategory',{
|
||||
CMNCODNAM: data
|
||||
}).then(res => {
|
||||
if(res.data.data == '1'){
|
||||
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
|
||||
const newCategory = { label: data, value: newValue };
|
||||
cateList.value = [newCategory, ...cateList.value];
|
||||
selectedCategory.value = newCategory.value;
|
||||
} else if(res.data.message == '이미 존재하는 카테고리명입니다.') {
|
||||
toastStore.onToast(res.data.message, 'e');
|
||||
// axios.post('worddict/insertCategory',{
|
||||
// CMNCODNAM: data
|
||||
// }).then(res => {
|
||||
// if(res.data.data == '1'){
|
||||
// toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
|
||||
// const newCategory = { label: data, value: newValue };
|
||||
// cateList.value = [newCategory, ...cateList.value];
|
||||
// selectedCategory.value = newCategory.value;
|
||||
// } else if(res.data.message == '이미 존재하는 카테고리명입니다.') {
|
||||
// toastStore.onToast(res.data.message, 'e');
|
||||
// }
|
||||
// })
|
||||
}
|
||||
})
|
||||
const addWord = (wordData, data) => {
|
||||
let category = null;
|
||||
// 카테고리 체크
|
||||
const existingCategory = cateList.value.find(item => item.label === data);
|
||||
if (existingCategory) {
|
||||
//카테고리 있을시 그냥 저장
|
||||
category = existingCategory.label == '' ? wordData.category : existingCategory.value;
|
||||
} else {
|
||||
//카테고리 없을시 카테고리 와 용어 둘다 저장
|
||||
console.log('카테고리 없음');
|
||||
const lastCategory = cateList.value[cateList.value.length - 1];
|
||||
category = lastCategory ? lastCategory.value + 1 : 600101;
|
||||
}
|
||||
//용어 등록
|
||||
const addWord = (wordData) => {
|
||||
axios.post('worddict/insertWord',{
|
||||
WRDDICCAT : wordData.category,
|
||||
sendWordRequest(category, wordData, data, !existingCategory);
|
||||
};
|
||||
const sendWordRequest = (category, wordData, data, isNewCategory) => {
|
||||
const payload = {
|
||||
WRDDICCAT: category,
|
||||
WRDDICTTL: wordData.title,
|
||||
WRDDICCON: $common.deltaAsJson(wordData.content),
|
||||
}).then(res => {
|
||||
if(res.data.data == '1'){
|
||||
};
|
||||
|
||||
if (isNewCategory) {
|
||||
payload.CMNCODNAM = data; // 새로운 카테고리 추가 시 포함
|
||||
}
|
||||
axios.post('worddict/insertWord', payload).then(res => {
|
||||
if (res.data.status === 'OK') {
|
||||
toastStore.onToast('용어가 등록 되었습니다.', 's');
|
||||
isWriteVisible.value = false;
|
||||
getwordList();
|
||||
//카테고리 리스트 다시 조회 해야야함
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 체크 상태 업데이트
|
||||
const updateCheckedItems = (checked, id, name) => {
|
||||
if (checked) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user