Merge branch 'main' into style

This commit is contained in:
yoon 2025-03-20 13:34:41 +09:00
commit 61f9fc51fd
14 changed files with 323 additions and 234 deletions

View File

@ -157,7 +157,7 @@
.fc-toolbar-title { .fc-toolbar-title {
cursor: pointer; cursor: pointer;
} }
/* 클릭 가능한 날짜 (오늘 + 미래) */ /* 클릭 가능한 날짜 */
.fc-daygrid-day.clickable { .fc-daygrid-day.clickable {
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease-in-out; transition: background-color 0.2s ease-in-out;
@ -362,6 +362,28 @@
background-color: #0b5ed7 !important; background-color: #0b5ed7 !important;
color: white; color: white;
} }
/* 풀 연차 버튼 스타일 */
.vac-btn-primary {
color: #fff;
background-color: #28a745; /* 녹색 */
border-color: #28a745;
box-shadow: 0 0.125rem 0.25rem 0 rgba(40, 167, 69, 0.4);
font-size: 28px;
transition: all 0.2s ease-in-out;
}
/* 풀 연차 버튼 활성화 스타일 */
.vac-btn-primary.active {
background-color: #218838 !important;
color: #fff;
border: 3px solid #91d091 !important;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
/* 풀 연차 버튼이 눌렸을 때 효과 */
.vac-btn-primary:active {
transform: scale(0.9);
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
}
/* 버튼 기본 */ /* 버튼 기본 */
.vac-btn-success { .vac-btn-success {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;

View File

@ -4,7 +4,7 @@
:unknown="comment.author === '익명'" :unknown="comment.author === '익명'"
:isCommentAuthor="isCommentAuthor" :isCommentAuthor="isCommentAuthor"
:boardId="comment.boardId" :boardId="comment.boardId"
:profileName="comment.author" :profileName="comment.nickname ? comment.nickname : comment.author"
:date="comment.createdAt" :date="comment.createdAt"
:comment="comment" :comment="comment"
:profileImg="comment.profileImg" :profileImg="comment.profileImg"
@ -17,7 +17,7 @@
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
<!-- 댓글 비밀번호 입력창 (익명일 경우) --> <!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="currentPasswordCommentId === comment.commentId && unknown && comment.author == '익명'" class="mt-3 w-20 ms-auto"> <div v-if="currentPasswordCommentId === comment.commentId && unknown && comment.author == '익명'" class="mt-3 w-px-200 ms-auto">
<div class="input-group"> <div class="input-group">
<input <input
type="password" type="password"

View File

@ -3,69 +3,71 @@
<div class="card-body"> <div class="card-body">
<!-- 댓글 입력 섹션 --> <!-- 댓글 입력 섹션 -->
<div class="d-flex justify-content-start align-items-top"> <div class="d-flex justify-content-start align-items-top">
<!-- 프로필섹션 -->
<!-- <div class="avatar-wrapper">
<div v-if="!unknown" class="avatar me-4">
<img src="/img/avatars/11.png" alt="Avatar" class="rounded-circle">
</div>
</div> -->
<!-- 텍스트박스 -->
<div class="w-100"> <div class="w-100">
<textarea <textarea
class="form-control mb-2" class="form-control mb-1"
placeholder="댓글 달기" placeholder="댓글 달기"
rows="3" rows="3"
:maxlength="maxLength" :maxlength="maxLength"
v-model="comment" v-model="comment"
@input="alertTextHandler" @input="clearAlert('comment')"
></textarea> ></textarea>
<span v-if="commentAlert" class="invalid-feedback d-inline text-start ms-2 mb-2">{{ commentAlert }}</span> <span v-if="commentAlert" class="invalid-feedback d-inline text-start ms-2 mb-2">{{ commentAlert }}</span>
<span v-else class="invalid-feedback d-inline text-start ms-2">{{ textAlert }}</span> <span v-else class="invalid-feedback d-inline">{{ textAlert }}</span>
</div> </div>
</div> </div>
<!-- 옵션 버튼 섹션 --> <!-- 옵션 버튼 섹션 -->
<div class="d-flex justify-content-between mt-1"> <div class="d-flex justify-content-between align-items-center mt-1 pb-4">
<div class="row g-2"> <!-- 왼쪽: 익명 체크박스 -->
<div class="d-flex flex-wrap align-items-center mb-2"> <div v-if="unknown" class="form-check form-check-inline mb-0 me-2">
<!-- 익명 체크박스 (익명게시판일 경우에만)--> <input
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4 d-flex align-items-center"> class="form-check-input"
<input type="checkbox"
class="form-check-input me-2" :id="`checkboxAnnonymous${commnetId}`"
type="checkbox" v-model="isCheck"
:id="`checkboxAnnonymous${commnetId}`" @change="pwd2AlertHandler"
v-model="isCheck" />
@change="pwd2AlertHandler" <label class="form-check-label text-nowrap" :for="`checkboxAnnonymous${commnetId}`">익명</label>
/> </div>
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
</div>
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) --> <!-- 중앙: 닉네임 & 비밀번호 입력 필드 (가로 정렬) -->
<template v-if="isCheck"> <div v-if="isCheck" class="d-flex flex-grow-1 gap-2">
<div class="d-flex align-items-center col"> <!-- 닉네임 입력 영역 -->
<input <div class="position-relative">
type="password" <input
id="basic-default-password" type="text"
class="form-control w-80" class="form-control mb-1"
autocomplete="new-password" v-model="nickname"
v-model="password" placeholder="닉네임"
placeholder="비밀번호 입력" @input="clearAlert('nickname')"
@input="passwordAlertTextHandler" />
/> <!-- 닉네임 경고 메시지 -->
</div> <div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0" >
</template> {{ nicknameAlert }}
</div>
</div> </div>
<div class="row">
<div style="width: 70px"></div> <!-- 비밀번호 입력 영역 -->
<div class="col"> <div class="position-relative">
<span v-if="passwordAlert" class="invalid-feedback d-inline">{{ passwordAlert }}</span> <input
<span v-else class="invalid-feedback d-inline">{{ passwordAlert2 }}</span> type="password"
id="basic-default-password"
class="form-control mb-1"
autocomplete="new-password"
v-model="password"
placeholder="비밀번호"
@input="clearAlert('password')"
/>
<!-- 비밀번호 경고 메시지 -->
<div v-if="passwordAlert2" class="position-absolute text-danger small top-100 start-0">
{{ passwordAlert2 }}
</div> </div>
</div> </div>
</div> </div>
<!-- 답변 쓰기 버튼 --> <!-- 오른쪽: 답변 쓰기 버튼 -->
<div class="ms-auto mt-3 mt-md-0"> <div class="ms-auto">
<SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn> <SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
</div> </div>
</div> </div>
@ -108,37 +110,54 @@
const password = ref(''); const password = ref('');
const isCheck = ref(false); const isCheck = ref(false);
const textAlert = ref(''); const textAlert = ref('');
const nicknameAlert = ref('');
const passwordAlert2 = ref(''); const passwordAlert2 = ref('');
const nickname = ref('');
const emit = defineEmits(['submitComment']); const emit = defineEmits(['submitComment']);
const alertTextHandler = () => { //
textAlert.value = ''; const clearAlert = field => {
}; if (field === 'comment') textAlert.value = '';
if (field === 'nickname') nicknameAlert.value = '';
const passwordAlertTextHandler = event => { if (field === 'password') passwordAlert2.value = '';
event.target.value = event.target.value.replace(/\s/g, '');
passwordAlert2.value = '';
}; };
const handleCommentSubmit = () => { const handleCommentSubmit = () => {
let isValid = true;
//
if (!$common.isNotEmpty(comment.value)) { if (!$common.isNotEmpty(comment.value)) {
textAlert.value = '댓글을 입력하세요'; textAlert.value = '댓글을 입력해주세요.';
return false; isValid = false;
} else { } else {
textAlert.value = ''; textAlert.value = '';
} }
if (isCheck.value && !$common.isNotEmpty(password.value)) { // &
passwordAlert2.value = '비밀번호를 입력하세요'; if (isCheck.value) {
return false; if (!$common.isNotEmpty(nickname.value)) {
} else { nicknameAlert.value = '닉네임을 입력해주세요.';
passwordAlert2.value = ''; isValid = false;
} else {
nicknameAlert.value = '';
}
if (!$common.isNotEmpty(password.value)) {
passwordAlert2.value = '비밀번호를 입력해주세요.';
isValid = false;
} else {
passwordAlert2.value = '';
}
} }
//
if (!isValid) return;
// //
emit('submitComment', { emit('submitComment', {
comment: comment.value, comment: comment.value,
nickname: isCheck.value ? nickname.value : '',
password: isCheck.value ? password.value : '', password: isCheck.value ? password.value : '',
isCheck: isCheck.value, isCheck: isCheck.value,
LOCBRDTYP: isCheck.value ? '300102' : null, // '300102' LOCBRDTYP: isCheck.value ? '300102' : null, // '300102'
@ -148,15 +167,19 @@
resetCommentForm(); resetCommentForm();
}; };
// // &
const pwd2AlertHandler = () => { const pwd2AlertHandler = () => {
if (isCheck.value === false) passwordAlert2.value = ''; if (!isCheck.value) {
passwordAlert2.value = '';
nicknameAlert.value = '';
}
}; };
// //
const resetCommentForm = () => { const resetCommentForm = () => {
comment.value = ''; comment.value = '';
password.value = ''; password.value = '';
nickname.value = '';
isCheck.value = false; isCheck.value = false;
}; };

View File

@ -1,67 +1,72 @@
<template> <template>
<div class="row gx-2 mb-4"> <div class="row gx-2 mb-4">
<div class="col-4"> <div class="col-3">
<div class="ratio ratio-1x1"> <div class="ratio ratio-1x1">
<!-- 오전 반차 버튼 --> <!-- 오전 반차 버튼 -->
<button class="vac-btn vac-btn-warning rounded-circle d-flex align-items-center justify-content-center" :class="{ active: halfDayType === 'AM' }" <button class="vac-btn vac-btn-warning rounded-circle d-flex align-items-center justify-content-center"
@click="toggleHalfDay('AM')"> :class="{ active: halfDayType === 'AM' }"
<i class="bi bi-sun"></i> @click="toggleHalfDay('AM')">
</button> <i class="bi bi-sun"></i>
</button>
</div> </div>
</div> </div>
<div class="col-4"> <div class="col-3">
<div class="ratio ratio-1x1"> <div class="ratio ratio-1x1">
<!-- 오후 반차 버튼 --> <!-- 오후 반차 버튼 -->
<button class="vac-btn vac-btn-info rounded-circle d-flex align-items-center justify-content-center" :class="{ active: halfDayType === 'PM' }" <button class="vac-btn vac-btn-info rounded-circle d-flex align-items-center justify-content-center"
@click="toggleHalfDay('PM')"> :class="{ active: halfDayType === 'PM' }"
<i class="bi bi-moon"></i> @click="toggleHalfDay('PM')">
</button> <i class="bi bi-moon"></i>
</button>
</div> </div>
</div> </div>
<div class="col-4"> <div class="col-3">
<div class="ratio ratio-1x1"> <div class="ratio ratio-1x1">
<button class="vac-btn-success rounded-circle d-flex align-items-center justify-content-center" @click="addVacationRequests" <!-- 연차 버튼 -->
:class="{ active: !isDisabled, disabled: isDisabled }"> <button class="vac-btn vac-btn-primary rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'FULL' }"
</button> @click="toggleHalfDay('FULL')">
<i class="bi bi-calendar"></i>
</button>
</div>
</div>
<div class="col-3">
<div class="ratio ratio-1x1">
<!-- 저장 버튼 -->
<button class="vac-btn-success rounded-circle d-flex align-items-center justify-content-center"
@click="addVacationRequests"
:class="{ active: !isDisabled, disabled: isDisabled }">
</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineEmits, ref, defineProps, watch } from "vue"; import { defineEmits, ref, defineProps } from "vue";
const props = defineProps({ const props = defineProps({
isDisabled: Boolean, isDisabled: Boolean
selectedDate: String // props
}); });
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]); const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
const halfDayType = ref(null); const halfDayType = ref(null);
const toggleHalfDay = (type) => { const toggleHalfDay = (type) => {
halfDayType.value = halfDayType.value === type ? null : type; halfDayType.value = halfDayType.value === type ? null : type;
emit("toggleHalfDay", halfDayType.value); emit("toggleHalfDay", halfDayType.value);
}; };
// `selectedDate` //
watch(() => props.selectedDate, (newDate) => {
if (newDate) {
resetHalfDay();
}
});
//
const resetHalfDay = () => { const resetHalfDay = () => {
halfDayType.value = null; halfDayType.value = null;
emit("resetHalfDay"); emit("resetHalfDay");
}; };
const addVacationRequests = () => { const addVacationRequests = () => {
emit("addVacationRequests"); emit("addVacationRequests");
}; };
defineExpose({ resetHalfDay }); defineExpose({ resetHalfDay });
</script> </script>

View File

@ -5,35 +5,33 @@
type="button" type="button"
class="btn" class="btn"
:class="{ :class="{
'btn-outline-primary': selectedCategory !== 'all', 'btn-outline-primary': selectedCategory !== 'all',
'btn-primary': selectedCategory === 'all' 'btn-primary': selectedCategory === 'all'
}" }"
@click="selectCategory('all')" @click="selectCategory('all')"
> >
All All
</button> </button>
</li> </li>
<li v-for="category in lists" :key="category.value" class="mt-2 me-2"> <li v-for="category in lists" :key="category.value" class="mt-2 me-2">
<button <button
type="button" type="button"
class="btn" class="btn"
:class="{ :class="{
'btn-outline-primary': category.value !== selectedCategory, 'btn-outline-primary': category.value.toString() !== selectedCategory?.toString(),
'btn-primary': category.value === selectedCategory 'btn-primary': category.value.toString() === selectedCategory?.toString()
}" }"
@click="selectCategory(category.value)" @click="selectCategory(category.value)"
> >
{{ category.label }} {{ category.label }}
</button> </button>
</li> </li>
</ul> </ul>
</template> </template>
<script setup> <script setup>
import { defineProps, ref, watch } from 'vue'; import { defineProps, defineEmits, ref, watch } from 'vue';
// lists prop
const props = defineProps({ const props = defineProps({
lists: { lists: {
type: Array, type: Array,
@ -44,7 +42,7 @@ const props = defineProps({
required: false, required: false,
}, },
selectedCategory: { selectedCategory: {
type: [String, Number], type: [String,Number],
default: null, default: null,
required: false, required: false,
}, },
@ -63,4 +61,5 @@ watch(() => props.selectedCategory, (newVal) => {
selectedCategory.value = newVal; selectedCategory.value = newVal;
}); });
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal"> <div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
<div class="vac-modal-content p-5 modal-scroll"> <div class="vac-modal-content p-5 modal-scroll">
<h5 class="vac-modal-title">📅 연차 내역</h5> <h5 class="vac-modal-title">📅 연차 (누적 개수)</h5>
<button class="close-btn" @click="closeModal"></button> <button class="close-btn" @click="closeModal"></button>
<!-- 연차 목록 --> <!-- 연차 목록 -->
<div class="vac-modal-body" v-if="mergedVacations.length > 0"> <div class="vac-modal-body" v-if="mergedVacations.length > 0">
@ -11,9 +11,6 @@
:key="vac._expandIndex" :key="vac._expandIndex"
class="vacation-item" class="vacation-item"
> >
<span v-if="vac.category === 'used'" class="fw-bold text-dark me-2">
{{ usedVacationIndexMap[vac._expandIndex] }})
</span>
<span :class="vac.category === 'used' ? 'fw-bold text-danger me-2' : 'fw-bold text-primary me-2'"> <span :class="vac.category === 'used' ? 'fw-bold text-danger me-2' : 'fw-bold text-primary me-2'">
{{ vac.category === 'used' ? '-' : '+' }} {{ vac.category === 'used' ? '-' : '+' }}
</span> </span>
@ -22,6 +19,9 @@
> >
{{ formatDate(vac.date) }} {{ formatDate(vac.date) }}
</span> </span>
<span v-if="vac.category === 'used'" class="fw-bold text-dark ms-1">
( {{ usedVacationIndexMap[vac._expandIndex] }} )
</span>
</li> </li>
</ol> </ol>
</div> </div>

View File

@ -9,16 +9,9 @@
:formValue="item.WRDDICCAT" :formValue="item.WRDDICCAT"
:titleValue="item.WRDDICTTL" :titleValue="item.WRDDICTTL"
:contentValue="item.WRDDICCON" :contentValue="item.WRDDICCON"
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'" :isDisabled="true"
/> />
<div v-else> <div v-else>
<input
v-if="userStore.user.role == 'ROLE_ADMIN'"
type="checkbox"
class="form-check-input admin-chk"
:name="item.WRDDICSEQ"
@change="toggleCheck($event)"
>
<div class="d-flex align-ite-center"> <div class="d-flex align-ite-center">
<div class="w-100 d-flex align-items-center"> <div class="w-100 d-flex align-items-center">
<span class="btn btn-primary pe-none">{{ item.category }}</span> <span class="btn btn-primary pe-none">{{ item.category }}</span>
@ -26,41 +19,43 @@
</div> </div>
</div> </div>
<p class="mt-5 dict-content-wrap" v-html="$common.contentToHtml(item.WRDDICCON)"></p> <p class="mt-5 dict-content-wrap" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2"> <div class="d-flex align-items-start">
<div class="d-flex flex-wrap align-items-center mb-50"> <!-- 최초 작성자 -->
<div class="d-flex flex-wrap align-items-center me-4">
<div class="avatar avatar-sm me-2"> <div class="avatar avatar-sm me-2">
<img <img
class="rounded-circle user-avatar" class="rounded-circle user-avatar"
:src="getProfileImage(item.author.profileImage)" :src="getProfileImage(item.author.profileImage)"
alt="최초 작성자" alt="최초 작성자"
:style="{ borderColor: item.author.color}" :style="{ borderColor: item.author.color }"
@error="setDefaultImage" @error="setDefaultImage"
/> />
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.author.createdAt) }}</p> <p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.author.createdAt) }}</p>
</div> </div>
</div> </div>
</div>
<div <!-- 최근 작성자 (조건부) -->
v-if="item.author.createdAt !== item.lastEditor.updatedAt" <div
class="d-flex justify-content-between flex-wrap gap-2 mb-2" v-if="item.author.createdAt !== item.lastEditor.updatedAt"
> class="d-flex flex-wrap align-items-center"
<div class="d-flex flex-wrap align-items-center mb-50"> >
<div class="avatar avatar-sm me-2"> <div class="avatar avatar-sm me-2">
<img <img
class="rounded-circle user-avatar" class="rounded-circle user-avatar"
:src="getProfileImage(item.lastEditor.profileImage)" :src="getProfileImage(item.lastEditor.profileImage)"
alt="최근 작성자" alt="최근 작성자"
:style="{ borderColor: item.lastEditor.color}" :style="{ borderColor: item.lastEditor.color }"
@error="setDefaultImage" @error="setDefaultImage"
/> />
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.lastEditor.updatedAt) }}</p> <p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.lastEditor.updatedAt) }}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="edit-btn"> <div class="edit-btn">
@ -129,7 +124,7 @@ const editWord = (data) => {
if (writeButton.value) { if (writeButton.value) {
writeButton.value.resetButton(); writeButton.value.resetButton();
} }
emit('refreshWordList'); emit('refreshWordList',data.category);
} else { } else {
console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data); console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data);
toastStore.onToast('용어 수정이 정상적으로 처리되지 않았습니다.', 'e'); toastStore.onToast('용어 수정이 정상적으로 처리되지 않았습니다.', 'e');

View File

@ -1,6 +1,7 @@
<template> <template>
<div> <div v-if="dataList.length > 0">
<FormSelect class="me-5" <FormSelect
class="me-5"
name="cate" name="cate"
title="카테고리" title="카테고리"
:data="dataList" :data="dataList"
@ -14,17 +15,19 @@
<PlusBtn @click="toggleInput"/> <PlusBtn @click="toggleInput"/>
</div> </div>
</div> </div>
<div v-if="dataList.length === 0 || showInput">
<FormInput
class="me-5 parent-class "
ref="categoryInputRef"
title="새 카테고리"
:isLabel="dataList.length === 0 ?true : false"
name="새 카테고리"
@update:modelValue="addCategory = $event"
:is-cate-alert="addCategoryAlert"
@focusout="handleCategoryFocusout(addCategory)"
<div v-if="showInput"> />
<FormInput class="me-5" </div>
ref="categoryInputRef"
title="새 카테고리"
name="새 카테고리"
@update:modelValue="addCategory = $event"
:is-cate-alert="addCategoryAlert"
@focusout="handleCategoryFocusout(addCategory)"
/>
</div>
<FormInput class="me-5" <FormInput class="me-5"
title="용어" title="용어"
type="text" type="text"
@ -217,4 +220,7 @@ const handleCategoryFocusout = (value) => {
right: 0.7rem; right: 0.7rem;
top: 1.2rem; top: 1.2rem;
} }
.parent-class {
justify-content: flex-end;
}
</style> </style>

View File

@ -1,4 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@s/useAuthStore'; import { useAuthStore } from '@s/useAuthStore';
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
@ -6,7 +6,7 @@ import { useUserInfoStore } from '@s/useUserInfoStore';
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: "Home", name: 'Home',
component: () => import('@v/MainView.vue'), component: () => import('@v/MainView.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
@ -18,23 +18,23 @@ const routes = [
{ {
path: '', path: '',
name: 'BoardList', name: 'BoardList',
component: () => import('@v/board/BoardList.vue') component: () => import('@v/board/BoardList.vue'),
}, },
{ {
path: 'write', path: 'write',
component: () => import('@v/board/BoardWrite.vue') component: () => import('@v/board/BoardWrite.vue'),
}, },
{ {
path: ':id', path: ':id',
name: 'BoardDetail', name: 'BoardDetail',
component: () => import('@v/board/BoardView.vue') component: () => import('@v/board/BoardView.vue'),
}, },
{ {
path: 'edit/:id', path: 'edit/:id',
name: 'BoardEdit', name: 'BoardEdit',
component: () => import('@v/board/BoardEdit.vue') component: () => import('@v/board/BoardEdit.vue'),
} },
] ],
}, },
{ {
path: '/wordDict', path: '/wordDict',
@ -71,14 +71,13 @@ const routes = [
children: [ children: [
{ {
path: '', path: '',
component: () => import('@v/voteboard/voteBoardList.vue') component: () => import('@v/voteboard/voteBoardList.vue'),
}, },
{ {
path: 'write', path: 'write',
component: () => import('@v/voteboard/voteboardWrite.vue') component: () => import('@v/voteboard/voteboardWrite.vue'),
}, },
],
]
}, },
{ {
path: '/projectlist', path: '/projectlist',
@ -93,25 +92,37 @@ const routes = [
{ {
path: '/authorization', path: '/authorization',
component: () => import('@v/admin/TheAuthorization.vue'), component: () => import('@v/admin/TheAuthorization.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true },
}, },
{ path: "/error/400", name: "Error400", component: () => import('@v/error/Error400.vue'), meta: {layout: 'NoLayout'} },
{ path: "/error/500", name: "Error500", component: () => import('@v/error/Error500.vue'), meta: {layout: 'NoLayout'} },
{ {
path: "/:anything(.*)", path: '/error/400',
name: "Error404", component: () => import('@v/error/Error404.vue'), meta: {layout: 'NoLayout'} name: 'Error400',
component: () => import('@v/error/Error400.vue'),
meta: { layout: 'NoLayout' },
},
{
path: '/error/500',
name: 'Error500',
component: () => import('@v/error/Error500.vue'),
meta: { layout: 'NoLayout' },
},
{
path: '/:anything(.*)',
name: 'Error404',
component: () => import('@v/error/Error404.vue'),
meta: { layout: 'NoLayout' },
}, },
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: routes, routes: routes,
}) });
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore(); const authStore = useAuthStore();
await authStore.checkAuthStatus(); // 로그인 상태 확인 await authStore.checkAuthStatus(); // 로그인 상태 확인
const allowedUserId = 1; // 특정 ID (변경필요!!) const allowedUserId = 1; // 특정 ID (변경필요!!)
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const userId = userStore.user?.id ?? null; const userId = userStore.user?.id ?? null;
@ -120,9 +131,9 @@ router.beforeEach(async (to, from, next) => {
return next({ name: 'Login', query: { redirect: to.fullPath } }); return next({ name: 'Login', query: { redirect: to.fullPath } });
} }
// Authorization 페이지는 ID가 1이 아니면 접근 차단 // Authorization 페이지는 ID가 26이 아니면 접근 차단
if (to.path === "/authorization" && userId !== allowedUserId) { if (to.path === '/authorization' && userId !== allowedUserId) {
return next("/"); return next('/');
} }
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동 // 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
@ -148,7 +159,7 @@ axios.interceptors.response.use(
} }
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default router export default router;

View File

@ -12,7 +12,7 @@
<h5>{{ user.name }}</h5> <h5>{{ user.name }}</h5>
</div> </div>
<!-- 권한 토글 버튼 --> <!-- 권한 토글 버튼 -->
<label class="switch"> <label class="switch me-0">
<input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" /> <input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" />
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
@ -32,7 +32,7 @@ const users = ref([]);
const toastStore = useToastStore(); const toastStore = useToastStore();
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, ""); const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
const defaultProfile = "/img/icons/icon.png"; const defaultProfile = "/img/icons/icon.png";
const allowedUserId = 1; // ID (!!)
// //
async function fetchUsers() { async function fetchUsers() {
try { try {
@ -43,14 +43,17 @@ async function fetchUsers() {
throw new Error("올바른 데이터 형식이 아닙니다."); throw new Error("올바른 데이터 형식이 아닙니다.");
} }
// ( ) // MEMBERSEQ 1
users.value = response.data.data.map(user => ({ users.value = response.data.data
id: user.MEMBERSEQ, .filter(user => user.MEMBERSEQ !== allowedUserId) // MEMBERSEQ 1
name: user.MEMBERNAM, .map(user => ({
photo: user.MEMBERPRF ? `${baseUrl}upload/img/profile/${user.MEMBERPRF}` : defaultProfile, id: user.MEMBERSEQ,
color: user.MEMBERCOL, name: user.MEMBERNAM,
isAdmin: user.MEMBERROL === 'ROLE_ADMIN', photo: user.MEMBERPRF ? `${baseUrl}upload/img/profile/${user.MEMBERPRF}` : defaultProfile,
})); color: user.MEMBERCOL,
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
}));
} catch (error) { } catch (error) {
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e'); toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
} }

View File

@ -232,12 +232,12 @@
} catch (error) {} } catch (error) {}
}; };
// //
const fetchNoticePosts = async () => { const fetchNoticePosts = async () => {
try { try {
const { data } = await axios.get('board/notices', { 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 => ({

View File

@ -21,7 +21,7 @@
/> />
<!-- 비밀번호 입력창 (익명일 경우) --> <!-- 비밀번호 입력창 (익명일 경우) -->
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto"> <div v-if="isPassword && unknown" class="mt-3 w-px-200 ms-auto">
<div class="input-group"> <div class="input-group">
<input <input
type="password" type="password"
@ -322,6 +322,7 @@
likeCount: comment.likeCount || 0, likeCount: comment.likeCount || 0,
dislikeCount: comment.dislikeCount || 0, dislikeCount: comment.dislikeCount || 0,
profileImg: comment.profileImg || '', profileImg: comment.profileImg || '',
nickname: comment.LOCCMTNIC,
likeClicked: comment.likeClicked || false, likeClicked: comment.likeClicked || false,
dislikeClicked: comment.dislikeClicked || false, dislikeClicked: comment.dislikeClicked || false,
createdAtRaw: comment.LOCCMTRDT, // createdAtRaw: comment.LOCCMTRDT, //
@ -351,6 +352,7 @@
parentId: reply.LOCCMTPNT, // ID parentId: reply.LOCCMTPNT, // ID
content: reply.LOCCMTRPY || '내용 없음', content: reply.LOCCMTRPY || '내용 없음',
createdAtRaw: reply.LOCCMTRDT, createdAtRaw: reply.LOCCMTRDT,
nickname: reply.LOCCMTNIC,
// createdAt: formattedDate(reply.LOCCMTRDT), // createdAt: formattedDate(reply.LOCCMTRDT),
//createdAtRaw: new Date(reply.LOCCMTUDT), //createdAtRaw: new Date(reply.LOCCMTUDT),
createdAt: formattedDate(reply.LOCCMTUDT) + (reply.LOCCMTUDT !== reply.LOCCMTRDT ? ' (수정됨)' : ''), createdAt: formattedDate(reply.LOCCMTUDT) + (reply.LOCCMTUDT !== reply.LOCCMTRDT ? ' (수정됨)' : ''),
@ -413,6 +415,7 @@
LOCCMTRPY: comment, LOCCMTRPY: comment,
LOCCMTPWD: isCheck ? password : '', LOCCMTPWD: isCheck ? password : '',
LOCCMTPNT: 1, LOCCMTPNT: 1,
LOCCMTNIC: data.isCheck ? data.nickname : null,
LOCBRDTYP: isCheck ? '300102' : null, LOCBRDTYP: isCheck ? '300102' : null,
}); });
@ -432,6 +435,7 @@
LOCCMTRPY: reply.comment, LOCCMTRPY: reply.comment,
LOCCMTPWD: reply.password || null, LOCCMTPWD: reply.password || null,
LOCCMTPNT: reply.parentId, LOCCMTPNT: reply.parentId,
LOCCMTNIC: data.isCheck ? data.nickname : null,
LOCBRDTYP: reply.isCheck ? '300102' : null, LOCBRDTYP: reply.isCheck ? '300102' : null,
}); });

View File

@ -106,6 +106,7 @@ const isGrantModalOpen = ref(false);
const fullCalendarRef = ref(null); const fullCalendarRef = ref(null);
const calendarEvents = ref([]); const calendarEvents = ref([]);
const selectedDates = ref(new Map()); const selectedDates = ref(new Map());
const halfDayType = ref(null); const halfDayType = ref(null);
const vacationCodeMap = ref({}); const vacationCodeMap = ref({});
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
@ -118,7 +119,6 @@ const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"
// ref // ref
const calendarDatepicker = ref(null); const calendarDatepicker = ref(null);
let fpInstance = null; let fpInstance = null;
/* 변경사항 여부 확인 */ /* 변경사항 여부 확인 */
const hasChanges = computed(() => { const hasChanges = computed(() => {
return ( return (
@ -173,40 +173,53 @@ function handleDateClick(info) {
return; return;
} }
const isMyVacation = myVacations.value.some(vac => { //
const vacDate = vac.date ? vac.date.substring(0, 10) : ""; const currentValue = selectedDates.value.get(clickedDateStr);
return vacDate === clickedDateStr && !vac.receiverId;
});
if (isMyVacation) { const isMyVacation = myVacations.value.some(vac => vac.date.substring(0, 10) === clickedDateStr && !vac.receiverId);
if (selectedDates.value.get(clickedDateStr) === "delete") {
selectedDates.value.delete(clickedDateStr); //
if (currentValue && currentValue !== "delete") {
console.log("🛑 활성화된 날짜 비활성화:", clickedDateStr);
selectedDates.value.delete(clickedDateStr);
updateCalendarEvents();
return;
}
// -
if (!halfDayType.value) {
if (isMyVacation) {
if (currentValue === "delete") {
selectedDates.value.delete(clickedDateStr);
} else {
selectedDates.value.set(clickedDateStr, "delete");
}
} else { } else {
selectedDates.value.set(clickedDateStr, "delete"); selectedDates.value.set(clickedDateStr, "700103");
} }
updateCalendarEvents(); updateCalendarEvents();
return; return;
} }
if (selectedDates.value.has(clickedDateStr)) { // -
selectedDates.value.delete(clickedDateStr); if (isMyVacation) {
updateCalendarEvents(); console.log("🗑 기존 휴가 삭제 후 새로운 상태 추가:", clickedDateStr);
return; selectedDates.value.set(clickedDateStr, "delete");
} }
const type = halfDayType.value
? (halfDayType.value === "AM" ? "700101" : "700102") const type = halfDayType.value === "AM" ? "700101" :
: "700103"; halfDayType.value === "PM" ? "700102" :
"700103"; //
selectedDates.value.set(clickedDateStr, type); selectedDates.value.set(clickedDateStr, type);
if (halfDayType.value) { // ()
halfDayType.value = null; halfDayType.value = null;
}
updateCalendarEvents();
if (halfDayButtonsRef.value) { if (halfDayButtonsRef.value) {
halfDayButtonsRef.value.resetHalfDay(); halfDayButtonsRef.value.resetHalfDay();
} }
updateCalendarEvents();
} }
function markClickableDates() { function markClickableDates() {

View File

@ -31,6 +31,7 @@
<!-- 단어 목록 --> <!-- 단어 목록 -->
<ul v-if="total > 0" class="ms-3 list-unstyled"> <ul v-if="total > 0" class="ms-3 list-unstyled">
<DictCard <DictCard
class="DictCard"
v-for="item in wordList" v-for="item in wordList"
:key="item.WRDDICSEQ" :key="item.WRDDICSEQ"
:item="item" :item="item"
@ -112,7 +113,8 @@
writeStore.closeAll(); writeStore.closeAll();
}); });
const refreshWordList = () => { const refreshWordList = (category) => {
selectedCategory.value = category;
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value); getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
}; };
@ -193,6 +195,7 @@
sendWordRequest(category, wordData, newCodName); sendWordRequest(category, wordData, newCodName);
}; };
const sendWordRequest = (category, wordData, data) => { const sendWordRequest = (category, wordData, data) => {
console.log(category,'category')
const payload = { const payload = {
WRDDICCAT: category, WRDDICCAT: category,
WRDDICTTL: wordData.title, WRDDICTTL: wordData.title,
@ -206,9 +209,9 @@
if (writeButton.value) { if (writeButton.value) {
writeButton.value.resetButton(); writeButton.value.resetButton();
} }
getwordList(); selectedCategory.value = category;
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
getIndex(); getIndex();
selectedCategory.value = 'all';
if(res.data.data == '2'){ if(res.data.data == '2'){
const newCategory = { label: data, value: category }; const newCategory = { label: data, value: category };
cateList.value = [...cateList.value,newCategory]; cateList.value = [...cateList.value,newCategory];
@ -280,4 +283,9 @@
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem !important;
} }
} }
.DictCard {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
</style> </style>