Merge branch 'main' into vacation

This commit is contained in:
dyhj625 2025-03-18 15:52:46 +09:00
commit 6d883aacb3
42 changed files with 1239 additions and 1155 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15304,12 +15304,12 @@ html:not(.layout-menu-fixed) .menu-inner-shadow {
} }
@media (max-width: 1199.98px) { @media (max-width: 1199.98px) {
.layout-navbar-fixed .layout-navbar.navbar-detached { .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) { @media (max-width: 991.98px) {
.layout-navbar-fixed .layout-navbar.navbar-detached { .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 { .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) { @media (max-width: 1199.98px) {
.layout-navbar.navbar-detached { .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) { @media (max-width: 991.98px) {
.layout-navbar.navbar-detached { .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 { .layout-menu-collapsed .layout-navbar.navbar-detached, .layout-without-menu .layout-navbar.navbar-detached {

File diff suppressed because one or more lines are too long

View File

@ -40,7 +40,31 @@ $api.interceptors.response.use(
function (response) { function (response) {
const loadingStore = useLoadingStore(); const loadingStore = useLoadingStore();
loadingStore.stopLoading(); 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; return response;
}, },
function (error) { function (error) {
@ -48,17 +72,25 @@ $api.interceptors.response.use(
loadingStore.stopLoading(); loadingStore.stopLoading();
const toastStore = useToastStore(); 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) { if (error.response) {
// 기본 HTTP 에러 처리
switch (error.response.status) { switch (error.response.status) {
case 400: case 400:
toastStore.onToast('잘못된 요청입니다.', 'e'); toastStore.onToast('잘못된 요청입니다.', 'e');
router.push('/error/400'); // 🚀 400 에러 발생 시 자동 이동 router.push('/error/400');
break; break;
case 401: case 401:
if (!error.config.headers.isLogin) { toastStore.onToast('인증이 필요합니다.', 'e');
// toastStore.onToast('인증이 필요합니다.', 'e');
}
router.push('/login'); router.push('/login');
break; break;
case 403: case 403:
@ -76,10 +108,8 @@ $api.interceptors.response.use(
toastStore.onToast('알 수 없는 오류가 발생했습니다.', 'e'); toastStore.onToast('알 수 없는 오류가 발생했습니다.', 'e');
} }
} else if (error.request) { } else if (error.request) {
// 요청이 전송되었으나 응답을 받지 못한 경우
toastStore.onToast('서버와 통신할 수 없습니다.', 'e'); toastStore.onToast('서버와 통신할 수 없습니다.', 'e');
} else { } else {
// 요청 설정 중에 오류가 발생한 경우
toastStore.onToast('요청 중 오류가 발생했습니다.', 'e'); toastStore.onToast('요청 중 오류가 발생했습니다.', 'e');
} }
@ -87,4 +117,5 @@ $api.interceptors.response.use(
}, },
); );
export default $api; export default $api;

View File

@ -194,13 +194,3 @@
emit('editClick', props.comment); emit('editClick', props.comment);
}; };
</script> </script>
<style>
.beforeRotate {
transition: transform 0.3s ease-in-out;
}
.rotate {
transform: rotate(45deg);
transition: transform 0.3s ease-in-out;
}
</style>

View File

@ -126,7 +126,7 @@
// //
const getProfileImage = profileImg => { const getProfileImage = profileImg => {
return $common.getProfileImage(profileImg, true); return $common.getProfileImage(profileImg, props.unknown);
}; };
const setDefaultImage = e => { const setDefaultImage = e => {

View File

@ -1,9 +1,9 @@
<template v-if="isRecommend"> <template v-if="isRecommend">
<button class="btn btn-label-primary btn-icon" :class="{ clicked: likeClicked, big: bigBtn }" @click="handleLike"> <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="num">{{ likeCount }}</span> <i class="fa-regular fa-thumbs-up"></i> <span class="ms-1">{{ likeCount }}</span>
</button> </button>
<button class="btn btn-label-danger btn-icon" :class="{ clicked: dislikeClicked, big: bigBtn }" @click="handleDislike"> <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> </button>
</template> </template>
@ -74,50 +74,3 @@
likeClicked.value = false; likeClicked.value = false;
}; };
</script> </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>

View File

@ -65,5 +65,3 @@ emit("addVacationRequests");
defineExpose({ resetHalfDay }); defineExpose({ resetHalfDay });
</script> </script>
<style scoped>
</style>

View File

@ -5,17 +5,28 @@
</template> </template>
<script setup> <script setup>
import { ref, defineProps, defineExpose } from 'vue'; import { ref, defineProps, defineExpose, watch } from 'vue';
const props = defineProps({ const props = defineProps({
isToggleEnabled: { isToggleEnabled: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isActive: {
type: Boolean,
required: false,
},
}); });
const buttonClass = ref("bx bx-edit"); const buttonClass = ref("bx bx-edit");
watch(
() => props.isActive,
newVal => {
buttonClass.value = newVal ? 'bx bx-x' : 'bx bx-edit';
},
);
const toggleText = () => { const toggleText = () => {
if (props.isToggleEnabled) { if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === "bx bx-edit" ? "bx bx-x" : "bx bx-edit"; buttonClass.value = buttonClass.value === "bx bx-edit" ? "bx bx-x" : "bx bx-edit";

View File

@ -64,13 +64,3 @@ watch(() => props.selectedCategory, (newVal) => {
}); });
</script> </script>
<style scoped>
@media (max-width: 768px) {
.cate-list {
overflow-x: scroll;
flex-wrap: nowrap !important;
}
}
</style>

View File

@ -2,7 +2,7 @@
<div class="container-xxl flex-grow-1 container-p-y pb-0"> <div class="container-xxl flex-grow-1 container-p-y pb-0">
<div class="card app-calendar-wrapper"> <div class="card app-calendar-wrapper">
<div class="row g-0"> <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"> <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'"/> <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"> <p class="mt-2 fw-bold">
@ -105,7 +105,6 @@ const eventDate = ref('');
const selectedProject = ref(null); const selectedProject = ref(null);
const checkedInProject = ref(null); const checkedInProject = ref(null);
//
const isModalOpen = ref(false); const isModalOpen = ref(false);
const commuters = ref([]); const commuters = ref([]);
@ -375,12 +374,6 @@ watch(() => projectStore.selectedProject, (newProject) => {
} }
}); });
//
const openModal = () => {
isModalOpen.value = true;
};
// //
const closeModal = () => { const closeModal = () => {
isModalOpen.value = false; isModalOpen.value = false;

View File

@ -217,10 +217,3 @@
}); });
</script> </script>
<style>
@import 'quill/dist/quill.snow.css';
.ql-editor {
min-height: 300px;
font-family: 'Nanum Gothic', sans-serif;
}
</style>

View File

@ -19,9 +19,6 @@
@focusout="$emit('focusout', modelValue)" @focusout="$emit('focusout', modelValue)"
@input="handleInput" @input="handleInput"
/> />
<div v-if="isBtn" class="ms-2">
<slot name="append"></slot>
</div>
</div> </div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div> <div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>

View File

@ -10,6 +10,9 @@
{{ isCommon ? item.label : item }} {{ isCommon ? item.label : item }}
</option> </option>
</select> </select>
<div v-if="isBtn" class="ms-2">
<slot name="append"></slot>
</div>
<div v-if="isColor && selected" <div v-if="isColor && selected"
class="w-px-40 h-px-30" class="w-px-40 h-px-30"
@ -70,6 +73,11 @@ const props = defineProps({
default: true, default: true,
required: false, required: false,
}, },
isBtn: {
type: Boolean,
default: false,
required: false,
},
isCommon: { isCommon: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -7,32 +7,32 @@
<h5 class="card-title fw-bold"> <h5 class="card-title fw-bold">
{{ title }} {{ title }}
</h5> </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"> <div v-if="!isProjectExpired" class="d-flex gap-1">
<EditBtn @click.stop="openEditModal" /> <EditBtn @click.stop="openEditModal" />
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/> <DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
</div> </div>
</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> <i class="bx bx-calendar"></i>
<div class="ms-2">날짜</div> <div class="ms-2">날짜</div>
<div class="ms-12">{{ strdate }} ~ {{ enddate }}</div> <div class="ms-12">{{ strdate }} ~ {{ enddate }}</div>
</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> <i class="bx bxs-user"></i>
<div class="ms-2">참여자</div> <div class="ms-2">참여자</div>
<UserList :projctSeq="projctSeq" :showOnlyActive="isProjectExpired" class="ms-8 mb-0" /> <UserList :projctSeq="projctSeq" :showOnlyActive="isProjectExpired" class="ms-8 mb-0" />
</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-detail"></i> <i class="bx bx-detail"></i>
<div class="ms-2">설명</div> <div class="ms-2">설명</div>
<div class="ms-12">{{ description }}</div> <div class="ms-12">{{ description }}</div>
</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"> <div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
<i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i> <i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i>
<div class="ms-2">주소</div> <div class="ms-2">주소</div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="toastStore.toastModal" v-if="toastStore.toastModal"
:class="['bs-toast toast m-2 fade show', toastClass]" :class="['bs-toast toast toast-placement-ex m-2 fade show', toastClass]"
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
aria-atomic="true" aria-atomic="true"
@ -35,24 +35,3 @@ const toastClass = computed(() => {
return toastStore.toastType === 'e' ? 'bg-danger' : 'bg-success'; // red, blue return toastStore.toastType === 'e' ? 'bg-danger' : 'bg-success'; // red, blue
}); });
</script> </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>

View File

@ -48,7 +48,6 @@ const fetchSentVacationCount = async () => {
availableQuota.value = Math.max(maxQuota - sentCount.value, 0); availableQuota.value = Math.max(maxQuota - sentCount.value, 0);
grantCount.value = availableQuota.value; grantCount.value = availableQuota.value;
} catch (error) { } catch (error) {
console.error("🚨 연차 전송 기록 조회 실패:", error);
availableQuota.value = maxQuota; availableQuota.value = maxQuota;
grantCount.value = maxQuota; grantCount.value = maxQuota;
} }

View File

@ -71,17 +71,17 @@
remember: remember.value, remember: remember.value,
}, { headers: { isLogin: true } }) }, { headers: { isLogin: true } })
.then(async res => { .then(async res => {
if (res.status === 200) { // (success false )
if (res.data && res.data.success === false) {
//
errorMessage.value = res.data.message || '로그인에 실패했습니다.';
return;
}
//
userStore.userInfo(); userStore.userInfo();
await nextTick(); await nextTick();
router.push('/') router.push('/');
} })
}).catch(error => {
if (error.response) {
error.config.isLoginRequest = true;
errorMessage.value = error.response.data.message;
console.clear();
}
});
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <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 <li
v-for="(user, index) in displayedUserList" v-for="(user, index) in displayedUserList"
:key="index" :key="index"
@ -26,15 +26,18 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref, nextTick, computed } from 'vue'; import { onMounted, ref, nextTick, computed, watch } from 'vue';
import { useUserStore } from '@s/userList'; import { useUserStore } from '@s/userList';
import { useProjectStore } from '@s/useProjectStore'; import { useProjectStore } from '@s/useProjectStore';
import $api from '@api'; import $api from '@api';
import { useToastStore } from "@s/toastStore";
const emit = defineEmits(['user-list-update']); const emit = defineEmits(['user-list-update']);
const userStore = useUserStore(); const userStore = useUserStore();
const userList = ref([]); const userList = ref([]);
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const userProjectPeriods = ref([]);
const toastStore = useToastStore();
const props = defineProps({ const props = defineProps({
projctSeq: { projctSeq: {
@ -44,6 +47,10 @@ const props = defineProps({
showOnlyActive: { showOnlyActive: {
type: Boolean, type: Boolean,
default: false 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 () => { onMounted(async () => {
await userStore.fetchUserList(); await userStore.fetchUserList();
@ -89,16 +117,25 @@ onMounted(async () => {
if (props.projctSeq) { if (props.projctSeq) {
await fetchProjectParticipation(); await fetchProjectParticipation();
await fetchUserProjectPeriods();
} }
nextTick(() => { nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); initTooltips();
tooltips.forEach((tooltip) => {
new bootstrap.Tooltip(tooltip);
});
}); });
}); });
//
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) => { const isUserDisabled = (user) => {
return props.projctSeq ? user.PROJCTYON === '0' : user.disabled; return props.projctSeq ? user.PROJCTYON === '0' : user.disabled;
@ -108,8 +145,16 @@ const isUserDisabled = (user) => {
// showOnlyActive true toggleDisable // showOnlyActive true toggleDisable
const toggleDisable = async (index) => { const toggleDisable = async (index) => {
if (props.showOnlyActive) return; if (props.showOnlyActive) return;
const user = displayedUserList.value[index]; const user = displayedUserList.value[index];
//
if (props.role === 'vote') {
if(user.MEMBERSEQ === userStore.userInfo.id) {
toastStore.onToast('본인은 비활성화할 수 없습니다.', 'e');
return;
}
}
if (user) { if (user) {
const newParticipationStatus = props.projctSeq const newParticipationStatus = props.projctSeq
? user.PROJCTYON === '1' ? user.PROJCTYON === '1'
@ -150,7 +195,26 @@ const emitUserListUpdate = () => {
emit('user-list-update', { activeUsers, disabledUsers }); 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) => { 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> </script>

View File

@ -1,11 +1,10 @@
<template> <template>
<div v-if="data.voteMembers.some(item => item.MEMBERSEQ === userStore.user.id)" <div class="card mb-6" :class="{'ps-none opacity-50': data.localVote.LOCVOTDDT && (topVoters.length == 1 || data.localVote.LOCVOTRES || voteResult == 0)}">
class="card mb-6" :class="{ 'disabled-class': data.localVote.LOCVOTDDT && (topVoters.length == 1 || data.localVote.LOCVOTRES || voteResult == 0)}">
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" > <div class="card-body" v-if="!data.localVote.LOCVOTDEL" >
<h5 class="card-title mb-1"> <h5 class="card-title mb-1">
<div class="list-unstyled users-list d-flex align-items-center gap-1"> <div class="list-unstyled users-list d-flex align-items-center gap-1">
<img <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}`" :src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
:style="`border-color: ${data.localVote.usercolor} !important;`" :style="`border-color: ${data.localVote.usercolor} !important;`"
@error="$event.target.src = '/img/icons/icon.png'" @error="$event.target.src = '/img/icons/icon.png'"
@ -29,7 +28,7 @@
</button> </button>
<DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" /> <DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" />
</div> </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> </div>
</div> </div>

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="card-text"> <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 <vote-card-check-list
:data="item" :data="item"
:multiIs = voteInfo.LOCVOTMUL :multiIs = voteInfo.LOCVOTMUL
@ -25,17 +24,19 @@
</form-input> </form-input>
<link-input v-model="item.url" class="mb-1"/> <link-input v-model="item.url" class="mb-1"/>
</div> </div>
<div class="d-flex justify-content"> <div class="d-flex justify-content align-items-center mt-3">
<plus-btn @click="addItem" :disabled="total >= 10" class="m-1" /> <plus-btn @click="addItem" :disabled="total >= 10" />
<button class="btn btn-primary btn-icon mt-1" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled"> <button class="btn btn-primary btn-icon m-1" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
<i class="bx bx-check"></i> <i class="bx bx-check"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="d-flex">
<save-btn class="mt-2 ms-auto" @click="selectVote"/>
</div>
</div> </div>
<save-btn class="mt-2" @click="selectVote"/>
</template> </template>
<script setup> <script setup>

View File

@ -10,8 +10,7 @@
@change="handleChange" @change="handleChange"
> >
{{ data.LOCVOTCON }} {{ data.LOCVOTCON }}
<div></div> <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">
<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">
{{ data.LOCVOTLIK }} {{ data.LOCVOTLIK }}
</a> </a>
</label> </label>

View File

@ -1,18 +1,18 @@
<template> <template>
<div> <div>
<card <card
@addContents="addContents" @addContents="addContents"
@checkedNames="checkedNames" @checkedNames="checkedNames"
@endVoteId="endVoteId" @endVoteId="endVoteId"
@voteEnded="voteEnded" @voteEnded="voteEnded"
@voteDelete="voteDelete" @voteDelete="voteDelete"
@randomList="randomList" @randomList="randomList"
@updateVote="updateVote" @updateVote="updateVote"
v-for="(item, index) in data" v-for="(item, index) in data"
:key="index" :key="index"
:data="item" :data="item"
/> />
</div> </div>
</template> </template>
<script setup> <script setup>

View File

@ -1,7 +1,7 @@
<template> <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> <i class='bx bxs-user-check link-info fa-3x'></i>
<vote-complete-user-list <vote-complete-user-list
v-for="(item, index) in voetedUsers" v-for="(item, index) in voetedUsers"
@ -10,7 +10,7 @@
/> />
</div> </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> <i class='bx bxs-user-x link-danger fa-3x'></i>
<vote-in-complete-user-list <vote-in-complete-user-list
v-for="(item, index) in noVoetedUsers" v-for="(item, index) in noVoetedUsers"

View File

@ -11,7 +11,6 @@
:contentValue="item.WRDDICCON" :contentValue="item.WRDDICCON"
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'" :isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
/> />
<div v-else> <div v-else>
<input <input
v-if="userStore.user.role == 'ROLE_ADMIN'" v-if="userStore.user.role == 'ROLE_ADMIN'"

View File

@ -1,38 +1,31 @@
<template> <template>
<div> <div>
<div class="row"> <FormSelect class="me-5"
<div class="col-10"> name="cate"
<FormSelect title="카테고리"
name="cate" :data="dataList"
title="카테고리" :is-common="true"
:data="dataList" @update:data="selectCategory = $event"
:is-common="true" @change="onChange"
@update:data="selectCategory = $event" :value="formValue"
@change="onChange" :is-essential="false"
:value="formValue" />
:is-essential="false" <div v-if="!isDisabled" class="add-btn">
/> <PlusBtn @click="toggleInput"/>
</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>
</div> </div>
</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="용어" title="용어"
type="text" type="text"
name="word" name="word"
@ -43,15 +36,14 @@
:disabled="isDisabled" :disabled="isDisabled"
@keyup="ValidHandler('title')" @keyup="ValidHandler('title')"
/> />
</div> <div>
<div> <QEditor class="" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
<QEditor @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/> <div class="text-end mt-5">
<div class="text-end mt-5"> <button class="btn btn-primary" @click="saveWord">
<button class="btn btn-primary" @click="saveWord"> <i class="bx bx-check"></i>
<i class="bx bx-check"></i> </button>
</button> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@ -174,54 +166,40 @@ const saveWord = () => {
} }
const wordData = { const wordData = {
id: props.NumValue || null, id: props.NumValue || null,
title: computedTitle.value, title: computedTitle.value.trim(),
category: selectedCategory.value, category: selectedCategory.value,
content: content.value, content: content.value,
}; };
if(valid){ if(valid){
emit('addWord', wordData, addCategory.value === '' emit('addWord', wordData, addCategory.value.trim() === ''
? (isNaN(selectedCategory.value) ? selectedCategory.value : Number(selectedCategory.value)) ? (isNaN(selectedCategory.value) ? selectedCategory.value : Number(selectedCategory.value))
: addCategory.value); : addCategory.value);
} }
} }
// focusout // focusout
const handleCategoryFocusout = (value) => { const handleCategoryFocusout = (value) => {
if (!value || value.trim() === '') {
return;
}
const valueTrim = value.trim(); const valueTrim = value.trim();
const existingCategory = props.dataList.find(item => item.label === valueTrim); const existingCategory = props.dataList.find(item => item.label === valueTrim);
// if (existingCategory) {
if(valueTrim == ''){
addCategoryAlert.value = true; addCategoryAlert.value = true;
// focus // focus
setTimeout(() => { setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input'); const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) { if (inputElement) {
inputElement.focus(); inputElement.focus();
} }
}, 0); }, 0);
}else if (existingCategory) {
addCategoryAlert.value = true;
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
} else { } else {
addCategoryAlert.value = false; addCategoryAlert.value = false;
} }
}; };
</script> </script>
<style scoped> <style scoped>
@ -234,4 +212,9 @@ const handleCategoryFocusout = (value) => {
margin-top: 2.5rem margin-top: 2.5rem
} }
} }
.add-btn {
position: absolute;
right: 0.7rem;
top: 1.2rem;
}
</style> </style>

View File

@ -51,48 +51,3 @@
loadScript('/js/main.js'); loadScript('/js/main.js');
}); });
</script> </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>

View File

@ -1,6 +1,6 @@
<template> <template>
<!-- Chat Sidebar --> <!-- 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> </aside>
</template> </template>
@ -25,19 +25,8 @@ const sendMessage = () => {
</script> </script>
<style scoped> <style scoped>
/* 채팅 사이드바 고정 */
.chat-sidebar { .chat-sidebar {
width: 20%; 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> </style>

View File

@ -96,7 +96,7 @@ import { computed } from "vue";
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const allowedUserId = 26; // ID (!!) const allowedUserId = 1; // ID (!!)
const userId = computed(() => userStore.user?.id ?? null); const userId = computed(() => userStore.user?.id ?? null);
</script> </script>

View File

@ -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="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-3" @click="handleLogout"></i>
<!-- Notification --> <!-- 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 <a
class="nav-link dropdown-toggle hide-arrow p-0" class="nav-link dropdown-toggle hide-arrow p-0"
href="javascript:void(0);" href="javascript:void(0);"
@ -161,7 +161,7 @@
<!--/ Notification --> <!--/ Notification -->
<!-- User --> <!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown"> <li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle hide-arrow p-1" href="javascript:void(0);" data-bs-toggle="dropdown">
<img <img
v-if="user" v-if="user"
:src="`${baseUrl}upload/img/profile/${user.profile}`" :src="`${baseUrl}upload/img/profile/${user.profile}`"
@ -311,7 +311,9 @@
await userStore.userInfo(); await userStore.userInfo();
user.value = userStore.user; user.value = userStore.user;
await projectStore.getProjectList('', '', 'true'); if (authStore.isAuthenticated) {
await projectStore.getProjectList('', '', 'true');
}
// //
await projectStore.getMemberProjects(); await projectStore.getMemberProjects();

View File

@ -8,11 +8,12 @@ const routes = [
path: '/', path: '/',
name: 'Home', name: 'Home',
component: () => import('@v/MainView.vue'), component: () => import('@v/MainView.vue'),
// meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{ {
path: '/board', path: '/board',
component: () => import('@v/board/TheBoard.vue'), component: () => import('@v/board/TheBoard.vue'),
meta: { requiresAuth: true },
children: [ children: [
{ {
path: '', path: '',
@ -38,6 +39,7 @@ const routes = [
{ {
path: '/wordDict', path: '/wordDict',
component: () => import('@v/wordDict/wordDict.vue'), component: () => import('@v/wordDict/wordDict.vue'),
meta: { requiresAuth: true }
}, },
{ {
path: '/login', path: '/login',
@ -60,10 +62,12 @@ const routes = [
{ {
path: '/vacation', path: '/vacation',
component: () => import('@v/vacation/VacationManagement.vue'), component: () => import('@v/vacation/VacationManagement.vue'),
meta: { requiresAuth: true }
}, },
{ {
path: '/voteboard', path: '/voteboard',
component: () => import('@v/voteboard/TheVoteBoard.vue'), component: () => import('@v/voteboard/TheVoteBoard.vue'),
meta: { requiresAuth: true },
children: [ children: [
{ {
path: '', path: '',
@ -78,10 +82,12 @@ const routes = [
{ {
path: '/projectlist', path: '/projectlist',
component: () => import('@v/projectlist/TheProjectList.vue'), component: () => import('@v/projectlist/TheProjectList.vue'),
meta: { requiresAuth: true }
}, },
{ {
path: '/commuters', path: '/commuters',
component: () => import('@v/commuters/TheCommuters.vue'), component: () => import('@v/commuters/TheCommuters.vue'),
meta: { requiresAuth: true }
}, },
{ {
path: '/authorization', path: '/authorization',

View 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,
},
);

View File

@ -52,7 +52,6 @@ async function fetchUsers() {
isAdmin: user.MEMBERROL === 'ROLE_ADMIN', isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
})); }));
} catch (error) { } catch (error) {
console.error('사용자 목록을 불러오는 중 오류 발생:', error);
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e'); toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
} }
} }
@ -74,7 +73,7 @@ async function toggleAdmin(user) {
role: user.isAdmin ? 'MEMBER' : 'ADMIN' role: user.isAdmin ? 'MEMBER' : 'ADMIN'
}; };
try { try {
const response = await axios.put('admin/role', requestData); // API const response = await axios.put('admin/role', requestData);
if (response.status === 200) { if (response.status === 200) {
user.isAdmin = !user.isAdmin; user.isAdmin = !user.isAdmin;
@ -83,7 +82,6 @@ async function toggleAdmin(user) {
throw new Error('권한 변경 실패'); throw new Error('권한 변경 실패');
} }
} catch (error) { } catch (error) {
console.error('권한 변경 중 오류 발생:', error);
toastStore.onToast('권한 변경에 실패했습니다.', 'e'); toastStore.onToast('권한 변경에 실패했습니다.', 'e');
} }
} }
@ -92,71 +90,4 @@ onMounted(fetchUsers);
</script> </script>
<style scoped> <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> </style>

View File

@ -50,7 +50,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="html5-tel-input" class="col-md-2 col-form-label"> <label for="html5-tel-input" class="col-md-2 col-form-label">
내용 내용
<span class="text-red">*</span> <span class="text-danger">*</span>
</label> </label>
<div class="col-md-12"> <div class="col-md-12">
<QEditor <QEditor
@ -65,7 +65,7 @@
<!-- 버튼 --> <!-- 버튼 -->
<div class="mb-4 d-flex justify-content-end"> <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> <i class="bx bx-left-arrow-alt"></i>
</button> </button>
<button type="button" class="btn btn-primary ms-1" @click="updateBoard"> <button type="button" class="btn btn-primary ms-1" @click="updateBoard">
@ -85,11 +85,13 @@
import { ref, onMounted, computed, watch, inject } from 'vue'; import { ref, onMounted, computed, watch, inject } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useBoardAccessStore } from '@s/useBoardAccessStore';
import axios from '@api'; import axios from '@api';
// //
const $common = inject('common'); const $common = inject('common');
const toastStore = useToastStore(); const toastStore = useToastStore();
const accessStore = useBoardAccessStore();
// //
const title = ref(''); const title = ref('');
@ -117,22 +119,30 @@
// //
const fetchBoardDetails = async () => { const fetchBoardDetails = async () => {
try { //
const response = await axios.get(`board/${currentBoardId.value}`); let password = accessStore.password;
const data = response.data.data; const params = {
password: `${password}` || '',
};
//const response = await axios.get(`board/${currentBoardId.value}`);
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
// if (data.code !== 200) {
if (data.hasAttachment && data.attachments.length > 0) { //toastStore.onToast(data.message, 'e');
attachFiles.value = addDisplayFileName([...data.attachments]); alert(data.message, 'e');
} router.back();
return;
//
title.value = data.title || '제목 없음';
content.value = data.content || '내용 없음';
contentLoaded.value = true;
} catch (error) {
console.error('게시물 가져오기 오류:', error.response || error.message);
} }
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 = () => { const goList = () => {
accessStore.$reset();
router.push('/board'); router.push('/board');
}; };
//
const goBack = () => {
accessStore.$reset();
router.back();
};
// //
const checkValidation = () => { const checkValidation = () => {
contentAlert.value = $common.isNotValidContent(content); contentAlert.value = $common.isNotValidContent(content);
@ -218,43 +235,42 @@
const updateBoard = async () => { const updateBoard = async () => {
if (checkValidation()) return; if (checkValidation()) return;
try { //
// const boardData = {
const boardData = { LOCBRDTTL: title.value.trim(),
LOCBRDTTL: title.value.trim(), LOCBRDCON: JSON.stringify(content.value),
LOCBRDCON: JSON.stringify(content.value), LOCBRDSEQ: currentBoardId.value,
LOCBRDSEQ: currentBoardId.value, };
};
// //
if (delFileIdx.value && delFileIdx.value.length > 0) { if (delFileIdx.value && delFileIdx.value.length > 0) {
boardData.delFileIdx = [...delFileIdx.value]; boardData.delFileIdx = [...delFileIdx.value];
} }
const fileArray = newFileFilter(attachFiles); const fileArray = newFileFilter(attachFiles);
const formData = new FormData(); const formData = new FormData();
// formData boardData // formData boardData
Object.entries(boardData).forEach(([key, value]) => { Object.entries(boardData).forEach(([key, value]) => {
formData.append(key, value); formData.append(key, value);
}); });
// formData // formData
fileArray.forEach((file, idx) => { fileArray.forEach((file, idx) => {
formData.append('files', file); 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'); toastStore.onToast('게시물이 수정되었습니다.', 's');
goList(); goList();
} catch (error) { } else {
console.error('게시물 수정 중 오류 발생:', error); toastStore.onToast('게시물 수정에 실패했습니다.', 'e');
toastStore.onToast('게시물 수정에 실패했습니다.');
} }
}; };
// //
onMounted(() => { onMounted(async () => {
if (currentBoardId.value) { if (currentBoardId.value) {
fetchBoardDetails(); fetchBoardDetails();
} else { } else {
@ -262,10 +278,3 @@
} }
}); });
</script> </script>
<style>
.text-red {
color: red;
text-align: center;
}
</style>

View File

@ -1,23 +1,24 @@
<template> <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">
<div class="card-header d-flex flex-column"> <div class="card-header d-flex flex-column">
<!-- 검색창 --> <!-- 검색창 -->
<div class="mb-3 w-100"> <div class="mb-3 w-100">
<search-bar @update:data="search" @keyup.enter="searchOnEnter" class="flex-grow-1" /> <search-bar @update:data="search" @keyup.enter="searchOnEnter" class="flex-grow-1" />
</div> </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="10">10개씩</option>
<option value="20">20개씩</option> <option value="20">20개씩</option>
<option value="30">30개씩</option> <option value="30">30개씩</option>
<option value="50">50개씩</option> <option value="50">50개씩</option>
<option value="100">100개씩</option>
</select> </select>
<!-- 셀렉트 박스 --> <!-- 셀렉트 박스 -->
<select class="form-select w-auto" v-model="selectedOrder" @change="handleSortChange"> <select class="form-select w-auto" v-model="selectedOrder" @change="handleSortChange">
<option value="date">최신날짜</option> <option value="date">날짜</option>
<option value="views">조회수</option> <option value="views">조회수</option>
</select> </select>
@ -38,26 +39,33 @@
<table class="datatables-users table border-top dataTable dtr-column"> <table class="datatables-users table border-top dataTable dtr-column">
<thead> <thead>
<tr> <tr>
<th style="width: 11%;" 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: 45%" class="text-center fw-bold">제목</th>
<th style="width: 10%;" 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: 15%" class="text-center fw-bold">작성일</th>
<th style="width: 9%;" class="text-center fw-bold">조회수</th> <th style="width: 9%" class="text-center fw-bold">조회수</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- 공지사항 --> <!-- 공지사항 -->
<template v-if="pagination.currentPage === 1 && !showNotices"> <template v-if="pagination.currentPage === 1 && !showNotices">
<tr v-for="(notice, index) in noticeList" <tr
v-for="(notice, index) in noticeList"
:key="'notice-' + index" :key="'notice-' + index"
class="bg-label-gray fw-bold" class="bg-label-gray fw-bold"
@click="goDetail(notice.id)"> @click="goDetail(notice.id)"
>
<td class="text-center">공지</td> <td class="text-center">공지</td>
<td class="cursor-pointer"> <td class="cursor-pointer">
📌 {{ notice.title }} 📌 {{ notice.title }}
<span v-if="notice.commentCount" class="comment-count">[ {{ notice.commentCount }} ]</span> <span v-if="notice.commentCount" class="text-danger fw-bold me-1"
<i v-if="notice.img" class="bi bi-image me-1"></i> >[ {{ notice.commentCount }} ]</span
<i v-if="Array.isArray(notice.hasAttachment) && notice.hasAttachment.length > 0" class="bi bi-paperclip"></i> >
<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> <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>
@ -66,16 +74,21 @@
</tr> </tr>
</template> </template>
<!-- 일반 게시물 --> <!-- 일반 게시물 -->
<tr v-for="(post, index) in generalList" <tr
v-for="(post, index) in generalList"
:key="'post-' + index" :key="'post-' + index"
class="invert-bg-white" class="invert-bg-white"
@click="goDetail(post.realId)"> @click="goDetail(post.realId)"
>
<td class="text-center">{{ post.id }}</td> <td class="text-center">{{ post.id }}</td>
<td class="cursor-pointer"> <td class="cursor-pointer">
{{ 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="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> <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>
@ -84,11 +97,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- 검색 결과가 없을 --> <!-- 게시물이 없을 -->
<div v-if="generalList.length === 0"> <div v-if="generalList.length === 0">
<p class="text-center pt-10 mt-2 mb-0 text-muted"> <p class="text-center pt-10 mt-2 mb-0 text-muted">게시물이 없습니다.</p>
검색 결과가 없습니다.
</p>
</div> </div>
</div> </div>
</div> </div>
@ -96,11 +107,7 @@
<!-- 페이지네이션 --> <!-- 페이지네이션 -->
<div class="row g-3"> <div class="row g-3">
<div class="mt-8"> <div class="mt-8">
<Pagination <Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
v-if="pagination.pages"
v-bind="pagination"
@update:currentPage="handlePageChange"
/>
</div> </div>
</div> </div>
</div> </div>
@ -109,174 +116,169 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import Pagination from '@c/pagination/Pagination.vue'; import Pagination from '@c/pagination/Pagination.vue';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import router from '@/router'; import router from '@/router';
import WriteButton from '@c/button/WriteBtn.vue'; import WriteButton from '@c/button/WriteBtn.vue';
import axios from '@api'; import axios from '@api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday'; import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday'; import isYesterday from 'dayjs/plugin/isYesterday';
import 'bootstrap-icons/font/bootstrap-icons.css'; 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 pagination = ref({
const generalList = ref([]); currentPage: 1,
const noticeList = ref([]); pages: 1,
const searchText = ref(''); prePage: 0,
const selectedOrder = ref('date'); nextPage: 1,
const selectedSize = ref(10); isFirstPage: true,
const showNotices = ref(false); isLastPage: false,
hasPreviousPage: false,
hasNextPage: false,
navigatePages: 10,
navigatepageNums: [1],
navigateFirstPage: 1,
navigateLastPage: 1,
});
const pagination = ref({ //
currentPage: 1, const goDetail = id => {
pages: 1, router.push({ name: 'BoardDetail', params: { id: id } });
prePage: 0, };
nextPage: 1,
isFirstPage: true,
isLastPage: false,
hasPreviousPage: false,
hasNextPage: false,
navigatePages: 10,
navigatepageNums: [1],
navigateFirstPage: 1,
navigateLastPage: 1
});
// // ( HH:mm, YYYY-MM-DD)
const goDetail = (id) => { const formatDate = dateString => {
router.push({ name: 'BoardDetail', params: { id: id } }); 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 isNewPost = dateString => {
const date = dayjs(dateString); const date = dayjs(dateString);
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD'); return date.isToday() || date.isYesterday();
}; };
// ( ) //
const isNewPost = (dateString) => { const search = e => {
const date = dayjs(dateString); searchText.value = e.trim();
return date.isToday() || date.isYesterday(); fetchGeneralPosts(1);
}; };
// //
const search = (e) => { const handleSortChange = () => {
searchText.value = e.trim(); fetchGeneralPosts(1);
fetchGeneralPosts(1); };
};
// //
const handleSortChange = () => { const handleSizeChange = () => {
fetchGeneralPosts(1); fetchGeneralPosts(1);
}; };
// //
const handleSizeChange = () => { const fetchGeneralPosts = async (page = 1) => {
fetchGeneralPosts(1); try {
}; const { data } = await axios.get('board/general', {
params: {
page,
size: selectedSize.value,
orderBy: selectedOrder.value,
searchKeyword: searchText.value,
},
});
// if (data?.data) {
const fetchGeneralPosts = async (page = 1) => { const totalPosts = data.data.total;
try { generalList.value = data.data.list.map((post, index) => ({
const { data } = await axios.get("board/general", { realId: post.id,
params: { id: totalPosts - (page - 1) * selectedSize.value - index,
page, title: post.title,
size: selectedSize.value, author: post.author || '익명',
orderBy: selectedOrder.value, rawDate: post.date,
searchKeyword: searchText.value 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) {}
};
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);
}
};
// //
const fetchNoticePosts = async () => { const fetchNoticePosts = async () => {
try { try {
const { data } = await axios.get("board/notices2", { const { data } = await axios.get("board/notices", {
params: { searchKeyword: searchText.value } params: { searchKeyword: searchText.value }
}); });
if (data?.data) { if (data?.data) {
noticeList.value = data.data.map(post => ({ noticeList.value = data.data.map(post => ({
id: post.id, id: post.id,
title: post.title, title: post.title,
author: post.author || '관리자', author: post.author || '관리자',
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, hasAttachment: post.hasAttachment,
img: post.firstImageUrl || null, img: post.firstImageUrl || null,
commentCount : post.commentCount 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 searchText.value = searchTextValue;
const searchOnEnter = (event) => { fetchGeneralPosts(1);
const searchTextValue = event.target.value.trim(); };
if (!searchTextValue || searchTextValue[0] === ' ') { //
return; // const handlePageChange = page => {
} if (page !== pagination.value.currentPage) {
fetchGeneralPosts(page);
}
};
searchText.value = searchTextValue; //
fetchGeneralPosts(1); onMounted(() => {
}; fetchNoticePosts();
fetchGeneralPosts();
// });
const handlePageChange = (page) => {
if (page !== pagination.value.currentPage) {
fetchGeneralPosts(page);
}
};
//
onMounted(() => {
fetchNoticePosts();
fetchGeneralPosts();
});
</script> </script>
<style scoped> <style scoped>
@ -288,7 +290,6 @@ onMounted(() => {
.comment-count { .comment-count {
font-size: 0.9rem; font-size: 0.9rem;
font-weight: bold; font-weight: bold;
color: #ff5733;
border-radius: 4px; border-radius: 4px;
padding: 2px 6px; padding: 2px 6px;
position: relative; position: relative;

View File

@ -71,7 +71,11 @@
</div> </div>
<!-- HTML 콘텐츠 렌더링 --> <!-- 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"> <div class="row justify-content-center my-10">
@ -139,6 +143,7 @@
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useBoardAccessStore } from '@s/useBoardAccessStore';
import axios from '@api'; import axios from '@api';
const $common = inject('common'); const $common = inject('common');
@ -161,6 +166,8 @@
const router = useRouter(); const router = useRouter();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const toastStore = useToastStore(); const toastStore = useToastStore();
const accessStore = useBoardAccessStore();
const currentBoardId = ref(Number(route.params.id)); const currentBoardId = ref(Number(route.params.id));
const unknown = computed(() => profileName.value === '익명'); const unknown = computed(() => profileName.value === '익명');
const currentUserId = computed(() => userStore?.user?.id); // id const currentUserId = computed(() => userStore?.user?.id); // id
@ -199,7 +206,6 @@
link.remove(); link.remove();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
console.error('파일 다운로드 오류:', error);
alert('파일 다운로드 중 오류가 발생했습니다.'); alert('파일 다운로드 중 오류가 발생했습니다.');
} }
}; };
@ -242,44 +248,44 @@
// //
const fetchBoardDetails = async () => { const fetchBoardDetails = async () => {
const response = await axios.get(`board/${currentBoardId.value}`); const { data } = await axios.get(`board/${currentBoardId.value}`);
const data = response.data.data; if (data?.data) {
const boardData = data.data;
profileName.value = data.author || '익명'; profileName.value = boardData.author || '익명';
authorId.value = data.authorId; authorId.value = boardData.authorId;
boardTitle.value = data.title || '제목 없음'; boardTitle.value = boardData.title || '제목 없음';
boardContent.value = data.content || ''; boardContent.value = boardData.content || '';
profileImg.value = data.profileImg || ''; profileImg.value = boardData.profileImg || '';
date.value = data.date || ''; date.value = boardData.date || '';
views.value = data.cnt || 0; views.value = boardData.cnt || 0;
likes.value = data.likeCount || 0; likes.value = boardData.likeCount || 0;
dislikes.value = data.dislikeCount || 0; dislikes.value = boardData.dislikeCount || 0;
attachment.value = data.hasAttachment || null; attachment.value = boardData.hasAttachment || null;
commentNum.value = data.commentCount || 0; commentNum.value = boardData.commentCount || 0;
attachments.value = data.attachments || []; attachments.value = boardData.attachments || [];
} else {
toastStore.onToast(data.message, 'e');
router.back();
}
}; };
// , // ,
const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => { const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => {
try { await axios.post(`/board/${boardId}/${commentId}/reaction`, {
await axios.post(`/board/${boardId}/${commentId}/reaction`, { LOCBRDSEQ: boardId, // id
LOCBRDSEQ: boardId, // id LOCCMTSEQ: commentId, // id
LOCCMTSEQ: commentId, // id LOCGOBGOD: isLike ? 'T' : 'F',
LOCGOBGOD: isLike ? 'T' : 'F', LOCGOBBAD: isDislike ? 'T' : 'F',
LOCGOBBAD: isDislike ? 'T' : 'F', });
});
const response = await axios.get(`board/${boardId}`); const response = await axios.get(`board/${boardId}`);
const updatedData = response.data.data; const updatedData = response.data.data;
likes.value = updatedData.likeCount; likes.value = updatedData.likeCount;
dislikes.value = updatedData.dislikeCount; dislikes.value = updatedData.dislikeCount;
likeClicked.value = isLike; likeClicked.value = isLike;
dislikeClicked.value = isDislike; dislikeClicked.value = isDislike;
} catch (error) {
alert('오류가 발생했습니다.');
}
}; };
// //
@ -293,7 +299,7 @@
LOCGOBBAD: isDislike ? 'T' : 'F', LOCGOBBAD: isDislike ? 'T' : 'F',
}); });
await fetchComments(); fetchComments(pagination.value.currentPage);
}; };
// //
@ -305,25 +311,27 @@
page, page,
}, },
}); });
const commentsList = response.data.data.list.map(comment => ({ const commentsList = response.data.data.list
commentId: comment.LOCCMTSEQ, // ID .map(comment => ({
boardId: comment.LOCBRDSEQ, commentId: comment.LOCCMTSEQ, // ID
parentId: comment.LOCCMTPNT, // ID boardId: comment.LOCBRDSEQ,
author: comment.author || '익명', parentId: comment.LOCCMTPNT, // ID
authorId: comment.authorId, author: comment.author || '익명',
content: comment.LOCCMTRPY, authorId: comment.authorId,
likeCount: comment.likeCount || 0, content: comment.LOCCMTRPY,
dislikeCount: comment.dislikeCount || 0, likeCount: comment.likeCount || 0,
profileImg: comment.profileImg || '', dislikeCount: comment.dislikeCount || 0,
likeClicked: comment.likeClicked || false, profileImg: comment.profileImg || '',
dislikeClicked: comment.dislikeClicked || false, likeClicked: comment.likeClicked || false,
createdAtRaw: new Date(comment.LOCCMTRDT), // dislikeClicked: comment.dislikeClicked || false,
createdAt: formattedDate(comment.LOCCMTRDT), // createdAtRaw: comment.LOCCMTRDT, //
children: [], // // createdAt: formattedDate(comment.LOCCMTRDT), // ()
updateAtRaw: comment.LOCCMTUDT, // createdAtRaw: new Date(comment.LOCCMTUDT), //
})); createdAt: formattedDate(comment.LOCCMTUDT) + (comment.LOCCMTUDT !== comment.LOCCMTRDT ? ' (수정됨)' : ''), // ()
children: [], //
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw); updateAtRaw: comment.LOCCMTUDT,
}))
.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
for (const comment of commentsList) { for (const comment of commentsList) {
if (!comment.commentId) continue; if (!comment.commentId) continue;
@ -333,21 +341,25 @@
}); });
if (replyResponse.data.data) { if (replyResponse.data.data) {
comment.children = replyResponse.data.data.map(reply => ({ comment.children = replyResponse.data.data
author: reply.author || '익명', .map(reply => ({
authorId: reply.authorId, author: reply.author || '익명',
profileImg: reply.profileImg || '', authorId: reply.authorId,
commentId: reply.LOCCMTSEQ, profileImg: reply.profileImg || '',
boardId: reply.LOCBRDSEQ, commentId: reply.LOCCMTSEQ,
parentId: reply.LOCCMTPNT, // ID boardId: reply.LOCBRDSEQ,
content: reply.LOCCMTRPY || '내용 없음', parentId: reply.LOCCMTPNT, // ID
createdAtRaw: new Date(reply.LOCCMTRDT), content: reply.LOCCMTRPY || '내용 없음',
createdAt: formattedDate(reply.LOCCMTRDT), createdAtRaw: reply.LOCCMTRDT,
likeCount: reply.likeCount || 0, // createdAt: formattedDate(reply.LOCCMTRDT),
dislikeCount: reply.dislikeCount || 0, //createdAtRaw: new Date(reply.LOCCMTUDT),
likeClicked: false, createdAt: formattedDate(reply.LOCCMTUDT) + (reply.LOCCMTUDT !== reply.LOCCMTRDT ? ' (수정됨)' : ''),
dislikeClicked: false, likeCount: reply.likeCount || 0,
})); dislikeCount: reply.dislikeCount || 0,
likeClicked: false,
dislikeClicked: false,
}))
.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
} else { } else {
comment.children = []; // comment.children = []; //
} }
@ -396,50 +408,35 @@
return; return;
} }
try { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { LOCBRDSEQ: currentBoardId.value,
LOCBRDSEQ: currentBoardId.value, LOCCMTRPY: comment,
LOCCMTRPY: comment, LOCCMTPWD: isCheck ? password : '',
LOCCMTPWD: isCheck ? password : '', LOCCMTPNT: 1,
LOCCMTPNT: 1, LOCBRDTYP: isCheck ? '300102' : null,
LOCBRDTYP: isCheck ? '300102' : null, });
});
if (response.status === 200) { if (response.status === 200) {
passwordAlert.value = ''; passwordAlert.value = '';
commentAlert.value = ''; commentAlert.value = '';
await fetchComments(); await fetchComments();
} else { } else {
alert('댓글 작성을 실패했습니다.'); alert('댓글 작성을 실패했습니다.');
}
} catch (error) {
alert('오류가 발생했습니다.');
} }
}; };
// //
const handleCommentReply = async reply => { const handleCommentReply = async reply => {
try { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { LOCBRDSEQ: currentBoardId.value,
LOCBRDSEQ: currentBoardId.value, LOCCMTRPY: reply.comment,
LOCCMTRPY: reply.comment, LOCCMTPWD: reply.password || null,
LOCCMTPWD: reply.password || null, LOCCMTPNT: reply.parentId,
LOCCMTPNT: reply.parentId, LOCBRDTYP: reply.isCheck ? '300102' : null,
LOCBRDTYP: reply.isCheck ? '300102' : null, });
});
if (response.status === 200) { if (response.status === 200) {
if (response.data.code === 200) { fetchComments(pagination.value.currentPage);
await fetchComments();
} else {
alert('대댓글 작성을 실패했습니다.');
}
}
} catch (error) {
if (error.response) {
alert('오류가 발생했습니다.');
}
alert('오류가 발생했습니다.');
} }
}; };
@ -483,7 +480,7 @@
return null; return null;
}; };
// ( ) // ( )
const editComment = comment => { const editComment = comment => {
password.value = ''; password.value = '';
passwordCommentAlert.value = ''; passwordCommentAlert.value = '';
@ -577,14 +574,17 @@
lastCommentClickedButton.value = button; lastCommentClickedButton.value = button;
}; };
//
const togglePassword = button => { const togglePassword = button => {
// close: . // close: .
boardPasswordAlert.value = '';
if (button === 'close') { if (button === 'close') {
isPassword.value = false; isPassword.value = false;
boardPasswordAlert.value = ''; boardPasswordAlert.value = '';
password.value = '';
return; return;
} }
closeAllPasswordAreas();
if (lastClickedButton.value === button) { if (lastClickedButton.value === button) {
isPassword.value = !isPassword.value; isPassword.value = !isPassword.value;
boardPasswordAlert.value = ''; boardPasswordAlert.value = '';
@ -603,20 +603,23 @@
} }
try { try {
const response = await axios.post(`board/${currentBoardId.value}/password`, { const { data } = await axios.post(`board/${currentBoardId.value}/password`, {
LOCBRDPWD: password.value, LOCBRDPWD: password.value,
LOCBRDSEQ: currentBoardId.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 = ''; boardPasswordAlert.value = '';
isPassword.value = false; isPassword.value = false;
if (lastClickedButton.value === 'edit') { if (lastClickedButton.value === 'edit') {
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } }); router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
return;
} else if (lastClickedButton.value === 'delete') { } else if (lastClickedButton.value === 'delete') {
await deletePost(); await deletePost();
} }
accessStore.$reset();
lastClickedButton.value = null; lastClickedButton.value = null;
} else { } else {
boardPasswordAlert.value = '비밀번호가 일치하지 않습니다.'; boardPasswordAlert.value = '비밀번호가 일치하지 않습니다.';
@ -648,9 +651,7 @@
// //
if (lastCommentClickedButton.value === 'edit') { if (lastCommentClickedButton.value === 'edit') {
if (targetComment) { if (targetComment) {
// closeAllEditTextareas(); //
closeAllEditTextareas();
targetComment.isEditTextarea = true; targetComment.isEditTextarea = true;
passwordCommentAlert.value = ''; passwordCommentAlert.value = '';
currentPasswordCommentId.value = null; currentPasswordCommentId.value = null;
@ -668,9 +669,6 @@
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다.'; passwordCommentAlert.value = '비밀번호가 일치하지 않습니다.';
} }
} catch (error) { } catch (error) {
if (error.response?.status === 401) {
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다';
}
passwordCommentAlert.value = '비밀번호가 일치하지 않습니다'; passwordCommentAlert.value = '비밀번호가 일치하지 않습니다';
} }
}; };
@ -712,7 +710,7 @@
}); });
if (response.data.code === 200) { if (response.data.code === 200) {
await fetchComments(); await fetchComments(pagination.value.currentPage);
closeAllPasswordAreas(); closeAllPasswordAreas();
if (targetComment) { if (targetComment) {
@ -730,29 +728,29 @@
} }
}; };
// //
const handleSubmitEdit = async (comment, editedContent) => { const handleSubmitEdit = async (comment, editedContent) => {
if (!checkValidation(comment, editedContent)) return; if (!checkValidation(comment, editedContent)) return; //
togglePassword(); togglePassword();
try {
const response = await axios.put(`board/comment/${comment.commentId}`, {
LOCCMTSEQ: comment.commentId,
LOCCMTRPY: editedContent,
});
if (response.status === 200) { const response = await axios.put(`board/comment/${comment.commentId}`, {
const targetComment = findCommentById(comment.commentId, comments.value); LOCCMTSEQ: comment.commentId,
if (targetComment) { LOCCMTRPY: editedContent.trim(),
targetComment.content = editedContent; // });
targetComment.isEditTextarea = false; //
} else { if (response.status === 200) {
toastStore.onToast('수정할 댓글을 찾을 수 없습니다.', 'e'); togglePassword('close');
} fetchComments(pagination.value.currentPage);
} else { return;
toastStore.onToast('댓글 수정 실패했습니다.', 'e'); // const targetComment = findCommentById(comment.commentId, comments.value);
}
} catch (error) { // if (targetComment) {
toastStore.onToast('댓글 수정 중 오류가 발생하였습니다.', 'e'); // targetComment.content = editedContent.trim(); //
// targetComment.isEditTextarea = false; //
// togglePassword('close');
// }
} else {
toastStore.onToast('댓글 수정을 실패하였습니다', 'e');
} }
}; };
@ -819,24 +817,3 @@
fetchComments(); fetchComments();
}); });
</script> </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>

View File

@ -129,15 +129,11 @@
const fileError = ref(''); const fileError = ref('');
const fetchCategories = async () => { const fetchCategories = async () => {
try { const response = await axios.get('board/categories');
const response = await axios.get('board/categories'); categoryList.value = response.data.data;
categoryList.value = response.data.data; const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유'); if (freeCategory) {
if (freeCategory) { categoryValue.value = freeCategory.CMNCODVAL;
categoryValue.value = freeCategory.CMNCODVAL;
}
} catch (error) {
console.error('카테고리 불러오기 오류:', error);
} }
}; };
@ -243,7 +239,6 @@
toastStore.onToast('게시물이 작성되었습니다.', 's'); toastStore.onToast('게시물이 작성되었습니다.', 's');
goList(); goList();
} catch (error) { } catch (error) {
console.error(error);
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e'); toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
} }
}; };

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="error-container"> <div class="error-page">
<div class="error-content"> <div class="error-content">
<img src="/img/illustrations/page-misc-error-dark.png" alt="Error Illustration" class="error-image" /> <img src="/img/illustrations/page-misc-error-dark.png" alt="Error Illustration" class="error-image" />
<h1>400</h1> <h1>400</h1>

View File

@ -41,7 +41,7 @@
<!-- Main Content: 캘린더 영역 --> <!-- Main Content: 캘린더 영역 -->
<div class="col app-calendar-content"> <div class="col app-calendar-content">
<div class="card shadow-none border-0"> <div class="card shadow-none border-0">
<div class="card-body pb-0" style="position: relative;"> <div class="card-body" style="position: relative;">
<full-calendar <full-calendar
ref="fullCalendarRef" ref="fullCalendarRef"
:options="calendarOptions" :options="calendarOptions"
@ -152,8 +152,10 @@ function handleMonthChange(viewInfo) {
loadCalendarData(year, month); loadCalendarData(year, month);
} }
// //
//
function handleDateClick(info) { function handleDateClick(info) {
if (!info.date || !info.dateStr) {
return;
}
const clickedDateStr = info.dateStr; const clickedDateStr = info.dateStr;
const clickedDate = info.date; const clickedDate = info.date;
const todayStr = new Date().toISOString().split("T")[0]; const todayStr = new Date().toISOString().split("T")[0];
@ -191,13 +193,15 @@ function handleDateClick(info) {
updateCalendarEvents(); updateCalendarEvents();
return; return;
} }
const type = halfDayType.value const type = halfDayType.value
? (halfDayType.value === "AM" ? "700101" : "700102") ? (halfDayType.value === "AM" ? "700101" : "700102")
: "700103"; : "700103";
selectedDates.value.set(clickedDateStr, type); selectedDates.value.set(clickedDateStr, type);
halfDayType.value = null;
if (halfDayType.value) {
halfDayType.value = null;
}
updateCalendarEvents(); updateCalendarEvents();
if (halfDayButtonsRef.value) { if (halfDayButtonsRef.value) {
@ -223,7 +227,7 @@ function markClickableDates() {
dateObj.getDay() === 0 || // dateObj.getDay() === 0 || //
dateObj.getDay() === 6 || // dateObj.getDay() === 6 || //
holidayDates.value.has(dateStr) || // holidayDates.value.has(dateStr) || //
dateObj.getTime() === oneWeekAgoObj.getTime() // -7 dateObj.getTime() <= oneWeekAgoObj.getTime() // -7
) { ) {
cell.classList.remove("clickable"); cell.classList.remove("clickable");
cell.classList.add("fc-day-sat-sun"); cell.classList.add("fc-day-sat-sun");
@ -278,7 +282,6 @@ const handleProfileClick = async (user) => {
isGrantModalOpen.value = true; isGrantModalOpen.value = true;
} }
} catch (error) { } catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error);
} }
}; };
// //
@ -300,7 +303,6 @@ const fetchUserList = async () => {
}); });
} catch (error) { } catch (error) {
console.error("📌 사용자 목록 불러오기 오류:", error);
} }
}; };
// //
@ -314,7 +316,6 @@ const fetchRemainingVacation = async () => {
}, {}); }, {});
} }
} catch (error) { } catch (error) {
console.error("🚨 남은 연차 데이터를 불러오지 못했습니다:", error);
} }
}; };
// //
@ -401,7 +402,6 @@ async function saveVacationChanges() {
const currentDate = fullCalendarRef.value.getApi().getDate(); const currentDate = fullCalendarRef.value.getApi().getDate();
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1); await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
} catch (error) { } catch (error) {
console.error("🚨 휴가 변경 저장 실패:", error);
toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e'); toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
} }
} }
@ -415,7 +415,6 @@ async function fetchVacationHistory(year) {
receivedVacations.value = response.data.data.receivedVacations || [] receivedVacations.value = response.data.data.receivedVacations || []
} }
} catch (error) { } catch (error) {
console.error(`🚨 휴가 데이터 불러오기 실패:`, error);
} }
} }
// //
@ -446,7 +445,6 @@ async function fetchVacationData(year, month) {
return []; return [];
} }
} catch (error) { } catch (error) {
console.error("Error fetching vacation data:", error);
return []; return [];
} }
} }
@ -521,7 +519,6 @@ const fetchVacationCodes = async () => {
console.warn("❌ 공통 코드 데이터를 불러오지 못했습니다."); console.warn("❌ 공통 코드 데이터를 불러오지 못했습니다.");
} }
} catch (error) { } catch (error) {
console.error("🚨 공통 코드 API 호출 실패:", error);
} }
}; };
const getVacationType = (typeCode) => { const getVacationType = (typeCode) => {
@ -563,11 +560,11 @@ watch(() => lastRemainingYear.value, async (newYear, oldYear) => {
await fetchVacationHistory(newYear); await fetchVacationHistory(newYear);
} }
}); });
// `selectedDates` //
watch( watch(
() => Array.from(selectedDates.value.keys()), // () => Array.from(selectedDates.value.keys()), //
(newKeys) => { (newKeys) => {
if (halfDayButtonsRef.value) { if (halfDayButtonsRef.value && !halfDayType.value) {
halfDayButtonsRef.value.resetHalfDay(); halfDayButtonsRef.value.resetHalfDay();
} }
}, },

View File

@ -22,7 +22,7 @@
</div> </div>
<!-- 투표리스트 --> <!-- 투표리스트 -->
<div v-if="voteListCardData.length == 0 " >투표가 없습니다.</div> <div v-if="voteListCardData.length == 0 " class="text-center">투표가 없습니다.</div>
<vote-list <vote-list
:data="voteListCardData" :data="voteListCardData"
@addContents="addContents" @addContents="addContents"
@ -81,6 +81,7 @@ const voteWrite = () => {
}; };
const changeCheck = () =>{ const changeCheck = () =>{
currentPage.value = 1;
getvoteList(); getvoteList();
} }
// //
@ -100,6 +101,7 @@ const getvoteList = () => {
}; };
const selectHandler = () =>{ const selectHandler = () =>{
currentPage.value = 1;
voteset.value = category.value; voteset.value = category.value;
getvoteList(); getvoteList();
} }

View File

@ -10,8 +10,8 @@
<div class="timeline-header mb-2"> <div class="timeline-header mb-2">
<h6 class="mb-0">투표 인원</h6> <h6 class="mb-0">투표 인원</h6>
</div> </div>
<UserList @userListInfo="userSet" @user-list-update="handleUserListUpdate" class="mb-3" /> <UserList :role="'vote'" @userListInfo="userSet" @user-list-update="handleUserListUpdate" class="mb-3" />
<div v-if="UserListAlert" class="red">2명이상 선택해주세요 </div> <div v-if="UserListAlert" class="invalid-feedback d-block">2명이상 선택해주세요.</div>
<form-input <form-input
title="제목" title="제목"
name="title" name="title"
@ -50,26 +50,28 @@
</div> </div>
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" /> <plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
<div> <div>
<label class="list-group-item"> <label>
<input <input
class="form-check-input me-1" class="form-check-input me-1"
type="checkbox" type="checkbox"
id="addvoteitem" id="addvoteitem"
v-model="addvoteitem" v-model="addvoteitem"
/> />
항목 추가여부 항목 추가여부
</label> </label>
<label class="list-group-item"> </div>
<input <div>
class="form-check-input me-1" <label >
type="checkbox" <input
id="addvotemulti" class="form-check-input me-1"
v-model="addvotemulti" type="checkbox"
/> id="addvotemulti"
다중투표 허용여부 v-model="addvotemulti"
</label> />
</div> 다중투표 허용여부
</label>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@ -127,6 +129,11 @@ const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers
activeUserList.value = activeUsers; activeUserList.value = activeUsers;
disabledUsers.value = updatedDisabledUsers; disabledUsers.value = updatedDisabledUsers;
userListTotal.value = activeUsers.length; userListTotal.value = activeUsers.length;
if(activeUserList.value.length<2){
UserListAlert.value = true;
}else{
UserListAlert.value = false;
}
}; };
const saveValid = () => { const saveValid = () => {
@ -146,18 +153,17 @@ const saveValid = () => {
} else { } else {
endDateAlert.value = false; endDateAlert.value = false;
} }
if (itemList.value[0].content.trim() === '') { itemList.value.forEach((item, index) => {
contentAlerts.value[0] = true; if (index < 2 && item.content.trim() === '') {
valid = false; contentAlerts.value[index] = true;
} else { valid = false;
contentAlerts.value[0] = false; } else if (index >= 2 && item.content.trim() === '' && item.url.trim() !== '') {
} contentAlerts.value[index] = true;
if (itemList.value[1].content.trim() === '') { valid = false;
contentAlerts.value[1] = true; } else {
valid = false; contentAlerts.value[index] = false;
} else { }
contentAlerts.value[1] = false; });
}
if (activeUserList.value.length < 2) { if (activeUserList.value.length < 2) {
UserListAlert.value = true; UserListAlert.value = true;
valid = false; valid = false;
@ -169,6 +175,7 @@ const saveValid = () => {
} }
}; };
const saveVote = () => { const saveVote = () => {
console.log('itemList',itemList)
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== ''); const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
const unwrappedUserList = toRaw(activeUserList.value); const unwrappedUserList = toRaw(activeUserList.value);
const listId = unwrappedUserList.map(item => ({ const listId = unwrappedUserList.map(item => ({
@ -189,16 +196,16 @@ const saveVote = () => {
}) })
}; };
const ValidHandler = (field) => { const ValidHandler = (field) => {
if(field == 'title'){ if (field === 'title') {
titleAlert.value = false; titleAlert.value = false;
} }
if(field == 'content1'){ if (field.startsWith('content')) {
contentAlerts.value[0] = false; const index = parseInt(field.replace('content', '')) - 1;
if (!isNaN(index)) {
contentAlerts.value[index] = false;
}
} }
if(field == 'content2'){ };
contentAlerts.value[1] = false;
}
}
const ValidHandlerendDate = () =>{ const ValidHandlerendDate = () =>{
endDateAlert.value = false; endDateAlert.value = false;
} }

View File

@ -6,9 +6,10 @@
<SearchBar @update:data="search"/> <SearchBar @update:data="search"/>
<div class="d-flex"> <div class="d-flex">
<!-- 단어 갯수, 작성하기 --> <!-- 단어 갯수, 작성하기 -->
<!-- 왼쪽 사이드바 --> <!-- 왼쪽 사이드바 -->
<div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;"> <div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/> <WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
:isActive="writeStore.activeItemId === 999999"/>
<!-- --> <!-- -->
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" :indexCategory="indexCategory" :selectedAl="selectedAlphabet" /> <DictAlphabetFilter @update:data="handleSelectedAlphabetChange" :indexCategory="indexCategory" :selectedAl="selectedAlphabet" />
<!-- 카테고리 --> <!-- 카테고리 -->
@ -19,16 +20,12 @@
<!-- 용어 리스트 컨텐츠 --> <!-- 용어 리스트 컨텐츠 -->
<div class="flex-grow-1"> <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"/> <DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
</div> </div>
<!-- 용어 리스트 --> <!-- 용어 리스트 -->
<div> <div>
<!-- 로딩 중일 -->
<LoadingSpinner v-if="loading"/>
<!-- 에러 메시지 --> <!-- 에러 메시지 -->
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="error">{{ error }}</div>
<!-- 단어 목록 --> <!-- 단어 목록 -->
@ -79,7 +76,6 @@
const toastStore = useToastStore(); const toastStore = useToastStore();
// //
const loading = ref(false);
const error = ref(''); const error = ref('');
// //
@ -135,12 +131,10 @@
wordList.value = res.data.data.data; wordList.value = res.data.data.data;
// //
total.value = res.data.data.total; total.value = res.data.data.total;
loading.value = false;
}) })
.catch(err => { .catch(err => {
console.error('데이터 로드 오류:', err); console.error('데이터 로드 오류:', err);
error.value = '데이터를 가져오는 중 문제가 발생했습니다.'; error.value = '데이터를 가져오는 중 문제가 발생했습니다.';
loading.value = false;
}); });
}; };
// //
@ -194,7 +188,7 @@
}else{ }else{
const lastCategory = cateList.value[cateList.value.length - 1]; const lastCategory = cateList.value[cateList.value.length - 1];
category = lastCategory ? lastCategory.value + 1 : 600101; category = lastCategory ? lastCategory.value + 1 : 600101;
newCodName = data; newCodName = data.trim();
} }
sendWordRequest(category, wordData, newCodName); sendWordRequest(category, wordData, newCodName);
}; };