Merge branch 'main' into commuters
This commit is contained in:
commit
65e620d579
@ -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 {
|
||||
|
||||
2
public/vendor/fonts/boxicons.css
vendored
2
public/vendor/fonts/boxicons.css
vendored
File diff suppressed because one or more lines are too long
6
public/vendor/fonts/fontawesome.css
vendored
6
public/vendor/fonts/fontawesome.css
vendored
File diff suppressed because one or more lines are too long
@ -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:
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -27,4 +27,4 @@ const resetButton = () => {
|
||||
};
|
||||
|
||||
defineExpose({ resetButton });
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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')">
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -69,6 +69,7 @@
|
||||
:type="'date'"
|
||||
name="endDay"
|
||||
:modelValue="endDay"
|
||||
:min = "today"
|
||||
@update:modelValue="endDay = $event"
|
||||
/>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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초
|
||||
}
|
||||
})
|
||||
}
|
||||
//수정
|
||||
|
||||
@ -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 = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user