Merge branch 'main' into commuters

This commit is contained in:
yoon 2025-03-11 09:24:00 +09:00
commit 65e620d579
36 changed files with 1446 additions and 1146 deletions

View File

@ -3,6 +3,9 @@
/* 휴가 */
.fc-daygrid-event {
pointer-events: none !important;
}
/* 이벤트 선 없게 */
.fc-event {
border: none;
@ -103,13 +106,14 @@ cursor: not-allowed !important;
padding-bottom: 20px;
}
.vac-modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.1); /* 위쪽 그림자만 적용 */
max-width: 500px;
width: 100%;
position: relative;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0px -4px 5px rgba(0, 0, 0, 0.1),
0px 4px 0px rgba(0, 0, 0, 0);
max-width: 500px;
width: 100%;
position: relative;
}
.vac-modal-body {
max-height: 140px;
@ -208,20 +212,25 @@ cursor: not-allowed !important;
.profile-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 10px;
gap: 15px;
padding: 0;
margin-left: 20px;
list-style: none;
justify-content: flex-start;
cursor: pointer;
}
.profile-img {
transition: all 0.2s ease-in-out;
.profile-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
width: calc(33.33% - 10px);
}
/* 오전/오후반차,저장버튼 */
/* 버튼 기본 스타일 */
.vac-btn {
width: 50px;
height: 50px;
width: 60px;
height: 60px;
border-radius: 50%;
font-size: 20px;
display: flex;
@ -250,20 +259,19 @@ cursor: not-allowed !important;
}
/* 선택된 (눌린) 버튼 */
.vac-btn.active {
border: 3px solid #ff0000; /* 붉은색 테두리 적용 */
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
box-shadow: 0px 4px 15px rgba(224, 224, 224, 0.3);
transform: scale(1.1);
}
.vac-btn-warning{
color: #fff;
background-color: #ffab00;
border-color: #ffab00;
background-color: #ffc144;
border-color: #ffe605;
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
}
/* AM 버튼 (선택된 상태) */
.vac-btn-warning.active {
background-color: #ffca2c !important;
color: black;
background-color: #ff7300 !important;
color: #fff;;
}
.vac-btn-info {
color: #fff;
@ -277,10 +285,10 @@ cursor: not-allowed !important;
color: white;
}
/* 버튼 기본 (비활성화일 때 기본 녹색) */
.btn-success {
.vac-btn-success {
font-size: 24px;
width: 50px;
height: 50px;
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
@ -292,7 +300,7 @@ cursor: not-allowed !important;
border: 1px solid transparent;
}
/* 버튼 활성화 */
.btn-success.active {
.vac-btn-success.active {
background-color: #ff0000 !important;
color: white !important;
border: 3px solid #eb9f9f !important;
@ -300,7 +308,8 @@ cursor: not-allowed !important;
transform: scale(1.1);
}
/* 버튼 비활성화 */
.btn-success.disabled {
.vac-btn-success.disabled {
border: 3px solid #e6e4e4; /* 붉은색 테두리 적용 */
background-color: #bbb8b8 !important;
color: white !important;
cursor: not-allowed !important;
@ -310,7 +319,7 @@ cursor: not-allowed !important;
}
/* 작은 화면에서 버튼 크기 조정 */
@media (max-width: 1600px) {
@media (max-width: 1700px) {
.count-btn {
width: 28px;
height: 28px;
@ -346,18 +355,39 @@ cursor: not-allowed !important;
text-align: center;
margin-bottom: 5px;
}
.vac-btn {
width: 50px;
height: 50px;
font-size: 18px;
}
.vac-btn-success {
font-size: 20px;
width: 50px;
height: 50px;
}
}
@media (max-width: 1500px) {
.close-btn {
top: 5px;
right: 5px;
font-size: 13px;
}
.vacation-item {
font-size: 11px;
text-align: center;
margin-bottom: 5px;
}
.vac-btn {
width: 40px;
height: 40px;
font-size: 18px;
}
.btn-success {
.vac-btn-success {
font-size: 20px;
width: 40px;
height: 40px;
}
}
.grayscaleImg {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -53,7 +53,7 @@ $api.interceptors.response.use(
switch (error.response.status) {
case 401:
if (!error.config.headers.isLogin) {
toastStore.onToast('인증이 필요합니다.', 'e');
// toastStore.onToast('인증이 필요합니다.', 'e');
}
break;
case 403:

View File

@ -81,6 +81,34 @@ const common = {
return true;
},
/**
* 에디터에 내용이 있는지 확인
*
* @param { Quill } content
* @returns true: 없음, false: 있음
*/
isNotValidContent(content) {
if (!content.value?.ops?.length) return true;
// 이미지 포함 여부 확인
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
// 텍스트 포함 여부 확인
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
return !(hasText || hasImage);
},
/**
* 확인
*
* @param {ref} text ex) inNotValidInput(data.value);
* @returns
*/
isNotValidInput(text) {
return text.trim().length === 0;
},
};
export default {

View File

@ -16,7 +16,7 @@
@updateReaction="handleUpdateReaction"
/>
<!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="currentPasswordCommentId === comment.commentId && unknown" class="mt-3 w-25 ms-auto">
<div v-if="currentPasswordCommentId === comment.commentId && unknown && comment.author == '익명'" class="mt-3 w-25 ms-auto">
<div class="input-group">
<input
type="password"
@ -43,155 +43,133 @@
</template>
</div>
<!-- <p>현재 isDeleted : {{ isDeleted }}</p> -->
<!-- <template v-if="isDeleted">
<p class="m-0 text-muted">댓글이 삭제되었습니다.</p>
</template> -->
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment"/>
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6" />
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment" :commnetId="comment.commentId" />
<!-- 대댓글 -->
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
<li
v-for="child in comment.children"
:key="child.commentId"
class="mt-8 pt-6 ps-10 border-top"
>
<BoardComment
:comment="child"
:unknown="child.author === '익명'"
:isPlusButton="false"
:isLike="true"
:isCommentProfile="true"
:isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword"
:currentPasswordCommentId="currentPasswordCommentId"
:passwordCommentAlert="passwordCommentAlert"
:password="password"
@editClick="handleReplyEditClick"
@deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment"
@updateReaction="handleUpdateReaction"
@submitPassword="$emit('submitPassword', child, password)"
@update:password="$emit('update:password', $event)"
/>
</li>
</ul>
<slot name="reply"></slot>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
import BoardProfile from './BoardProfile.vue';
import BoardCommentArea from './BoardCommentArea.vue';
import PlusButton from '../button/PlusBtn.vue';
import SaveBtn from '../button/SaveBtn.vue';
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
import BoardProfile from './BoardProfile.vue';
import BoardCommentArea from './BoardCommentArea.vue';
import PlusButton from '../button/PlusBtn.vue';
import SaveBtn from '../button/SaveBtn.vue';
const props = defineProps({
comment: {
type: Object,
required: true,
},
unknown: {
type: Boolean,
default: false,
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isPlusButton: {
type: Boolean,
default: true,
},
isLike: {
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false
},
isDeleted: {
type: Boolean,
default: false
},
isCommentPassword: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: ''
},
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
});
// emits
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'submitEdit', 'cancelEdit', 'update:password']);
const localEditedContent = ref(props.comment.content);
//
const isComment = ref(false);
const toggleComment = () => {
isComment.value = !isComment.value;
};
//
const submitComment = (newComment) => {
emit('submitComment', { parentId: props.comment.commentId, ...newComment, LOCBRDTYP: newComment.LOCBRDTYP });
isComment.value = false;
};
// ,
const handleUpdateReaction = (reactionData) => {
emit('updateReaction', {
boardId: props.comment.boardId,
commentId: props.comment.commentId || reactionData.commentId,
...reactionData,
const props = defineProps({
comment: {
type: Object,
required: true,
},
unknown: {
type: Boolean,
default: false,
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isPlusButton: {
type: Boolean,
default: true,
},
isLike: {
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false,
},
isDeleted: {
type: Boolean,
default: false,
},
isCommentPassword: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: '',
},
currentPasswordCommentId: {
type: Number,
},
password: {
type: String,
},
});
};
// emits
const emit = defineEmits([
'submitComment',
'updateReaction',
'editClick',
'deleteClick',
'submitPassword',
'submitEdit',
'cancelEdit',
'update:password',
]);
//
const logPasswordAndEmit = () => {
emit('submitPassword', props.comment, props.password);
};
const localEditedContent = ref(props.comment.content);
watch(() => props.comment.isEditTextarea, (newVal) => {
if (newVal) {
localEditedContent.value = props.comment.content;
}
});
//
const isComment = ref(false);
const toggleComment = () => {
isComment.value = !isComment.value;
};
// watch(() => props.comment.isDeleted, () => {
// console.log("BoardComment - isDeleted :", newVal);
//
const submitComment = newComment => {
emit('submitComment', { parentId: props.comment.commentId, ...newComment, LOCBRDTYP: newComment.LOCBRDTYP });
isComment.value = false;
};
// if (newVal) {
// localEditedContent.value = " ."; // UI
// props.comment.isEditTextarea = false;
// }
// });
// ,
const handleUpdateReaction = reactionData => {
emit('updateReaction', {
boardId: props.comment.boardId,
commentId: props.comment.commentId || reactionData.commentId,
...reactionData,
});
};
//
const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value);
};
//
const logPasswordAndEmit = () => {
emit('submitPassword', props.comment, props.password);
};
const handleEditClick = () => {
emit('editClick', props.comment);
}
watch(
() => props.comment.isEditTextarea,
newVal => {
if (newVal) {
localEditedContent.value = props.comment.content;
}
},
);
const handleReplyEditClick = (comment) => {
emit('editClick', comment);
}
// watch(() => props.comment.isDeleted, () => {
// console.log("BoardComment - isDeleted :", newVal);
// if (newVal) {
// localEditedContent.value = " ."; // UI
// props.comment.isEditTextarea = false;
// }
// });
//
const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value);
};
const handleEditClick = () => {
emit('editClick', props.comment);
};
</script>

View File

@ -11,7 +11,14 @@
</div> -->
<!-- 텍스트박스 -->
<div class="w-100">
<textarea class="form-control" placeholder="댓글 달기" rows="3" v-model="comment"></textarea>
<textarea
class="form-control"
placeholder="댓글 달기"
rows="3"
:maxlength="maxLength"
v-model="comment"
@input="alertTextHandler"
></textarea>
<span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span>
<span v-else class="invalid-feedback d-block text-start ms-2">{{ textAlert }}</span>
</div>
@ -22,8 +29,8 @@
<div class="d-flex flex-wrap align-items-center">
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
<input class="form-check-input" type="checkbox" id="inlineCheckbox1" v-model="isCheck" />
<label class="form-check-label" for="inlineCheckbox1">익명</label>
<input class="form-check-input" type="checkbox" :id="`checkboxAnnonymous${commnetId}`" v-model="isCheck" />
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
</div>
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
@ -35,6 +42,7 @@
class="form-control flex-grow-1"
v-model="password"
placeholder="비밀번호 입력"
@input="passwordAlertTextHandler"
/>
<span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
<span v-else class="invalid-feedback d-block text-start ms-2">{{ passwordAlert2 }}</span>
@ -51,78 +59,92 @@
</template>
<script setup>
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
import SaveBtn from '../button/SaveBtn.vue';
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
import SaveBtn from '../button/SaveBtn.vue';
const props = defineProps({
unknown: {
type: Boolean,
default: true,
},
parentId: {
type: Number,
default: 0,
},
passwordAlert: {
type: String,
default: '',
},
commentAlert: {
type: String,
default: '',
},
});
const $common = inject('common');
const comment = ref('');
const password = ref('');
const isCheck = ref(false);
const textAlert = ref('');
const passwordAlert2 = ref('');
const emit = defineEmits(['submitComment']);
const handleCommentSubmit = () => {
if (!$common.isNotEmpty(comment.value)) {
textAlert.value = '댓글을 입력하세요';
return false;
} else {
textAlert.value = '';
}
if (isCheck.value && !$common.isNotEmpty(password.value)) {
passwordAlert2.value = '비밀번호를 입력하세요';
return false;
} else {
passwordAlert2.value = '';
}
//
emit('submitComment', {
comment: comment.value,
password: isCheck.value ? password.value : '',
isCheck: isCheck.value,
LOCBRDTYP: isCheck.value ? '300102' : null, // '300102'
const props = defineProps({
unknown: {
type: Boolean,
default: true,
},
parentId: {
type: Number,
default: 0,
},
passwordAlert: {
type: String,
default: '',
},
commentAlert: {
type: String,
default: '',
},
maxLength: {
type: Number,
default: 500,
},
commnetId: {
type: Number,
},
});
//
resetCommentForm();
};
const $common = inject('common');
const comment = ref('');
const password = ref('');
const isCheck = ref(false);
const textAlert = ref('');
const passwordAlert2 = ref('');
//
const resetCommentForm = () => {
comment.value = '';
password.value = '';
isCheck.value = false;
};
const emit = defineEmits(['submitComment']);
watch(
() => props.passwordAlert,
() => {
if (!props.passwordAlert) {
resetCommentForm();
const alertTextHandler = () => {
textAlert.value = '';
};
const passwordAlertTextHandler = () => {
passwordAlert2.value = '';
};
const handleCommentSubmit = () => {
if (!$common.isNotEmpty(comment.value)) {
textAlert.value = '댓글을 입력하세요';
return false;
} else {
textAlert.value = '';
}
}
);
</script>
if (isCheck.value && !$common.isNotEmpty(password.value)) {
passwordAlert2.value = '비밀번호를 입력하세요';
return false;
} else {
passwordAlert2.value = '';
}
//
emit('submitComment', {
comment: comment.value,
password: isCheck.value ? password.value : '',
isCheck: isCheck.value,
LOCBRDTYP: isCheck.value ? '300102' : null, // '300102'
});
//
resetCommentForm();
};
//
const resetCommentForm = () => {
comment.value = '';
password.value = '';
isCheck.value = false;
};
watch(
() => props.passwordAlert,
() => {
if (!props.passwordAlert) {
resetCommentForm();
}
},
);
</script>

View File

@ -1,17 +1,13 @@
<template>
<ul class="list-unstyled mt-10">
<li
v-for="comment in comments"
:key="comment.commentId"
class="mt-6 border-bottom pb-6"
>
<li v-for="comment in comments" :key="comment.commentId" class="mt-6 border-bottom pb-6">
<BoardComment
:unknown="unknown"
:comment="comment"
:isCommentAuthor="comment.isCommentAuthor"
:isEditTextarea="comment.isEditTextarea"
:isDeleted="isDeleted"
:isCommentPassword="isCommentPassword"
:isCommentPassword="isCommentPassword"
:passwordCommentAlert="passwordCommentAlert || ''"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
@ -21,104 +17,148 @@
@submitComment="submitComment"
@submitEdit="handleSubmitEdit"
@cancelEdit="handleCancelEdit"
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
@update:password="updatePassword"
/>
>
<!-- 대댓글 -->
<template #reply>
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
<li v-for="(child, index) in comment.children" :key="child.commentId" class="mt-8 pt-6 ps-10 border-top">
<BoardComment
:comment="child"
:unknown="child.author === '익명'"
:isPlusButton="false"
:isLike="true"
:isCommentProfile="true"
:isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword"
:currentPasswordCommentId="currentPasswordCommentId"
:passwordCommentAlert="passwordCommentAlert"
:password="password"
@editClick="handleReplyEditClick"
@deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment"
@updateReaction="handleUpdateReaction"
@submitPassword="$emit('submitPassword', child, password)"
@update:password="$emit('update:password', $event)"
/>
</li>
</ul>
</template>
</BoardComment>
</li>
</ul>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import BoardComment from './BoardComment.vue'
import { defineProps, defineEmits } from 'vue';
import BoardComment from './BoardComment.vue';
const props = defineProps({
comments: {
type: Array,
required: true,
default: () => []
},
unknown: {
type: Boolean,
default: true,
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isCommentPassword: {
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false,
},
isDeleted: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: ''
},
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
});
const props = defineProps({
comments: {
type: Array,
required: true,
default: () => [],
},
unknown: {
type: Boolean,
default: true,
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isCommentPassword: {
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false,
},
isDeleted: {
type: Boolean,
default: false,
},
passwordCommentAlert: {
type: String,
default: '',
},
currentPasswordCommentId: {
type: Number,
},
password: {
type: String,
},
index: {
type: Number,
},
});
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword','submitEdit', 'update:password']);
const emit = defineEmits([
'submitComment',
'updateReaction',
'editClick',
'deleteClick',
'submitPassword',
'clearPassword',
'submitEdit',
'update:password',
]);
const submitComment = (replyData) => {
emit('submitComment', replyData);
};
const handleUpdateReaction = (reactionData, commentId, boardId) => {
const updatedReactionData = {
...reactionData,
commentId: commentId || reactionData.commentId,
boardId: boardId || reactionData.boardId,
const submitComment = replyData => {
emit('submitComment', replyData);
};
emit('updateReaction', updatedReactionData);
}
const handleUpdateReaction = (reactionData, commentId, boardId) => {
const updatedReactionData = {
...reactionData,
commentId: commentId || reactionData.commentId,
boardId: boardId || reactionData.boardId,
};
const submitPassword = (comment, password) => {
emit('submitPassword', comment, password);
};
emit('updateReaction', updatedReactionData);
};
const handleEditClick = (comment) => {
if (comment.parentId) {
emit('editClick', comment); //
} else {
emit('editClick', comment); //
}
};
const submitPassword = (comment, password) => {
emit('submitPassword', comment, password);
};
const handleSubmitEdit = (comment, editedContent) => {
emit("submitEdit", comment, editedContent);
};
const handleEditClick = comment => {
if (comment.parentId) {
emit('editClick', comment); //
} else {
emit('editClick', comment); //
}
};
const handleDeleteClick = (comment) => {
if (comment.parentId) {
emit('deleteClick', comment); //
} else {
emit('deleteClick', comment); //
}
};
const handleSubmitEdit = (comment, editedContent) => {
emit('submitEdit', comment, editedContent);
};
const handleCancelEdit = (comment) => {
if (comment.parentId) {
emit('cancelEdit', comment); //
} else {
emit('cancelEdit', comment); //
}
};
const handleDeleteClick = comment => {
if (comment.parentId) {
emit('deleteClick', comment); //
} else {
emit('deleteClick', comment); //
}
};
const updatePassword = (newPassword) => {
emit('update:password', newPassword);
};
const handleCancelEdit = comment => {
if (comment.parentId) {
emit('cancelEdit', comment); //
} else {
emit('cancelEdit', comment); //
}
};
const updatePassword = newPassword => {
emit('update:password', newPassword);
};
const handleReplyEditClick = comment => {
emit('editClick', comment);
};
</script>

View File

@ -40,7 +40,7 @@
const defaultProfile = '/img/icons/icon.png';
// (Vue )
const baseUrl = 'http://localhost:10325/'; // API URL
const baseUrl = import.meta.env.VITE_SERVER; // API URL
// Props
const props = defineProps({

View File

@ -1,124 +1,123 @@
<template v-if="isRecommend">
<button class="btn btn-label-primary btn-icon" :class="{'clicked': likeClicked, 'big': bigBtn}" @click="handleLike">
<button class="btn btn-label-primary btn-icon" :class="{ clicked: likeClicked, big: bigBtn }" @click="handleLike">
<i class="fa-regular fa-thumbs-up"></i> <span class="num">{{ likeCount }}</span>
</button>
<button class="btn btn-label-danger btn-icon" :class="{'clicked': dislikeClicked, 'big': bigBtn}" @click="handleDislike">
<button class="btn btn-label-danger btn-icon" :class="{ clicked: dislikeClicked, big: bigBtn }" @click="handleDislike">
<i class="fa-regular fa-thumbs-down"></i> <span class="num">{{ dislikeCount }}</span>
</button>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed } from 'vue';
const props = defineProps({
comment: {
type: Object,
default: () => ({}),
},
likeClicked : {
type : Boolean,
default : false,
},
dislikeClicked : {
type : Boolean,
default : false,
},
bigBtn : {
type :Boolean,
default : false,
},
isRecommend: {
type:Boolean,
default:true,
},
boardId: {
type: Number,
required: true,
},
commentId: {
type: [Number, null],
default: null,
},
likeCount: {
type: Number,
default: 0,
},
dislikeCount: {
type: Number,
default: 0,
},
});
const props = defineProps({
comment: {
type: Object,
default: () => ({}),
},
likeClicked: {
type: Boolean,
default: false,
},
dislikeClicked: {
type: Boolean,
default: false,
},
bigBtn: {
type: Boolean,
default: false,
},
isRecommend: {
type: Boolean,
default: true,
},
boardId: {
type: Number,
required: true,
},
commentId: {
type: [Number, null],
default: null,
},
likeCount: {
type: Number,
default: 0,
},
dislikeCount: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['updateReaction']);
const emit = defineEmits(['updateReaction']);
const likeClicked = ref(props.likeClicked);
const dislikeClicked = ref(props.dislikeClicked);
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
const likeClicked = ref(props.likeClicked);
const dislikeClicked = ref(props.dislikeClicked);
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
const handleLike = () => {
const isLike = !likeClicked.value;
const isDislike = false;
const handleLike = () => {
const isLike = !likeClicked.value;
const isDislike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
likeClicked.value = isLike;
dislikeClicked.value = false;
};
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
likeClicked.value = isLike;
dislikeClicked.value = false;
};
const handleDislike = () => {
const isDislike = !dislikeClicked.value;
const isLike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
dislikeClicked.value = isDislike;
likeClicked.value = false;
};
const handleDislike = () => {
const isDislike = !dislikeClicked.value;
const isLike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
dislikeClicked.value = isDislike;
likeClicked.value = false;
};
</script>
<style scoped>
.btn + .btn {
margin-left: 5px;
}
.num {
margin-left: 5px;
}
.btn-label-danger.clicked {
background-color: #e6381a;
}
.btn-label-danger.clicked i,
.btn-label-danger.clicked span {
color: #fff;
}
.btn-label-primary.clicked {
background-color: #5f61e6;
}
.btn-label-primary.clicked i,
.btn-label-primary.clicked span {
color : #fff;
}
.btn {
width: 55px;
height: 30px;
}
.btn.big {
width: 70px;
height: 70px;
font-size: 18px;
}
@media screen and (max-width:450px) {
.btn {
width: 50px;
height: 20px;
font-size: 12px;
.btn + .btn {
margin-left: 5px;
}
.num {
margin-left: 5px;
}
.btn-label-danger.clicked {
background-color: #e6381a;
}
.btn-label-danger.clicked i,
.btn-label-danger.clicked span {
color: #fff;
}
.btn-label-primary.clicked {
background-color: #5f61e6;
}
.btn-label-primary.clicked i,
.btn-label-primary.clicked span {
color: #fff;
}
.btn {
width: 55px;
/* height: 30px; */
}
.btn.big {
width: 70px;
height: 70px;
font-size: 18px;
}
@media screen and (max-width: 450px) {
.btn {
width: 50px;
height: 20px;
font-size: 12px;
}
}
}
</style>

View File

@ -1,30 +1,39 @@
<template>
<button class="btn btn-label-primary btn-icon me-1" @click="toggleText">
<button class="btn btn-label-primary btn-icon float-end" @click="toggleText">
<i :class="buttonClass"></i>
</button>
</template>
<script setup>
import { ref, defineProps } from 'vue';
import { ref, watch, defineProps } from 'vue';
const props = defineProps({
isToggleEnabled: {
type: Boolean,
default: false,
},
});
const props = defineProps({
isToggleEnabled: {
type: Boolean,
default: false,
},
isActive: {
type: Boolean,
required: false,
},
});
const buttonClass = ref('bx bx-edit-alt');
const buttonClass = ref("bx bx-edit-alt");
const toggleText = () => {
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === 'bx bx-edit-alt' ? 'bx bx-x' : 'bx bx-edit-alt';
}
};
watch(() => props.isActive, (newVal) => {
buttonClass.value = newVal ? "bx bx-x" : "bx bx-edit-alt";
});
const resetButton = () => {
buttonClass.value = 'bx bx-edit-alt';
};
const toggleText = () => {
if (props.isToggleEnabled) {
buttonClass.value = buttonClass.value === "bx bx-edit-alt" ? "bx bx-x" : "bx bx-edit-alt";
}
};
const resetButton = () => {
buttonClass.value = "bx bx-edit-alt";
};
defineExpose({ resetButton });
defineExpose({ resetButton });
</script>

View File

@ -18,7 +18,7 @@
</button>
<!-- 저장 버튼 -->
<div class="save-button-container">
<button class="btn-success" @click="addVacationRequests"
<button class="vac-btn-success" @click="addVacationRequests"
:class="{ active: !isDisabled, disabled: isDisabled }">
</button>

View File

@ -27,4 +27,4 @@ const resetButton = () => {
};
defineExpose({ resetButton });
</script>
</script>

View File

@ -1,5 +1,18 @@
<template>
<ul class="cate-list list-unstyled d-flex flex-wrap mb-0">
<li v-if="showAll" class="mt-2 me-2">
<button
type="button"
class="btn"
:class="{
'btn-outline-primary': selectedCategory !== 'all',
'btn-primary': selectedCategory === 'all'
}"
@click="selectCategory('all')"
>
All
</button>
</li>
<li v-for="category in lists" :key="category.value" class="mt-2 me-2">
<button
type="button"
@ -18,7 +31,7 @@
</template>
<script setup>
import { defineProps, ref } from 'vue';
import { defineProps, ref, watch } from 'vue';
// lists prop
const props = defineProps({
@ -26,29 +39,38 @@ const props = defineProps({
type: Array,
required: true,
},
showAll: {
type: Boolean,
required: false,
},
selectedCategory: {
type: [String, Number],
default: null,
required: false,
},
});
//
const selectedCategory = ref(null);
const emit = defineEmits();
const selectedCategory = ref(props.selectedCategory);
const emit = defineEmits(['update:data']);
const selectCategory = (cate) => {
selectedCategory.value = selectedCategory.value === cate ? null : cate;
emit('update:data', selectedCategory.value);
};
watch(() => props.selectedCategory, (newVal) => {
selectedCategory.value = newVal;
});
</script>
<style scoped>
@media (max-width: 768px) {
.cate-list {
overflow-x: scroll;
flex-wrap: nowrap !important;
li {
flex: 0 0 auto;
}
}
}
</style>

View File

@ -114,7 +114,6 @@
// , HTML
if (props.initialData) {
console.log(props.initialData);
quillInstance.setContents(JSON.parse(props.initialData));
}
@ -126,7 +125,6 @@
//
quillInstance.on('text-change', (delta, oldDelta, source) => {
emit('update:data', quillInstance.getContents());
delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
const imageUrl = op.insert.image; // URL
@ -135,7 +133,9 @@
checkForDeletedImages(); //
}
});
emit('update:data', quillInstance.getContents());
});
//
async function selectLocalImage() {
const input = document.createElement('input');
@ -165,17 +165,43 @@
}
};
}
//
async function uploadImageToServer(formData) {
try {
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
const imageUrl = response.data.data;
return imageUrl; // URL
} catch (error) {
toastStore.onToast('잠시후 다시 시도해주세요.', 'e');
throw error;
}
try {
// Make the POST request to upload the image
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
// Check if the response contains the expected data
if (response.data && response.data.data) {
const imageUrl = response.data.data;
return imageUrl; // Return the image URL received from the server
} else {
throw new Error('Image URL not returned from server');
}
} catch (error) {
// Log detailed error information for debugging purposes
console.error('Image upload failed:', error);
// Handle specific error cases (e.g., network issues, authorization issues)
if (error.response) {
// If the error is from the server (e.g., 4xx or 5xx error)
console.error('Error response:', error.response.data);
toastStore.onToast('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 'e');
} else if (error.request) {
// If no response is received from the server
console.error('No response received:', error.request);
toastStore.onToast('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', 'e');
} else {
// If the error is due to something else (e.g., invalid request setup)
console.error('Error message:', error.message);
toastStore.onToast('파일 업로드 중 문제가 발생했습니다. 다시 시도해주세요.', 'e');
}
// Throw the error so the caller knows something went wrong
throw error;
}
}
//
function checkForDeletedImages() {
@ -190,6 +216,7 @@
}
});
</script>
<style>
@import 'quill/dist/quill.snow.css';
.ql-editor {

View File

@ -18,6 +18,7 @@
type="text"
v-model="postcode"
placeholder="우편번호"
disabled="true"
readonly
/>
@ -26,6 +27,7 @@
type="text"
v-model="address"
placeholder="기본주소"
disabled="true"
readonly
/>
</div>

View File

@ -14,101 +14,98 @@
:placeholder="title"
:disabled="disabled"
:min="min"
autocomplete="off"
@focusout="$emit('focusout', modelValue)"
@input="handleInput"
/>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
{{ title }} 확인해주세요.
</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
<!-- 카테고리 중복 -->
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">
카테고리 중복입니다.
</div>
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ref, watch } from 'vue';
// Props
const props = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
},
modelValue: {
type: String,
default: '',
},
maxlength: {
type: Number,
default: 30,
},
isAlert: {
type: Boolean,
default: false,
},
isCateAlert : {
type :Boolean,
default: false,
},
isLabel : {
type: Boolean,
default: true,
required: false,
},
disabled: {
type: Boolean,
default: false,
},
min: {
type: String,
default: '',
required: false,
}
});
// Props
const props = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
},
modelValue: {
type: String,
default: '',
},
maxlength: {
type: Number,
default: 30,
},
isAlert: {
type: Boolean,
default: false,
},
isCateAlert: {
type: Boolean,
default: false,
},
isLabel: {
type: Boolean,
default: true,
required: false,
},
disabled: {
type: Boolean,
default: false,
},
min: {
type: String,
default: '',
required: false,
},
});
// Emits
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
// Emits
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
// `inputValue`
const inputValue = ref(props.modelValue);
// `inputValue`
const inputValue = ref(props.modelValue);
//
watch(inputValue, newValue => {
emits('update:modelValue', newValue);
});
//
watch(inputValue, (newValue) => {
emits('update:modelValue', newValue);
});
//
watch(
() => props.modelValue,
newValue => {
if (inputValue.value !== newValue) {
inputValue.value = newValue;
}
},
);
//
watch(() => props.modelValue, (newValue) => {
if (inputValue.value !== newValue) {
inputValue.value = newValue;
}
});
const handleInput = (event) => {
const newValue = event.target.value.slice(0, props.maxlength);
if (newValue.trim() !== '') {
emits('update:alert', false);
}
};
const handleInput = event => {
const newValue = event.target.value.slice(0, props.maxlength);
if (newValue.trim() !== '') {
emits('update:alert', false);
}
};
</script>

View File

@ -2,7 +2,7 @@
<div class="mb-2" :class="isRow ? 'row' : ''">
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
{{ title }}
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
<span v-if="isEssential" class="link-danger">*</span>
</label>
<div :class="isRow ? 'col-md-10' : 'col-md-12'" class="d-flex gap-2 align-items-center">
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}" @blur="$emit('blur')">

View File

@ -8,7 +8,7 @@
{{ title }}
</h5>
<p v-if="isProjectExpired" class="btn-icon btn-danger rounded-2"><i class='bx bx-power-off'></i></p>
<div v-if="!isProjectExpired">
<div v-if="!isProjectExpired" class="d-flex gap-1">
<EditBtn @click.stop="openEditModal" />
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
</div>
@ -128,6 +128,7 @@
title="종료일"
type="date"
name="endDay"
:min="todays"
:modelValue="selectedProject.PROJCTEND"
@update:modelValue="selectedProject.PROJCTEND = $event"
/>
@ -160,7 +161,7 @@
</template>
<script setup>
import { defineProps, onMounted, ref, computed, watch } from 'vue';
import { defineProps, onMounted, ref, computed, watch, inject } from 'vue';
import UserList from '@c/user/UserList.vue';
import CenterModal from '@c/modal/CenterModal.vue';
import $api from '@api';
@ -253,6 +254,12 @@ const isProjectCreator = computed(() => {
return user.value?.id === props.projctCreatorId;
});
// dayjs
const dayjs = inject('dayjs');
// YYYY-MM-DD
const todays = dayjs().format('YYYY-MM-DD');
// ( )
const isProjectExpired = computed(() => {
if (!props.enddate) return false;
@ -356,19 +363,6 @@ const hasChanges = computed(() => {
selectedProject.value.PROJCTDES !== props.description ||
selectedProject.value.PROJCTCOL !== props.projctCol;
});
//
watch(
() => selectedProject.value,
() => {
const start = new Date(selectedProject.value.PROJCTSTR);
const end = new Date(selectedProject.value.PROJCTEND);
if (end < start) {
selectedProject.value.PROJCTEND = selectedProject.value.PROJCTSTR;
}
},
{ deep: true, flush: 'post' }
);
//
const handleUpdate = () => {

View File

@ -1,7 +1,7 @@
<template>
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
<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>
<!-- 연차 목록 -->
<div class="vac-modal-body" v-if="mergedVacations.length > 0">
@ -26,8 +26,8 @@
</ol>
</div>
<!-- 연차 데이터 없음 -->
<p v-else class="text-sm-center mt-10 text-gray">
🚫 사용한 연차가 없습니다.
<p v-else class="text-sm-center mt-10 text-gray vac-modal-title">
🚫 연차 내역이 없습니다.
</p>
</div>
</div>
@ -58,19 +58,15 @@ const emit = defineEmits(["close"]);
// (,)
let globalCounter = 0;
const usedVacations = computed(() => {
const result = [];
props.myVacations.forEach((v) => {
return props.myVacations.flatMap((v) => {
const count = v.used_quota || 1;
for (let i = 0; i < count; i++) {
result.push({
...v,
category: "used",
code: v.LOCVACTYP,
_expandIndex: globalCounter++,
});
}
return Array.from({ length: count }, (_, i) => ({
...v,
category: "used",
code: v.LOCVACTYP,
_expandIndex: globalCounter++,
}));
});
return result;
});
//
@ -114,7 +110,7 @@ const mergedVacations = computed(() => {
//
const closeModal = () => {
emit("close");
emit("close");
};
</script>
<style scoped>

View File

@ -69,6 +69,7 @@
:type="'date'"
name="endDay"
:modelValue="endDay"
:min = "today"
@update:modelValue="endDay = $event"
/>

View File

@ -94,8 +94,7 @@
:is-common="true"
:is-color="true"
:data="colorList"
@update:data="color = $event"
@blur="checkColorDuplicate"
@update:data="handleColorUpdate"
class="w-50"
/>
</div>
@ -136,6 +135,7 @@
@update:data="handleAddressUpdate"
@update:alert="addressAlert = $event"
:value="address"
:disabled="true"
/>
<UserFormInput
@ -143,7 +143,7 @@
name="phone"
:isEssential="true"
:is-alert="phoneAlert"
@update:data="phone = $event"
@update:data="phone = $event.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1')"
@update:alert="phoneAlert = $event"
@blur="checkPhoneDuplicate"
:maxlength="11"
@ -298,7 +298,6 @@
}
};
//
const checkColorDuplicate = async () => {
const response = await $api.get(`/user/checkColor?memberCol=${color.value}`);
@ -312,6 +311,14 @@
}
};
const handleColorUpdate = async (newColor) => {
color.value = newColor;
colorError.value = '';
colorErrorAlert.value = false;
await checkColorDuplicate();
}
//
const handleSubmit = async () => {
@ -366,4 +373,3 @@
};
</script>
<style></style>

View File

@ -1,10 +1,11 @@
<template>
<div class="card-body d-flex justify-content-center m-n5">
<ul class="list-unstyled profile-list">
<ul class="profile-list">
<li
v-for="(user, index) in sortedUserList"
:key="index"
:class="{ disabled: user.disabled }"
class="profile-item"
:class="{ newRow: (index + 1) % 4 === 0 }"
@click="$emit('profileClick', user)"
data-bs-placement="top"
:aria-label="user.MEMBERSEQ"
@ -69,12 +70,18 @@ nextTick(() => {
});
const sortedUserList = computed(() => {
if (!employeeId.value) return userList.value;
const myProfile = userList.value.find(user => user.MEMBERSEQ === employeeId.value);
const otherUsers = userList.value.filter(user => user.MEMBERSEQ !== employeeId.value);
return myProfile ? [myProfile, ...otherUsers] : userList.value;
if (!employeeId.value) return [];
//
const nonAdminUsers = userList.value.filter(user => user.MEMBERROL !== "ROLE_ADMIN");
const myProfile = nonAdminUsers.find(user => user.MEMBERSEQ === employeeId.value);
const otherUsers = nonAdminUsers.filter(user => user.MEMBERSEQ !== employeeId.value);
return myProfile ? [myProfile, ...otherUsers] : otherUsers;
});
const getUserProfileImage = (profilePath) =>
profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
@ -85,20 +92,20 @@ const showImage = (event) => (event.target.style.visibility = "visible");
const profileSize = computed(() => {
const totalUsers = userList.value.length;
if (windowWidth.value >= 1650) {
if (totalUsers <= 10) return "68px";
if (totalUsers <= 15) return "55px";
if (windowWidth.value >= 1850) {
if (totalUsers <= 10) return "80px";
if (totalUsers <= 15) return "60px";
return "45px";
} else if (windowWidth.value >= 1300) {
if (totalUsers <= 10) return "45px";
} else if (windowWidth.value >= 1500) {
if (totalUsers <= 10) return "60px";
if (totalUsers <= 15) return "40px";
return "30px";
} else if (windowWidth.value >= 1024) {
if (totalUsers <= 10) return "40px";
} else if (windowWidth.value >= 900) {
if (totalUsers <= 10) return "48px";
if (totalUsers <= 15) return "30px";
return "20px";
} else {
return "20px";
return "35px";
}
});

View File

@ -1,5 +1,5 @@
<template>
<div class="card mb-6">
<div class="card mb-6" :class="{'disabled-class': data.localVote.LOCVOTDDT}" >
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" >
<h5 class="card-title mb-1">
<div class="list-unstyled users-list d-flex align-items-center gap-1">
@ -7,6 +7,7 @@
class="rounded-circle user-avatar border border-3 w-px-40"
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
:style="`border-color: ${data.localVote.usercolor} !important;`"
@error="$event.target.src = '/img/icons/icon.png'"
alt="user"
/>
@ -17,7 +18,7 @@
</div>
<div class="add-btn d-flex align-items-center">
<!-- 투표완료시 -->
<i v-if="data.yesVotetotal == '1'" class="bx bxs-check-circle link-success"></i>
<i v-if="yesVotetotal != '0'" class="bx bxs-check-circle link-success"></i>
<!-- 투표작성자만 수정/삭제/종료 가능 -->
<div v-if="userStore.user.id === data.localVote.LOCVOTREG">
<button
@ -26,8 +27,9 @@
class="bx btn btn-danger"
@click="endBtn(data.localVote.LOCVOTSEQ)"
>종료</button>
<DeleteBtn @click="voteDelete(data.localVote.LOCVOTSEQ)" />
<DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" />
</div>
<p v-if="data.localVote.LOCVOTDDT" class="btn-icon btn-danger rounded-2"><i class="bx bx-power-off"></i></p>
</div>
</div>
</div>
@ -36,21 +38,23 @@
<h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}</h5>
<small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
<!-- 투표안했을시-->
<div v-if="data.localVote.LOCVOTDDT && data.voteResult.length == 0">
<div v-if="data.localVote.LOCVOTDDT && voteResult == 0">
<small class="text-primary text-uppercase">투표 결과없음 (😂아무도 투표하지 않았습니다)</small>
</div>
<div v-else>
<vote-card-check
v-if="data.yesVotetotal == 0"
v-if="yesVotetotal == 0 && !data.localVote.LOCVOTDDT"
@addContents="addContents"
@checkedNames="checkedNames"
:data="data.voteDetails"
:voteInfo="data.localVote"
:total="data.voteDetails.length "/>
<small v-if="yesVotetotal != 0 && !data.localVote.LOCVOTDDT">투표 완료 : 종료시 투표 결과가 나타납니다.</small>
<!-- 투표 결과 -->
<div v-if="data.localVote.LOCVOTDDT" class="mt-3">
<vote-result-list :data="data.voteResult" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES"/>
<vote-result-list :data="data.voteDetails" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES"/>
</div>
<!-- 투표완/미완 인원 -->
<vote-user-list
@ -84,6 +88,13 @@ const props = defineProps({
required: false,
},
});
const voteResult = computed(() => {
return props.data.voteDetails.reduce((sum, item) => sum + item.VOTE_COUNT, 0);
});
const yesVotetotal = computed(() => {
return props.data.voteDetails.reduce((sum, item) => sum + item.yesvote, 0);
});
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const userStore = useUserInfoStore();
const currentDate = new Date();

View File

@ -10,27 +10,26 @@
:selectedValues="checkedNames"
@update:selectedValues="updateCheckedNames"
/>
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1" class="d-flex align-items-center">
<div class="d-flex flex-column gap-2">
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start">
<form-input
class="flex-grow-1 me-2"
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1">
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start mt-2">
<div class="flex-grow-1 me-2 ">
<form-input
:title="'항목 ' + (index + data.length + 1)"
:name="'content' + index"
:is-essential="false"
:is-alert="contentAlerts[index]"
v-model="item.content"
/>
<link-input v-model="item.url" />
<delete-btn @click="removeItem(index)" class="ms-2" />
/>
<link-input v-model="item.url" />
</div>
<div class="mb-4 d-flex justify-content">
<plus-btn @click="addItem" :disabled="total >= 10" class="mb-3" />
<delete-btn @click="removeItem(index)" />
</div>
<div class="mb-4 d-flex justify-content mt-3">
<plus-btn @click="addItem" :disabled="total >= 10" class="mb-2" />
<button class="btn btn-primary btn-icon mb-3" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
<i class="bx bx-check"></i>
</button>
</div>
</div>
</div>
</div>
@ -89,7 +88,9 @@ const updateCheckedNames = (newValues) => {
checkedNames.value = newValues;
};
const selectVote = () =>{
emit('checkedNames',checkedNames.value);
if(checkedNames.value != ''){
emit('checkedNames',checkedNames.value);
}
}

View File

@ -40,7 +40,6 @@ const emit = defineEmits(["update:selectedValues"]);
const handleChange = (event) => {
const value = event.target.value;
let updatedValues = [];
//
if (props.multiIs === "1") {
updatedValues = event.target.checked

View File

@ -24,7 +24,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['addContents','checkedNames','endVoteId','voteEnded','voteDelete']);
const emit = defineEmits(['addContents','checkedNames','endVoteId','voteEnded','voteDelete','randomList']);
const addContents = (itemList ,voteId) =>{
emit('addContents',itemList ,voteId);
}

View File

@ -1,56 +1,99 @@
<template>
<div>
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
<li v-for="char in koreanChars" :key="char" class="mt-2 me-2">
<ul class="d-flex p-0 mb-0">
<li class="d-flex">
<button
type="button"
class="alphabet-btn"
:class="{ active: selectedAl === 'all' }"
@click="selectAlphabet('all')"
> 전체 ({{ totalCount}})
</button>
<span class="divider">|</span>
</li>
<li v-for="(char, index) in koreanChars" :key="char.CHARACTER_" class="d-flex">
<button
type="button"
class="btn"
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
@click="selectAlphabet(char)"
class="alphabet-btn"
:class="{ active: selectedAl === char.CHARACTER_ }"
@click="selectAlphabet(char.CHARACTER_)"
>
{{ char }}
{{ char.CHARACTER_ }} ({{ char.COUNT }})
</button>
<span v-if="index !== koreanChars.length - 1" class="divider">|</span>
</li>
</ul>
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
<li v-for="char in englishChars" :key="char" class="mt-2 me-2">
<ul class="d-flex p-0 mb-0">
<li v-for="(char, index) in englishChars" :key="char.CHARACTER_" class="d-flex">
<button
type="button"
class="btn"
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
@click="selectAlphabet(char)"
class="alphabet-btn"
:class="{ active: selectedAl === char.CHARACTER_ }"
@click="selectAlphabet(char.CHARACTER_)"
>
{{ char }}
{{ char.CHARACTER_ }} ({{ char.COUNT }})
</button>
<span v-if="index !== englishChars.length - 1" class="divider">|</span>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
const koreanChars = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
const englishChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
const props = defineProps({
indexCategory: {
type: Array,
required: true,
},
selectedAl: {
type: String,
default : '',
required: false,
},
});
const selectedAlphabet = ref(props.selectedAl);
const totalCount = computed(() => {
return props.indexCategory.reduce((sum, item) => sum + item.COUNT, 0);
});
const koreanChars = computed(() => {
return props.indexCategory.filter(char => /[-ㅎ가-]/.test(char.CHARACTER_));
});
const englishChars = computed(() => {
return props.indexCategory.filter(char => /^[a-zA-Z]$/.test(char.CHARACTER_));
});
const selectedAlphabet = ref(null);
//emit
const emit = defineEmits();
const selectAlphabet = (alphabet) => {
selectedAlphabet.value = selectedAlphabet.value === alphabet ? null : alphabet;
emit('update:data',selectedAlphabet.value);
emit('update:data', selectedAlphabet.value);
};
</script>
<style scoped>
.btn {
min-width: 56px;
.alphabet-btn {
background: none;
border: none;
font-size: 13px;
font-weight: bold;
color: #6c757d;
cursor: pointer;
width: 70%;
height: 40px;
}
@media (max-width: 768px) {
.alphabet-list {
overflow-x: scroll;
flex-wrap: nowrap !important;
}
.alphabet-btn:hover {
color: #0d6efd;
}
.alphabet-btn.active {
color: #0d6efd;
text-decoration: underline;
}
.divider {
color: #bbb;
font-size: 14px;
font-weight: bold;
}
</style>

View File

@ -1,23 +1,23 @@
<template>
<li class="mt-5 card p-5">
<DictWrite
v-if="writeStore.isItemActive(item.WRDDICSEQ)"
@close="writeStore.closeAll();"
:dataList="cateList"
<DictWrite
v-if="writeStore.isItemActive(item.WRDDICSEQ)"
@close="writeStore.closeAll();"
:dataList="cateList"
@addWord="editWord"
:NumValue="item.WRDDICSEQ"
:formValue="item.WRDDICCAT"
:formValue="item.WRDDICCAT"
:titleValue="item.WRDDICTTL"
:contentValue="item.WRDDICCON"
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
/>
/>
<div v-else>
<input
<input
v-if="userStore.user.role == 'ROLE_ADMIN'"
type="checkbox"
type="checkbox"
class="form-check-input admin-chk"
:name="item.WRDDICSEQ"
:name="item.WRDDICSEQ"
@change="toggleCheck($event)"
>
<div class="d-flex align-ite-center">
@ -43,8 +43,8 @@
</div>
</div>
</div>
<div
v-if="item.author.createdAt !== item.lastEditor.updatedAt"
<div
v-if="item.author.createdAt !== item.lastEditor.updatedAt"
class="d-flex justify-content-between flex-wrap gap-2 mb-2"
>
<div class="d-flex flex-wrap align-items-center mb-50">
@ -65,7 +65,8 @@
</div>
<div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
<EditBtn ref="writeButton" @click="writeStore.toggleItem(item.WRDDICSEQ)" :isToggleEnabled="true"/>
<EditBtn ref="writeButton" @click="writeStore.toggleItem(item.WRDDICSEQ)" :isToggleEnabled="true"
:isActive="writeStore.activeItemId === item.WRDDICSEQ"/>
</div>
</li>
</template>
@ -124,7 +125,7 @@ const editWord = (data) => {
})
.then((res) => {
if (res.data.data === 1) {
toastStore.onToast('용어가 수정되었습니다.', 's');
toastStore.onToast('용어가 수정되었습니다.', 's');
writeStore.closeAll();
if (writeButton.value) {
writeButton.value.resetButton();
@ -183,4 +184,4 @@ const toggleCheck = (event) => {
top: -0.5rem;
--bs-form-check-bg: #fff;
}
</style>
</style>

View File

@ -11,10 +11,11 @@
@change="onChange"
:value="formValue"
:disabled="isDisabled"
:is-essential="false"
/>
</div>
<div class="col-2 btn-margin" v-if="!isDisabled">
<PlusBtn @click="toggleInput" />
<PlusBtn @click="toggleInput"/>
</div>
</div>
@ -41,15 +42,11 @@
:modelValue="titleValue"
@update:modelValue="wordTitle = $event"
:disabled="isDisabled"
@keyup="ValidHandler('title')"
/>
</div>
<div>
<QEditor
@update:data="content = $event"
@update:imageUrls="imageUrls = $event"
:is-alert="wordContentAlert"
:initialData="contentValue"
/>
<QEditor @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
<div class="text-end mt-5">
<button class="btn btn-primary" @click="saveWord">
<i class="bx bx-check"></i>
@ -59,153 +56,183 @@
</template>
<script setup>
import { defineProps, computed, ref, defineEmits } from 'vue';
import { defineProps, computed, ref, defineEmits } from 'vue';
import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue';
import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue';
const emit = defineEmits(['close', 'addCategory', 'addWord']);
const emit = defineEmits(['close','addCategory','addWord']);
//
const wordTitle = ref('');
const addCategory = ref('');
const content = ref('');
const imageUrls = ref([]);
//
const wordTitle = ref('');
const addCategory = ref('');
const content = ref('');
const imageUrls = ref([]);
// Vaildation
const wordTitleAlert = ref(false);
const wordContentAlert = ref(false);
const addCategoryAlert = ref(false);
// Vaildation
const wordTitleAlert = ref(false);
const wordContentAlert = ref(false);
const addCategoryAlert = ref(false);
//
const selectCategory = ref('');
//
const selectCategory = ref('');
//
const computedTitle = computed(() => (wordTitle.value === '' ? props.titleValue : wordTitle.value));
//
const computedTitle = computed(() =>
wordTitle.value === '' ? props.titleValue : wordTitle.value
);
//
const selectedCategory = computed(() => (selectCategory.value === '' ? props.formValue : selectCategory.value));
//
const selectedCategory = computed(() =>
selectCategory.value === '' ? props.formValue : selectCategory.value
// ref
const categoryInputRef = ref(null);
);
const props = defineProps({
dataList: {
type: Array,
default: () => [],
},
NumValue: {
type: Number,
},
formValue: {
type: [String, Number],
},
titleValue: {
type: String,
},
contentValue: {
type: String,
},
isDisabled: {
type: Boolean,
default: false,
},
});
// ref
const categoryInputRef = ref(null);
//
const showInput = ref(false);
const props = defineProps({
dataList: {
type: Array,
default: () => []
},
NumValue : {
type: Number
},
formValue : {
type:[String, Number]
},
titleValue : {
type:String,
},contentValue : {
type:String,
},
isDisabled: {
type: Boolean,
default: false
}
});
//
const toggleInput = () => {
showInput.value = !showInput.value;
//
const showInput = ref(false);
//
const toggleInput = () => {
showInput.value = !showInput.value;
};
const onChange = (newValue) => {
selectCategory.value = newValue.target.value;
};
const ValidHandler = (field) => {
if(field == 'title'){
wordTitleAlert.value = false;
}
if(field == 'content'){
wordContentAlert.value = false;
}
}
const handleContentUpdate = (newContent) => {
content.value = newContent;
ValidHandler("content"); //
};
//
const saveWord = () => {
let valid = true;
//validation
let computedTitleTrim;
if(computedTitle.value != undefined){
computedTitleTrim = computedTitle.value.trim()
}
//
if(computedTitleTrim == undefined || computedTitleTrim == ''){
wordTitleAlert.value = true;
valid = false;
} else {
wordTitleAlert.value = false;
}
//
let inserts = [];
if (inserts.length === 0 && content.value?.ops?.length > 0) {
inserts = content.value.ops.map(op =>
typeof op.insert === 'string' ? op.insert.trim() : op.insert
);
}
//
if(content.value == '' || inserts.join('') === ''){
wordContentAlert.value = true;
valid = false;
}else{
wordContentAlert.value = false;
}
const wordData = {
id: props.NumValue || null,
title: computedTitle.value,
category: selectedCategory.value,
content: content.value,
};
if(valid){
emit('addWord', wordData, addCategory.value === ''
? (isNaN(selectedCategory.value) ? selectedCategory.value : Number(selectedCategory.value))
: addCategory.value);
}
}
const onChange = newValue => {
selectCategory.value = newValue.target.value;
};
//
const saveWord = () => {
//validation
let computedTitleTrim;
// focusout
const handleCategoryFocusout = (value) => {
const valueTrim = value.trim();
if (computedTitle.value != undefined) {
computedTitleTrim = computedTitle.value.trim();
}
const existingCategory = props.dataList.find(item => item.label === valueTrim);
//
if (computedTitleTrim == undefined || computedTitleTrim == '') {
wordTitleAlert.value = true;
return;
} else {
wordTitleAlert.value = false;
}
//
if(valueTrim == ''){
addCategoryAlert.value = true;
//
let inserts = [];
if (inserts.length === 0 && content.value?.ops?.length > 0) {
inserts = content.value.ops.map(op => (typeof op.insert === 'string' ? op.insert.trim() : op.insert));
}
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
//
if (content.value == '' || inserts.join('') === '') {
wordContentAlert.value = true;
return;
}
const wordData = {
id: props.NumValue || null,
title: computedTitle.value,
category: selectedCategory.value,
content: content.value,
};
}else if (existingCategory) {
addCategoryAlert.value = true;
emit('addWord', wordData, addCategory.value);
};
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
// focusout
const handleCategoryFocusout = value => {
const valueTrim = value.trim();
} else {
addCategoryAlert.value = false;
}
};
const existingCategory = props.dataList.find(item => item.label === valueTrim);
//
if (valueTrim == '') {
addCategoryAlert.value = true;
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
} else if (existingCategory) {
addCategoryAlert.value = true;
// focus
setTimeout(() => {
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
if (inputElement) {
inputElement.focus();
}
}, 0);
} else {
addCategoryAlert.value = false;
}
};
</script>
<style scoped>
.dict-w {
width: 83%;
}
.dict-w {
width: 83%;
}
@media (max-width: 768px) {
.btn-margin {
margin-top: 2.5rem;
}
@media (max-width: 768px) {
.btn-margin {
margin-top: 2.5rem
}
}
</style>

View File

@ -10,7 +10,15 @@
<div class="col-xl-12">
<div class="card-body">
<!-- 제목 입력 -->
<FormInput title="제목" name="title" :is-essential="true" :is-alert="titleAlert" v-model="title" />
<FormInput
title="제목"
name="title"
:is-essential="true"
:is-alert="titleAlert"
v-model="title"
@update:alert="titleAlert = $event"
@input.once="validateTitle"
/>
<!-- 첨부파일 업로드 -->
<FormFile
@ -43,7 +51,6 @@
<label for="html5-tel-input" class="col-md-2 col-form-label">
내용
<span class="text-red">*</span>
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
</label>
<div class="col-md-12">
<QEditor
@ -53,6 +60,7 @@
:initialData="content"
/>
</div>
<div v-if="contentAlert" class="invalid-feedback d-block">내용을 확인해주세요.</div>
</div>
<!-- 버튼 -->
@ -74,10 +82,15 @@
import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue';
import { ref, onMounted, computed, watch } from 'vue';
import { ref, onMounted, computed, watch, inject } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore';
import axios from '@api';
//
const $common = inject('common');
const toastStore = useToastStore();
//
const title = ref('');
const content = ref('');
@ -134,51 +147,18 @@
router.push('/board');
};
//
const updateBoard = async () => {
//
if (!title.value) {
titleAlert.value = true;
return;
}
titleAlert.value = false;
//
const checkValidation = () => {
contentAlert.value = $common.isNotValidContent(content);
titleAlert.value = $common.isNotValidInput(title.value);
if (!content.value) {
contentAlert.value = true;
return;
}
contentAlert.value = false;
try {
//
const boardData = {
LOCBRDTTL: title.value,
LOCBRDCON: JSON.stringify(content.value),
LOCBRDSEQ: currentBoardId.value,
};
if (delFileIdx.value && delFileIdx.value.length > 0) {
boardData.delFileIdx = [...delFileIdx.value];
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
if (titleAlert.value) {
title.value = '';
}
const fileArray = newFileFilter(attachFiles);
const formData = new FormData();
Object.entries(boardData).forEach(([key, value]) => {
formData.append(key, value);
});
fileArray.forEach((file, idx) => {
formData.append('files', file);
});
await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
alert('게시물이 수정되었습니다.');
goList();
} catch (error) {
console.error('게시물 수정 중 오류 발생:', error);
alert('게시물 수정에 실패했습니다.');
return true;
} else {
return false;
}
};
@ -219,6 +199,60 @@
};
////////////////// fileSection[E] ////////////////////
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
watch(content, () => {
contentAlert.value = $common.isNotValidContent(content);
});
//
const validateTitle = () => {
titleAlert.value = title.value.trim().length === 0;
};
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
// watch(content, () => {
// contentAlert.value = $common.isNotValidContent(content);
// });
//
const updateBoard = async () => {
if (checkValidation()) return;
try {
//
const boardData = {
LOCBRDTTL: title.value.trim(),
LOCBRDCON: JSON.stringify(content.value),
LOCBRDSEQ: currentBoardId.value,
};
//
if (delFileIdx.value && delFileIdx.value.length > 0) {
boardData.delFileIdx = [...delFileIdx.value];
}
const fileArray = newFileFilter(attachFiles);
const formData = new FormData();
// formData boardData
Object.entries(boardData).forEach(([key, value]) => {
formData.append(key, value);
});
// formData
fileArray.forEach((file, idx) => {
formData.append('files', file);
});
await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
toastStore.onToast('게시물이 수정되었습니다.', 's');
goList();
} catch (error) {
console.error('게시물 수정 중 오류 발생:', error);
toastStore.onToast('게시물 수정에 실패했습니다.');
}
};
//
onMounted(() => {
if (currentBoardId.value) {

View File

@ -25,9 +25,13 @@
<input
type="password"
class="form-control"
autocomplete="off"
v-model="password"
placeholder="비밀번호 입력"
@input="password = password.replace(/\s/g, '')"
@input="
password = password.replace(/\s/g, '');
inputCheck();
"
/>
<button class="btn btn-primary" @click="submitPassword">확인</button>
</div>
@ -88,6 +92,7 @@
:unknown="unknown"
:commentAlert="commentAlert"
:passwordAlert="passwordAlert"
:maxLength="500"
@submitComment="handleCommentSubmit"
/>
</div>
@ -130,6 +135,7 @@
import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useToastStore } from '@s/toastStore';
import axios from '@api';
//
const profileName = ref('');
@ -148,6 +154,7 @@
const route = useRoute();
const router = useRouter();
const userStore = useUserInfoStore();
const toastStore = useToastStore();
const currentBoardId = ref(Number(route.params.id));
const unknown = computed(() => profileName.value === '익명');
const currentUserId = computed(() => userStore.user.id); // id
@ -221,6 +228,9 @@
navigateLastPage: 1,
});
const inputCheck = () => {
passwordAlert.value = '';
};
//
const fetchBoardDetails = async () => {
try {
@ -228,7 +238,6 @@
const data = response.data.data;
profileName.value = data.author || '익명';
console.log(data.author);
authorId.value = data.authorId;
boardTitle.value = data.title || '제목 없음';
boardContent.value = data.content || '';
@ -562,6 +571,7 @@
const submitPassword = async () => {
if (!password.value.trim()) {
passwordAlert.value = '비밀번호를 입력해주세요.';
return;
}
@ -585,18 +595,7 @@
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
}
} catch (error) {
if (error.reponse && error.reponse.status === 401) passwordAlert.value = '비밀번호가 일치하지 않습니다.';
// if (error.response) {
// if (error.response.status === 401) {
// passwordAlert.value = ' .';
// } else {
// passwordAlert.value = error.response.data?.message || ' .';
// }
// } else if (error.request) {
// passwordAlert.value = ' . .';
// } else {
// passwordAlert.value = ' .';
// }
if (error.response && error.response.status === 401) passwordAlert.value = '비밀번호가 일치하지 않습니다.';
}
};
@ -658,7 +657,7 @@
});
if (response.data.code === 200) {
alert('게시물이 삭제되었습니다.');
toastStore.onToast('게시물이 삭제되었습니다.');
router.push({ name: 'BoardList' });
} else {
alert('삭제 실패: ' + response.data.message);

View File

@ -23,11 +23,7 @@
<div class="mb-4 d-flex align-items-center">
<label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label>
<div class="d-flex flex-wrap align-items-center mt-3 ms-1">
<div
v-for="(category, index) in categoryList"
:key="index"
class="form-check me-3"
>
<div v-for="(category, index) in categoryList" :key="index" class="form-check me-3">
<input
class="form-check-input"
type="radio"
@ -41,9 +37,7 @@
</label>
</div>
</div>
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
카테고리를 선택해주세요.
</div>
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">카테고리를 선택해주세요.</div>
</div>
<!-- 비밀번호 필드 (익명게시판 선택 활성화) -->
@ -52,6 +46,7 @@
title="비밀번호"
name="pw"
type="password"
autocomplete="new-password"
:is-essential="true"
:is-alert="passwordAlert"
v-model="password"
@ -74,7 +69,11 @@
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
<ul class="list-group mt-2" v-if="attachFiles.length">
<li v-for="(file, index) in attachFiles" :key="index" class="list-group-item d-flex justify-content-between align-items-center">
<li
v-for="(file, index) in attachFiles"
:key="index"
class="list-group-item d-flex justify-content-between align-items-center"
>
{{ file.name }}
<button class="close-btn" @click="removeFile(index)"></button>
</li>
@ -82,15 +81,11 @@
<!-- 내용 입력 (에디터) -->
<div class="mb-4">
<label class="col-md-2 col-form-label">
내용 <span class="text-danger">*</span>
</label>
<label class="col-md-2 col-form-label"> 내용 <span class="text-danger">*</span> </label>
<div class="col-md-12">
<QEditor @update:data="content = $event" />
</div>
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
내용을 입력해주세요.
</div>
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div>
</div>
<div class="mb-4 d-flex justify-content-end">
@ -104,162 +99,162 @@
</template>
<script setup>
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue';
import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue';
import { useToastStore } from '@s/toastStore';
import router from '@/router';
import axios from '@api';
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue';
import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue';
import { useToastStore } from '@s/toastStore';
import router from '@/router';
import axios from '@api';
const toastStore = useToastStore();
const categoryList = ref([]);
const title = ref('');
const password = ref('');
const categoryValue = ref(null);
const content = ref({ ops: [] });
const isFileValid = ref(true);
const toastStore = useToastStore();
const categoryList = ref([]);
const title = ref('');
const password = ref('');
const categoryValue = ref(null);
const content = ref({ ops: [] });
const isFileValid = ref(true);
const titleAlert = ref(false);
const passwordAlert = ref(false);
const contentAlert = ref(false);
const categoryAlert = ref(false);
const attachFilesAlert = ref(false);
const titleAlert = ref(false);
const passwordAlert = ref(false);
const contentAlert = ref(false);
const categoryAlert = ref(false);
const attachFilesAlert = ref(false);
const attachFiles = ref([]);
const maxFiles = 5;
const maxSize = 10 * 1024 * 1024;
const fileError = ref('');
const attachFiles = ref([]);
const maxFiles = 5;
const maxSize = 10 * 1024 * 1024;
const fileError = ref('');
const fetchCategories = async () => {
try {
const response = await axios.get('board/categories');
categoryList.value = response.data.data;
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
if (freeCategory) {
categoryValue.value = freeCategory.CMNCODVAL;
const fetchCategories = async () => {
try {
const response = await axios.get('board/categories');
categoryList.value = response.data.data;
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
if (freeCategory) {
categoryValue.value = freeCategory.CMNCODVAL;
}
} catch (error) {
console.error('카테고리 불러오기 오류:', error);
}
} catch (error) {
console.error('카테고리 불러오기 오류:', error);
}
};
};
onMounted(() => {
fetchCategories();
});
onMounted(() => {
fetchCategories();
});
const fileCount = computed(() => attachFiles.value.length);
const fileCount = computed(() => attachFiles.value.length);
const handleFileUpload = (files) => {
const validFiles = files.filter(file => file.size <= maxSize);
if (files.some(file => file.size > maxSize)) {
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
return;
}
if (attachFiles.value.length + validFiles.length > maxFiles) {
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
return;
}
fileError.value = '';
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
};
const removeFile = (index) => {
attachFiles.value.splice(index, 1);
if (attachFiles.value.length <= maxFiles) {
const handleFileUpload = files => {
const validFiles = files.filter(file => file.size <= maxSize);
if (files.some(file => file.size > maxSize)) {
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
return;
}
if (attachFiles.value.length + validFiles.length > maxFiles) {
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
return;
}
fileError.value = '';
}
};
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
};
watch(attachFiles, () => {
isFileValid.value = attachFiles.value.length <= maxFiles;
});
const removeFile = index => {
attachFiles.value.splice(index, 1);
if (attachFiles.value.length <= maxFiles) {
fileError.value = '';
}
};
const validateTitle = () => {
titleAlert.value = title.value.trim().length === 0;
};
watch(attachFiles, () => {
isFileValid.value = attachFiles.value.length <= maxFiles;
});
const validatePassword = () => {
if (categoryValue.value === 300102) {
password.value = password.value.replace(/\s/g, ''); //
passwordAlert.value = password.value.length === 0;
} else {
passwordAlert.value = false;
}
};
const validateTitle = () => {
titleAlert.value = title.value.trim().length === 0;
};
const validateContent = () => {
if (!content.value?.ops?.length) {
contentAlert.value = true;
return;
}
const validatePassword = () => {
if (categoryValue.value === 300102) {
password.value = password.value.replace(/\s/g, ''); //
passwordAlert.value = password.value.length === 0;
} else {
passwordAlert.value = false;
}
};
//
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
//
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
//
contentAlert.value = !(hasText || hasImage);
};
/** 글쓰기 */
const write = async () => {
validateTitle();
validatePassword();
validateContent();
categoryAlert.value = categoryValue.value == null;
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return;
}
try {
const boardData = {
LOCBRDTTL: title.value,
LOCBRDCON: JSON.stringify(content.value), // Delta JSON
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
LOCBRDTYP: categoryValue.value
};
const { data: boardResponse } = await axios.post('board', boardData);
const boardId = boardResponse.data;
// ( )
if (attachFiles.value && attachFiles.value.length > 0) {
await Promise.all(attachFiles.value.map(async (file) => {
console.log(file);
const formData = new FormData();
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
formData.append('CMNBRDSEQ', boardId);
formData.append('CMNFLEORG', fileNameWithoutExt);
formData.append('CMNFLEEXT', file.name.split('.').pop());
formData.append('CMNFLESIZ', file.size);
formData.append('file', file); // 📌
await axios.post(`board/${boardId}/attachments`, formData,
{ isFormData : true }
);
}));
const validateContent = () => {
if (!content.value?.ops?.length) {
contentAlert.value = true;
return;
}
toastStore.onToast('게시물이 작성되었습니다.', 's');
goList();
} catch (error) {
console.error(error);
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
}
};
//
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
//
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
/** 목록으로 이동 */
const goList = () => {
router.push('/board');
};
//
contentAlert.value = !(hasText || hasImage);
};
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
watch(content, () => {
validateContent();
});
/** 글쓰기 */
const write = async () => {
validateTitle();
validatePassword();
validateContent();
categoryAlert.value = categoryValue.value == null;
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return;
}
try {
const boardData = {
LOCBRDTTL: title.value,
LOCBRDCON: JSON.stringify(content.value), // Delta JSON
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
LOCBRDTYP: categoryValue.value,
};
const { data: boardResponse } = await axios.post('board', boardData);
const boardId = boardResponse.data;
// ( )
if (attachFiles.value && attachFiles.value.length > 0) {
await Promise.all(
attachFiles.value.map(async file => {
console.log(file);
const formData = new FormData();
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
formData.append('CMNBRDSEQ', boardId);
formData.append('CMNFLEORG', fileNameWithoutExt);
formData.append('CMNFLEEXT', file.name.split('.').pop());
formData.append('CMNFLESIZ', file.size);
formData.append('file', file); // 📌
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
}),
);
}
toastStore.onToast('게시물이 작성되었습니다.', 's');
goList();
} catch (error) {
console.error(error);
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
}
};
/** 목록으로 이동 */
const goList = () => {
router.push('/board');
};
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
watch(content, () => {
validateContent();
});
</script>

View File

@ -142,28 +142,29 @@ const calendarOptions = reactive({
datesSet: handleMonthChange,
events: calendarEvents,
});
//
//
function handleMonthChange(viewInfo) {
const currentDate = viewInfo.view.currentStart;
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
loadCalendarData(year, month);
}
}
//
function handleDateClick(info) {
const clickedDateStr = info.dateStr;
const clickedDate = info.date;
const todayStr = new Date().toISOString().split("T")[0];
if (
clickedDate.getDay() === 0 ||
clickedDate.getDay() === 6 ||
holidayDates.value.has(clickedDateStr) ||
clickedDateStr < todayStr
){
) {
return;
}
const isMyVacation = myVacations.value.some(vac => {
const vacDate = vac.date ? String(vac.date).substring(0, 10) : "";
const vacDate = vac.date ? vac.date.substring(0, 10) : "";
return vacDate === clickedDateStr && !vac.receiverId;
});
if (isMyVacation) {
@ -186,7 +187,6 @@ function handleDateClick(info) {
selectedDates.value.set(clickedDateStr, type);
halfDayType.value = null;
updateCalendarEvents();
//
if (halfDayButtonsRef.value) {
halfDayButtonsRef.value.resetHalfDay();
}
@ -266,15 +266,24 @@ const handleProfileClick = async (user) => {
const fetchUserList = async () => {
try {
await userListStore.fetchUserList();
userList.value = userListStore.userList;
// "ROLE_ADMIN"
const filteredUsers = userListStore.userList.filter(user => user.MEMBERROL !== "ROLE_ADMIN");
// userList
userList.value = [...filteredUsers];
if (!userList.value.length) {
console.warn("📌 사용자 목록이 비어 있음!");
return;
}
//
userColors.value = {};
userList.value.forEach((user) => {
userColors.value[user.MEMBERSEQ] = user.usercolor;
userColors.value[user.MEMBERSEQ] = user.usercolor;
});
} catch (error) {
console.error("📌 사용자 목록 불러오기 오류:", error);
}
@ -310,47 +319,57 @@ const filteredReceivedVacations = computed(() => {
});
});
/* 휴가 변경사항 저장 */
/* 휴가 저장 */
//
async function saveVacationChanges() {
if (!hasChanges.value) return;
const selectedDatesArray = Array.from(selectedDates.value);
const vacationsToAdd = selectedDatesArray
.filter(([date, type]) => type !== "delete")
.filter(([date, type]) =>
!myVacations.value.some(vac => vac.date && vac.date.startsWith(date)) ||
myVacations.value.some(vac => vac.date && vac.date.startsWith(date) && vac.receiverId)
)
.map(([date, type]) => ({ date, type }));
const vacationsToDelete = myVacations.value
.filter(vac => {
if (!vac.date) return false;
const date = vac.date.split("T")[0];
return selectedDates.value.get(date) === "delete" && !vac.receiverId;
})
.map(vac => {
const id = vac.id;
return typeof id === "number" ? Number(id) : id;
});
try {
const response = await axios.post("vacation/batchUpdate", {
add: vacationsToAdd,
delete: vacationsToDelete
});
if (response.data && response.data.status === "OK") {
toastStore.onToast('휴가 변경 사항이 저장되었습니다.', 's');
await fetchVacationHistory(lastRemainingYear.value);
await fetchRemainingVacation();
if (isModalOpen.value) {
await fetchVacationHistory(lastRemainingYear.value);
}
const currentDate = fullCalendarRef.value.getApi().getDate();
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
selectedDates.value.clear();
updateCalendarEvents();
const vacationChangesByYear = selectedDatesArray.reduce((acc, [date, type]) => {
const year = date.split("-")[0]; // YYYY-MM-DD YYYY
if (!acc[year]) acc[year] = { add: [], delete: [] };
if (type !== "delete") {
acc[year].add.push({ date, type });
} else {
toastStore.onToast('휴가 저장 중 오류가 발생했습니다.', 'e');
acc[year].delete.push(date);
}
return acc;
}, {});
try {
for (const year of Object.keys(vacationChangesByYear)) {
const vacationsToAdd = vacationChangesByYear[year].add;
// id
const vacationsToDeleteForYear = myVacations.value
.filter(vac => {
if (!vac.date) return false;
const vacDate = vac.date.split("T")[0];
return vacationChangesByYear[year].delete.includes(vacDate);
});
const vacationIdsToDelete = vacationsToDeleteForYear.map(vac => vac.id);
if (vacationsToAdd.length > 0 || vacationIdsToDelete.length > 0) {
const response = await axios.post("vacation/batchUpdate", {
add: vacationsToAdd,
delete: vacationIdsToDelete,
});
if (response.data && response.data.status === "OK") {
toastStore.onToast(`휴가 변경 사항이 저장되었습니다.`, 's');
// : myVacations ID
myVacations.value = myVacations.value.filter(vac => !vacationIdsToDelete.includes(vac.id));
//
const updatedVacations = await fetchVacationHistory(year);
if (updatedVacations) {
myVacations.value = updatedVacations; //
}
} else {
toastStore.onToast(`휴가 변경 중 오류가 발생했습니다.`, 'e');
}
}
}
await fetchRemainingVacation();
selectedDates.value.clear();
updateCalendarEvents();
//
const currentDate = fullCalendarRef.value.getApi().getDate();
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
} catch (error) {
console.error("🚨 휴가 변경 저장 실패:", error);
toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
@ -363,15 +382,17 @@ async function fetchVacationHistory(year) {
try {
const response = await axios.get(`vacation/history?year=${year}`);
if (response.status === 200 && response.data) {
myVacations.value = response.data.data.usedVacations || [];
receivedVacations.value = response.data.data.receivedVacations || [];
} else {
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
myVacations.value = [];
receivedVacations.value = [];
const newVacations = response.data.data.usedVacations || [];
const uniqueVacations = Array.from(
new Map([...myVacations.value, ...newVacations].map(v => [`${v.date}-${v.type}`, v]))
.values()
);
myVacations.value = uniqueVacations;
}
} catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error);
console.error(`🚨 ${year}년 휴가 데이터 불러오기 실패:`, error);
}
}
//
@ -462,16 +483,20 @@ function toggleHalfDay(type) {
/* 페이지 이동 시 변경 사항 확인 */
router.beforeEach((to, from, next) => {
if (hasChanges.value) {
console.log('휴가!!!!!');
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
if (!answer) {
return next(false);
}
}
selectedDates.value.clear();
next();
});
onBeforeUnmount(() => {
window.removeEventListener("beforeunload", preventUnsavedChanges);
});
/* 새로고침 또는 페이지 종료 시 알림 */
function preventUnsavedChanges(event) {
if (hasChanges.value) {
event.preventDefault();
@ -480,8 +505,10 @@ function preventUnsavedChanges(event) {
}
/* watch */
watch(lastRemainingYear, async (newYear, oldYear) => {
await fetchVacationHistory(newYear);
watch(() => lastRemainingYear.value, async (newYear, oldYear) => {
if (newYear !== oldYear) {
await fetchVacationHistory(newYear);
}
});
// `selectedDates`
watch(
@ -533,6 +560,9 @@ onMounted(async () => {
altFormat: "F Y"
})
],
onOpen: function() {
document.querySelector('.flatpickr-input').style.visibility = 'hidden';
},
onChange: function(selectedDatesArr, dateStr) {
//
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
@ -551,17 +581,17 @@ onMounted(async () => {
if (titleEl) {
titleEl.style.cursor = 'pointer';
titleEl.addEventListener('click', () => {
const dpEl = calendarDatepicker.value;
dpEl.style.display = 'block';
dpEl.style.position = 'fixed';
dpEl.style.top = '25%';
dpEl.style.left = '50%';
dpEl.style.transform = 'translate(-50%, -50%)';
dpEl.style.zIndex = '9999';
dpEl.style.border = 'none';
dpEl.style.outline = 'none';
dpEl.style.backgroundColor = 'transparent';
fpInstance.open();
const dpEl = calendarDatepicker.value;
dpEl.style.display = 'block';
dpEl.style.position = 'fixed';
dpEl.style.top = '25%';
dpEl.style.left = '50%';
dpEl.style.transform = 'translate(-50%, -50%)';
dpEl.style.zIndex = '9999';
dpEl.style.border = 'none';
dpEl.style.outline = 'none';
dpEl.style.backgroundColor = 'transparent';
fpInstance.open();
});
}
});

View File

@ -77,20 +77,21 @@ const changeCheck = () =>{
getvoteList();
}
//
const getvoteList = async () => {
const response = await $api.get('vote/getVoteList',{
params:
{
page: currentPage.value
,voteset:voteset.value
,myVote:ischeked.value ? '1':'0'
}
});
if (response.data.status === "OK") {
PageData.value = response.data.data;
voteListCardData.value = response.data.data.list;
}
};
const getvoteList = () => {
$api.get('vote/getVoteList',{
//
params:
{
page: currentPage.value
,voteset:voteset.value
,myVote:ischeked.value ? '1':'0'
}
}).then(res => {
PageData.value = res.data.data;
voteListCardData.value = res.data.data.list;
})
};
const selectHandler = () =>{
voteset.value = category.value;
getvoteList();
@ -148,14 +149,17 @@ const voteDelete =(id) =>{
}
// 1
const randomList = (data,id) =>{
isLoading.value = false;
$api.post('vote/randomList',{
randomList :data
,voteid:id
}).then((res)=>{
if(res.data.status === 'OK'){
toastStore.onToast('랜덤뽑기 진행되었습니다.', 's');
getvoteList();
}
toastStore.onToast('랜덤뽑기 진행되었습니다.', 's');
setTimeout(() => {
getvoteList();
}, 2000); // 3000ms = 3
}
})
}
//

View File

@ -1,45 +1,28 @@
<template>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card p-5">
<div >
<!-- 타이틀, 검색 -->
<div class="row">
<div class="col-12 col-md-6">
<h5 class="mb-0 title">용어집</h5>
</div>
<div class="col-12 col-md-6">
<SearchBar @update:data="search"/>
</div>
</div>
<SearchBar @update:data="search"/>
<!-- 단어 갯수, 작성하기 -->
<div class="mt-4">
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
</div>
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
<!-- -->
<div>
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" />
</div>
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" :indexCategory="indexCategory" :selectedAl="selectedAlphabet" />
<!-- 카테고리 -->
<div v-if="cateList.length" class="mt-5">
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange"/>
<div v-if="cateList.length">
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange" :showAll="true"/>
</div>
<!-- 작성 -->
<div v-if="writeStore.isItemActive(999999)" class="mt-5">
<div v-if="writeStore.isItemActive(999999)" class="mt-5 card p-5">
<DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
</div>
</div>
<!-- 용어 리스트 -->
<div class="mt-10">
<div >
<!-- 로딩 중일 -->
<div v-if="loading">로딩 중...</div>
<LoadingSpinner v-if="loading"/>
<!-- 에러 메시지 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 단어 목록 -->
<ul v-if="total > 0" class="px-0 list-unstyled">
<DictCard
@ -47,14 +30,12 @@
:key="item.WRDDICSEQ"
:item="item"
v-model:cateList="cateList"
@refreshWordList="getwordList"
@refreshWordList="refreshWordList"
@updateChecked="updateCheckedItems"
/>
</ul>
<!-- 데이터가 없을 -->
<div v-else-if="!loading && !error" class="card p-5 text-center">용어집의 용어가 없습니다.</div>
<div v-if="total == 0" class="text-center mt-5">용어를 선택 / 작성해 주세요 </div>
</div>
</div>
@ -77,6 +58,7 @@
import commonApi from '@/common/commonApi';
import { useToastStore } from '@s/toastStore';
import { useWriteVisibleStore } from '@s/writeVisible';
import LoadingSpinner from "@v/LoadingPage.vue";
//
const writeStore = useWriteVisibleStore();
@ -116,11 +98,19 @@
//
const searchText = ref('');
//
const indexCategory = ref([]);
//
onMounted(() => {
getwordList();
getIndex();
writeStore.closeAll();
});
const refreshWordList = () => {
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
};
//
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
axios.get('worddict/getWordList',{
@ -144,6 +134,14 @@
loading.value = false;
});
};
//
const getIndex = () => {
axios.get('worddict/getIndexCategory').then(res=>{
if(res.data.status ="OK"){
indexCategory.value = res.data.data;
}
})
}
//
const search = (e) => {
@ -154,41 +152,49 @@
//
const handleSelectedAlphabetChange = (newAlphabet) => {
selectedAlphabet.value = newAlphabet;
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
if (newAlphabet !== null) {
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
} else {
wordList.value = [];
total.value = 0;
}
};
//
const handleSelectedCategoryChange = (category) => {
selectedCategory.value = category;
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
if (category !== null ) {
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
if(category == 'all'){
getwordList(searchText.value, selectedAlphabet.value, '');
}
} else {
wordList.value = [];
total.value = 0;
}
}
//
const addWord = (wordData, data) => {
let category = null;
let newCodName = '';
//
const existingCategory = cateList.value.find(item => item.label === data.trim());
if (existingCategory) {
//
category = existingCategory.label == '' ? wordData.category : existingCategory.value;
} else {
//
if(typeof(data) == 'number'){
category = data;
newCodName = '';
}else{
const lastCategory = cateList.value[cateList.value.length - 1];
category = lastCategory ? lastCategory.value + 1 : 600101;
newCodName = data;
}
sendWordRequest(category, wordData, data, !existingCategory);
sendWordRequest(category, wordData, newCodName);
};
const sendWordRequest = (category, wordData, data, isNewCategory) => {
const sendWordRequest = (category, wordData, data) => {
const payload = {
WRDDICCAT: category,
WRDDICTTL: wordData.title,
WRDDICCON: $common.deltaAsJson(wordData.content),
};
if (isNewCategory) {
payload.CMNCODNAM = data;
axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') {
@ -198,22 +204,14 @@
writeButton.value.resetButton();
}
getwordList();
const newCategory = { label: data, value: category };
cateList.value = [newCategory, ...cateList.value];
}
});
} else {
axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's');
writeStore.closeAll();
if (writeButton.value) {
writeButton.value.resetButton();
getIndex();
if(res.data.data == '2'){
const newCategory = { label: data, value: category };
cateList.value = [...cateList.value,newCategory];
}
getwordList();
selectedAlphabet.value = '';
}
});
}
};