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) {
.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 {

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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>

View File

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

View File

@ -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>

View File

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

View File

@ -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";

View File

@ -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>

View File

@ -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;

View File

@ -217,10 +217,3 @@
});
</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)"
@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>

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

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="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();

View File

@ -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',

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

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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');
}
};

View File

@ -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>

View File

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

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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);
};