Merge branch 'main' into commuters

This commit is contained in:
yoon 2025-02-25 14:21:05 +09:00
commit 56fc9615de
17 changed files with 608 additions and 348 deletions

View File

@ -2,116 +2,121 @@
* Main * Main
*/ */
'use strict' (function () {
// Initialize menu
//-----------------
let menu, animate;
<<<<<<< HEAD
var menu, animate; var menu, animate;
(function () { (function () {
// Initialize menu // Initialize menu
//----------------- //-----------------
=======
let layoutMenuEl = document.querySelectorAll('#layout-menu');
layoutMenuEl.forEach(function (element) {
menu = new Menu(element, {
orientation: 'vertical',
closeChildren: false,
});
// Change parameter to true if you want scroll animation
window.Helpers.scrollToActive((animate = false));
window.Helpers.mainMenu = menu;
});
>>>>>>> board-comment
let layoutMenuEl = document.querySelectorAll('#layout-menu') // Initialize menu togglers and bind click on each
layoutMenuEl.forEach(function (element) { let menuToggler = document.querySelectorAll('.layout-menu-toggle');
menu = new Menu(element, { menuToggler.forEach(item => {
orientation: 'vertical', item.addEventListener('click', event => {
closeChildren: false, event.preventDefault();
}) window.Helpers.toggleCollapsed();
// Change parameter to true if you want scroll animation });
window.Helpers.scrollToActive((animate = false)) });
window.Helpers.mainMenu = menu
})
// Initialize menu togglers and bind click on each // Display menu toggle (layout-menu-toggle) on hover with delay
let menuToggler = document.querySelectorAll('.layout-menu-toggle') let delay = function (elem, callback) {
menuToggler.forEach((item) => { let timeout = null;
item.addEventListener('click', (event) => { elem.onmouseenter = function () {
event.preventDefault() // Set timeout to be a timer which will invoke callback after 300ms (not for small screen)
window.Helpers.toggleCollapsed() if (!Helpers.isSmallScreen()) {
}) timeout = setTimeout(callback, 300);
}) } else {
timeout = setTimeout(callback, 0);
}
};
// Display menu toggle (layout-menu-toggle) on hover with delay elem.onmouseleave = function () {
let delay = function (elem, callback) { // Clear any timers set to timeout
let timeout = null document.querySelector('.layout-menu-toggle').classList.remove('d-block');
elem.onmouseenter = function () { clearTimeout(timeout);
// Set timeout to be a timer which will invoke callback after 300ms (not for small screen) };
if (!Helpers.isSmallScreen()) { };
timeout = setTimeout(callback, 300) if (document.getElementById('layout-menu')) {
} else { delay(document.getElementById('layout-menu'), function () {
timeout = setTimeout(callback, 0) // not for small screen
} if (!Helpers.isSmallScreen()) {
document.querySelector('.layout-menu-toggle').classList.add('d-block');
}
});
} }
elem.onmouseleave = function () { // Display in main menu when menu scrolls
// Clear any timers set to timeout let menuInnerContainer = document.getElementsByClassName('menu-inner'),
document.querySelector('.layout-menu-toggle').classList.remove('d-block') menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0];
clearTimeout(timeout) if (menuInnerContainer.length > 0 && menuInnerShadow) {
menuInnerContainer[0].addEventListener('ps-scroll-y', function () {
if (this.querySelector('.ps__thumb-y').offsetTop) {
menuInnerShadow.style.display = 'block';
} else {
menuInnerShadow.style.display = 'none';
}
});
} }
}
if (document.getElementById('layout-menu')) {
delay(document.getElementById('layout-menu'), function () {
// not for small screen
if (!Helpers.isSmallScreen()) {
document.querySelector('.layout-menu-toggle').classList.add('d-block')
}
})
}
// Display in main menu when menu scrolls // Init helpers & misc
let menuInnerContainer = document.getElementsByClassName('menu-inner'), // --------------------
menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0]
if (menuInnerContainer.length > 0 && menuInnerShadow) {
menuInnerContainer[0].addEventListener('ps-scroll-y', function () {
if (this.querySelector('.ps__thumb-y').offsetTop) {
menuInnerShadow.style.display = 'block'
} else {
menuInnerShadow.style.display = 'none'
}
})
}
// Init helpers & misc // Init BS Tooltip
// -------------------- const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Init BS Tooltip // Accordion active class
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) const accordionActiveFunction = function (e) {
tooltipTriggerList.map(function (tooltipTriggerEl) { if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') {
return new bootstrap.Tooltip(tooltipTriggerEl) e.target.closest('.accordion-item').classList.add('active');
}) } else {
e.target.closest('.accordion-item').classList.remove('active');
}
};
// Accordion active class const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion'));
const accordionActiveFunction = function (e) { const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') { accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction);
e.target.closest('.accordion-item').classList.add('active') accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction);
} else { });
e.target.closest('.accordion-item').classList.remove('active')
// Auto update layout based on screen size
window.Helpers.setAutoUpdate(true);
// Toggle Password Visibility
window.Helpers.initPasswordToggle();
// Speech To Text
window.Helpers.initSpeechToText();
// Manage menu expanded/collapsed with templateCustomizer & local storage
//------------------------------------------------------------------
// If current layout is horizontal OR current window screen is small (overlay menu) than return from here
if (window.Helpers.isSmallScreen()) {
return;
} }
}
const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion')) // If current layout is vertical and current window screen is > small
const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction)
accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction)
})
// Auto update layout based on screen size // Auto update menu collapsed/expanded based on the themeConfig
window.Helpers.setAutoUpdate(true) window.Helpers.setCollapsed(true, false);
})();
// Toggle Password Visibility
window.Helpers.initPasswordToggle()
// Speech To Text
window.Helpers.initSpeechToText()
// Manage menu expanded/collapsed with templateCustomizer & local storage
//------------------------------------------------------------------
// If current layout is horizontal OR current window screen is small (overlay menu) than return from here
if (window.Helpers.isSmallScreen()) {
return
}
// If current layout is vertical and current window screen is > small
// Auto update menu collapsed/expanded based on the themeConfig
window.Helpers.setCollapsed(true, false)
})()

View File

@ -15,35 +15,69 @@ import Quill from 'quill';
$common.변수 $common.변수
*/ */
const common = { const common = {
// JSON 문자열로 Delta 타입을 변환 // JSON 문자열로 Delta 타입을 변환
contentToHtml(content) { contentToHtml(content) {
try { try {
if (content.startsWith('{') || content.startsWith('[')) { if (content.startsWith('{') || content.startsWith('[')) {
// Delta 형식으로 변환 // Delta 형식으로 변환
const delta = JSON.parse(content); const delta = JSON.parse(content);
const quill = new Quill(document.createElement('div')); const quill = new Quill(document.createElement('div'));
quill.setContents(delta); quill.setContents(delta);
return quill.root.innerHTML; // HTML 반환 return quill.root.innerHTML; // HTML 반환
} }
return content; // 이미 HTML일 경우 그대로 반환 return content; // 이미 HTML일 경우 그대로 반환
} catch (error) { } catch (error) {
console.error('콘텐츠 변환 오류:', error); console.error('콘텐츠 변환 오류:', error);
return content; // 오류 발생 시 원본 반환 return content; // 오류 발생 시 원본 반환
} }
}, },
// Delta 타입을 JSON 문자열로 변환 // Delta 타입을 JSON 문자열로 변환
deltaAsJson(content) { deltaAsJson(content) {
if (content && content.ops) { if (content && content.ops) {
return JSON.stringify(content.ops); // Delta 객체에서 ops 속성만 JSON 문자열로 변환 return JSON.stringify(content.ops); // Delta 객체에서 ops 속성만 JSON 문자열로 변환
} }
console.error('잘못된 Delta 객체:', content); console.error('잘못된 Delta 객체:', content);
return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환 return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환
} },
} /**
* Date 타입 문자열 포멧팅
*
* @param {string} dateStr
* @return
* 1. Date type 경우 예시 '25-02-24 12:02'
* 2. Date type 아닌 경우 입력값 리턴
*
*/
dateFormatter(dateStr) {
const date = new Date(dateStr);
const dateCheck = date.getTime();
if (isNaN(dateCheck)) {
return dateStr;
} else {
const { year, month, day, hours, minutes } = this.formatDateTime(date);
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
},
formatDateTime(date) {
const zeroFormat = num => (num < 10 ? `0${num}` : num);
return {
year: date.getFullYear(),
month: zeroFormat(date.getMonth() + 1),
day: zeroFormat(date.getDate()),
hours: zeroFormat(date.getHours()),
minutes: zeroFormat(date.getMinutes()),
seconds: zeroFormat(date.getSeconds()),
};
},
};
export default { export default {
install(app) { install(app) {
app.config.globalProperties.$common = common; app.config.globalProperties.$common = common;
} app.provide('common', common);
},
}; };

View File

@ -0,0 +1,13 @@
/** 날짜 포맷1 (YYYY-MM-DD HH:MM) */
export const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음";
const dateObj = new Date(dateString);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;
};
/** 날짜 포맷2 (YYYY-MM-DD) */
export const formatDate = (dateString) => {
if (!dateString) return "날짜 없음";
const dateObj = new Date(dateString);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')}`;
};

View File

@ -2,18 +2,20 @@
<div> <div>
<BoardProfile <BoardProfile
:unknown="unknown" :unknown="unknown"
:isCommentAuthor="isCommentAuthor"
:boardId="comment.boardId" :boardId="comment.boardId"
:profileName="comment.author" :profileName="comment.author"
:date="comment.createdAt" :date="comment.createdAt"
:comment="comment" :comment="comment"
:showDetail="false" :showDetail="false"
:author="true"
:isLike="!isLike" :isLike="!isLike"
:isCommentPassword="comment.isCommentPassword" :isCommentPassword="comment.isCommentPassword"
:isCommentProfile="true"
@editClick="$emit('editClick', comment)" @editClick="$emit('editClick', comment)"
@deleteClick="$emit('deleteClick', comment)" @deleteClick="$emit('deleteClick', comment)"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
<!-- :author="true" -->
<!-- 댓글 비밀번호 입력창 (익명일 경우) --> <!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="isCommentPassword && unknown" class="mt-3 w-25 ms-auto"> <div v-if="isCommentPassword && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group"> <div class="input-group">
@ -27,6 +29,8 @@
</div> </div>
<span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span> <span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span>
</div> </div>
<p>authorId:{{ comment.authorId }}</p>
<p>코멘트 비교: {{comment.isCommentAuthor}}</p>
<div class="mt-6"> <div class="mt-6">
@ -77,9 +81,13 @@ const props = defineProps({
required: true, required: true,
}, },
unknown: { unknown: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
isCommentAuthor: {
type: Boolean,
default: false,
},
isPlusButton: { isPlusButton: {
type: Boolean, type: Boolean,
default: true, default: true,

View File

@ -43,7 +43,7 @@
class="form-control flex-grow-1" class="form-control flex-grow-1"
v-model="password" v-model="password"
/> />
<!-- <span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span> --> <span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
</div> </div>
</div> </div>
@ -70,6 +70,10 @@ const props = defineProps({
parentId: { parentId: {
type: Number, type: Number,
default: 0 default: 0
},
passwordAlert: {
type: String,
default: false
} }
}); });

View File

@ -8,6 +8,7 @@
<BoardComment <BoardComment
:unknown="unknown" :unknown="unknown"
:comment="comment" :comment="comment"
:isCommentAuthor="comment.isCommentAuthor"
:isCommentPassword="comment.isCommentPassword" :isCommentPassword="comment.isCommentPassword"
:isEditTextarea="comment.isEditTextarea" :isEditTextarea="comment.isEditTextarea"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert"
@ -38,6 +39,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
isCommentAuthor: {
type: Boolean,
default: false,
},
isCommentPassword: { isCommentPassword: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -52,7 +57,7 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'submitPassword', 'clearPassword']); const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword']);
const submitComment = (replyData) => { const submitComment = (replyData) => {
emit('submitComment', replyData); emit('submitComment', replyData);

View File

@ -22,7 +22,8 @@
<!-- 버튼 영역 --> <!-- 버튼 영역 -->
<div class="ms-auto text-end"> <div class="ms-auto text-end">
<!-- 수정, 삭제 버튼 --> <!-- 수정, 삭제 버튼 -->
<template v-if="author || showDetail"> <!-- <template v-if="isAuthor || showDetail"> -->
<template v-if="isCommentProfile ? isCommentAuthor : isAuthor">
<EditButton @click.stop="editClick" /> <EditButton @click.stop="editClick" />
<DeleteButton @click.stop="deleteClick" /> <DeleteButton @click.stop="deleteClick" />
</template> </template>
@ -49,7 +50,7 @@ import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
const props = defineProps({ const props = defineProps({
comment: { comment: {
type: Object, type: Object,
required: true, required: false,
}, },
boardId: { boardId: {
type: Number, type: Number,
@ -61,7 +62,11 @@ const props = defineProps({
}, },
profileName: { profileName: {
type: String, type: String,
<<<<<<< HEAD
default: '익명', default: '익명',
=======
default: '',
>>>>>>> board-comment
}, },
unknown: { unknown: {
type: Boolean, type: Boolean,
@ -72,10 +77,12 @@ const props = defineProps({
default: true, default: true,
}, },
// : // :
author: { isAuthor: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCommentAuthor: Boolean, //
isCommentProfile: Boolean, //
date: { date: {
type: String, type: String,
required: true, required: true,
@ -98,6 +105,7 @@ const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
// //
const editClick = () => { const editClick = () => {
console.log('클릭 확인')
emit('editClick', props.unknown); emit('editClick', props.unknown);
}; };

View File

@ -14,10 +14,15 @@
:placeholder="title" :placeholder="title"
:disabled="disabled" :disabled="disabled"
:min="min" :min="min"
@focusout="$emit('focusout', modelValue)"
/> />
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''"> <div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">
{{ title }} 확인해주세요. {{ title }} 확인해주세요.
</div> </div>
<!-- 카테고리 중복 -->
<div class="invalid-feedback" :class="isCateAlert ? 'display-block' : ''">
카테고리 중복입니다.
</div>
</div> </div>
</div> </div>
</template> </template>
@ -57,6 +62,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCateAlert : {
type :Boolean,
default: false,
},
isLabel : { isLabel : {
type: Boolean, type: Boolean,
default: true, default: true,
@ -73,7 +82,7 @@ const props = defineProps({
}); });
// Emits // Emits
const emits = defineEmits(['update:modelValue']); const emits = defineEmits(['update:modelValue', 'focusout']);
// `inputValue` // `inputValue`
const inputValue = ref(props.modelValue); const inputValue = ref(props.modelValue);

View File

@ -41,6 +41,7 @@
<script setup> <script setup>
import { defineProps, defineEmits, computed } from "vue"; import { defineProps, defineEmits, computed } from "vue";
import { formatDate } from '@/common/formattedDate.js';
const props = defineProps({ const props = defineProps({
isOpen: Boolean, isOpen: Boolean,
@ -130,14 +131,6 @@ const mergedVacations = computed(() => {
return all; return all;
}); });
/** 날짜 포맷 (YYYY-MM-DD) */
const formatDate = (dateString) => {
if (!dateString) return "";
// dateString "YYYY-MM-DD"
// "YYYY-MM-DD..." 10
return dateString.substring(0, 10);
};
/** 모달 닫기 */ /** 모달 닫기 */
const closeModal = () => { const closeModal = () => {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card-body d-flex justify-content-center"> <div class="card-body d-flex justify-content-center m-n5">
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2"> <ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
<li <li
v-for="(user, index) in sortedUserList" v-for="(user, index) in sortedUserList"

View File

@ -4,12 +4,12 @@
v-if="isWriteVisible" v-if="isWriteVisible"
@close="isWriteVisible = false" @close="isWriteVisible = false"
:dataList="cateList" :dataList="cateList"
@addCategory="addCategory"
@addWord="editWord" @addWord="editWord"
:NumValue="item.WRDDICSEQ" :NumValue="item.WRDDICSEQ"
:formValue="item.WRDDICCAT" :formValue="item.WRDDICCAT"
:titleValue="item.WRDDICTTL" :titleValue="item.WRDDICTTL"
:contentValue="item.WRDDICCON" :contentValue="item.WRDDICCON"
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
/> />
<div v-else> <div v-else>
@ -38,7 +38,7 @@
:style="{ borderColor: item.author.color}"/> :style="{ borderColor: item.author.color}"/>
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p> <p class="mb-0 small fw-medium">{{ formattedDate(item.author.createdAt) }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -55,13 +55,13 @@
:style="{ borderColor: item.lastEditor.color}"/> :style="{ borderColor: item.lastEditor.color}"/>
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p> <p class="mb-0 small fw-medium">{{ formattedDate(item.lastEditor.updatedAt) }}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="edit-btn"> <div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
<EditBtn @click="toggleWriteVisible" /> <EditBtn @click="toggleWriteVisible" />
</div> </div>
</li> </li>
@ -74,6 +74,7 @@ import { ref, toRefs, getCurrentInstance, } from 'vue';
import EditBtn from '@/components/button/EditBtn.vue'; import EditBtn from '@/components/button/EditBtn.vue';
import $api from '@api'; import $api from '@api';
import DictWrite from './DictWrite.vue'; import DictWrite from './DictWrite.vue';
import { formattedDate } from "@/common/formattedDate";
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
@ -98,9 +99,9 @@ const props = defineProps({
}); });
// //
const localCateList = ref([...props.cateList]); // const localCateList = ref([...props.cateList]);
// //
const selectedCategory = ref(''); // const selectedCategory = ref('');
// cateList emit // cateList emit
const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']); const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']);
@ -115,41 +116,41 @@ const toggleWriteVisible = () => {
}; };
// //
const addCategory = (data) => { // const addCategory = (data) => {
try { // try {
const lastCategory = localCateList.value.length > 0 // const lastCategory = localCateList.value.length > 0
? localCateList.value[localCateList.value.length - 1] // ? localCateList.value[localCateList.value.length - 1]
: null; // : null;
const newValue = lastCategory ? lastCategory.value + 1 : 600101; // const newValue = lastCategory ? lastCategory.value + 1 : 600101;
// console.log('lastCategory', lastCategory); // // // console.log('lastCategory', lastCategory);
// console.log('newValue', newValue); // // // console.log('newValue', newValue);
axios.post('worddict/insertCategory', { // axios.post('worddict/insertCategory', {
CMNCODNAM: data // CMNCODNAM: data
}).then(res => { // }).then(res => {
if(res.data.data === 1){ // if(res.data.data === 1){
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's'); // toastStore.onToast(' .', 's');
const newCategory = { label: data, value: newValue }; // const newCategory = { label: data, value: newValue };
localCateList.value = [newCategory, ...localCateList.value]; // localCateList.value = [newCategory, ...localCateList.value];
selectedCategory.value = newCategory.value; // selectedCategory.value = newCategory.value;
// console.log('newCategory', newCategory); // // // console.log('newCategory', newCategory);
// console.log('localCateList.value', localCateList.value); // // // console.log('localCateList.value', localCateList.value);
// console.log('selectedCategory.value', selectedCategory.value); // // // console.log('selectedCategory.value', selectedCategory.value);
// // // //
emit('update:cateList', localCateList.value); // emit('update:cateList', localCateList.value);
} else if(res.data.message == '이미 존재하는 카테고리명입니다.') { // } else if(res.data.message == ' .') {
toastStore.onToast(res.data.message, 'e'); // toastStore.onToast(res.data.message, 'e');
} // }
}).catch(err => { // }).catch(err => {
console.error('카테고리 추가 중 오류:', err); // console.error(' :', err);
}); // });
} catch (err) { // } catch (err) {
console.error('카테고리 추가 함수 오류:', err); // console.error(' :', err);
} // }
} // }
// //
@ -191,11 +192,14 @@ const editWord = (data) => {
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
// //
const formatDate = (dateString) => new Date(dateString).toLocaleString(); // const formatDate = (dateString) => new Date(dateString).toLocaleString();
// //
const getProfileImage = (imagePath) => const defaultProfile = "/img/icons/icon.png";
imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg';
const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
};
// //

View File

@ -13,20 +13,27 @@
:disabled="isDisabled" :disabled="isDisabled"
/> />
</div> </div>
<div class="col-2 btn-margin"> <div class="col-2 btn-margin" v-if="!isDisabled">
<PlusBtn v-if="userStore.user.role == 'ROLE_ADMIN'" @click="toggleInput"/> <PlusBtn @click="toggleInput"/>
</div> </div>
</div> </div>
<div class="row" v-if="showInput"> <div class="row" v-if="showInput">
<div class="col-10"> <div class="col-10">
<FormInput title="카테고리 입력" name="카테고리" @update:modelValue="addCategory = $event" :is-alert="addCategoryAlert"/> <FormInput
ref="categoryInputRef"
title="카테고리 입력"
name="카테고리"
@update:modelValue="addCategory = $event"
:is-cate-alert="addCategoryAlert"
@focusout="handleCategoryFocusout(addCategory)"
/>
</div> </div>
<div class="col-2 btn-margin"> <!-- <div class="col-2 btn-margin">
<button class="btn btn-primary btn-icon" @click="saveInput"> <button class="btn btn-primary btn-icon" @click="saveInput">
<i class="bx bx-check"></i> <i class="bx bx-check"></i>
</button> </button>
</div> </div> -->
</div> </div>
</div> </div>
<div class="dict-w"> <div class="dict-w">
@ -58,13 +65,13 @@ import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue'; import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue'; import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue'; import PlusBtn from '../button/PlusBtn.vue';
import { useUserInfoStore } from '@s/useUserInfoStore'; // import { useUserInfoStore } from '@s/useUserInfoStore';
// //
const userStore = useUserInfoStore(); // const userStore = useUserInfoStore();
// disabled // disabled
const isDisabled = computed(() => userStore.user.role !== 'ROLE_ADMIN'); // const isDisabled = computed(() => userStore.user.role !== 'ROLE_ADMIN');
const emit = defineEmits(['close','addCategory','addWord']); const emit = defineEmits(['close','addCategory','addWord']);
@ -92,6 +99,9 @@ const selectedCategory = computed(() =>
selectCategory.value === '' ? props.formValue : selectCategory.value selectCategory.value === '' ? props.formValue : selectCategory.value
); );
// ref
const categoryInputRef = ref(null);
const props = defineProps({ const props = defineProps({
dataList: { dataList: {
type: Array, type: Array,
@ -106,7 +116,11 @@ const props = defineProps({
titleValue : { titleValue : {
type:String, type:String,
},contentValue : { },contentValue : {
type:String type:String,
},
isDisabled: {
type: Boolean,
default: false
} }
}); });
@ -120,17 +134,17 @@ const toggleInput = () => {
// //
const saveInput = () => { // const saveInput = () => {
if(addCategory.value == ''){ // if(addCategory.value == ''){
addCategoryAlert.value = true; // addCategoryAlert.value = true;
return; // return;
}else { // }else {
addCategoryAlert.value = false; // addCategoryAlert.value = false;
} // }
// console.log(' !',addCategory.value); // console.log(' !',addCategory.value);
emit('addCategory', addCategory.value); // emit('addCategory', addCategory.value);
// showInput.value = false; // showInput.value = false;
}; // };
const onChange = (newValue) => { const onChange = (newValue) => {
selectCategory.value = newValue.target.value; selectCategory.value = newValue.target.value;
@ -138,18 +152,12 @@ const onChange = (newValue) => {
// //
const saveWord = () => { const saveWord = () => {
// if(addCategory.value == ''){
// addCategoryAlert.value = true;
// return;
// }else {
// addCategoryAlert.value = false;
// }
//validation //validation
console.log('computedTitle.value', computedTitle.value);
// //
if(computedTitle.value == '' || computedTitle.length == 0){ if(computedTitle.value == undefined){
wordTitleAlert.value = true; wordTitleAlert.value = true;
return; return;
} }
@ -166,8 +174,35 @@ const saveWord = () => {
category: selectedCategory.value, category: selectedCategory.value,
content: content.value, content: content.value,
}; };
emit('addWord', wordData ,addCategory.value );
emit('addWord', wordData, addCategory.value);
}
// focusout
const handleCategoryFocusout = (value) => {
const existingCategory = props.dataList.find(item => item.label === value);
// console.log('existingCategory', existingCategory);
if (existingCategory) {
// console.log(' :', value);
addCategoryAlert.value = true;
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
} else {
addCategoryAlert.value = false;
}
}; };
</script> </script>
<style scoped> <style scoped>

View File

@ -24,14 +24,15 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import TheTop from './TheTop.vue'; import TheTop from './TheTop.vue';
import TheFooter from './TheFooter.vue'; import TheFooter from './TheFooter.vue';
import TheMenu from './TheMenu.vue'; import TheMenu from './TheMenu.vue';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { wait } from '@/common/utils'; import { wait } from '@/common/utils';
window.isDarkStyle = window.Helpers.isDarkStyle(); window.isDarkStyle = window.Helpers.isDarkStyle();
<<<<<<< HEAD
const loadScript = src => { const loadScript = src => {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = src; script.src = src;
@ -44,6 +45,25 @@ nextTick(async () => {
loadScript('/vendor/js/menu.js'); loadScript('/vendor/js/menu.js');
// loadScript('/js/main.js'); // loadScript('/js/main.js');
}); });
=======
const loadScript = src => {
const script = document.createElement('script');
script.src = src;
script.type = 'text/javascript';
script.async = true;
document.body.appendChild(script);
// script.onload = () => {
// console.log(`${src} loaded successfully.`);
// };
script.onerror = () => {
console.error(`Failed to load script: ${src}`);
};
};
nextTick(async () => {
await wait(200);
loadScript('/vendor/js/menu.js');
loadScript('/js/main.js');
});
>>>>>>> board-comment
</script> </script>
<style> <style></style>
</style>

View File

@ -152,7 +152,7 @@
<!-- User --> <!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown"> <li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/> <img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-px-40 rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>

View File

@ -10,14 +10,14 @@
:boardId="currentBoardId" :boardId="currentBoardId"
:profileName="profileName" :profileName="profileName"
:unknown="unknown" :unknown="unknown"
:author="isAuthor"
:views="views" :views="views"
:commentNum="commentNum" :commentNum="commentNum"
:date="formattedBoardDate" :date="formattedBoardDate"
:isLike="false" :isLike="false"
:isAuthor="isAuthor"
@editClick="editClick" @editClick="editClick"
@deleteClick="deleteClick" @deleteClick="deleteClick"
/> />
<!-- 비밀번호 입력창 (익명일 경우) --> <!-- 비밀번호 입력창 (익명일 경우) -->
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto"> <div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto">
@ -74,8 +74,14 @@
:likeClicked="likeClicked" :likeClicked="likeClicked"
:dislikeClicked="dislikeClicked" :dislikeClicked="dislikeClicked"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
</div> </div>
<p>현재 로그인한 사용자 ID: {{ currentUserId }}</p>
<p>게시글 작성자: {{ authorId }}</p>
<p>isAuthor : {{ isAuthor }}</p>
<!-- <p>use이미지:{{userStore.user.img}}</p> -->
<!-- <img :src="`http://localhost:10325/upload/img/profile/${userStore.user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle"/> -->
<!-- 첨부파일 목록 --> <!-- 첨부파일 목록 -->
<!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled"> <!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
@ -88,6 +94,7 @@
<BoardCommentArea <BoardCommentArea
:profileName="profileName" :profileName="profileName"
:unknown="unknown" :unknown="unknown"
:passwordAlert="passwordAlert"
@submitComment="handleCommentSubmit" @submitComment="handleCommentSubmit"
/> />
<!-- <BoardCommentArea :profileName="profileName" :unknown="unknown" /> --> <!-- <BoardCommentArea :profileName="profileName" :unknown="unknown" /> -->
@ -97,7 +104,7 @@
<div class="card-footer"> <div class="card-footer">
<BoardCommentList <BoardCommentList
:unknown="unknown" :unknown="unknown"
:comments="comments" :comments="commentsWithAuthStatus"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
:isEditTextarea="isEditTextarea" :isEditTextarea="isEditTextarea"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert"
@ -130,6 +137,7 @@ import BoardRecommendBtn from '@c/button/BoardRecommendBtn.vue';
import Pagination from '@c/pagination/Pagination.vue'; import Pagination from '@c/pagination/Pagination.vue';
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import axios from '@api'; import axios from '@api';
// //
@ -148,12 +156,28 @@ const comments = ref([]);
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserInfoStore();
const currentBoardId = ref(Number(route.params.id)); const currentBoardId = ref(Number(route.params.id));
<<<<<<< HEAD
const unknown = computed(() => profileName.value === '익명'); const unknown = computed(() => profileName.value === '익명');
const currentUserId = ref('김자바'); // id const currentUserId = ref('김자바'); // id
const authorId = ref(null); // id const authorId = ref(null); // id
=======
// const unknown = computed(() => profileName.value === '');
const currentUserId = computed(() => userStore.user.id); // id
const authorId = ref(''); // id
>>>>>>> board-comment
const isAuthor = computed(() => currentUserId.value === authorId.value); const isAuthor = computed(() => currentUserId.value === authorId.value);
// const isCommentAuthor =
const commentsWithAuthStatus = computed(() => {
const updatedComments = comments.value.map(comment => ({
...comment,
isCommentAuthor: comment.authorId === currentUserId.value
}));
// console.log(" commentsWithAuthStatus :", updatedComments);
return updatedComments;
});
const password = ref(''); const password = ref('');
@ -187,9 +211,12 @@ const fetchBoardDetails = async () => {
const response = await axios.get(`board/${currentBoardId.value}`); const response = await axios.get(`board/${currentBoardId.value}`);
const data = response.data.data; const data = response.data.data;
console.log(data)
// API // API
// const boardDetail = data.boardDetail || {}; // const boardDetail = data.boardDetail || {};
<<<<<<< HEAD
profileName.value = data.author || '익명'; profileName.value = data.author || '익명';
// //
@ -198,6 +225,15 @@ const fetchBoardDetails = async () => {
// : // :
authorId.value = data.author; authorId.value = data.author;
=======
profileName.value = data.author;
//
// profileName.value = '';
authorId.value = data.authorId; // id
>>>>>>> board-comment
boardTitle.value = data.title || '제목 없음'; boardTitle.value = data.title || '제목 없음';
boardContent.value = data.content || ''; boardContent.value = data.content || '';
date.value = data.date || ''; date.value = data.date || '';
@ -262,7 +298,7 @@ const handleCommentReaction = async ({ boardId, commentId, isLike, isDislike })
} }
}; };
// ( ) //
const fetchComments = async (page = 1) => { const fetchComments = async (page = 1) => {
try { try {
// //
@ -273,11 +309,20 @@ const fetchComments = async (page = 1) => {
} }
}); });
<<<<<<< HEAD
=======
console.log(response.data.data)
>>>>>>> board-comment
const commentsList = response.data.data.list.map(comment => ({ const commentsList = response.data.data.list.map(comment => ({
commentId: comment.LOCCMTSEQ, // ID commentId: comment.LOCCMTSEQ, // ID
boardId: comment.LOCBRDSEQ, boardId: comment.LOCBRDSEQ,
parentId: comment.LOCCMTPNT, // ID parentId: comment.LOCCMTPNT, // ID
author: comment.author || '익명', author: comment.author || '익명',
<<<<<<< HEAD
=======
authorId: comment.authorId,
>>>>>>> board-comment
content: comment.LOCCMTRPY, content: comment.LOCCMTRPY,
likeCount: comment.likeCount || 0, likeCount: comment.likeCount || 0,
dislikeCount: comment.dislikeCount || 0, dislikeCount: comment.dislikeCount || 0,
@ -286,8 +331,6 @@ const fetchComments = async (page = 1) => {
createdAtRaw: new Date(comment.LOCCMTRDT), // createdAtRaw: new Date(comment.LOCCMTRDT), //
createdAt: formattedDate(comment.LOCCMTRDT), // createdAt: formattedDate(comment.LOCCMTRDT), //
children: [], // children: [], //
// isCommentPassword: false, //
// isEditTextarea: false //
})); }));
for (const comment of commentsList) { for (const comment of commentsList) {
@ -297,7 +340,7 @@ const fetchComments = async (page = 1) => {
params: { LOCCMTPNT: comment.commentId } params: { LOCCMTPNT: comment.commentId }
}); });
// console.log(` (${comment.commentId} ):`, replyResponse.data); console.log(`대댓글 데이터 (${comment.commentId}의 대댓글):`, replyResponse.data);
if (replyResponse.data.data) { if (replyResponse.data.data) {
comment.children = replyResponse.data.data.map(reply => ({ comment.children = replyResponse.data.data.map(reply => ({
@ -343,21 +386,22 @@ const fetchComments = async (page = 1) => {
console.log('댓글 목록 불러오기 오류:', error); console.log('댓글 목록 불러오기 오류:', error);
} }
}; };
const isSubmitting = ref(false); // const isSubmitting = ref(false);
// //
const handleCommentSubmit = async ({ comment, password }) => { const handleCommentSubmit = async ({ comment, password }) => {
// if (unknown.value && !password) { console.log('댓글')
// passwordAlert.value = " ."; // UI //
// return;
// }
// if (!password) { // if (!password) {
// passwordAlert.value = " ."; // UI // passwordAlert.value = " .";
// return; // return;
// } // }
// console.log(' ')
// //
if (isSubmitting.value) return; // if (isSubmitting.value) return;
isSubmitting.value = true; // isSubmitting.value = true;
try { try {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
@ -368,7 +412,7 @@ const handleCommentSubmit = async ({ comment, password }) => {
}); });
if (response.status === 200) { if (response.status === 200) {
// console.log(' :', response.data.message); console.log('댓글 작성 성공:', response.data.message);
await fetchComments(); await fetchComments();
} else { } else {
console.log('댓글 작성 실패:', response.data.message); console.log('댓글 작성 실패:', response.data.message);
@ -439,19 +483,28 @@ const deleteClick = (unknown) => {
} }
}; };
// ( ) //
const editComment = (comment) => { const editComment = (comment) => {
if (comment.isEditTextarea) { const targetComment = comments.value.find(c => c.commentId === comment.commentId);
//
comment.isEditTextarea = false; if (!targetComment) {
return; return;
} }
// text ,
if (targetComment.isEditTextarea) {
targetComment.isEditTextarea = false;
} else {
targetComment.isEditTextarea = true;
}
//
if (unknown.value) { if (unknown.value) {
toggleCommentPassword(comment, "edit"); toggleCommentPassword(comment, "edit");
} else { } else {
comment.isEditTextarea = true; comment.isEditTextarea = true;
} }
<<<<<<< HEAD
// comments.value.forEach(c => { // comments.value.forEach(c => {
// c.isEditTextarea = false; // c.isEditTextarea = false;
@ -463,10 +516,13 @@ const editComment = (comment) => {
// } else { // } else {
// comment.isEditTextarea = true; // comment.isEditTextarea = true;
// } // }
=======
>>>>>>> board-comment
} }
// ( ) //
const deleteComment = (comment) => { const deleteComment = async (comment) => {
//
if (unknown.value) { if (unknown.value) {
if (comment.isEditTextarea) { if (comment.isEditTextarea) {
// , // ,
@ -477,8 +533,8 @@ const deleteComment = (comment) => {
toggleCommentPassword(comment, "delete"); toggleCommentPassword(comment, "delete");
} }
} else { } else {
// // ( )
comments.value = comments.value.filter(c => c.commentId !== comment.commentId); deleteReplyComment(comment)
} }
}; };
@ -531,7 +587,7 @@ const submitPassword = async () => {
} }
lastClickedButton.value = null; lastClickedButton.value = null;
} else { } else {
passwordAlert.value = "비밀번호가 일치하지 않습니다.????"; passwordAlert.value = "비밀번호가 일치하지 않습니다.";
} }
} catch (error) { } catch (error) {
// console.log("📌 :", error); // console.log("📌 :", error);
@ -552,6 +608,7 @@ const submitPassword = async () => {
// ( ) // ( )
const submitCommentPassword = async (comment, password) => { const submitCommentPassword = async (comment, password) => {
if (!password) { if (!password) {
passwordCommentAlert.value = "비밀번호를 입력해주세요."; passwordCommentAlert.value = "비밀번호를 입력해주세요.";
return; return;
@ -564,12 +621,19 @@ const submitCommentPassword = async (comment, password) => {
}); });
if (response.data.code === 200 && response.data.data === true) { if (response.data.code === 200 && response.data.data === true) {
passwordCommentAlert.value = "";
comment.isCommentPassword = false; comment.isCommentPassword = false;
if (lastCommentClickedButton.value === "edit") { if (lastCommentClickedButton.value === "edit") {
comment.isEditTextarea = true; comment.isEditTextarea = true;
<<<<<<< HEAD
=======
passwordCommentAlert.value = "";
>>>>>>> board-comment
// handleSubmitEdit(comment, comment.content); // handleSubmitEdit(comment, comment.content);
} else if (lastCommentClickedButton.value === "delete") { } else if (lastCommentClickedButton.value === "delete") {
passwordCommentAlert.value = "";
deleteReplyComment(comment) deleteReplyComment(comment)
} }
@ -631,7 +695,7 @@ const deleteReplyComment = async (comment) => {
} }
}; };
// ( ) //
const handleSubmitEdit = async (comment, editedContent) => { const handleSubmitEdit = async (comment, editedContent) => {
try { try {
const response = await axios.put(`board/comment/${comment.commentId}`, { const response = await axios.put(`board/comment/${comment.commentId}`, {
@ -640,8 +704,19 @@ const handleSubmitEdit = async (comment, editedContent) => {
}); });
// //
<<<<<<< HEAD
comment.content = editedContent; comment.content = editedContent;
comment.isEditTextarea = false; comment.isEditTextarea = false;
=======
// comment.content = editedContent;
// comment.isEditTextarea = false;
if (response.status === 200) {
//
await fetchComments();
} else {
console.log("❌ 댓글 수정 실패:", response.data);
}
>>>>>>> board-comment
} catch (error) { } catch (error) {
console.error("댓글 수정 중 오류 발생:", error); console.error("댓글 수정 중 오류 발생:", error);
} }
@ -650,7 +725,16 @@ const handleSubmitEdit = async (comment, editedContent) => {
// ( ) // ( )
const handleCancelEdit = (comment) => { const handleCancelEdit = (comment) => {
console.log("BoardView.vue - 댓글 수정 취소:", comment); console.log("BoardView.vue - 댓글 수정 취소:", comment);
comment.isEditTextarea = false;
// comments comment
const targetComment = comments.value.find(c => c.commentId === comment.commentId);
if (targetComment) {
console.log("✅ 원본 데이터 찾음, 수정 취소 처리 가능");
targetComment.isEditTextarea = false;
} else {
console.error("❌ 원본 데이터 찾을 수 없음, 수정 취소 실패");
}
}; };
// //

View File

@ -5,12 +5,16 @@
<!-- Sidebar: 사이드바 영역 --> <!-- Sidebar: 사이드바 영역 -->
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar"> <div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
<div class="sidebar-content"> <div class="sidebar-content">
<!-- 사원 프로필 리스트 --> <div class="sidebar-actions text-center my-3">
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
<ProfileList <ProfileList
@profileClick="handleProfileClick" @profileClick="handleProfileClick"
:remainingVacationData="remainingVacationData" :remainingVacationData="remainingVacationData"
/> />
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
<VacationModal <VacationModal
v-if="isModalOpen" v-if="isModalOpen"
:isOpen="isModalOpen" :isOpen="isModalOpen"
@ -28,24 +32,23 @@
@updateVacation="fetchRemainingVacation" @updateVacation="fetchRemainingVacation"
/> />
</div> </div>
<div class="sidebar-actions text-center my-3">
<!-- 액션 버튼 -->
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
</div> </div>
<!-- Main Content: 캘린더 영역 --> <!-- Main Content: 캘린더 영역 -->
<div class="col app-calendar-content"> <div class="col app-calendar-content">
<div class="card shadow-none border-0"> <div class="card shadow-none border-0">
<div class="card-body pb-0"> <div class="card-body pb-0" style="position: relative;">
<full-calendar <full-calendar
ref="fullCalendarRef" ref="fullCalendarRef"
:options="calendarOptions" :options="calendarOptions"
class="flatpickr-calendar-only" class="flatpickr-calendar-only"
/> />
<!-- 숨겨진 데이트피커 인풋 -->
<input
ref="calendarDatepicker"
type="text"
style="display: none; position: absolute;"
/>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +63,12 @@
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect
import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue"; import HalfDayButtons from "@c/button/HalfDayButtons.vue";
@ -75,11 +83,11 @@
const userListStore = useUserStore(); const userListStore = useUserStore();
const userList = ref([]); const userList = ref([]);
const userColors = ref({}); const userColors = ref({});
const myVacations = ref([]); // " " ( ) const myVacations = ref([]); //
const receivedVacations = ref([]); // " " const receivedVacations = ref([]);
const isModalOpen = ref(false); const isModalOpen = ref(false);
const remainingVacationData = ref({}); const remainingVacationData = ref({});
const modalYear = ref(new Date().getFullYear());
const lastRemainingYear = ref(new Date().getFullYear()); const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0")); const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false); const isGrantModalOpen = ref(false);
@ -94,8 +102,9 @@
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
const fetchedEvents = ref([]); const fetchedEvents = ref([]);
// : ( ) // ref
const toggledDates = ref(new Set()); const calendarDatepicker = ref(null);
let fpInstance = null;
const calendarOptions = reactive({ const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin], plugins: [dayGridPlugin, interactionPlugin],
@ -115,9 +124,42 @@
onMounted(async () => { onMounted(async () => {
await userStore.userInfo(); await userStore.userInfo();
await fetchRemainingVacation(); await fetchRemainingVacation();
// vacation history
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
await fetchVacationHistory(currentYear); await fetchVacationHistory(currentYear);
// Flatpickr ( )
fpInstance = flatpickr(calendarDatepicker.value, {
dateFormat: "Y-m",
plugins: [
new monthSelectPlugin({
shorthand: true,
dateFormat: "Y-m",
altFormat: "F Y"
})
],
onChange: function(selectedDatesArr, dateStr) {
//
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
const [year, month] = dateStr.split("-");
lastRemainingYear.value = parseInt(year, 10);
lastRemainingMonth.value = month;
loadCalendarData(lastRemainingYear.value, lastRemainingMonth.value);
},
onClose: function() {
calendarDatepicker.value.style.display = "none";
}
});
// FullCalendar (.fc-toolbar-title)
nextTick(() => {
const titleEl = document.querySelector('.fc-toolbar-title');
if (titleEl) {
titleEl.style.cursor = 'pointer';
titleEl.addEventListener('click', () => {
fpInstance.open();
});
}
});
}); });
// API ( ) // API ( )
@ -137,7 +179,6 @@
} }
} }
// lastRemainingYear
watch(lastRemainingYear, async (newYear, oldYear) => { watch(lastRemainingYear, async (newYear, oldYear) => {
await fetchVacationHistory(newYear); await fetchVacationHistory(newYear);
}); });
@ -156,27 +197,18 @@
} }
}; };
//
const handleProfileClick = async (user) => { const handleProfileClick = async (user) => {
try { try {
if (isModalOpen.value) { //
isModalOpen.value = false; isModalOpen.value = false;
return; isGrantModalOpen.value = false;
}
if (isGrantModalOpen.value) {
isGrantModalOpen.value = false;
return;
}
if (user.MEMBERSEQ === userStore.user.id) { if (user.MEMBERSEQ === userStore.user.id) {
const year = new Date().getFullYear(); const displayedYear = lastRemainingYear.value;
await fetchVacationHistory(year); await fetchVacationHistory(displayedYear);
isModalOpen.value = true; isModalOpen.value = true;
lastRemainingYear.value = year;
isGrantModalOpen.value = false;
} else { } else {
selectedUser.value = user; selectedUser.value = user;
isGrantModalOpen.value = true; isGrantModalOpen.value = true;
isModalOpen.value = false;
} }
} catch (error) { } catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error); console.error("🚨 연차 데이터 불러오기 실패:", error);
@ -220,29 +252,23 @@
return vacationCodeMap.value[typeCode] || "기타"; return vacationCodeMap.value[typeCode] || "기타";
}; };
// computed: lastRemainingYear
const filteredMyVacations = computed(() => { const filteredMyVacations = computed(() => {
const filtered = myVacations.value.filter(vac => { return myVacations.value.filter(vac => {
const dateStr = vac.date || vac.LOCVACUDT; const dateStr = vac.date;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null; const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return year === String(lastRemainingYear.value); return year === String(lastRemainingYear.value);
}); });
console.log("filteredMyVacations:", filtered);
return filtered;
}); });
const filteredReceivedVacations = computed(() => { const filteredReceivedVacations = computed(() => {
return receivedVacations.value.filter(vac => { return receivedVacations.value.filter(vac => {
const dateStr = vac.date || vac.LOCVACUDT; const dateStr = vac.date;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null; const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return dateStr && year === String(lastRemainingYear.value); return dateStr && year === String(lastRemainingYear.value);
}); });
}); });
function updateCalendarEvents() { function updateCalendarEvents() {
// selectedDates "delete"
const selectedEvents = Array.from(selectedDates.value) const selectedEvents = Array.from(selectedDates.value)
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
.map(([date, type]) => ({ .map(([date, type]) => ({
@ -254,8 +280,6 @@
classNames: [getVacationTypeClass(type), "selected-event"] classNames: [getVacationTypeClass(type), "selected-event"]
})); }));
// fetchedEvents, "delete"
// () , .
const filteredFetchedEvents = fetchedEvents.value.filter(event => { const filteredFetchedEvents = fetchedEvents.value.filter(event => {
if (event.saved && selectedDates.value.get(event.start) === "delete") { if (event.saved && selectedDates.value.get(event.start) === "delete") {
if (event.memberSeq === userStore.user.id) { if (event.memberSeq === userStore.user.id) {
@ -287,15 +311,12 @@
) { ) {
return; return;
} }
// : LOCVACUDT 10 clickedDateStr , LOCVACRMM
const isMyVacation = myVacations.value.some(vac => { const isMyVacation = myVacations.value.some(vac => {
const vacDate = vac.date ? String(vac.date).substring(0, 10) : ""; const vacDate = vac.date ? String(vac.date).substring(0, 10) : "";
return vacDate === clickedDateStr && return vacDate === clickedDateStr && !vac.receiverId;
(!vac.LOCVACRMM || String(vac.LOCVACRMM).trim() === "");
}); });
if (isMyVacation) { if (isMyVacation) {
// : selectedDates "delete" ( )
if (selectedDates.value.get(clickedDateStr) === "delete") { if (selectedDates.value.get(clickedDateStr) === "delete") {
selectedDates.value.delete(clickedDateStr); selectedDates.value.delete(clickedDateStr);
} else { } else {
@ -305,9 +326,7 @@
return; return;
} }
// : /
if (selectedDates.value.has(clickedDateStr)) { if (selectedDates.value.has(clickedDateStr)) {
console.log("일반 날짜 토글 off: 기존 선택 해제");
selectedDates.value.delete(clickedDateStr); selectedDates.value.delete(clickedDateStr);
updateCalendarEvents(); updateCalendarEvents();
return; return;
@ -315,7 +334,6 @@
const type = halfDayType.value const type = halfDayType.value
? (halfDayType.value === "AM" ? "700101" : "700102") ? (halfDayType.value === "AM" ? "700101" : "700102")
: "700103"; : "700103";
console.log("일반 날짜 토글 on: 선택 및 타입", type);
selectedDates.value.set(clickedDateStr, type); selectedDates.value.set(clickedDateStr, type);
halfDayType.value = null; halfDayType.value = null;
updateCalendarEvents(); updateCalendarEvents();
@ -331,7 +349,6 @@
if (response.status === 200) { if (response.status === 200) {
const vacationList = response.data; const vacationList = response.data;
if (lastRemainingYear.value !== year) { if (lastRemainingYear.value !== year) {
//
myVacations.value = vacationList.filter( myVacations.value = vacationList.filter(
(vac) => vac.MEMBERSEQ === userStore.user.id (vac) => vac.MEMBERSEQ === userStore.user.id
); );
@ -348,7 +365,7 @@
backgroundColor, backgroundColor,
classNames: [getVacationTypeClass(vac.LOCVACTYP)], classNames: [getVacationTypeClass(vac.LOCVACTYP)],
saved: true, saved: true,
memberSeq: vac.MEMBERSEQ, // memberSeq: vac.MEMBERSEQ,
}; };
}) })
.filter((event) => event.start); .filter((event) => event.start);
@ -368,18 +385,18 @@
const vacationsToAdd = selectedDatesArray const vacationsToAdd = selectedDatesArray
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
.filter(([date, type]) => .filter(([date, type]) =>
!myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date)) || !myVacations.value.some(vac => vac.date && vac.date.startsWith(date)) ||
myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date) && vac.LOCVACRMM) myVacations.value.some(vac => vac.date && vac.date.startsWith(date) && vac.receiverId)
) )
.map(([date, type]) => ({ date, type })); .map(([date, type]) => ({ date, type }));
const vacationsToDelete = myVacations.value const vacationsToDelete = myVacations.value
.filter(vac => { .filter(vac => {
if (!vac.LOCVACUDT) return false; if (!vac.date) return false;
const date = vac.LOCVACUDT.split("T")[0]; const date = vac.date.split("T")[0];
return selectedDates.value.get(date) === "delete" && !vac.LOCVACRMM; return selectedDates.value.get(date) === "delete" && !vac.receiverId;
}) })
.map(vac => { .map(vac => {
const id = vac.LOCVACSEQ; const id = vac.id;
return typeof id === "number" ? Number(id) : id; return typeof id === "number" ? Number(id) : id;
}); });
try { try {
@ -390,6 +407,9 @@
if (response.data && response.data.status === "OK") { if (response.data && response.data.status === "OK") {
alert("✅ 휴가 변경 사항이 저장되었습니다."); alert("✅ 휴가 변경 사항이 저장되었습니다.");
await fetchRemainingVacation(); await fetchRemainingVacation();
if (isModalOpen.value) {
await fetchVacationHistory(lastRemainingYear.value);
}
const currentDate = fullCalendarRef.value.getApi().getDate(); const currentDate = fullCalendarRef.value.getApi().getDate();
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1); await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
selectedDates.value.clear(); selectedDates.value.clear();
@ -440,5 +460,10 @@
</script> </script>
<style> <style>
/* 스타일 정의 */ /* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
/* 데이트피커 인풋은 Flatpickr에서 동적으로 스타일 적용됨 */
</style> </style>

View File

@ -1,6 +1,5 @@
<template> <template>
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<!-- {{ userStore.user.role == 'ROLE_ADMIN' ? '관리자' : '일반인'}} -->
<div class="card p-5"> <div class="card p-5">
<!-- 타이틀, 검색 --> <!-- 타이틀, 검색 -->
<div class="row"> <div class="row">
@ -30,7 +29,7 @@
<!-- 작성 --> <!-- 작성 -->
<div v-if="isWriteVisible" class="mt-5"> <div v-if="isWriteVisible" class="mt-5">
<DictWrite @close="isWriteVisible = false" :dataList="cateList" @addCategory="addCategory" @addWord="addWord"/> <DictWrite @close="isWriteVisible = false" :dataList="cateList" @addWord="addWord"/>
</div> </div>
</div> </div>
@ -174,30 +173,34 @@
}; };
// //
const addCategory = (data) =>{ // const addCategory = (data) =>{
const lastCategory = cateList.value[cateList.value.length - 1]; // const lastCategory = cateList.value[cateList.value.length - 1];
const newValue = lastCategory ? lastCategory.value + 1 : 600101; // const newValue = lastCategory ? lastCategory.value + 1 : 600101;
const newCategory = { label: data, value: newValue }; // const newCategory = { label: data, value: newValue };
cateList.value = [newCategory, ...cateList.value];
selectedCategory.value = newCategory.value;
// axios.post('worddict/insertCategory',{
// CMNCODNAM: data
// }).then(res => {
// if(res.data.data == '1'){
// toastStore.onToast(' .', 's');
// const newCategory = { label: data, value: newValue };
// cateList.value = [newCategory, ...cateList.value]; // cateList.value = [newCategory, ...cateList.value];
// selectedCategory.value = newCategory.value; // selectedCategory.value = newCategory.value;
// } else if(res.data.message == ' .') { //// axios.post('worddict/insertCategory',{
// toastStore.onToast(res.data.message, 'e'); //// CMNCODNAM: data
// } //// }).then(res => {
// }) //// if(res.data.data == '1'){
} //// toastStore.onToast(' .', 's');
//// const newCategory = { label: data, value: newValue };
//// cateList.value = [newCategory, ...cateList.value];
//// selectedCategory.value = newCategory.value;
//// } else if(res.data.message == ' .') {
//// toastStore.onToast(res.data.message, 'e');
//// }
//// })
// }
//
const addWord = (wordData, data) => { const addWord = (wordData, data) => {
let category = null; let category = null;
// //
const existingCategory = cateList.value.find(item => item.label === data); const existingCategory = cateList.value.find(item => item.label === data);
if (existingCategory) { if (existingCategory) {
console.log('카테고리 중복');
// //
category = existingCategory.label == '' ? wordData.category : existingCategory.value; category = existingCategory.label == '' ? wordData.category : existingCategory.value;
} else { } else {
@ -208,6 +211,7 @@
} }
sendWordRequest(category, wordData, data, !existingCategory); sendWordRequest(category, wordData, data, !existingCategory);
}; };
const sendWordRequest = (category, wordData, data, isNewCategory) => { const sendWordRequest = (category, wordData, data, isNewCategory) => {
const payload = { const payload = {
WRDDICCAT: category, WRDDICCAT: category,
@ -216,17 +220,26 @@
}; };
if (isNewCategory) { if (isNewCategory) {
payload.CMNCODNAM = data; // payload.CMNCODNAM = data;
axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false;
getwordList();
const newCategory = { label: data, value: category }; // data
cateList.value = [newCategory, ...cateList.value];
}
});
} else {
axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false;
getwordList();
}
});
} }
axios.post('worddict/insertWord', payload).then(res => { };
if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false;
getwordList();
//
}
});
};
// //