Merge branch 'main' into vacation
This commit is contained in:
commit
6d883aacb3
File diff suppressed because it is too large
Load Diff
8
public/vendor/css/core.css
vendored
8
public/vendor/css/core.css
vendored
@ -15304,12 +15304,12 @@ html:not(.layout-menu-fixed) .menu-inner-shadow {
|
||||
}
|
||||
@media (max-width: 1199.98px) {
|
||||
.layout-navbar-fixed .layout-navbar.navbar-detached {
|
||||
width: calc(100% - (1.625rem * 2)) !important;
|
||||
width: calc(100% - (1.625rem * 2))
|
||||
}
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.layout-navbar-fixed .layout-navbar.navbar-detached {
|
||||
width: calc(100% - (1rem * 2)) !important;
|
||||
width: calc(100% - (1rem * 2))
|
||||
}
|
||||
}
|
||||
.layout-navbar-fixed.layout-menu-collapsed .layout-navbar.navbar-detached {
|
||||
@ -15317,12 +15317,12 @@ html:not(.layout-menu-fixed) .menu-inner-shadow {
|
||||
}
|
||||
@media (max-width: 1199.98px) {
|
||||
.layout-navbar.navbar-detached {
|
||||
width: calc(100vw - (100vw - 100%) - (1.625rem * 2)) !important;
|
||||
width: calc(100vw - (100vw - 100%) - (1.625rem * 2))
|
||||
}
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.layout-navbar.navbar-detached {
|
||||
width: calc(100vw - (100vw - 100%) - (1rem * 2)) !important;
|
||||
width: calc(100vw - (100vw - 100%) - (1rem * 2))
|
||||
}
|
||||
}
|
||||
.layout-menu-collapsed .layout-navbar.navbar-detached, .layout-without-menu .layout-navbar.navbar-detached {
|
||||
|
||||
2
public/vendor/css/rtl/core.css
vendored
2
public/vendor/css/rtl/core.css
vendored
File diff suppressed because one or more lines are too long
@ -40,7 +40,31 @@ $api.interceptors.response.use(
|
||||
function (response) {
|
||||
const loadingStore = useLoadingStore();
|
||||
loadingStore.stopLoading();
|
||||
// 2xx 범위의 응답 처리
|
||||
|
||||
// 로그인 실패, 커스텀 에러 응답 처리 (status는 200 success가 false인 경우)
|
||||
if (response.data && response.data.success === false) {
|
||||
const toastStore = useToastStore();
|
||||
const errorCode = response.data.code;
|
||||
const errorMessage = response.data.message || '알 수 없는 오류가 발생했습니다.';
|
||||
|
||||
// 로그인 요청일 경우 (헤더에 isLogin이 true로 설정된 경우)
|
||||
if (response.config.headers && response.config.headers.isLogin) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 서버에서 보낸 메시지 사용
|
||||
toastStore.onToast(errorMessage, 'e');
|
||||
|
||||
// 특정 에러 코드에 대한 추가 처리만 수행
|
||||
if (errorCode === 'USER_NOT_FOUND') {
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
// 오류 응답 반환
|
||||
return response;
|
||||
}
|
||||
|
||||
// 일반 성공 응답 처리
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
@ -48,17 +72,25 @@ $api.interceptors.response.use(
|
||||
loadingStore.stopLoading();
|
||||
const toastStore = useToastStore();
|
||||
|
||||
// 오류 응답 처리
|
||||
// 로그인 요청 별도 처리 (헤더에 isLogin이 true로 설정된 경우)
|
||||
if (error.config && error.config.headers && error.config.headers.isLogin) {
|
||||
// 로그인 페이지 오류 토스트 메시지 표시 X
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 에러 응답에 커스텀 메시지가 포함되어 있다면 해당 메시지 사용
|
||||
// if (error.response && error.response.data && error.response.data.message) {
|
||||
// toastStore.onToast(error.response.data.message, 'e');
|
||||
// } else if (error.response) {
|
||||
if (error.response) {
|
||||
// 기본 HTTP 에러 처리
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
toastStore.onToast('잘못된 요청입니다.', 'e');
|
||||
router.push('/error/400'); // 🚀 400 에러 발생 시 자동 이동
|
||||
router.push('/error/400');
|
||||
break;
|
||||
case 401:
|
||||
if (!error.config.headers.isLogin) {
|
||||
// toastStore.onToast('인증이 필요합니다.', 'e');
|
||||
}
|
||||
toastStore.onToast('인증이 필요합니다.', 'e');
|
||||
router.push('/login');
|
||||
break;
|
||||
case 403:
|
||||
@ -76,10 +108,8 @@ $api.interceptors.response.use(
|
||||
toastStore.onToast('알 수 없는 오류가 발생했습니다.', 'e');
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 요청이 전송되었으나 응답을 받지 못한 경우
|
||||
toastStore.onToast('서버와 통신할 수 없습니다.', 'e');
|
||||
} else {
|
||||
// 요청 설정 중에 오류가 발생한 경우
|
||||
toastStore.onToast('요청 중 오류가 발생했습니다.', 'e');
|
||||
}
|
||||
|
||||
@ -87,4 +117,5 @@ $api.interceptors.response.use(
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
export default $api;
|
||||
|
||||
@ -194,13 +194,3 @@
|
||||
emit('editClick', props.comment);
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.beforeRotate {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
|
||||
// 프로필 이미지 경로 설정
|
||||
const getProfileImage = profileImg => {
|
||||
return $common.getProfileImage(profileImg, true);
|
||||
return $common.getProfileImage(profileImg, props.unknown);
|
||||
};
|
||||
|
||||
const setDefaultImage = e => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template v-if="isRecommend">
|
||||
<button class="btn btn-label-primary btn-icon" :class="{ clicked: likeClicked, big: bigBtn }" @click="handleLike">
|
||||
<i class="fa-regular fa-thumbs-up"></i> <span class="num">{{ likeCount }}</span>
|
||||
<button class="btn btn-label-primary btn-icon me-1" :class="{ clicked: likeClicked, big: bigBtn }" @click="handleLike">
|
||||
<i class="fa-regular fa-thumbs-up"></i> <span class="ms-1">{{ likeCount }}</span>
|
||||
</button>
|
||||
<button class="btn btn-label-danger btn-icon" :class="{ clicked: dislikeClicked, big: bigBtn }" @click="handleDislike">
|
||||
<i class="fa-regular fa-thumbs-down"></i> <span class="num">{{ dislikeCount }}</span>
|
||||
<i class="fa-regular fa-thumbs-down"></i> <span class="ms-1">{{ dislikeCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@ -74,50 +74,3 @@
|
||||
likeClicked.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn + .btn {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.num {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn-label-danger.clicked {
|
||||
background-color: #e6381a;
|
||||
}
|
||||
|
||||
.btn-label-danger.clicked i,
|
||||
.btn-label-danger.clicked span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-label-primary.clicked {
|
||||
background-color: #5f61e6;
|
||||
}
|
||||
|
||||
.btn-label-primary.clicked i,
|
||||
.btn-label-primary.clicked span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 55px;
|
||||
/* height: 30px; */
|
||||
}
|
||||
|
||||
.btn.big {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
.btn {
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -65,5 +65,3 @@ emit("addVacationRequests");
|
||||
defineExpose({ resetHalfDay });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@ -5,17 +5,28 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineExpose } from 'vue';
|
||||
import { ref, defineProps, defineExpose, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isToggleEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonClass = ref("bx bx-edit");
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
newVal => {
|
||||
buttonClass.value = newVal ? 'bx bx-x' : 'bx bx-edit';
|
||||
},
|
||||
);
|
||||
|
||||
const toggleText = () => {
|
||||
if (props.isToggleEnabled) {
|
||||
buttonClass.value = buttonClass.value === "bx bx-edit" ? "bx bx-x" : "bx bx-edit";
|
||||
|
||||
@ -64,13 +64,3 @@ watch(() => props.selectedCategory, (newVal) => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media (max-width: 768px) {
|
||||
.cate-list {
|
||||
overflow-x: scroll;
|
||||
flex-wrap: nowrap !important;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="container-xxl flex-grow-1 container-p-y pb-0">
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col-3 border-end text-center">
|
||||
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
|
||||
<div class="card-body">
|
||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||
<p class="mt-2 fw-bold">
|
||||
@ -105,7 +105,6 @@ const eventDate = ref('');
|
||||
const selectedProject = ref(null);
|
||||
const checkedInProject = ref(null);
|
||||
|
||||
// 모달 상태
|
||||
const isModalOpen = ref(false);
|
||||
|
||||
const commuters = ref([]);
|
||||
@ -375,12 +374,6 @@ watch(() => projectStore.selectedProject, (newProject) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 모달 열기
|
||||
const openModal = () => {
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
|
||||
// 모달 닫기
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
|
||||
@ -217,10 +217,3 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import 'quill/dist/quill.snow.css';
|
||||
.ql-editor {
|
||||
min-height: 300px;
|
||||
font-family: 'Nanum Gothic', sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -19,9 +19,6 @@
|
||||
@focusout="$emit('focusout', modelValue)"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<div v-if="isBtn" class="ms-2">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}을 확인해주세요.</div>
|
||||
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
{{ isCommon ? item.label : item }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="isBtn" class="ms-2">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
|
||||
<div v-if="isColor && selected"
|
||||
class="w-px-40 h-px-30"
|
||||
@ -70,6 +73,11 @@ const props = defineProps({
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
isBtn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
isCommon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@ -7,32 +7,32 @@
|
||||
<h5 class="card-title fw-bold">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<p v-if="isProjectExpired" class="btn-icon btn-danger rounded-2"><i class='bx bx-power-off'></i></p>
|
||||
<p v-if="isProjectExpired" class="btn-icon btn-danger rounded-2 pe-none"><i class='bx bx-power-off'></i></p>
|
||||
<div v-if="!isProjectExpired" class="d-flex gap-1">
|
||||
<EditBtn @click.stop="openEditModal" />
|
||||
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 날짜 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bx-calendar"></i>
|
||||
<div class="ms-2">날짜</div>
|
||||
<div class="ms-12">{{ strdate }} ~ {{ enddate }}</div>
|
||||
</div>
|
||||
<!-- 참여자 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bxs-user"></i>
|
||||
<div class="ms-2">참여자</div>
|
||||
<UserList :projctSeq="projctSeq" :showOnlyActive="isProjectExpired" class="ms-8 mb-0" />
|
||||
</div>
|
||||
<!-- 설명 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bx-detail"></i>
|
||||
<div class="ms-2">설명</div>
|
||||
<div class="ms-12">{{ description }}</div>
|
||||
</div>
|
||||
<!-- 주소 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
|
||||
<i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i>
|
||||
<div class="ms-2">주소</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="toastStore.toastModal"
|
||||
:class="['bs-toast toast m-2 fade show', toastClass]"
|
||||
<div
|
||||
v-if="toastStore.toastModal"
|
||||
:class="['bs-toast toast toast-placement-ex m-2 fade show', toastClass]"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
@ -9,10 +9,10 @@
|
||||
<div class="toast-header">
|
||||
<i class="bx bx-bell me-2"></i>
|
||||
<div class="me-auto fw-semibold">알림</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
aria-label="Close"
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
aria-label="Close"
|
||||
@click="offToast"
|
||||
></button>
|
||||
</div>
|
||||
@ -35,24 +35,3 @@ const toastClass = computed(() => {
|
||||
return toastStore.toastType === 'e' ? 'bg-danger' : 'bg-success'; // 에러일 경우 red, 정상일 경우 blue
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bs-toast {
|
||||
position: fixed;
|
||||
bottom: 20px; /* 화면 하단에 위치 */
|
||||
right: 20px; /* 오른쪽에 위치 */
|
||||
z-index: 2000; /* 충분히 높은 값으로 설정 */
|
||||
max-width: 300px; /* 최대 너비 제한 */
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 그림자 추가 */
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #007bff !important; /* 성공 색상 */
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #ff3e1d !important; /* 에러 색상 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -48,7 +48,6 @@ const fetchSentVacationCount = async () => {
|
||||
availableQuota.value = Math.max(maxQuota - sentCount.value, 0);
|
||||
grantCount.value = availableQuota.value;
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 전송 기록 조회 실패:", error);
|
||||
availableQuota.value = maxQuota;
|
||||
grantCount.value = maxQuota;
|
||||
}
|
||||
|
||||
@ -71,17 +71,17 @@
|
||||
remember: remember.value,
|
||||
}, { headers: { isLogin: true } })
|
||||
.then(async res => {
|
||||
if (res.status === 200) {
|
||||
// 로그인 실패 확인 (success가 false인 경우)
|
||||
if (res.data && res.data.success === false) {
|
||||
// 로그인 실패 시 에러 메시지 표시
|
||||
errorMessage.value = res.data.message || '로그인에 실패했습니다.';
|
||||
return;
|
||||
}
|
||||
|
||||
// 로그인 성공 처리
|
||||
userStore.userInfo();
|
||||
await nextTick();
|
||||
router.push('/')
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error.response) {
|
||||
error.config.isLoginRequest = true;
|
||||
errorMessage.value = error.response.data.message;
|
||||
console.clear();
|
||||
}
|
||||
});
|
||||
router.push('/');
|
||||
})
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ul class="list-unstyled users-list d-flex align-items-center gap-1">
|
||||
<ul class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
|
||||
<li
|
||||
v-for="(user, index) in displayedUserList"
|
||||
:key="index"
|
||||
@ -26,15 +26,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, nextTick, computed } from 'vue';
|
||||
import { onMounted, ref, nextTick, computed, watch } from 'vue';
|
||||
import { useUserStore } from '@s/userList';
|
||||
import { useProjectStore } from '@s/useProjectStore';
|
||||
import $api from '@api';
|
||||
import { useToastStore } from "@s/toastStore";
|
||||
|
||||
const emit = defineEmits(['user-list-update']);
|
||||
const userStore = useUserStore();
|
||||
const userList = ref([]);
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const userProjectPeriods = ref([]);
|
||||
const toastStore = useToastStore();
|
||||
|
||||
const props = defineProps({
|
||||
projctSeq: {
|
||||
@ -44,6 +47,10 @@ const props = defineProps({
|
||||
showOnlyActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
role: {
|
||||
type:String,
|
||||
required:false
|
||||
}
|
||||
});
|
||||
|
||||
@ -82,6 +89,27 @@ const fetchProjectParticipation = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 사용자별 프로젝트 참여 기간 조회
|
||||
const fetchUserProjectPeriods = async () => {
|
||||
if (props.projctSeq) {
|
||||
try {
|
||||
const response = await $api.get(`project/period/${props.projctSeq}`);
|
||||
if (response.status === 200) {
|
||||
userProjectPeriods.value = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('프로젝트 참여 기간 조회 실패:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// projctSeq가 변경될 때마다 참여 기간 데이터 다시 불러오기
|
||||
watch(() => props.projctSeq, async (newVal) => {
|
||||
if (newVal) {
|
||||
await fetchUserProjectPeriods();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 사용자 목록 호출
|
||||
onMounted(async () => {
|
||||
await userStore.fetchUserList();
|
||||
@ -89,16 +117,25 @@ onMounted(async () => {
|
||||
|
||||
if (props.projctSeq) {
|
||||
await fetchProjectParticipation();
|
||||
await fetchUserProjectPeriods();
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach((tooltip) => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
initTooltips();
|
||||
});
|
||||
});
|
||||
|
||||
// 툴팁 초기화 함수
|
||||
const initTooltips = () => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach((tooltip) => {
|
||||
if (tooltip._tooltip) {
|
||||
tooltip._tooltip.dispose();
|
||||
}
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
};
|
||||
|
||||
// 사용자 비활성화 상태 확인
|
||||
const isUserDisabled = (user) => {
|
||||
return props.projctSeq ? user.PROJCTYON === '0' : user.disabled;
|
||||
@ -108,8 +145,16 @@ const isUserDisabled = (user) => {
|
||||
// showOnlyActive가 true일 때는 toggleDisable 함수가 실행되지 않음
|
||||
const toggleDisable = async (index) => {
|
||||
if (props.showOnlyActive) return;
|
||||
|
||||
const user = displayedUserList.value[index];
|
||||
|
||||
// 본인 계정이면 비활성화 방지
|
||||
if (props.role === 'vote') {
|
||||
if(user.MEMBERSEQ === userStore.userInfo.id) {
|
||||
toastStore.onToast('본인은 비활성화할 수 없습니다.', 'e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const newParticipationStatus = props.projctSeq
|
||||
? user.PROJCTYON === '1'
|
||||
@ -150,7 +195,26 @@ const emitUserListUpdate = () => {
|
||||
emit('user-list-update', { activeUsers, disabledUsers });
|
||||
};
|
||||
|
||||
// 날짜 포맷 함수
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 툴팁 제목 생성 함수
|
||||
const getTooltipTitle = (user) => {
|
||||
return user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
|
||||
const userName = user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
|
||||
|
||||
if (props.projctSeq) {
|
||||
const periodInfo = userProjectPeriods.value.find(p => p.MEMBERSEQ === user.MEMBERSEQ);
|
||||
|
||||
if (periodInfo) {
|
||||
return `${formatDate(periodInfo.userStartDate)} ~ ${formatDate(periodInfo.userEndDate)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return userName;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div v-if="data.voteMembers.some(item => item.MEMBERSEQ === userStore.user.id)"
|
||||
class="card mb-6" :class="{ 'disabled-class': data.localVote.LOCVOTDDT && (topVoters.length == 1 || data.localVote.LOCVOTRES || voteResult == 0)}">
|
||||
<div class="card mb-6" :class="{'ps-none opacity-50': data.localVote.LOCVOTDDT && (topVoters.length == 1 || data.localVote.LOCVOTRES || voteResult == 0)}">
|
||||
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" >
|
||||
<h5 class="card-title mb-1">
|
||||
<div class="list-unstyled users-list d-flex align-items-center gap-1">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3 w-px-40"
|
||||
class="rounded-circle user-avatar border border-3 w-px-40 h-px-40"
|
||||
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
@ -29,7 +28,7 @@
|
||||
</button>
|
||||
<DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" />
|
||||
</div>
|
||||
<p v-if="data.localVote.LOCVOTDDT" class="btn-icon btn-danger rounded-2"><i class="bx bx-power-off"></i></p>
|
||||
<p v-if="data.localVote.LOCVOTDDT" class="btn-icon btn-danger rounded-2 pe-none"><i class="bx bx-power-off"></i></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="card-text">
|
||||
<div class="demo-inline-spacing">
|
||||
<!-- 투표리스트 -->
|
||||
<div v-for="(item, index) in data" :key="index">
|
||||
<div v-for="(item, index) in data" :key="index" class="mt-3">
|
||||
<vote-card-check-list
|
||||
:data="item"
|
||||
:multiIs = voteInfo.LOCVOTMUL
|
||||
@ -25,17 +24,19 @@
|
||||
</form-input>
|
||||
<link-input v-model="item.url" class="mb-1"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content">
|
||||
<plus-btn @click="addItem" :disabled="total >= 10" class="m-1" />
|
||||
<button class="btn btn-primary btn-icon mt-1" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
|
||||
<div class="d-flex justify-content align-items-center mt-3">
|
||||
<plus-btn @click="addItem" :disabled="total >= 10" />
|
||||
<button class="btn btn-primary btn-icon m-1" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<save-btn class="mt-2 ms-auto" @click="selectVote"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<save-btn class="mt-2" @click="selectVote"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@ -10,8 +10,7 @@
|
||||
@change="handleChange"
|
||||
>
|
||||
{{ data.LOCVOTCON }}
|
||||
<div></div>
|
||||
<a v-if="data.LOCVOTLIK" :href="data.LOCVOTLIK.startsWith('http') ? data.LOCVOTLIK : 'http://' + data.LOCVOTLIK" class="d-inline-block text-truncate" target="_blank" rel="noopener noreferrer">
|
||||
<a v-if="data.LOCVOTLIK" :href="data.LOCVOTLIK.startsWith('http') ? data.LOCVOTLIK : 'http://' + data.LOCVOTLIK" class="d-block text-truncate" target="_blank" rel="noopener noreferrer">
|
||||
{{ data.LOCVOTLIK }}
|
||||
</a>
|
||||
</label>
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<card
|
||||
@addContents="addContents"
|
||||
@checkedNames="checkedNames"
|
||||
@endVoteId="endVoteId"
|
||||
@voteEnded="voteEnded"
|
||||
@voteDelete="voteDelete"
|
||||
@randomList="randomList"
|
||||
@updateVote="updateVote"
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:data="item"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<card
|
||||
@addContents="addContents"
|
||||
@checkedNames="checkedNames"
|
||||
@endVoteId="endVoteId"
|
||||
@voteEnded="voteEnded"
|
||||
@voteDelete="voteDelete"
|
||||
@randomList="randomList"
|
||||
@updateVote="updateVote"
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:data="item"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-start mt-3">
|
||||
<!--투표한 사람 목록 -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<i class='bx bxs-user-check link-info fa-3x'></i>
|
||||
<vote-complete-user-list
|
||||
v-for="(item, index) in voetedUsers"
|
||||
@ -10,7 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- 투표안한 사람 목록 -->
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<div class="d-flex align-items-center gap-2 ms-auto flex-wrap">
|
||||
<i class='bx bxs-user-x link-danger fa-3x'></i>
|
||||
<vote-in-complete-user-list
|
||||
v-for="(item, index) in noVoetedUsers"
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
:contentValue="item.WRDDICCON"
|
||||
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
<input
|
||||
v-if="userStore.user.role == 'ROLE_ADMIN'"
|
||||
|
||||
@ -1,38 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<FormSelect
|
||||
name="cate"
|
||||
title="카테고리"
|
||||
:data="dataList"
|
||||
:is-common="true"
|
||||
@update:data="selectCategory = $event"
|
||||
@change="onChange"
|
||||
:value="formValue"
|
||||
:is-essential="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2" v-if="!isDisabled">
|
||||
<PlusBtn @click="toggleInput"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="showInput">
|
||||
<div class="col-10">
|
||||
<FormInput
|
||||
ref="categoryInputRef"
|
||||
title="새 카테고리"
|
||||
name="카테고리"
|
||||
@update:modelValue="addCategory = $event"
|
||||
:is-cate-alert="addCategoryAlert"
|
||||
@focusout="handleCategoryFocusout(addCategory)"
|
||||
/>
|
||||
</div>
|
||||
<FormSelect class="me-5"
|
||||
name="cate"
|
||||
title="카테고리"
|
||||
:data="dataList"
|
||||
:is-common="true"
|
||||
@update:data="selectCategory = $event"
|
||||
@change="onChange"
|
||||
:value="formValue"
|
||||
:is-essential="false"
|
||||
/>
|
||||
<div v-if="!isDisabled" class="add-btn">
|
||||
<PlusBtn @click="toggleInput"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dict-w">
|
||||
<FormInput
|
||||
|
||||
<div v-if="showInput">
|
||||
<FormInput class="me-5"
|
||||
ref="categoryInputRef"
|
||||
title="새 카테고리"
|
||||
name="새 카테고리"
|
||||
@update:modelValue="addCategory = $event"
|
||||
:is-cate-alert="addCategoryAlert"
|
||||
@focusout="handleCategoryFocusout(addCategory)"
|
||||
/>
|
||||
</div>
|
||||
<FormInput class="me-5"
|
||||
title="용어"
|
||||
type="text"
|
||||
name="word"
|
||||
@ -43,15 +36,14 @@
|
||||
:disabled="isDisabled"
|
||||
@keyup="ValidHandler('title')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<QEditor @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||
<div class="text-end mt-5">
|
||||
<button class="btn btn-primary" @click="saveWord">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
<div>
|
||||
<QEditor class="" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||
<div class="text-end mt-5">
|
||||
<button class="btn btn-primary" @click="saveWord">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -174,54 +166,40 @@ const saveWord = () => {
|
||||
}
|
||||
const wordData = {
|
||||
id: props.NumValue || null,
|
||||
title: computedTitle.value,
|
||||
title: computedTitle.value.trim(),
|
||||
category: selectedCategory.value,
|
||||
content: content.value,
|
||||
};
|
||||
if(valid){
|
||||
emit('addWord', wordData, addCategory.value === ''
|
||||
emit('addWord', wordData, addCategory.value.trim() === ''
|
||||
? (isNaN(selectedCategory.value) ? selectedCategory.value : Number(selectedCategory.value))
|
||||
: addCategory.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 카테고리 focusout 이벤트 핸들러 추가
|
||||
const handleCategoryFocusout = (value) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
const valueTrim = value.trim();
|
||||
|
||||
const existingCategory = props.dataList.find(item => item.label === valueTrim);
|
||||
|
||||
// 카테고리 입력시 공백
|
||||
if(valueTrim == ''){
|
||||
if (existingCategory) {
|
||||
addCategoryAlert.value = true;
|
||||
|
||||
// 공백시 강제 focus
|
||||
// 중복 시 강제 focus
|
||||
setTimeout(() => {
|
||||
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
||||
}else if (existingCategory) {
|
||||
addCategoryAlert.value = true;
|
||||
|
||||
// 중복시 강제 focus
|
||||
setTimeout(() => {
|
||||
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
} else {
|
||||
addCategoryAlert.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -234,4 +212,9 @@ const handleCategoryFocusout = (value) => {
|
||||
margin-top: 2.5rem
|
||||
}
|
||||
}
|
||||
.add-btn {
|
||||
position: absolute;
|
||||
right: 0.7rem;
|
||||
top: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -51,48 +51,3 @@
|
||||
loadScript('/js/main.js');
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
/* 중앙 콘텐츠 자동 조정 */
|
||||
.layout-page {
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* flexbox 내에서 올바른 크기 계산 */
|
||||
margin-right: 20%; /* 채팅 사이드바의 너비만큼 밀리도록 설정 */
|
||||
}
|
||||
|
||||
/* 탑바 범위조정 */
|
||||
nav#layout-navbar {
|
||||
left: 0 !important;
|
||||
margin-right: 20%;
|
||||
margin-left: 260px;
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
right: 26px !important;
|
||||
left: 26px !important;
|
||||
}
|
||||
|
||||
/* 탑바 범위조정(1200px 이하) */
|
||||
@media (max-width: 1200px) {
|
||||
nav#layout-navbar {
|
||||
left: 0 !important;
|
||||
margin-right: 20%;
|
||||
margin-left: 0px;
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
right: 26px !important;
|
||||
left: 26px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 탑바 범위조정(992px 이하) */
|
||||
@media (max-width: 992px) {
|
||||
nav#layout-navbar {
|
||||
left: 0 !important;
|
||||
margin-right: 20%;
|
||||
margin-left: 0px;
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
right: 26px !important;
|
||||
left: 26px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- Chat Sidebar -->
|
||||
<aside id="chat-sidebar" class="chat-sidebar">
|
||||
<aside id="chat-sidebar" class="chat-sidebar bg-white position-fixed top-0 end-0 z-4 vh-100 menu border shadow">
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@ -25,19 +25,8 @@ const sendMessage = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 채팅 사이드바 고정 */
|
||||
|
||||
.chat-sidebar {
|
||||
width: 20%;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
border-left: 1px solid #ddd;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -96,7 +96,7 @@ import { computed } from "vue";
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
|
||||
const userStore = useUserInfoStore();
|
||||
const allowedUserId = 26; // 특정 ID (변경필요!!)
|
||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||
|
||||
const userId = computed(() => userStore.user?.id ?? null);
|
||||
</script>
|
||||
|
||||
@ -22,10 +22,10 @@
|
||||
<!-- <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> -->
|
||||
|
||||
<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-3" @click="handleLogout"></i>
|
||||
|
||||
<!-- Notification -->
|
||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-1">
|
||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
||||
<a
|
||||
class="nav-link dropdown-toggle hide-arrow p-0"
|
||||
href="javascript:void(0);"
|
||||
@ -161,7 +161,7 @@
|
||||
<!--/ Notification -->
|
||||
<!-- User -->
|
||||
<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-1" href="javascript:void(0);" data-bs-toggle="dropdown">
|
||||
<img
|
||||
v-if="user"
|
||||
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||
@ -311,7 +311,9 @@
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
|
||||
await projectStore.getProjectList('', '', 'true');
|
||||
if (authStore.isAuthenticated) {
|
||||
await projectStore.getProjectList('', '', 'true');
|
||||
}
|
||||
|
||||
// 사용자가 참여하고 있는 프로젝트 목록
|
||||
await projectStore.getMemberProjects();
|
||||
|
||||
@ -8,11 +8,12 @@ const routes = [
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@v/MainView.vue'),
|
||||
// meta: { requiresAuth: true }
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/board',
|
||||
component: () => import('@v/board/TheBoard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@ -38,6 +39,7 @@ const routes = [
|
||||
{
|
||||
path: '/wordDict',
|
||||
component: () => import('@v/wordDict/wordDict.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@ -60,10 +62,12 @@ const routes = [
|
||||
{
|
||||
path: '/vacation',
|
||||
component: () => import('@v/vacation/VacationManagement.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/voteboard',
|
||||
component: () => import('@v/voteboard/TheVoteBoard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@ -78,10 +82,12 @@ const routes = [
|
||||
{
|
||||
path: '/projectlist',
|
||||
component: () => import('@v/projectlist/TheProjectList.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/commuters',
|
||||
component: () => import('@v/commuters/TheCommuters.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/authorization',
|
||||
|
||||
45
src/stores/useBoardAccessStore.js
Normal file
45
src/stores/useBoardAccessStore.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
작성자 : 박성용
|
||||
작성일 : 2025-03-14
|
||||
수정자 :
|
||||
수정일 :
|
||||
설명 : 게시글 수정 시 비밀번호 적재용.
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useBoardAccessStore = defineStore(
|
||||
'access',
|
||||
() => {
|
||||
const password = ref('');
|
||||
|
||||
// watch(password, newValue => {
|
||||
// localStorage.setItem('tempPassword', JSON.stringify(newValue.value));
|
||||
// });
|
||||
|
||||
if (localStorage.getItem('tempPassword')) {
|
||||
// 저장된 값을 불러와 상태에 할당
|
||||
const tempPassword = localStorage.getItem('tempPassword');
|
||||
if (typeof tempPassword === 'string') password.value = JSON.parse(tempPassword);
|
||||
}
|
||||
|
||||
function setBoardPassword(input) {
|
||||
password.value = input;
|
||||
if (typeof input === 'string') JSON.stringify(localStorage.setItem('tempPassword', input));
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
password.value = '';
|
||||
localStorage.removeItem('tempPassword');
|
||||
}
|
||||
|
||||
return {
|
||||
password,
|
||||
setBoardPassword,
|
||||
$reset,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
@ -52,7 +52,6 @@ async function fetchUsers() {
|
||||
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('사용자 목록을 불러오는 중 오류 발생:', error);
|
||||
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
||||
}
|
||||
}
|
||||
@ -74,7 +73,7 @@ async function toggleAdmin(user) {
|
||||
role: user.isAdmin ? 'MEMBER' : 'ADMIN'
|
||||
};
|
||||
try {
|
||||
const response = await axios.put('admin/role', requestData); // API 경로 수정
|
||||
const response = await axios.put('admin/role', requestData);
|
||||
|
||||
if (response.status === 200) {
|
||||
user.isAdmin = !user.isAdmin;
|
||||
@ -83,7 +82,6 @@ async function toggleAdmin(user) {
|
||||
throw new Error('권한 변경 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('권한 변경 중 오류 발생:', error);
|
||||
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||
}
|
||||
}
|
||||
@ -92,71 +90,4 @@ onMounted(fetchUsers);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.user-card {
|
||||
width: 200px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.profile-img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 4px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<div class="mb-4">
|
||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||
내용
|
||||
<span class="text-red">*</span>
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="col-md-12">
|
||||
<QEditor
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="mb-4 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-info right" @click="goList">
|
||||
<button type="button" class="btn btn-info right" @click="goBack">
|
||||
<i class="bx bx-left-arrow-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-1" @click="updateBoard">
|
||||
@ -85,11 +85,13 @@
|
||||
import { ref, onMounted, computed, watch, inject } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useBoardAccessStore } from '@s/useBoardAccessStore';
|
||||
import axios from '@api';
|
||||
|
||||
// 공통
|
||||
const $common = inject('common');
|
||||
const toastStore = useToastStore();
|
||||
const accessStore = useBoardAccessStore();
|
||||
|
||||
// 상태 변수
|
||||
const title = ref('');
|
||||
@ -117,22 +119,30 @@
|
||||
|
||||
// 게시물 데이터 로드
|
||||
const fetchBoardDetails = async () => {
|
||||
try {
|
||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const data = response.data.data;
|
||||
// 수정 데이터 전송
|
||||
let password = accessStore.password;
|
||||
const params = {
|
||||
password: `${password}` || '',
|
||||
};
|
||||
//const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
||||
|
||||
// 기존 첨부파일 추가
|
||||
if (data.hasAttachment && data.attachments.length > 0) {
|
||||
attachFiles.value = addDisplayFileName([...data.attachments]);
|
||||
}
|
||||
|
||||
// 데이터 설정
|
||||
title.value = data.title || '제목 없음';
|
||||
content.value = data.content || '내용 없음';
|
||||
contentLoaded.value = true;
|
||||
} catch (error) {
|
||||
console.error('게시물 가져오기 오류:', error.response || error.message);
|
||||
if (data.code !== 200) {
|
||||
//toastStore.onToast(data.message, 'e');
|
||||
alert(data.message, 'e');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
const boardData = data.data;
|
||||
// 기존 첨부파일 추가
|
||||
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
||||
attachFiles.value = addDisplayFileName([...boardData.attachments]);
|
||||
}
|
||||
|
||||
// 데이터 설정
|
||||
title.value = boardData.title || '제목 없음';
|
||||
content.value = boardData.content || '내용 없음';
|
||||
contentLoaded.value = true;
|
||||
};
|
||||
|
||||
// 기존 첨부파일명을 노출
|
||||
@ -144,9 +154,16 @@
|
||||
|
||||
// 목록 페이지로 이동
|
||||
const goList = () => {
|
||||
accessStore.$reset();
|
||||
router.push('/board');
|
||||
};
|
||||
|
||||
// 전 페이지로 이동
|
||||
const goBack = () => {
|
||||
accessStore.$reset();
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 유효성 확인
|
||||
const checkValidation = () => {
|
||||
contentAlert.value = $common.isNotValidContent(content);
|
||||
@ -218,43 +235,42 @@
|
||||
const updateBoard = async () => {
|
||||
if (checkValidation()) return;
|
||||
|
||||
try {
|
||||
// 수정 데이터 전송
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value.trim(),
|
||||
LOCBRDCON: JSON.stringify(content.value),
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
};
|
||||
// 수정 데이터 전송
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value.trim(),
|
||||
LOCBRDCON: JSON.stringify(content.value),
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
};
|
||||
|
||||
// 업로드 된 첨부파일의 삭제목록
|
||||
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
||||
boardData.delFileIdx = [...delFileIdx.value];
|
||||
}
|
||||
// 업로드 된 첨부파일의 삭제목록
|
||||
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
||||
boardData.delFileIdx = [...delFileIdx.value];
|
||||
}
|
||||
|
||||
const fileArray = newFileFilter(attachFiles);
|
||||
const formData = new FormData();
|
||||
const fileArray = newFileFilter(attachFiles);
|
||||
const formData = new FormData();
|
||||
|
||||
// formData에 boardData 추가
|
||||
Object.entries(boardData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
// formData에 boardData 추가
|
||||
Object.entries(boardData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
// formData에 새로 추가한 파일 추가
|
||||
fileArray.forEach((file, idx) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
// formData에 새로 추가한 파일 추가
|
||||
fileArray.forEach((file, idx) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
||||
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
||||
if (data.code === 200) {
|
||||
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
||||
goList();
|
||||
} catch (error) {
|
||||
console.error('게시물 수정 중 오류 발생:', error);
|
||||
toastStore.onToast('게시물 수정에 실패했습니다.');
|
||||
} else {
|
||||
toastStore.onToast('게시물 수정에 실패했습니다.', 'e');
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if (currentBoardId.value) {
|
||||
fetchBoardDetails();
|
||||
} else {
|
||||
@ -262,10 +278,3 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text-red {
|
||||
color: red;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<div class="container flex-grow-1 container-p-y">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-column">
|
||||
<!-- 검색창 -->
|
||||
<div class="mb-3 w-100">
|
||||
<search-bar @update:data="search" @keyup.enter="searchOnEnter" class="flex-grow-1" />
|
||||
</div>
|
||||
<div class="d-flex align-items-center" style="gap: 15px;">
|
||||
<div class="d-flex align-items-center" style="gap: 15px">
|
||||
<!-- 리스트 갯수 선택 -->
|
||||
<select class="form-select w-auto" v-model="selectedSize" @change="handleSizeChange" style="margin-left: 0;">
|
||||
<select class="form-select w-auto" v-model="selectedSize" @change="handleSizeChange" style="margin-left: 0">
|
||||
<option value="10">10개씩</option>
|
||||
<option value="20">20개씩</option>
|
||||
<option value="30">30개씩</option>
|
||||
<option value="50">50개씩</option>
|
||||
<option value="100">100개씩</option>
|
||||
</select>
|
||||
|
||||
<!-- 셀렉트 박스 -->
|
||||
<select class="form-select w-auto" v-model="selectedOrder" @change="handleSortChange">
|
||||
<option value="date">최신날짜</option>
|
||||
<option value="date">날짜</option>
|
||||
<option value="views">조회수</option>
|
||||
</select>
|
||||
|
||||
@ -38,26 +39,33 @@
|
||||
<table class="datatables-users table border-top dataTable dtr-column">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 11%;" class="text-center fw-bold">번호</th>
|
||||
<th style="width: 45%;" class="text-center fw-bold">제목</th>
|
||||
<th style="width: 10%;" class="text-center fw-bold">작성자</th>
|
||||
<th style="width: 15%;" class="text-center fw-bold">작성일</th>
|
||||
<th style="width: 9%;" class="text-center fw-bold">조회수</th>
|
||||
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
||||
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
||||
<th style="width: 10%" class="text-center fw-bold">작성자</th>
|
||||
<th style="width: 15%" class="text-center fw-bold">작성일</th>
|
||||
<th style="width: 9%" class="text-center fw-bold">조회수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 공지사항 -->
|
||||
<template v-if="pagination.currentPage === 1 && !showNotices">
|
||||
<tr v-for="(notice, index) in noticeList"
|
||||
<tr
|
||||
v-for="(notice, index) in noticeList"
|
||||
:key="'notice-' + index"
|
||||
class="bg-label-gray fw-bold"
|
||||
@click="goDetail(notice.id)">
|
||||
@click="goDetail(notice.id)"
|
||||
>
|
||||
<td class="text-center">공지</td>
|
||||
<td class="cursor-pointer">
|
||||
📌 {{ notice.title }}
|
||||
<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="Array.isArray(notice.hasAttachment) && notice.hasAttachment.length > 0" class="bi bi-paperclip"></i>
|
||||
<span v-if="notice.commentCount" class="text-danger fw-bold me-1"
|
||||
>[ {{ notice.commentCount }} ]</span
|
||||
>
|
||||
<i v-if="notice.img" class="bi bi-image me-1 align-middle"></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>
|
||||
</td>
|
||||
<td class="text-center">{{ notice.author }}</td>
|
||||
@ -66,16 +74,21 @@
|
||||
</tr>
|
||||
</template>
|
||||
<!-- 일반 게시물 -->
|
||||
<tr v-for="(post, index) in generalList"
|
||||
<tr
|
||||
v-for="(post, index) in generalList"
|
||||
:key="'post-' + index"
|
||||
class="invert-bg-white"
|
||||
@click="goDetail(post.realId)">
|
||||
@click="goDetail(post.realId)"
|
||||
>
|
||||
<td class="text-center">{{ post.id }}</td>
|
||||
<td class="cursor-pointer">
|
||||
{{ post.title }}
|
||||
<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="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0" 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>
|
||||
</td>
|
||||
<td class="text-center">{{ post.author }}</td>
|
||||
@ -84,11 +97,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 검색 결과가 없을 때 -->
|
||||
<!-- 게시물이 없을 때 -->
|
||||
<div v-if="generalList.length === 0">
|
||||
<p class="text-center pt-10 mt-2 mb-0 text-muted">
|
||||
검색 결과가 없습니다.
|
||||
</p>
|
||||
<p class="text-center pt-10 mt-2 mb-0 text-muted">게시물이 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -96,11 +107,7 @@
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="row g-3">
|
||||
<div class="mt-8">
|
||||
<Pagination
|
||||
v-if="pagination.pages"
|
||||
v-bind="pagination"
|
||||
@update:currentPage="handlePageChange"
|
||||
/>
|
||||
<Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -109,174 +116,169 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Pagination from '@c/pagination/Pagination.vue';
|
||||
import SearchBar from '@c/search/SearchBar.vue';
|
||||
import router from '@/router';
|
||||
import WriteButton from '@c/button/WriteBtn.vue';
|
||||
import axios from '@api';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Pagination from '@c/pagination/Pagination.vue';
|
||||
import SearchBar from '@c/search/SearchBar.vue';
|
||||
import router from '@/router';
|
||||
import WriteButton from '@c/button/WriteBtn.vue';
|
||||
import axios from '@api';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
// 데이터 초기화
|
||||
const generalList = ref([]);
|
||||
const noticeList = ref([]);
|
||||
const searchText = ref('');
|
||||
const selectedOrder = ref('date');
|
||||
const selectedSize = ref(10);
|
||||
const showNotices = ref(false);
|
||||
|
||||
// 데이터 초기화
|
||||
const generalList = ref([]);
|
||||
const noticeList = ref([]);
|
||||
const searchText = ref('');
|
||||
const selectedOrder = ref('date');
|
||||
const selectedSize = ref(10);
|
||||
const showNotices = ref(false);
|
||||
const pagination = ref({
|
||||
currentPage: 1,
|
||||
pages: 1,
|
||||
prePage: 0,
|
||||
nextPage: 1,
|
||||
isFirstPage: true,
|
||||
isLastPage: false,
|
||||
hasPreviousPage: false,
|
||||
hasNextPage: false,
|
||||
navigatePages: 10,
|
||||
navigatepageNums: [1],
|
||||
navigateFirstPage: 1,
|
||||
navigateLastPage: 1,
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
currentPage: 1,
|
||||
pages: 1,
|
||||
prePage: 0,
|
||||
nextPage: 1,
|
||||
isFirstPage: true,
|
||||
isLastPage: false,
|
||||
hasPreviousPage: false,
|
||||
hasNextPage: false,
|
||||
navigatePages: 10,
|
||||
navigatepageNums: [1],
|
||||
navigateFirstPage: 1,
|
||||
navigateLastPage: 1
|
||||
});
|
||||
// 상세 페이지 이동
|
||||
const goDetail = id => {
|
||||
router.push({ name: 'BoardDetail', params: { id: id } });
|
||||
};
|
||||
|
||||
// 상세 페이지 이동
|
||||
const goDetail = (id) => {
|
||||
router.push({ name: 'BoardDetail', params: { id: id } });
|
||||
};
|
||||
// 날짜 포맷 변환 함수 (오늘이면 HH:mm, 아니면 YYYY-MM-DD)
|
||||
const formatDate = dateString => {
|
||||
const date = dayjs(dateString);
|
||||
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
// 날짜 포맷 변환 함수 (오늘이면 HH:mm, 아니면 YYYY-MM-DD)
|
||||
const formatDate = (dateString) => {
|
||||
const date = dayjs(dateString);
|
||||
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
|
||||
};
|
||||
// 새로 올라온 게시물 여부 판단 (오늘 또는 어제 작성된 경우)
|
||||
const isNewPost = dateString => {
|
||||
const date = dayjs(dateString);
|
||||
return date.isToday() || date.isYesterday();
|
||||
};
|
||||
|
||||
// 새로 올라온 게시물 여부 판단 (오늘 또는 어제 작성된 경우)
|
||||
const isNewPost = (dateString) => {
|
||||
const date = dayjs(dateString);
|
||||
return date.isToday() || date.isYesterday();
|
||||
};
|
||||
// 검색 처리
|
||||
const search = e => {
|
||||
searchText.value = e.trim();
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
|
||||
// 검색 처리
|
||||
const search = (e) => {
|
||||
searchText.value = e.trim();
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
// 정렬 변경 핸들러
|
||||
const handleSortChange = () => {
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
|
||||
// 정렬 변경 핸들러
|
||||
const handleSortChange = () => {
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
// 리스트 개수 변경 핸들러
|
||||
const handleSizeChange = () => {
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
|
||||
// 리스트 개수 변경 핸들러
|
||||
const handleSizeChange = () => {
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
// 일반 게시물 데이터 로드
|
||||
const fetchGeneralPosts = async (page = 1) => {
|
||||
try {
|
||||
const { data } = await axios.get('board/general', {
|
||||
params: {
|
||||
page,
|
||||
size: selectedSize.value,
|
||||
orderBy: selectedOrder.value,
|
||||
searchKeyword: searchText.value,
|
||||
},
|
||||
});
|
||||
|
||||
// 일반 게시물 데이터 로드
|
||||
const fetchGeneralPosts = async (page = 1) => {
|
||||
try {
|
||||
const { data } = await axios.get("board/general", {
|
||||
params: {
|
||||
page,
|
||||
size: selectedSize.value,
|
||||
orderBy: selectedOrder.value,
|
||||
searchKeyword: searchText.value
|
||||
if (data?.data) {
|
||||
const totalPosts = data.data.total;
|
||||
generalList.value = data.data.list.map((post, index) => ({
|
||||
realId: post.id,
|
||||
id: totalPosts - (page - 1) * selectedSize.value - index,
|
||||
title: post.title,
|
||||
author: post.author || '익명',
|
||||
rawDate: post.date,
|
||||
date: formatDate(post.date), // 날짜 변환 적용
|
||||
views: post.cnt || 0,
|
||||
hasAttachment: post.hasAttachment,
|
||||
img: post.firstImageUrl || null,
|
||||
commentCount: post.commentCount,
|
||||
}));
|
||||
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
currentPage: data.data.pageNum,
|
||||
pages: data.data.pages,
|
||||
prePage: data.data.prePage,
|
||||
nextPage: data.data.nextPage,
|
||||
isFirstPage: data.data.isFirstPage,
|
||||
isLastPage: data.data.isLastPage,
|
||||
hasPreviousPage: data.data.hasPreviousPage,
|
||||
hasNextPage: data.data.hasNextPage,
|
||||
navigatePages: data.data.navigatePages,
|
||||
navigatepageNums: data.data.navigatepageNums,
|
||||
navigateFirstPage: data.data.navigateFirstPage,
|
||||
navigateLastPage: data.data.navigateLastPage,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (data?.data) {
|
||||
const totalPosts = data.data.total;
|
||||
generalList.value = data.data.list.map((post, index) => ({
|
||||
realId: post.id,
|
||||
id: totalPosts - ((page - 1) * selectedSize.value) - index,
|
||||
title: post.title,
|
||||
author: post.author || '익명',
|
||||
rawDate: post.date,
|
||||
date: formatDate(post.date), // 날짜 변환 적용
|
||||
views: post.cnt || 0,
|
||||
hasAttachment: post.hasAttachment,
|
||||
img: post.firstImageUrl || null,
|
||||
commentCount : post.commentCount
|
||||
}));
|
||||
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
currentPage: data.data.pageNum,
|
||||
pages: data.data.pages,
|
||||
prePage: data.data.prePage,
|
||||
nextPage: data.data.nextPage,
|
||||
isFirstPage: data.data.isFirstPage,
|
||||
isLastPage: data.data.isLastPage,
|
||||
hasPreviousPage: data.data.hasPreviousPage,
|
||||
hasNextPage: data.data.hasNextPage,
|
||||
navigatePages: data.data.navigatePages,
|
||||
navigatepageNums: data.data.navigatepageNums,
|
||||
navigateFirstPage: data.data.navigateFirstPage,
|
||||
navigateLastPage: data.data.navigateLastPage
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 오류:", error);
|
||||
}
|
||||
};
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
// 공지사항 데이터 로드
|
||||
const fetchNoticePosts = async () => {
|
||||
try {
|
||||
const { data } = await axios.get("board/notices2", {
|
||||
const { data } = await axios.get("board/notices", {
|
||||
params: { searchKeyword: searchText.value }
|
||||
});
|
||||
|
||||
if (data?.data) {
|
||||
noticeList.value = data.data.map(post => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
author: post.author || '관리자',
|
||||
date: formatDate(post.date),
|
||||
rawDate: post.date,
|
||||
views: post.cnt || 0,
|
||||
hasAttachment: post.hasAttachment,
|
||||
img: post.firstImageUrl || null,
|
||||
commentCount : post.commentCount
|
||||
}));
|
||||
if (data?.data) {
|
||||
noticeList.value = data.data.map(post => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
author: post.author || '관리자',
|
||||
date: formatDate(post.date),
|
||||
rawDate: post.date,
|
||||
views: post.cnt || 0,
|
||||
hasAttachment: post.hasAttachment,
|
||||
img: post.firstImageUrl || null,
|
||||
commentCount: post.commentCount,
|
||||
}));
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
// Enter 키를 눌렀을 때
|
||||
const searchOnEnter = event => {
|
||||
const searchTextValue = event.target.value.trim();
|
||||
|
||||
if (!searchTextValue || searchTextValue[0] === ' ') {
|
||||
return; // 검색어가 비어있거나 첫 글자가 공백이면 실행 안 함
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 오류:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Enter 키를 눌렀을 때
|
||||
const searchOnEnter = (event) => {
|
||||
const searchTextValue = event.target.value.trim();
|
||||
searchText.value = searchTextValue;
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
|
||||
if (!searchTextValue || searchTextValue[0] === ' ') {
|
||||
return; // 검색어가 비어있거나 첫 글자가 공백이면 실행 안 함
|
||||
}
|
||||
// 페이지 변경
|
||||
const handlePageChange = page => {
|
||||
if (page !== pagination.value.currentPage) {
|
||||
fetchGeneralPosts(page);
|
||||
}
|
||||
};
|
||||
|
||||
searchText.value = searchTextValue;
|
||||
fetchGeneralPosts(1);
|
||||
};
|
||||
|
||||
// 페이지 변경
|
||||
const handlePageChange = (page) => {
|
||||
if (page !== pagination.value.currentPage) {
|
||||
fetchGeneralPosts(page);
|
||||
}
|
||||
};
|
||||
|
||||
// 데이터 로드
|
||||
onMounted(() => {
|
||||
fetchNoticePosts();
|
||||
fetchGeneralPosts();
|
||||
});
|
||||
// 데이터 로드
|
||||
onMounted(() => {
|
||||
fetchNoticePosts();
|
||||
fetchGeneralPosts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -288,7 +290,6 @@ onMounted(() => {
|
||||
.comment-count {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: #ff5733;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
position: relative;
|
||||
|
||||
@ -71,7 +71,11 @@
|
||||
</div>
|
||||
|
||||
<!-- HTML 콘텐츠 렌더링 -->
|
||||
<div class="board-content text-body" style="line-height: 1.6" v-html="$common.contentToHtml(boardContent)"></div>
|
||||
<div
|
||||
class="board-content text-body mw-100 overflow-hidden text-break"
|
||||
style="line-height: 1.6"
|
||||
v-html="$common.contentToHtml(boardContent)"
|
||||
></div>
|
||||
|
||||
<!-- 좋아요 버튼 -->
|
||||
<div class="row justify-content-center my-10">
|
||||
@ -139,6 +143,7 @@
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useBoardAccessStore } from '@s/useBoardAccessStore';
|
||||
import axios from '@api';
|
||||
|
||||
const $common = inject('common');
|
||||
@ -161,6 +166,8 @@
|
||||
const router = useRouter();
|
||||
const userStore = useUserInfoStore();
|
||||
const toastStore = useToastStore();
|
||||
const accessStore = useBoardAccessStore();
|
||||
|
||||
const currentBoardId = ref(Number(route.params.id));
|
||||
const unknown = computed(() => profileName.value === '익명');
|
||||
const currentUserId = computed(() => userStore?.user?.id); // 현재 로그인한 사용자 id
|
||||
@ -199,7 +206,6 @@
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('파일 다운로드 오류:', error);
|
||||
alert('파일 다운로드 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
@ -242,44 +248,44 @@
|
||||
|
||||
// 게시물 상세 데이터 불러오기
|
||||
const fetchBoardDetails = async () => {
|
||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const data = response.data.data;
|
||||
|
||||
profileName.value = data.author || '익명';
|
||||
authorId.value = data.authorId;
|
||||
boardTitle.value = data.title || '제목 없음';
|
||||
boardContent.value = data.content || '';
|
||||
profileImg.value = data.profileImg || '';
|
||||
date.value = data.date || '';
|
||||
views.value = data.cnt || 0;
|
||||
likes.value = data.likeCount || 0;
|
||||
dislikes.value = data.dislikeCount || 0;
|
||||
attachment.value = data.hasAttachment || null;
|
||||
commentNum.value = data.commentCount || 0;
|
||||
attachments.value = data.attachments || [];
|
||||
const { data } = await axios.get(`board/${currentBoardId.value}`);
|
||||
if (data?.data) {
|
||||
const boardData = data.data;
|
||||
profileName.value = boardData.author || '익명';
|
||||
authorId.value = boardData.authorId;
|
||||
boardTitle.value = boardData.title || '제목 없음';
|
||||
boardContent.value = boardData.content || '';
|
||||
profileImg.value = boardData.profileImg || '';
|
||||
date.value = boardData.date || '';
|
||||
views.value = boardData.cnt || 0;
|
||||
likes.value = boardData.likeCount || 0;
|
||||
dislikes.value = boardData.dislikeCount || 0;
|
||||
attachment.value = boardData.hasAttachment || null;
|
||||
commentNum.value = boardData.commentCount || 0;
|
||||
attachments.value = boardData.attachments || [];
|
||||
} else {
|
||||
toastStore.onToast(data.message, 'e');
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
// 좋아요, 싫어요
|
||||
const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => {
|
||||
try {
|
||||
await axios.post(`/board/${boardId}/${commentId}/reaction`, {
|
||||
LOCBRDSEQ: boardId, // 게시글 id
|
||||
LOCCMTSEQ: commentId, //댓글 id
|
||||
LOCGOBGOD: isLike ? 'T' : 'F',
|
||||
LOCGOBBAD: isDislike ? 'T' : 'F',
|
||||
});
|
||||
await axios.post(`/board/${boardId}/${commentId}/reaction`, {
|
||||
LOCBRDSEQ: boardId, // 게시글 id
|
||||
LOCCMTSEQ: commentId, //댓글 id
|
||||
LOCGOBGOD: isLike ? 'T' : 'F',
|
||||
LOCGOBBAD: isDislike ? 'T' : 'F',
|
||||
});
|
||||
|
||||
const response = await axios.get(`board/${boardId}`);
|
||||
const updatedData = response.data.data;
|
||||
const response = await axios.get(`board/${boardId}`);
|
||||
const updatedData = response.data.data;
|
||||
|
||||
likes.value = updatedData.likeCount;
|
||||
dislikes.value = updatedData.dislikeCount;
|
||||
likes.value = updatedData.likeCount;
|
||||
dislikes.value = updatedData.dislikeCount;
|
||||
|
||||
likeClicked.value = isLike;
|
||||
dislikeClicked.value = isDislike;
|
||||
} catch (error) {
|
||||
alert('오류가 발생했습니다.');
|
||||
}
|
||||
likeClicked.value = isLike;
|
||||
dislikeClicked.value = isDislike;
|
||||
};
|
||||
|
||||
// 대댓글 좋아요
|
||||
@ -293,7 +299,7 @@
|
||||
LOCGOBBAD: isDislike ? 'T' : 'F',
|
||||
});
|
||||
|
||||
await fetchComments();
|
||||
fetchComments(pagination.value.currentPage);
|
||||
};
|
||||
|
||||
// 댓글 목록 조회
|
||||
@ -305,25 +311,27 @@
|
||||
page,
|
||||
},
|
||||
});
|
||||
const commentsList = response.data.data.list.map(comment => ({
|
||||
commentId: comment.LOCCMTSEQ, // 댓글 ID
|
||||
boardId: comment.LOCBRDSEQ,
|
||||
parentId: comment.LOCCMTPNT, // 부모 ID
|
||||
author: comment.author || '익명',
|
||||
authorId: comment.authorId,
|
||||
content: comment.LOCCMTRPY,
|
||||
likeCount: comment.likeCount || 0,
|
||||
dislikeCount: comment.dislikeCount || 0,
|
||||
profileImg: comment.profileImg || '',
|
||||
likeClicked: comment.likeClicked || false,
|
||||
dislikeClicked: comment.dislikeClicked || false,
|
||||
createdAtRaw: new Date(comment.LOCCMTRDT), // 정렬용
|
||||
createdAt: formattedDate(comment.LOCCMTRDT), // 표시용
|
||||
children: [], // 대댓글을 담을 배열
|
||||
updateAtRaw: comment.LOCCMTUDT,
|
||||
}));
|
||||
|
||||
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
|
||||
const commentsList = response.data.data.list
|
||||
.map(comment => ({
|
||||
commentId: comment.LOCCMTSEQ, // 댓글 ID
|
||||
boardId: comment.LOCBRDSEQ,
|
||||
parentId: comment.LOCCMTPNT, // 부모 ID
|
||||
author: comment.author || '익명',
|
||||
authorId: comment.authorId,
|
||||
content: comment.LOCCMTRPY,
|
||||
likeCount: comment.likeCount || 0,
|
||||
dislikeCount: comment.dislikeCount || 0,
|
||||
profileImg: comment.profileImg || '',
|
||||
likeClicked: comment.likeClicked || false,
|
||||
dislikeClicked: comment.dislikeClicked || false,
|
||||
createdAtRaw: comment.LOCCMTRDT, // 작성일
|
||||
// createdAt: formattedDate(comment.LOCCMTRDT), // 작성일(노출용)
|
||||
// createdAtRaw: new Date(comment.LOCCMTUDT), // 수정순
|
||||
createdAt: formattedDate(comment.LOCCMTUDT) + (comment.LOCCMTUDT !== comment.LOCCMTRDT ? ' (수정됨)' : ''), // 수정일(노출용)
|
||||
children: [], // 대댓글을 담을 배열
|
||||
updateAtRaw: comment.LOCCMTUDT,
|
||||
}))
|
||||
.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
|
||||
|
||||
for (const comment of commentsList) {
|
||||
if (!comment.commentId) continue;
|
||||
@ -333,21 +341,25 @@
|
||||
});
|
||||
|
||||
if (replyResponse.data.data) {
|
||||
comment.children = replyResponse.data.data.map(reply => ({
|
||||
author: reply.author || '익명',
|
||||
authorId: reply.authorId,
|
||||
profileImg: reply.profileImg || '',
|
||||
commentId: reply.LOCCMTSEQ,
|
||||
boardId: reply.LOCBRDSEQ,
|
||||
parentId: reply.LOCCMTPNT, // 부모 댓글 ID
|
||||
content: reply.LOCCMTRPY || '내용 없음',
|
||||
createdAtRaw: new Date(reply.LOCCMTRDT),
|
||||
createdAt: formattedDate(reply.LOCCMTRDT),
|
||||
likeCount: reply.likeCount || 0,
|
||||
dislikeCount: reply.dislikeCount || 0,
|
||||
likeClicked: false,
|
||||
dislikeClicked: false,
|
||||
}));
|
||||
comment.children = replyResponse.data.data
|
||||
.map(reply => ({
|
||||
author: reply.author || '익명',
|
||||
authorId: reply.authorId,
|
||||
profileImg: reply.profileImg || '',
|
||||
commentId: reply.LOCCMTSEQ,
|
||||
boardId: reply.LOCBRDSEQ,
|
||||
parentId: reply.LOCCMTPNT, // 부모 댓글 ID
|
||||
content: reply.LOCCMTRPY || '내용 없음',
|
||||
createdAtRaw: reply.LOCCMTRDT,
|
||||
// createdAt: formattedDate(reply.LOCCMTRDT),
|
||||
//createdAtRaw: new Date(reply.LOCCMTUDT),
|
||||
createdAt: formattedDate(reply.LOCCMTUDT) + (reply.LOCCMTUDT !== reply.LOCCMTRDT ? ' (수정됨)' : ''),
|
||||
likeCount: reply.likeCount || 0,
|
||||
dislikeCount: reply.dislikeCount || 0,
|
||||
likeClicked: false,
|
||||
dislikeClicked: false,
|
||||
}))
|
||||
.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
|
||||
} else {
|
||||
comment.children = []; // 대댓글이 없으면 빈 배열로 초기화
|
||||
}
|
||||
@ -396,50 +408,35 @@
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
LOCCMTRPY: comment,
|
||||
LOCCMTPWD: isCheck ? password : '',
|
||||
LOCCMTPNT: 1,
|
||||
LOCBRDTYP: isCheck ? '300102' : null,
|
||||
});
|
||||
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
LOCCMTRPY: comment,
|
||||
LOCCMTPWD: isCheck ? password : '',
|
||||
LOCCMTPNT: 1,
|
||||
LOCBRDTYP: isCheck ? '300102' : null,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
passwordAlert.value = '';
|
||||
commentAlert.value = '';
|
||||
await fetchComments();
|
||||
} else {
|
||||
alert('댓글 작성을 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('오류가 발생했습니다.');
|
||||
if (response.status === 200) {
|
||||
passwordAlert.value = '';
|
||||
commentAlert.value = '';
|
||||
await fetchComments();
|
||||
} else {
|
||||
alert('댓글 작성을 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 대댓글 추가
|
||||
const handleCommentReply = async reply => {
|
||||
try {
|
||||
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
LOCCMTRPY: reply.comment,
|
||||
LOCCMTPWD: reply.password || null,
|
||||
LOCCMTPNT: reply.parentId,
|
||||
LOCBRDTYP: reply.isCheck ? '300102' : null,
|
||||
});
|
||||
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
LOCCMTRPY: reply.comment,
|
||||
LOCCMTPWD: reply.password || null,
|
||||
LOCCMTPNT: reply.parentId,
|
||||
LOCBRDTYP: reply.isCheck ? '300102' : null,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.data.code === 200) {
|
||||
await fetchComments();
|
||||
} else {
|
||||
alert('대댓글 작성을 실패했습니다.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
alert('오류가 발생했습니다.');
|
||||
}
|
||||
alert('오류가 발생했습니다.');
|
||||
if (response.status === 200) {
|
||||
fetchComments(pagination.value.currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
@ -483,7 +480,7 @@
|
||||
return null;
|
||||
};
|
||||
|
||||
// 댓글 수정(대댓글 포함)
|
||||
// 댓글 수정 클릭 시 이벤트(대댓글 포함)
|
||||
const editComment = comment => {
|
||||
password.value = '';
|
||||
passwordCommentAlert.value = '';
|
||||
@ -577,14 +574,17 @@
|
||||
lastCommentClickedButton.value = button;
|
||||
};
|
||||
|
||||
// 게시글 비밀번호 토글
|
||||
const togglePassword = button => {
|
||||
// close: 게시글 비밀번호 란을 초기화 한다.
|
||||
boardPasswordAlert.value = '';
|
||||
if (button === 'close') {
|
||||
isPassword.value = false;
|
||||
boardPasswordAlert.value = '';
|
||||
password.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
closeAllPasswordAreas();
|
||||
if (lastClickedButton.value === button) {
|
||||
isPassword.value = !isPassword.value;
|
||||
boardPasswordAlert.value = '';
|
||||
@ -603,20 +603,23 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`board/${currentBoardId.value}/password`, {
|
||||
const { data } = await axios.post(`board/${currentBoardId.value}/password`, {
|
||||
LOCBRDPWD: password.value,
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
});
|
||||
|
||||
if (response.data.code === 200 && response.data.data === true) {
|
||||
if (data.code === 200 && data.data === true) {
|
||||
accessStore.setBoardPassword(password.value);
|
||||
boardPasswordAlert.value = '';
|
||||
isPassword.value = false;
|
||||
|
||||
if (lastClickedButton.value === 'edit') {
|
||||
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
|
||||
return;
|
||||
} else if (lastClickedButton.value === 'delete') {
|
||||
await deletePost();
|
||||
}
|
||||
accessStore.$reset();
|
||||
lastClickedButton.value = null;
|
||||
} else {
|
||||
boardPasswordAlert.value = '비밀번호가 일치하지 않습니다.';
|
||||
@ -648,9 +651,7 @@
|
||||
// 수정
|
||||
if (lastCommentClickedButton.value === 'edit') {
|
||||
if (targetComment) {
|
||||
// 다른 모든 댓글의 수정 창 닫기
|
||||
closeAllEditTextareas();
|
||||
|
||||
closeAllEditTextareas(); // 다른 모든 댓글의 수정 창 닫기
|
||||
targetComment.isEditTextarea = true;
|
||||
passwordCommentAlert.value = '';
|
||||
currentPasswordCommentId.value = null;
|
||||
@ -668,9 +669,6 @@
|
||||
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다.';
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다';
|
||||
}
|
||||
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다';
|
||||
}
|
||||
};
|
||||
@ -712,7 +710,7 @@
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
await fetchComments();
|
||||
await fetchComments(pagination.value.currentPage);
|
||||
closeAllPasswordAreas();
|
||||
|
||||
if (targetComment) {
|
||||
@ -730,29 +728,29 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 댓글 수정 확인
|
||||
// 댓글 수정
|
||||
const handleSubmitEdit = async (comment, editedContent) => {
|
||||
if (!checkValidation(comment, editedContent)) return;
|
||||
if (!checkValidation(comment, editedContent)) return; //빈값 확인
|
||||
togglePassword();
|
||||
try {
|
||||
const response = await axios.put(`board/comment/${comment.commentId}`, {
|
||||
LOCCMTSEQ: comment.commentId,
|
||||
LOCCMTRPY: editedContent,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const targetComment = findCommentById(comment.commentId, comments.value);
|
||||
if (targetComment) {
|
||||
targetComment.content = editedContent; // 댓글 내용 업데이트
|
||||
targetComment.isEditTextarea = false; // 수정 모드 닫기
|
||||
} else {
|
||||
toastStore.onToast('수정할 댓글을 찾을 수 없습니다.', 'e');
|
||||
}
|
||||
} else {
|
||||
toastStore.onToast('댓글 수정 실패했습니다.', 'e');
|
||||
}
|
||||
} catch (error) {
|
||||
toastStore.onToast('댓글 수정 중 오류가 발생하였습니다.', 'e');
|
||||
const response = await axios.put(`board/comment/${comment.commentId}`, {
|
||||
LOCCMTSEQ: comment.commentId,
|
||||
LOCCMTRPY: editedContent.trim(),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
togglePassword('close');
|
||||
fetchComments(pagination.value.currentPage);
|
||||
return;
|
||||
// const targetComment = findCommentById(comment.commentId, comments.value);
|
||||
|
||||
// if (targetComment) {
|
||||
// targetComment.content = editedContent.trim(); // 댓글 내용 업데이트
|
||||
// targetComment.isEditTextarea = false; // 수정 모드 닫기
|
||||
// togglePassword('close');
|
||||
// }
|
||||
} else {
|
||||
toastStore.onToast('댓글 수정을 실패하였습니다', 'e');
|
||||
}
|
||||
};
|
||||
|
||||
@ -819,24 +817,3 @@
|
||||
fetchComments();
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.board-content {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.board-content img {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.board-content table {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -129,15 +129,11 @@
|
||||
const fileError = ref('');
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await axios.get('board/categories');
|
||||
categoryList.value = response.data.data;
|
||||
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
||||
if (freeCategory) {
|
||||
categoryValue.value = freeCategory.CMNCODVAL;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 불러오기 오류:', error);
|
||||
const response = await axios.get('board/categories');
|
||||
categoryList.value = response.data.data;
|
||||
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
||||
if (freeCategory) {
|
||||
categoryValue.value = freeCategory.CMNCODVAL;
|
||||
}
|
||||
};
|
||||
|
||||
@ -243,7 +239,6 @@
|
||||
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
||||
goList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="error-container">
|
||||
<div class="error-page">
|
||||
<div class="error-content">
|
||||
<img src="/img/illustrations/page-misc-error-dark.png" alt="Error Illustration" class="error-image" />
|
||||
<h1>400</h1>
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
<!-- Main Content: 캘린더 영역 -->
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body pb-0" style="position: relative;">
|
||||
<div class="card-body" style="position: relative;">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:options="calendarOptions"
|
||||
@ -152,8 +152,10 @@ function handleMonthChange(viewInfo) {
|
||||
loadCalendarData(year, month);
|
||||
}
|
||||
// 캘린더 클릭
|
||||
// 캘린더 클릭
|
||||
function handleDateClick(info) {
|
||||
if (!info.date || !info.dateStr) {
|
||||
return;
|
||||
}
|
||||
const clickedDateStr = info.dateStr;
|
||||
const clickedDate = info.date;
|
||||
const todayStr = new Date().toISOString().split("T")[0];
|
||||
@ -191,13 +193,15 @@ function handleDateClick(info) {
|
||||
updateCalendarEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
const type = halfDayType.value
|
||||
? (halfDayType.value === "AM" ? "700101" : "700102")
|
||||
: "700103";
|
||||
|
||||
selectedDates.value.set(clickedDateStr, type);
|
||||
halfDayType.value = null;
|
||||
|
||||
if (halfDayType.value) {
|
||||
halfDayType.value = null;
|
||||
}
|
||||
updateCalendarEvents();
|
||||
|
||||
if (halfDayButtonsRef.value) {
|
||||
@ -223,7 +227,7 @@ function markClickableDates() {
|
||||
dateObj.getDay() === 0 || // 일요일
|
||||
dateObj.getDay() === 6 || // 토요일
|
||||
holidayDates.value.has(dateStr) || // 공휴일
|
||||
dateObj.getTime() === oneWeekAgoObj.getTime() // 오늘 -7일 날짜
|
||||
dateObj.getTime() <= oneWeekAgoObj.getTime() // 오늘 -7일 날짜
|
||||
) {
|
||||
cell.classList.remove("clickable");
|
||||
cell.classList.add("fc-day-sat-sun");
|
||||
@ -278,7 +282,6 @@ const handleProfileClick = async (user) => {
|
||||
isGrantModalOpen.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
||||
}
|
||||
};
|
||||
// 프로필 사원 리스트
|
||||
@ -300,7 +303,6 @@ const fetchUserList = async () => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("📌 사용자 목록 불러오기 오류:", error);
|
||||
}
|
||||
};
|
||||
// 사원별 남은 연차 개수
|
||||
@ -314,7 +316,6 @@ const fetchRemainingVacation = async () => {
|
||||
}, {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 남은 연차 데이터를 불러오지 못했습니다:", error);
|
||||
}
|
||||
};
|
||||
// 로그인한 사원이 사용한 휴가 필터링
|
||||
@ -401,7 +402,6 @@ async function saveVacationChanges() {
|
||||
const currentDate = fullCalendarRef.value.getApi().getDate();
|
||||
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
|
||||
} catch (error) {
|
||||
console.error("🚨 휴가 변경 저장 실패:", error);
|
||||
toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
|
||||
}
|
||||
}
|
||||
@ -415,7 +415,6 @@ async function fetchVacationHistory(year) {
|
||||
receivedVacations.value = response.data.data.receivedVacations || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`🚨 휴가 데이터 불러오기 실패:`, error);
|
||||
}
|
||||
}
|
||||
// 모든 사원 연차 내역 및 그래프화
|
||||
@ -446,7 +445,6 @@ async function fetchVacationData(year, month) {
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching vacation data:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -521,7 +519,6 @@ const fetchVacationCodes = async () => {
|
||||
console.warn("❌ 공통 코드 데이터를 불러오지 못했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 공통 코드 API 호출 실패:", error);
|
||||
}
|
||||
};
|
||||
const getVacationType = (typeCode) => {
|
||||
@ -563,11 +560,11 @@ watch(() => lastRemainingYear.value, async (newYear, oldYear) => {
|
||||
await fetchVacationHistory(newYear);
|
||||
}
|
||||
});
|
||||
// `selectedDates` 변경 시 반차 버튼 초기화
|
||||
// 새로운 휴가추가 시 반차 버튼 초기화
|
||||
watch(
|
||||
() => Array.from(selectedDates.value.keys()), // 선택된 날짜 리스트 감시
|
||||
(newKeys) => {
|
||||
if (halfDayButtonsRef.value) {
|
||||
if (halfDayButtonsRef.value && !halfDayType.value) {
|
||||
halfDayButtonsRef.value.resetHalfDay();
|
||||
}
|
||||
},
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 투표리스트 -->
|
||||
<div v-if="voteListCardData.length == 0 " >투표가 없습니다.</div>
|
||||
<div v-if="voteListCardData.length == 0 " class="text-center">투표가 없습니다.</div>
|
||||
<vote-list
|
||||
:data="voteListCardData"
|
||||
@addContents="addContents"
|
||||
@ -81,6 +81,7 @@ const voteWrite = () => {
|
||||
};
|
||||
|
||||
const changeCheck = () =>{
|
||||
currentPage.value = 1;
|
||||
getvoteList();
|
||||
}
|
||||
//투표목록
|
||||
@ -100,6 +101,7 @@ const getvoteList = () => {
|
||||
};
|
||||
|
||||
const selectHandler = () =>{
|
||||
currentPage.value = 1;
|
||||
voteset.value = category.value;
|
||||
getvoteList();
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
<div class="timeline-header mb-2">
|
||||
<h6 class="mb-0">투표 인원</h6>
|
||||
</div>
|
||||
<UserList @userListInfo="userSet" @user-list-update="handleUserListUpdate" class="mb-3" />
|
||||
<div v-if="UserListAlert" class="red">2명이상 선택해주세요 </div>
|
||||
<UserList :role="'vote'" @userListInfo="userSet" @user-list-update="handleUserListUpdate" class="mb-3" />
|
||||
<div v-if="UserListAlert" class="invalid-feedback d-block">2명이상 선택해주세요.</div>
|
||||
<form-input
|
||||
title="제목"
|
||||
name="title"
|
||||
@ -50,26 +50,28 @@
|
||||
</div>
|
||||
|
||||
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
|
||||
<div>
|
||||
<label class="list-group-item">
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvoteitem"
|
||||
v-model="addvoteitem"
|
||||
/>
|
||||
항목 추가여부
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvotemulti"
|
||||
v-model="addvotemulti"
|
||||
/>
|
||||
다중투표 허용여부
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvoteitem"
|
||||
v-model="addvoteitem"
|
||||
/>
|
||||
항목 추가여부
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label >
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvotemulti"
|
||||
v-model="addvotemulti"
|
||||
/>
|
||||
다중투표 허용여부
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -127,6 +129,11 @@ const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers
|
||||
activeUserList.value = activeUsers;
|
||||
disabledUsers.value = updatedDisabledUsers;
|
||||
userListTotal.value = activeUsers.length;
|
||||
if(activeUserList.value.length<2){
|
||||
UserListAlert.value = true;
|
||||
}else{
|
||||
UserListAlert.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveValid = () => {
|
||||
@ -146,18 +153,17 @@ const saveValid = () => {
|
||||
} else {
|
||||
endDateAlert.value = false;
|
||||
}
|
||||
if (itemList.value[0].content.trim() === '') {
|
||||
contentAlerts.value[0] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[0] = false;
|
||||
}
|
||||
if (itemList.value[1].content.trim() === '') {
|
||||
contentAlerts.value[1] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[1] = false;
|
||||
}
|
||||
itemList.value.forEach((item, index) => {
|
||||
if (index < 2 && item.content.trim() === '') {
|
||||
contentAlerts.value[index] = true;
|
||||
valid = false;
|
||||
} else if (index >= 2 && item.content.trim() === '' && item.url.trim() !== '') {
|
||||
contentAlerts.value[index] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[index] = false;
|
||||
}
|
||||
});
|
||||
if (activeUserList.value.length < 2) {
|
||||
UserListAlert.value = true;
|
||||
valid = false;
|
||||
@ -169,6 +175,7 @@ const saveValid = () => {
|
||||
}
|
||||
};
|
||||
const saveVote = () => {
|
||||
console.log('itemList',itemList)
|
||||
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
|
||||
const unwrappedUserList = toRaw(activeUserList.value);
|
||||
const listId = unwrappedUserList.map(item => ({
|
||||
@ -189,16 +196,16 @@ const saveVote = () => {
|
||||
})
|
||||
};
|
||||
const ValidHandler = (field) => {
|
||||
if(field == 'title'){
|
||||
if (field === 'title') {
|
||||
titleAlert.value = false;
|
||||
}
|
||||
if(field == 'content1'){
|
||||
contentAlerts.value[0] = false;
|
||||
if (field.startsWith('content')) {
|
||||
const index = parseInt(field.replace('content', '')) - 1;
|
||||
if (!isNaN(index)) {
|
||||
contentAlerts.value[index] = false;
|
||||
}
|
||||
}
|
||||
if(field == 'content2'){
|
||||
contentAlerts.value[1] = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
const ValidHandlerendDate = () =>{
|
||||
endDateAlert.value = false;
|
||||
}
|
||||
|
||||
@ -6,9 +6,10 @@
|
||||
<SearchBar @update:data="search"/>
|
||||
<div class="d-flex">
|
||||
<!-- 단어 갯수, 작성하기 -->
|
||||
<!-- 왼쪽 사이드바 -->
|
||||
<div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
|
||||
<!-- 왼쪽 사이드바 -->
|
||||
<div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
||||
:isActive="writeStore.activeItemId === 999999"/>
|
||||
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
||||
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" :indexCategory="indexCategory" :selectedAl="selectedAlphabet" />
|
||||
<!-- 카테고리 -->
|
||||
@ -19,16 +20,12 @@
|
||||
|
||||
<!-- 용어 리스트 컨텐츠 -->
|
||||
<div class="flex-grow-1">
|
||||
|
||||
<!-- 작성 -->
|
||||
<div v-if="writeStore.isItemActive(999999)" class="ms-3 card p-5">
|
||||
<div v-if="writeStore.isItemActive(999999)" class="ms-3 card p-5 mb-2">
|
||||
<DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
|
||||
</div>
|
||||
|
||||
<!-- 용어 리스트 -->
|
||||
<div>
|
||||
<!-- 로딩 중일 때 -->
|
||||
<LoadingSpinner v-if="loading"/>
|
||||
<!-- 에러 메시지 -->
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<!-- 단어 목록 -->
|
||||
@ -79,7 +76,6 @@
|
||||
const toastStore = useToastStore();
|
||||
|
||||
// 공통
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
// 용어집
|
||||
@ -135,12 +131,10 @@
|
||||
wordList.value = res.data.data.data;
|
||||
// 총 개수 저장
|
||||
total.value = res.data.data.total;
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('데이터 로드 오류:', err);
|
||||
error.value = '데이터를 가져오는 중 문제가 발생했습니다.';
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
//정렬 목록
|
||||
@ -194,7 +188,7 @@
|
||||
}else{
|
||||
const lastCategory = cateList.value[cateList.value.length - 1];
|
||||
category = lastCategory ? lastCategory.value + 1 : 600101;
|
||||
newCodName = data;
|
||||
newCodName = data.trim();
|
||||
}
|
||||
sendWordRequest(category, wordData, newCodName);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user