Merge branch 'main' into login
This commit is contained in:
commit
17fb20ae1e
@ -78,14 +78,14 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
/* 오전 반차 (왼쪽 절반) */
|
/* 오전 반차 활성화 영역 (왼쪽 절반) */
|
||||||
.selected-event.half-day-am {
|
.selected-event.half-day-am {
|
||||||
width: 50% !important;
|
width: 50% !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
/* 오후 반차 (오른쪽 절반) */
|
/* 오후 반차 활성화 영역 (오른쪽 절반) */
|
||||||
.selected-event.half-day-pm {
|
.selected-event.half-day-pm {
|
||||||
width: 50% !important;
|
width: 50% !important;
|
||||||
margin-left: auto !important;
|
margin-left: auto !important;
|
||||||
@ -105,6 +105,30 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #525252;
|
||||||
|
}
|
||||||
|
/* 모달 배경 투명하게 */
|
||||||
|
.vac-modal-dialog {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 본문 스타일 */
|
||||||
|
.vac-modal-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.1); /* 위쪽 그림자만 적용 */
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
/* 리스트 아이템 */
|
/* 리스트 아이템 */
|
||||||
.vacation-item {
|
.vacation-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -51,65 +51,78 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, defineProps, computed, watch, inject } from 'vue';
|
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
|
||||||
import SaveBtn from '../button/SaveBtn.vue';
|
import SaveBtn from '../button/SaveBtn.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
parentId: {
|
parentId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
passwordAlert: {
|
passwordAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
commentAlert: {
|
commentAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const $common = inject('common');
|
|
||||||
const comment = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const isCheck = ref(props.unknown);
|
|
||||||
const textAlert = ref('');
|
|
||||||
const passwordAlert2 = ref('');
|
|
||||||
|
|
||||||
const emit = defineEmits(['submitComment']);
|
const $common = inject('common');
|
||||||
const LOCBRDTYP = isCheck.value ? '300102' : null;
|
const comment = ref('');
|
||||||
function handleCommentSubmit() {
|
const password = ref('');
|
||||||
if (!$common.isNotEmpty(comment.value)) {
|
const isCheck = ref(false);
|
||||||
textAlert.value = '댓글을 입력하세요';
|
const textAlert = ref('');
|
||||||
return false;
|
const passwordAlert2 = ref('');
|
||||||
} else {
|
|
||||||
textAlert.value = '';
|
|
||||||
}
|
|
||||||
if (isCheck.value && !$common.isNotEmpty(password.value)) {
|
|
||||||
passwordAlert2.value = '비밀번호를 입력하세요';
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
passwordAlert2.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('submitComment', {
|
const emit = defineEmits(['submitComment']);
|
||||||
comment: comment.value,
|
|
||||||
password: isCheck.value ? password.value : '',
|
const handleCommentSubmit = () => {
|
||||||
isCheck: isCheck.value,
|
if (!$common.isNotEmpty(comment.value)) {
|
||||||
LOCBRDTYP, // 익명일 경우 '300102' 설정
|
textAlert.value = '댓글을 입력하세요';
|
||||||
});
|
return false;
|
||||||
|
} else {
|
||||||
|
textAlert.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
if (isCheck.value && !$common.isNotEmpty(password.value)) {
|
||||||
() => props.passwordAlert,
|
passwordAlert2.value = '비밀번호를 입력하세요';
|
||||||
() => {
|
return false;
|
||||||
if (!props.passwordAlert) {
|
} else {
|
||||||
comment.value = '';
|
passwordAlert2.value = '';
|
||||||
password.value = '';
|
}
|
||||||
}
|
|
||||||
},
|
// 댓글 제출
|
||||||
);
|
emit('submitComment', {
|
||||||
|
comment: comment.value,
|
||||||
|
password: isCheck.value ? password.value : '',
|
||||||
|
isCheck: isCheck.value,
|
||||||
|
LOCBRDTYP: isCheck.value ? '300102' : null, // 익명일 경우 '300102' 설정
|
||||||
|
});
|
||||||
|
|
||||||
|
// 제출 후 입력 필드 리셋
|
||||||
|
resetCommentForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 필드 리셋 함수 추가
|
||||||
|
const resetCommentForm = () => {
|
||||||
|
comment.value = '';
|
||||||
|
password.value = '';
|
||||||
|
isCheck.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.passwordAlert,
|
||||||
|
() => {
|
||||||
|
if (!props.passwordAlert) {
|
||||||
|
resetCommentForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div v-if="!unknown" class="avatar me-2">
|
<div v-if="!unknown" class="avatar me-2">
|
||||||
<img src="/img/avatars/2.png" alt="Avatar" class="rounded-circle" />
|
<img :src="getProfileImage(profilePath)" alt="Avatar" class="rounded-circle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="me-2">
|
<div class="me-2">
|
||||||
@ -23,8 +23,7 @@
|
|||||||
<!-- 버튼 영역 -->
|
<!-- 버튼 영역 -->
|
||||||
<div class="ms-auto text-end">
|
<div class="ms-auto text-end">
|
||||||
<!-- 수정, 삭제 버튼 -->
|
<!-- 수정, 삭제 버튼 -->
|
||||||
<!-- <template v-if="isAuthor || showDetail"> -->
|
<template v-if="!isDeletedComment && (unknown || isCommentAuthor || isAuthor)">
|
||||||
<template v-if="unknown || isCommentAuthor || isAuthor">
|
|
||||||
<EditButton @click.stop="editClick" />
|
<EditButton @click.stop="editClick" />
|
||||||
<DeleteButton @click.stop="deleteClick" />
|
<DeleteButton @click.stop="deleteClick" />
|
||||||
</template>
|
</template>
|
||||||
@ -35,18 +34,23 @@
|
|||||||
:boardId="boardId"
|
:boardId="boardId"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
@updateReaction="handleUpdateReaction"
|
@updateReaction="handleUpdateReaction"
|
||||||
>
|
/>
|
||||||
</BoardRecommendBtn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits } from 'vue';
|
import { computed, defineProps, defineEmits } from 'vue';
|
||||||
import DeleteButton from '../button/DeleteBtn.vue';
|
import DeleteButton from '../button/DeleteBtn.vue';
|
||||||
import EditButton from '../button/EditBtn.vue';
|
import EditButton from '../button/EditBtn.vue';
|
||||||
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
|
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
|
||||||
|
|
||||||
|
// 기본 프로필 이미지 경로
|
||||||
|
const defaultProfile = "/img/icons/icon.png";
|
||||||
|
|
||||||
|
// 서버의 이미지 경로 (Vue 환경 변수 사용 가능)
|
||||||
|
const baseUrl = "http://localhost:10325/"; // API 서버 URL
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comment: {
|
comment: {
|
||||||
@ -65,6 +69,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
profilePath: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@ -73,13 +81,12 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
// 게시글의 작성자 여부를 확인 : 현재 로그인한 사용자가 이 게시글의 작성자인지 여부
|
|
||||||
isAuthor: {
|
isAuthor: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isCommentAuthor: Boolean, // 댓글 작성자인지 여부
|
isCommentAuthor: Boolean,
|
||||||
isCommentProfile: Boolean, // 현재 컴포넌트가 댓글용인지 여부
|
isCommentProfile: Boolean,
|
||||||
date: {
|
date: {
|
||||||
type: String,
|
type: String,
|
||||||
required: '',
|
required: '',
|
||||||
@ -100,6 +107,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
|
const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
|
||||||
|
|
||||||
|
const isDeletedComment = computed(() => {
|
||||||
|
return props.comment?.content === '삭제된 댓글입니다' &&
|
||||||
|
props.comment?.updateAtRaw !== props.comment?.createdAtRaw;
|
||||||
|
});
|
||||||
|
|
||||||
// 수정
|
// 수정
|
||||||
const editClick = () => {
|
const editClick = () => {
|
||||||
emit('editClick', { ...props.comment, unknown: props.unknown });
|
emit('editClick', { ...props.comment, unknown: props.unknown });
|
||||||
@ -110,42 +122,19 @@ const deleteClick = () => {
|
|||||||
emit('deleteClick', { ...props.comment, unknown: props.unknown });
|
emit('deleteClick', { ...props.comment, unknown: props.unknown });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 좋아요/싫어요 업데이트
|
||||||
const handleUpdateReaction = (reactionData) => {
|
const handleUpdateReaction = (reactionData) => {
|
||||||
emit("updateReaction", {
|
emit("updateReaction", {
|
||||||
boardId: props.boardId,
|
boardId: props.boardId,
|
||||||
commentId: props.comment?.commentId,
|
commentId: props.comment?.commentId,
|
||||||
...reactionData,
|
...reactionData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 프로필 이미지 경로 설정
|
||||||
const getProfileImage = (profilePath) => {
|
const getProfileImage = (profilePath) => {
|
||||||
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
return profilePath && profilePath.trim()
|
||||||
};
|
? `${baseUrl}upload/img/profile/${profilePath}`
|
||||||
|
: defaultProfile;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.profile-detail span ~ span {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-auto button + button {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.author {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
|
||||||
.btn-area {
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.author {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,26 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
|
||||||
<div class="modal-content p-5">
|
<div class="vac-modal-content p-5">
|
||||||
<h5 class="modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
|
<div class="modal-header">
|
||||||
<button class="close-btn" @click="closeModal">✖</button>
|
<h5 class="modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
|
||||||
|
<button class="close-btn" @click="closeModal">✖</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>선물할 연차 개수를 선택하세요.</p>
|
<p>선물할 연차 개수를 선택하세요.</p>
|
||||||
|
|
||||||
<div class="justify-content-center d-sm-flex gap-sm-3 align-items-md-center mt-8">
|
<div class="justify-content-center d-sm-flex gap-sm-3 align-items-md-center mt-8">
|
||||||
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
||||||
<span class="text-dark fw-bold fs-4">{{ grantCount }}</span>
|
<span class="text-dark fw-bold fs-4">{{ grantCount }}</span>
|
||||||
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
|
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-button-container">
|
|
||||||
<button class="custom-button" @click="saveVacationGrant" :disabled="grantCount === 0">
|
<div class="custom-button-container">
|
||||||
<i class="bx bx-gift"></i>
|
<button class="custom-button" @click="saveVacationGrant" :disabled="grantCount === 0">
|
||||||
</button>
|
<i class="bx bx-gift"></i>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
|
import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
|
||||||
@ -121,25 +124,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 모달 배경 투명하게 */
|
|
||||||
.modal-dialog {
|
|
||||||
background: none !important; /* 배경 제거 */
|
|
||||||
box-shadow: none !important; /* 음영 제거 */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 모달 내용 스타일 */
|
</style>
|
||||||
.modal-content {
|
|
||||||
background: #fff; /* 기존 흰색 배경 유지 */
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: none !important; /* 내부 음영 제거 */
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 500px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
|
||||||
<div class="modal-content p-5 modal-scroll">
|
<div class="vac-modal-content p-5 modal-scroll">
|
||||||
<h5 class="modal-title">📅 내 연차 사용 내역</h5>
|
<h5 class="modal-title">📅 내 연차 사용 내역</h5>
|
||||||
<button class="close-btn" @click="closeModal">✖</button>
|
<button class="close-btn" @click="closeModal">✖</button>
|
||||||
|
|
||||||
@ -138,25 +138,4 @@ const closeModal = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
/* 모달 배경 투명하게 */
|
|
||||||
.modal-dialog {
|
|
||||||
background: none !important; /* 배경 제거 */
|
|
||||||
box-shadow: none !important; /* 음영 제거 */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 모달 내용 스타일 */
|
|
||||||
.modal-content {
|
|
||||||
background: #fff; /* 기존 흰색 배경 유지 */
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: none !important; /* 내부 음영 제거 */
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 500px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
||||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||||
<button class="btn p-1" @click="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button>
|
<!-- <button class="btn p-1" @click="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button> -->
|
||||||
<button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button>
|
<!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
|
||||||
|
|
||||||
<i class="bx bx-bell bx-md bx-log-out cursor-pointer p-1" @click="handleLogout"></i>
|
<i class="bx bx-bell bx-md bx-log-out cursor-pointer p-1" @click="handleLogout"></i>
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
📌 {{ notice.title }}
|
📌 {{ notice.title }}
|
||||||
<span v-if="notice.commentCount" class="comment-count">[ {{ notice.commentCount }} ]</span>
|
<span v-if="notice.commentCount" class="comment-count">[ {{ notice.commentCount }} ]</span>
|
||||||
<i v-if="notice.img" class="bi bi-image me-1"></i>
|
<i v-if="notice.img" class="bi bi-image me-1"></i>
|
||||||
<i v-if="notice.hasAttachment" class="bi bi-paperclip"></i>
|
<i v-if="Array.isArray(notice.hasAttachment) && notice.hasAttachment.length > 0" class="bi bi-paperclip"></i>
|
||||||
<span v-if="isNewPost(notice.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
<span v-if="isNewPost(notice.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ notice.author }}</td>
|
<td class="text-center">{{ notice.author }}</td>
|
||||||
@ -79,7 +79,7 @@
|
|||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
|
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
|
||||||
<i v-if="post.img" class="bi bi-image me-1"></i>
|
<i v-if="post.img" class="bi bi-image me-1"></i>
|
||||||
<i v-if="post.hasAttachment" class="bi bi-paperclip"></i>
|
<i v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0" class="bi bi-paperclip"></i>
|
||||||
<span v-if="isNewPost(post.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
<span v-if="isNewPost(post.rawDate)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ post.author }}</td>
|
<td class="text-center">{{ post.author }}</td>
|
||||||
@ -206,7 +206,7 @@ const fetchGeneralPosts = async (page = 1) => {
|
|||||||
rawDate: post.date,
|
rawDate: post.date,
|
||||||
date: formatDate(post.date), // 날짜 변환 적용
|
date: formatDate(post.date), // 날짜 변환 적용
|
||||||
views: post.cnt || 0,
|
views: post.cnt || 0,
|
||||||
hasAttachment: post.hasAttachment || false,
|
hasAttachment: post.hasAttachment,
|
||||||
img: post.firstImageUrl || null,
|
img: post.firstImageUrl || null,
|
||||||
commentCount : post.commentCount
|
commentCount : post.commentCount
|
||||||
}));
|
}));
|
||||||
@ -247,7 +247,7 @@ const fetchNoticePosts = async () => {
|
|||||||
date: formatDate(post.date),
|
date: formatDate(post.date),
|
||||||
rawDate: post.date,
|
rawDate: post.date,
|
||||||
views: post.cnt || 0,
|
views: post.cnt || 0,
|
||||||
hasAttachment: post.hasAttachment || false,
|
hasAttachment: post.hasAttachment,
|
||||||
img: post.firstImageUrl || null,
|
img: post.firstImageUrl || null,
|
||||||
commentCount : post.commentCount
|
commentCount : post.commentCount
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -17,47 +17,35 @@
|
|||||||
:isAuthor="isAuthor"
|
:isAuthor="isAuthor"
|
||||||
@editClick="editClick"
|
@editClick="editClick"
|
||||||
@deleteClick="deleteClick"
|
@deleteClick="deleteClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 비밀번호 입력창 (익명일 경우) -->
|
|
||||||
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
v-model="password"
|
|
||||||
placeholder="비밀번호 입력"
|
|
||||||
@input="password = password.replace(/\s/g, '')"
|
|
||||||
/>
|
|
||||||
<button class="btn btn-primary" @click="submitPassword">확인</button>
|
|
||||||
</div>
|
|
||||||
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 게시글 내용 -->
|
<!-- 게시글 내용 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
|
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
|
||||||
<!-- 제목 섹션 -->
|
<!-- 제목 섹션 -->
|
||||||
<div class="me-1">
|
<div class="me-1">
|
||||||
<h5 class="mb-4">{{ boardTitle }}</h5>
|
<h5 class="mb-4">{{ boardTitle }}</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 첨부파일 섹션 -->
|
<!-- 첨부파일 다운로드 버튼 -->
|
||||||
<div v-if="attachment" class="btn-group">
|
<div v-if="attachments.length" class="btn-group">
|
||||||
<button type="button" class="btn btn-label-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-label-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fa-solid fa-download me-2"></i>
|
<i class="fa-solid fa-download me-2"></i>
|
||||||
첨부파일
|
첨부파일 ({{ attachments.length }}개)
|
||||||
<!-- (<span class="attachment-num">{{ dropdownItems.length }}</span>) -->
|
|
||||||
</button>
|
</button>
|
||||||
<!-- <ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li v-for="(item, index) in dropdownItems" :key="index">
|
<li v-for="(attachment, index) in attachments" :key="index">
|
||||||
<a class="dropdown-item" href="javascript:void(0);">
|
<a
|
||||||
{{ item.label }}
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="downloadFile(attachment)"
|
||||||
|
>
|
||||||
|
{{ attachment.originalName }}.{{ attachment.extension }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul> -->
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -75,16 +63,9 @@
|
|||||||
:likeClicked="likeClicked"
|
:likeClicked="likeClicked"
|
||||||
:dislikeClicked="dislikeClicked"
|
:dislikeClicked="dislikeClicked"
|
||||||
@updateReaction="handleUpdateReaction"
|
@updateReaction="handleUpdateReaction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 첨부파일 목록 -->
|
|
||||||
<!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
|
|
||||||
<li v-for="(attachment, index) in attachments" :key="index" class="mb-2">
|
|
||||||
<a :href="attachment.url" target="_blank" class="text-decoration-none">{{ attachment.name }}</a>
|
|
||||||
</li>
|
|
||||||
</ul> -->
|
|
||||||
|
|
||||||
<!-- 댓글 입력 영역 -->
|
<!-- 댓글 입력 영역 -->
|
||||||
<BoardCommentArea
|
<BoardCommentArea
|
||||||
:profileName="profileName"
|
:profileName="profileName"
|
||||||
@ -174,6 +155,31 @@ const commentsWithAuthStatus = computed(() => {
|
|||||||
return updatedComments;
|
return updatedComments;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const attachments = ref([]);
|
||||||
|
// 첨부파일 다운로드 URL 생성
|
||||||
|
const downloadFile = async (attachment) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`board/download`, {
|
||||||
|
params: { path: attachment.path },
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blob에서 파일 다운로드 링크 생성
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', attachment.originalName + '.' + attachment.extension);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('파일 다운로드 오류:', error);
|
||||||
|
alert('파일 다운로드 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const passwordAlert = ref("");
|
const passwordAlert = ref("");
|
||||||
@ -214,12 +220,9 @@ const fetchBoardDetails = async () => {
|
|||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
|
|
||||||
// API 응답 데이터 반영
|
|
||||||
// const boardDetail = data.boardDetail || {};
|
|
||||||
|
|
||||||
profileName.value = data.author || '익명';
|
profileName.value = data.author || '익명';
|
||||||
|
|
||||||
authorId.value = data.authorId; //게시글 작성자 id
|
authorId.value = data.authorId;
|
||||||
boardTitle.value = data.title || '제목 없음';
|
boardTitle.value = data.title || '제목 없음';
|
||||||
boardContent.value = data.content || '';
|
boardContent.value = data.content || '';
|
||||||
date.value = data.date || '';
|
date.value = data.date || '';
|
||||||
@ -228,6 +231,7 @@ const fetchBoardDetails = async () => {
|
|||||||
dislikes.value = data.dislikeCount || 0;
|
dislikes.value = data.dislikeCount || 0;
|
||||||
attachment.value = data.hasAttachment || null;
|
attachment.value = data.hasAttachment || null;
|
||||||
commentNum.value = data.commentCount || 0;
|
commentNum.value = data.commentCount || 0;
|
||||||
|
attachments.value = data.attachments || [];
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
||||||
@ -288,7 +292,6 @@ const fetchComments = async (page = 1) => {
|
|||||||
page
|
page
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const commentsList = response.data.data.list.map(comment => ({
|
const commentsList = response.data.data.list.map(comment => ({
|
||||||
commentId: comment.LOCCMTSEQ, // 댓글 ID
|
commentId: comment.LOCCMTSEQ, // 댓글 ID
|
||||||
boardId: comment.LOCBRDSEQ,
|
boardId: comment.LOCBRDSEQ,
|
||||||
@ -303,6 +306,8 @@ const fetchComments = async (page = 1) => {
|
|||||||
createdAtRaw: new Date(comment.LOCCMTRDT), // 정렬용
|
createdAtRaw: new Date(comment.LOCCMTRDT), // 정렬용
|
||||||
createdAt: formattedDate(comment.LOCCMTRDT), // 표시용
|
createdAt: formattedDate(comment.LOCCMTRDT), // 표시용
|
||||||
children: [], // 대댓글을 담을 배열
|
children: [], // 대댓글을 담을 배열
|
||||||
|
updateAtRaw: comment.LOCCMTUDT,
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
|
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
|
||||||
|
|||||||
@ -60,14 +60,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 첨부파일 업로드 -->
|
||||||
<FormFile
|
<FormFile
|
||||||
title="첨부파일"
|
title="첨부파일"
|
||||||
name="files"
|
name="files"
|
||||||
:is-alert="attachFilesAlert"
|
:is-alert="attachFilesAlert"
|
||||||
@update:data="attachFiles = $event"
|
@update:data="handleFileUpload"
|
||||||
@update:isValid="isFileValid = $event"
|
@update:isValid="isFileValid = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 실시간 반영된 파일 개수 표시 -->
|
||||||
|
<p class="text-muted mt-1">첨부파일: {{ fileCount }} / 5개</p>
|
||||||
|
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
||||||
|
|
||||||
|
<ul class="list-group mt-2" v-if="attachFiles.length">
|
||||||
|
<li v-for="(file, index) in attachFiles" :key="index" class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
{{ file.name }}
|
||||||
|
<button class="close-btn" @click="removeFile(index)">✖</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<!-- 내용 입력 (에디터) -->
|
<!-- 내용 입력 (에디터) -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="col-md-2 col-form-label">
|
<label class="col-md-2 col-form-label">
|
||||||
@ -92,7 +104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, getCurrentInstance, watch } from 'vue';
|
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
@ -107,8 +119,7 @@ const categoryList = ref([]);
|
|||||||
const title = ref('');
|
const title = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const categoryValue = ref(null);
|
const categoryValue = ref(null);
|
||||||
const content = ref({ ops: [] }); // Delta 데이터 유지
|
const content = ref({ ops: [] });
|
||||||
const attachFiles = ref(null);
|
|
||||||
const isFileValid = ref(true);
|
const isFileValid = ref(true);
|
||||||
|
|
||||||
const titleAlert = ref(false);
|
const titleAlert = ref(false);
|
||||||
@ -117,8 +128,10 @@ const contentAlert = ref(false);
|
|||||||
const categoryAlert = ref(false);
|
const categoryAlert = ref(false);
|
||||||
const attachFilesAlert = ref(false);
|
const attachFilesAlert = ref(false);
|
||||||
|
|
||||||
const { appContext } = getCurrentInstance();
|
const attachFiles = ref([]);
|
||||||
const $common = appContext.config.globalProperties.$common;
|
const maxFiles = 5;
|
||||||
|
const maxSize = 10 * 1024 * 1024;
|
||||||
|
const fileError = ref('');
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
@ -137,12 +150,37 @@ onMounted(() => {
|
|||||||
fetchCategories();
|
fetchCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
/** ✅ 제목 유효성 검사 */
|
const fileCount = computed(() => attachFiles.value.length);
|
||||||
|
|
||||||
|
const handleFileUpload = (files) => {
|
||||||
|
const validFiles = files.filter(file => file.size <= maxSize);
|
||||||
|
if (files.some(file => file.size > maxSize)) {
|
||||||
|
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
||||||
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileError.value = '';
|
||||||
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
attachFiles.value.splice(index, 1);
|
||||||
|
if (attachFiles.value.length <= maxFiles) {
|
||||||
|
fileError.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(attachFiles, () => {
|
||||||
|
isFileValid.value = attachFiles.value.length <= maxFiles;
|
||||||
|
});
|
||||||
|
|
||||||
const validateTitle = () => {
|
const validateTitle = () => {
|
||||||
titleAlert.value = title.value.trim().length === 0;
|
titleAlert.value = title.value.trim().length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ✅ 비밀번호 유효성 검사 (공백 제거) */
|
|
||||||
const validatePassword = () => {
|
const validatePassword = () => {
|
||||||
if (categoryValue.value === 300102) {
|
if (categoryValue.value === 300102) {
|
||||||
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
||||||
@ -152,7 +190,6 @@ const validatePassword = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ✅ 내용 유효성 검사 */
|
|
||||||
const validateContent = () => {
|
const validateContent = () => {
|
||||||
if (!content.value?.ops?.length) {
|
if (!content.value?.ops?.length) {
|
||||||
contentAlert.value = true;
|
contentAlert.value = true;
|
||||||
@ -168,7 +205,7 @@ const validateContent = () => {
|
|||||||
contentAlert.value = !(hasText || hasImage);
|
contentAlert.value = !(hasText || hasImage);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ✅ 글쓰기 */
|
/** 글쓰기 */
|
||||||
const write = async () => {
|
const write = async () => {
|
||||||
validateTitle();
|
validateTitle();
|
||||||
validatePassword();
|
validatePassword();
|
||||||
@ -189,10 +226,10 @@ const write = async () => {
|
|||||||
|
|
||||||
const { data: boardResponse } = await axios.post('board', boardData);
|
const { data: boardResponse } = await axios.post('board', boardData);
|
||||||
const boardId = boardResponse.data;
|
const boardId = boardResponse.data;
|
||||||
|
// 첨부파일 업로드 (비동기 병렬 처리)
|
||||||
// ✅ 첨부파일 업로드 (비동기 병렬 처리)
|
|
||||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||||
await Promise.all(attachFiles.value.map(async (file) => {
|
await Promise.all(attachFiles.value.map(async (file) => {
|
||||||
|
console.log(file);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
@ -200,12 +237,11 @@ const write = async () => {
|
|||||||
formData.append('CMNFLEORG', fileNameWithoutExt);
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
formData.append('CMNFLESIZ', file.size);
|
formData.append('CMNFLESIZ', file.size);
|
||||||
formData.append('CMNFLEPAT', 'boardfile');
|
formData.append('file', file); // 📌 실제 파일 추가
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
await axios.post(`board/${boardId}/attachments`, formData, {
|
await axios.post(`board/${boardId}/attachments`, formData,
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
{ isFormData : true }
|
||||||
});
|
);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,12 +253,12 @@ const write = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ✅ 목록으로 이동 */
|
/** 목록으로 이동 */
|
||||||
const goList = () => {
|
const goList = () => {
|
||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ✅ `content` 변경 감지하여 자동 유효성 검사 실행 */
|
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
validateContent();
|
validateContent();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -148,7 +148,7 @@ watch(
|
|||||||
const calendarDatepicker = ref(null);
|
const calendarDatepicker = ref(null);
|
||||||
let fpInstance = null;
|
let fpInstance = null;
|
||||||
|
|
||||||
/** ✅ 변경사항 여부 확인 */
|
/** 변경사항 여부 확인 */
|
||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
return (
|
return (
|
||||||
selectedDates.value.size > 0 ||
|
selectedDates.value.size > 0 ||
|
||||||
@ -157,7 +157,7 @@ const hasChanges = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/** ✅ selectedDates가 변경될 때 버튼 상태 즉시 업데이트 */
|
/** selectedDates가 변경될 때 버튼 상태 즉시 업데이트 */
|
||||||
watch(
|
watch(
|
||||||
() => Array.from(selectedDates.value.keys()), // keys()를 Array로 변환해서 감시
|
() => Array.from(selectedDates.value.keys()), // keys()를 Array로 변환해서 감시
|
||||||
(newKeys) => {
|
(newKeys) => {
|
||||||
@ -204,7 +204,7 @@ function handleDateClick(info) {
|
|||||||
selectedDates.value.set(clickedDateStr, type);
|
selectedDates.value.set(clickedDateStr, type);
|
||||||
halfDayType.value = null;
|
halfDayType.value = null;
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
// ✅ 날짜 선택 후 버튼 초기화
|
// 날짜 선택 후 버튼 초기화
|
||||||
if (halfDayButtonsRef.value) {
|
if (halfDayButtonsRef.value) {
|
||||||
halfDayButtonsRef.value.resetHalfDay();
|
halfDayButtonsRef.value.resetHalfDay();
|
||||||
}
|
}
|
||||||
@ -417,42 +417,42 @@ function handleDateClick(info) {
|
|||||||
halfDayType.value = halfDayType.value === type ? null : type;
|
halfDayType.value = halfDayType.value === type ? null : type;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchVacationData(year, month) {
|
// 연차 리스트 조회회
|
||||||
try {
|
async function fetchVacationData(year, month) {
|
||||||
const response = await axios.get(`vacation/list/${year}/${month}`);
|
try {
|
||||||
if (response.status === 200) {
|
const response = await axios.get(`vacation/list/${year}/${month}`);
|
||||||
const vacationList = response.data;
|
if (response.status === 200) {
|
||||||
if (lastRemainingYear.value !== year) {
|
const vacationList = response.data;
|
||||||
myVacations.value = vacationList.filter(
|
|
||||||
(vac) => vac.MEMBERSEQ === userStore.user.id
|
// 회원 정보가 없거나 색상 정보가 없는 데이터는 제외
|
||||||
);
|
const filteredVacations = vacationList.filter(vac =>
|
||||||
lastRemainingYear.value = year;
|
userColors.value[vac.MEMBERSEQ] && userColors.value[vac.MEMBERSEQ] !== "#FFFFFF"
|
||||||
}
|
);
|
||||||
const events = vacationList
|
|
||||||
.filter((vac) => !vac.LOCVACRMM)
|
const events = filteredVacations.map(vac => {
|
||||||
.map((vac) => {
|
let dateStr = vac.LOCVACUDT ? vac.LOCVACUDT.split("T")[0] : "";
|
||||||
let dateStr = vac.LOCVACUDT ? vac.LOCVACUDT.split("T")[0] : "";
|
let backgroundColor = userColors.value[vac.MEMBERSEQ];
|
||||||
let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
|
|
||||||
return {
|
return {
|
||||||
title: getVacationType(vac.LOCVACTYP),
|
title: getVacationType(vac.LOCVACTYP),
|
||||||
start: dateStr,
|
start: dateStr,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
classNames: [getVacationTypeClass(vac.LOCVACTYP)],
|
classNames: [getVacationTypeClass(vac.LOCVACTYP)],
|
||||||
saved: true,
|
saved: true,
|
||||||
memberSeq: vac.MEMBERSEQ,
|
memberSeq: vac.MEMBERSEQ,
|
||||||
};
|
};
|
||||||
})
|
}).filter(event => event.start);
|
||||||
.filter((event) => event.start);
|
|
||||||
return events;
|
return events;
|
||||||
} else {
|
} else {
|
||||||
console.warn("📌 휴가 데이터를 불러오지 못함");
|
console.warn("📌 휴가 데이터를 불러오지 못함");
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching vacation data:", error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching vacation data:", error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveVacationChanges() {
|
async function saveVacationChanges() {
|
||||||
if (!hasChanges.value) return;
|
if (!hasChanges.value) return;
|
||||||
@ -522,7 +522,7 @@ function handleDateClick(info) {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
fullCalendarRef.value.getApi().refetchEvents();
|
fullCalendarRef.value.getApi().refetchEvents();
|
||||||
}
|
}
|
||||||
/** ✅ 오늘 이후의 날짜만 클릭 가능하도록 설정 */
|
/** 오늘 이후의 날짜만 클릭 가능하도록 설정 */
|
||||||
function markClickableDates() {
|
function markClickableDates() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const todayStr = new Date().toISOString().split("T")[0]; // 오늘 날짜 YYYY-MM-DD
|
const todayStr = new Date().toISOString().split("T")[0]; // 오늘 날짜 YYYY-MM-DD
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user