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 {
|
.fc-event {
|
||||||
border: none;
|
border: none;
|
||||||
@ -103,13 +106,14 @@ cursor: not-allowed !important;
|
|||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
.vac-modal-content {
|
.vac-modal-content {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.1); /* 위쪽 그림자만 적용 */
|
box-shadow: 0px -4px 5px rgba(0, 0, 0, 0.1),
|
||||||
max-width: 500px;
|
0px 4px 0px rgba(0, 0, 0, 0);
|
||||||
width: 100%;
|
max-width: 500px;
|
||||||
position: relative;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.vac-modal-body {
|
.vac-modal-body {
|
||||||
max-height: 140px;
|
max-height: 140px;
|
||||||
@ -208,20 +212,25 @@ cursor: not-allowed !important;
|
|||||||
.profile-list {
|
.profile-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-start;
|
gap: 15px;
|
||||||
gap: 10px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 20px;
|
list-style: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.profile-img {
|
.profile-item {
|
||||||
transition: all 0.2s ease-in-out;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: calc(33.33% - 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 오전/오후반차,저장버튼 */
|
/* 오전/오후반차,저장버튼 */
|
||||||
/* 버튼 기본 스타일 */
|
/* 버튼 기본 스타일 */
|
||||||
.vac-btn {
|
.vac-btn {
|
||||||
width: 50px;
|
width: 60px;
|
||||||
height: 50px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -250,20 +259,19 @@ cursor: not-allowed !important;
|
|||||||
}
|
}
|
||||||
/* 선택된 (눌린) 버튼 */
|
/* 선택된 (눌린) 버튼 */
|
||||||
.vac-btn.active {
|
.vac-btn.active {
|
||||||
border: 3px solid #ff0000; /* 붉은색 테두리 적용 */
|
box-shadow: 0px 4px 15px rgba(224, 224, 224, 0.3);
|
||||||
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
|
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
.vac-btn-warning{
|
.vac-btn-warning{
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #ffab00;
|
background-color: #ffc144;
|
||||||
border-color: #ffab00;
|
border-color: #ffe605;
|
||||||
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
|
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
|
||||||
}
|
}
|
||||||
/* AM 버튼 (선택된 상태) */
|
/* AM 버튼 (선택된 상태) */
|
||||||
.vac-btn-warning.active {
|
.vac-btn-warning.active {
|
||||||
background-color: #ffca2c !important;
|
background-color: #ff7300 !important;
|
||||||
color: black;
|
color: #fff;;
|
||||||
}
|
}
|
||||||
.vac-btn-info {
|
.vac-btn-info {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -277,10 +285,10 @@ cursor: not-allowed !important;
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
/* 버튼 기본 (비활성화일 때 기본 녹색) */
|
/* 버튼 기본 (비활성화일 때 기본 녹색) */
|
||||||
.btn-success {
|
.vac-btn-success {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
width: 50px;
|
width: 60px;
|
||||||
height: 50px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -292,7 +300,7 @@ cursor: not-allowed !important;
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
/* 버튼 활성화 */
|
/* 버튼 활성화 */
|
||||||
.btn-success.active {
|
.vac-btn-success.active {
|
||||||
background-color: #ff0000 !important;
|
background-color: #ff0000 !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
border: 3px solid #eb9f9f !important;
|
border: 3px solid #eb9f9f !important;
|
||||||
@ -300,7 +308,8 @@ cursor: not-allowed !important;
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
/* 버튼 비활성화 */
|
/* 버튼 비활성화 */
|
||||||
.btn-success.disabled {
|
.vac-btn-success.disabled {
|
||||||
|
border: 3px solid #e6e4e4; /* 붉은색 테두리 적용 */
|
||||||
background-color: #bbb8b8 !important;
|
background-color: #bbb8b8 !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
@ -310,7 +319,7 @@ cursor: not-allowed !important;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 작은 화면에서 버튼 크기 조정 */
|
/* 작은 화면에서 버튼 크기 조정 */
|
||||||
@media (max-width: 1600px) {
|
@media (max-width: 1700px) {
|
||||||
.count-btn {
|
.count-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@ -346,12 +355,34 @@ cursor: not-allowed !important;
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 5px;
|
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 {
|
.vac-btn {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.btn-success {
|
.vac-btn-success {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -359,7 +390,6 @@ cursor: not-allowed !important;
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.grayscaleImg {
|
.grayscaleImg {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|||||||
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) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
if (!error.config.headers.isLogin) {
|
if (!error.config.headers.isLogin) {
|
||||||
toastStore.onToast('인증이 필요합니다.', 'e');
|
// toastStore.onToast('인증이 필요합니다.', 'e');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
|
|||||||
@ -81,6 +81,34 @@ const common = {
|
|||||||
|
|
||||||
return true;
|
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 {
|
export default {
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
@updateReaction="handleUpdateReaction"
|
@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">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -47,151 +47,129 @@
|
|||||||
<!-- <template v-if="isDeleted">
|
<!-- <template v-if="isDeleted">
|
||||||
<p class="m-0 text-muted">댓글이 삭제되었습니다.</p>
|
<p class="m-0 text-muted">댓글이 삭제되었습니다.</p>
|
||||||
</template> -->
|
</template> -->
|
||||||
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
|
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6" />
|
||||||
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment"/>
|
<BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment" :commnetId="comment.commentId" />
|
||||||
|
|
||||||
<!-- 대댓글 -->
|
<slot name="reply"></slot>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
|
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
|
||||||
import BoardProfile from './BoardProfile.vue';
|
import BoardProfile from './BoardProfile.vue';
|
||||||
import BoardCommentArea from './BoardCommentArea.vue';
|
import BoardCommentArea from './BoardCommentArea.vue';
|
||||||
import PlusButton from '../button/PlusBtn.vue';
|
import PlusButton from '../button/PlusBtn.vue';
|
||||||
import SaveBtn from '../button/SaveBtn.vue';
|
import SaveBtn from '../button/SaveBtn.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comment: {
|
comment: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isCommentAuthor: {
|
isCommentAuthor: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isPlusButton: {
|
isPlusButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
isLike: {
|
isLike: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isEditTextarea: {
|
isEditTextarea: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
isDeleted: {
|
isDeleted: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
isCommentPassword: {
|
isCommentPassword: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
passwordCommentAlert: {
|
passwordCommentAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
},
|
},
|
||||||
currentPasswordCommentId: {
|
currentPasswordCommentId: {
|
||||||
type: Number
|
type: Number,
|
||||||
},
|
},
|
||||||
password:{
|
password: {
|
||||||
type: String
|
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
// emits 정의
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submitComment',
|
||||||
|
'updateReaction',
|
||||||
|
'editClick',
|
||||||
|
'deleteClick',
|
||||||
|
'submitPassword',
|
||||||
|
'submitEdit',
|
||||||
|
'cancelEdit',
|
||||||
|
'update:password',
|
||||||
|
]);
|
||||||
|
|
||||||
// 비밀번호 확인
|
const localEditedContent = ref(props.comment.content);
|
||||||
const logPasswordAndEmit = () => {
|
|
||||||
emit('submitPassword', props.comment, props.password);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.comment.isEditTextarea, (newVal) => {
|
// 댓글 입력 창 토글
|
||||||
if (newVal) {
|
const isComment = ref(false);
|
||||||
localEditedContent.value = props.comment.content;
|
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 반영
|
const handleUpdateReaction = reactionData => {
|
||||||
// props.comment.isEditTextarea = false;
|
emit('updateReaction', {
|
||||||
// }
|
boardId: props.comment.boardId,
|
||||||
// });
|
commentId: props.comment.commentId || reactionData.commentId,
|
||||||
|
...reactionData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 수정버튼
|
// 비밀번호 확인
|
||||||
const submitEdit = () => {
|
const logPasswordAndEmit = () => {
|
||||||
emit('submitEdit', props.comment, localEditedContent.value);
|
emit('submitPassword', props.comment, props.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditClick = () => {
|
watch(
|
||||||
emit('editClick', props.comment);
|
() => props.comment.isEditTextarea,
|
||||||
}
|
newVal => {
|
||||||
|
if (newVal) {
|
||||||
|
localEditedContent.value = props.comment.content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleReplyEditClick = (comment) => {
|
// watch(() => props.comment.isDeleted, () => {
|
||||||
emit('editClick', comment);
|
// 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>
|
</script>
|
||||||
|
|||||||
@ -11,7 +11,14 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
<!-- 텍스트박스 -->
|
<!-- 텍스트박스 -->
|
||||||
<div class="w-100">
|
<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-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>
|
<span v-else class="invalid-feedback d-block text-start ms-2">{{ textAlert }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -22,8 +29,8 @@
|
|||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="d-flex flex-wrap align-items-center">
|
||||||
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
||||||
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
|
<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" />
|
<input class="form-check-input" type="checkbox" :id="`checkboxAnnonymous${commnetId}`" v-model="isCheck" />
|
||||||
<label class="form-check-label" for="inlineCheckbox1">익명</label>
|
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
|
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
|
||||||
@ -35,6 +42,7 @@
|
|||||||
class="form-control flex-grow-1"
|
class="form-control flex-grow-1"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
placeholder="비밀번호 입력"
|
placeholder="비밀번호 입력"
|
||||||
|
@input="passwordAlertTextHandler"
|
||||||
/>
|
/>
|
||||||
<span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
|
<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>
|
<span v-else class="invalid-feedback d-block text-start ms-2">{{ passwordAlert2 }}</span>
|
||||||
@ -51,78 +59,92 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
|
import { ref, defineEmits, defineProps, watch, inject } from 'vue';
|
||||||
import SaveBtn from '../button/SaveBtn.vue';
|
import SaveBtn from '../button/SaveBtn.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
parentId: {
|
parentId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
passwordAlert: {
|
passwordAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
commentAlert: {
|
commentAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
const $common = inject('common');
|
default: 500,
|
||||||
const comment = ref('');
|
},
|
||||||
const password = ref('');
|
commnetId: {
|
||||||
const isCheck = ref(false);
|
type: Number,
|
||||||
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 $common = inject('common');
|
||||||
resetCommentForm();
|
const comment = ref('');
|
||||||
};
|
const password = ref('');
|
||||||
|
const isCheck = ref(false);
|
||||||
|
const textAlert = ref('');
|
||||||
|
const passwordAlert2 = ref('');
|
||||||
|
|
||||||
// 입력 필드 리셋 함수 추가
|
const emit = defineEmits(['submitComment']);
|
||||||
const resetCommentForm = () => {
|
|
||||||
comment.value = '';
|
|
||||||
password.value = '';
|
|
||||||
isCheck.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
const alertTextHandler = () => {
|
||||||
() => props.passwordAlert,
|
textAlert.value = '';
|
||||||
() => {
|
};
|
||||||
if (!props.passwordAlert) {
|
|
||||||
resetCommentForm();
|
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,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="list-unstyled mt-10">
|
<ul class="list-unstyled mt-10">
|
||||||
<li
|
<li v-for="comment in comments" :key="comment.commentId" class="mt-6 border-bottom pb-6">
|
||||||
v-for="comment in comments"
|
|
||||||
:key="comment.commentId"
|
|
||||||
class="mt-6 border-bottom pb-6"
|
|
||||||
>
|
|
||||||
<BoardComment
|
<BoardComment
|
||||||
:unknown="unknown"
|
:unknown="unknown"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
@ -21,104 +17,148 @@
|
|||||||
@submitComment="submitComment"
|
@submitComment="submitComment"
|
||||||
@submitEdit="handleSubmitEdit"
|
@submitEdit="handleSubmitEdit"
|
||||||
@cancelEdit="handleCancelEdit"
|
@cancelEdit="handleCancelEdit"
|
||||||
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
|
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
|
||||||
@update:password="updatePassword"
|
@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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits } from 'vue';
|
import { defineProps, defineEmits } from 'vue';
|
||||||
import BoardComment from './BoardComment.vue'
|
import BoardComment from './BoardComment.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comments: {
|
comments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
isCommentAuthor: {
|
isCommentAuthor: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isCommentPassword: {
|
isCommentPassword: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isEditTextarea: {
|
isEditTextarea: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isDeleted: {
|
isDeleted: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
passwordCommentAlert: {
|
passwordCommentAlert: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
},
|
},
|
||||||
currentPasswordCommentId: {
|
currentPasswordCommentId: {
|
||||||
type: Number
|
type: Number,
|
||||||
},
|
},
|
||||||
password:{
|
password: {
|
||||||
type: String
|
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) => {
|
const submitComment = replyData => {
|
||||||
emit('submitComment', replyData);
|
emit('submitComment', replyData);
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateReaction = (reactionData, commentId, boardId) => {
|
|
||||||
const updatedReactionData = {
|
|
||||||
...reactionData,
|
|
||||||
commentId: commentId || reactionData.commentId,
|
|
||||||
boardId: boardId || reactionData.boardId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
emit('updateReaction', updatedReactionData);
|
const handleUpdateReaction = (reactionData, commentId, boardId) => {
|
||||||
}
|
const updatedReactionData = {
|
||||||
|
...reactionData,
|
||||||
|
commentId: commentId || reactionData.commentId,
|
||||||
|
boardId: boardId || reactionData.boardId,
|
||||||
|
};
|
||||||
|
|
||||||
const submitPassword = (comment, password) => {
|
emit('updateReaction', updatedReactionData);
|
||||||
emit('submitPassword', comment, password);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditClick = (comment) => {
|
const submitPassword = (comment, password) => {
|
||||||
if (comment.parentId) {
|
emit('submitPassword', comment, password);
|
||||||
emit('editClick', comment); // 대댓글
|
};
|
||||||
} else {
|
|
||||||
emit('editClick', comment); // 댓글
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitEdit = (comment, editedContent) => {
|
const handleEditClick = comment => {
|
||||||
emit("submitEdit", comment, editedContent);
|
if (comment.parentId) {
|
||||||
};
|
emit('editClick', comment); // 대댓글
|
||||||
|
} else {
|
||||||
|
emit('editClick', comment); // 댓글
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteClick = (comment) => {
|
const handleSubmitEdit = (comment, editedContent) => {
|
||||||
if (comment.parentId) {
|
emit('submitEdit', comment, editedContent);
|
||||||
emit('deleteClick', comment); // 대댓글 삭제
|
};
|
||||||
} else {
|
|
||||||
emit('deleteClick', comment); // 댓글 삭제
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelEdit = (comment) => {
|
const handleDeleteClick = comment => {
|
||||||
if (comment.parentId) {
|
if (comment.parentId) {
|
||||||
emit('cancelEdit', comment); // 대댓글 수정 취소
|
emit('deleteClick', comment); // 대댓글 삭제
|
||||||
} else {
|
} else {
|
||||||
emit('cancelEdit', comment); // 댓글 수정 취소
|
emit('deleteClick', comment); // 댓글 삭제
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePassword = (newPassword) => {
|
const handleCancelEdit = comment => {
|
||||||
emit('update:password', newPassword);
|
if (comment.parentId) {
|
||||||
};
|
emit('cancelEdit', comment); // 대댓글 수정 취소
|
||||||
|
} else {
|
||||||
|
emit('cancelEdit', comment); // 댓글 수정 취소
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePassword = newPassword => {
|
||||||
|
emit('update:password', newPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReplyEditClick = comment => {
|
||||||
|
emit('editClick', comment);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
const defaultProfile = '/img/icons/icon.png';
|
const defaultProfile = '/img/icons/icon.png';
|
||||||
|
|
||||||
// 서버의 이미지 경로 (Vue 환경 변수 사용 가능)
|
// 서버의 이미지 경로 (Vue 환경 변수 사용 가능)
|
||||||
const baseUrl = 'http://localhost:10325/'; // API 서버 URL
|
const baseUrl = import.meta.env.VITE_SERVER; // API 서버 URL
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -1,124 +1,123 @@
|
|||||||
<template v-if="isRecommend">
|
<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>
|
<i class="fa-regular fa-thumbs-up"></i> <span class="num">{{ likeCount }}</span>
|
||||||
</button>
|
</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>
|
<i class="fa-regular fa-thumbs-down"></i> <span class="num">{{ dislikeCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comment: {
|
comment: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
likeClicked : {
|
likeClicked: {
|
||||||
type : Boolean,
|
type: Boolean,
|
||||||
default : false,
|
default: false,
|
||||||
},
|
},
|
||||||
dislikeClicked : {
|
dislikeClicked: {
|
||||||
type : Boolean,
|
type: Boolean,
|
||||||
default : false,
|
default: false,
|
||||||
},
|
},
|
||||||
bigBtn : {
|
bigBtn: {
|
||||||
type :Boolean,
|
type: Boolean,
|
||||||
default : false,
|
default: false,
|
||||||
},
|
},
|
||||||
isRecommend: {
|
isRecommend: {
|
||||||
type:Boolean,
|
type: Boolean,
|
||||||
default:true,
|
default: true,
|
||||||
},
|
},
|
||||||
boardId: {
|
boardId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
commentId: {
|
commentId: {
|
||||||
type: [Number, null],
|
type: [Number, null],
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
likeCount: {
|
likeCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
dislikeCount: {
|
dislikeCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['updateReaction']);
|
const emit = defineEmits(['updateReaction']);
|
||||||
|
|
||||||
const likeClicked = ref(props.likeClicked);
|
const likeClicked = ref(props.likeClicked);
|
||||||
const dislikeClicked = ref(props.dislikeClicked);
|
const dislikeClicked = ref(props.dislikeClicked);
|
||||||
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
|
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
|
||||||
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
|
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
|
||||||
|
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
const isLike = !likeClicked.value;
|
const isLike = !likeClicked.value;
|
||||||
const isDislike = false;
|
const isDislike = false;
|
||||||
|
|
||||||
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
|
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
|
||||||
likeClicked.value = isLike;
|
likeClicked.value = isLike;
|
||||||
dislikeClicked.value = false;
|
dislikeClicked.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDislike = () => {
|
const handleDislike = () => {
|
||||||
const isDislike = !dislikeClicked.value;
|
const isDislike = !dislikeClicked.value;
|
||||||
const isLike = false;
|
const isLike = false;
|
||||||
|
|
||||||
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
|
|
||||||
dislikeClicked.value = isDislike;
|
|
||||||
likeClicked.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
|
||||||
|
dislikeClicked.value = isDislike;
|
||||||
|
likeClicked.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.btn + .btn {
|
.btn + .btn {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.num {
|
.num {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label-danger.clicked {
|
.btn-label-danger.clicked {
|
||||||
background-color: #e6381a;
|
background-color: #e6381a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label-danger.clicked i,
|
.btn-label-danger.clicked i,
|
||||||
.btn-label-danger.clicked span {
|
.btn-label-danger.clicked span {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label-primary.clicked {
|
.btn-label-primary.clicked {
|
||||||
background-color: #5f61e6;
|
background-color: #5f61e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-label-primary.clicked i,
|
.btn-label-primary.clicked i,
|
||||||
.btn-label-primary.clicked span {
|
.btn-label-primary.clicked span {
|
||||||
color : #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 55px;
|
width: 55px;
|
||||||
height: 30px;
|
/* height: 30px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.big {
|
.btn.big {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width:450px) {
|
@media screen and (max-width: 450px) {
|
||||||
.btn {
|
.btn {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,30 +1,39 @@
|
|||||||
<template>
|
<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>
|
<i :class="buttonClass"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps } from 'vue';
|
import { ref, watch, defineProps } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isToggleEnabled: {
|
isToggleEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const buttonClass = ref('bx bx-edit-alt');
|
const buttonClass = ref("bx bx-edit-alt");
|
||||||
|
|
||||||
const toggleText = () => {
|
watch(() => props.isActive, (newVal) => {
|
||||||
if (props.isToggleEnabled) {
|
buttonClass.value = newVal ? "bx bx-x" : "bx bx-edit-alt";
|
||||||
buttonClass.value = buttonClass.value === 'bx bx-edit-alt' ? 'bx bx-x' : 'bx bx-edit-alt';
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetButton = () => {
|
const toggleText = () => {
|
||||||
buttonClass.value = 'bx bx-edit-alt';
|
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>
|
</script>
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- 저장 버튼 -->
|
<!-- 저장 버튼 -->
|
||||||
<div class="save-button-container">
|
<div class="save-button-container">
|
||||||
<button class="btn-success" @click="addVacationRequests"
|
<button class="vac-btn-success" @click="addVacationRequests"
|
||||||
:class="{ active: !isDisabled, disabled: isDisabled }">
|
:class="{ active: !isDisabled, disabled: isDisabled }">
|
||||||
✔
|
✔
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="cate-list list-unstyled d-flex flex-wrap mb-0">
|
<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">
|
<li v-for="category in lists" :key="category.value" class="mt-2 me-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -18,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, ref } from 'vue';
|
import { defineProps, ref, watch } from 'vue';
|
||||||
|
|
||||||
// lists prop 정의
|
// lists prop 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -26,29 +39,38 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showAll: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selectedCategory: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 카테고리 선택
|
// 카테고리 선택
|
||||||
const selectedCategory = ref(null);
|
const selectedCategory = ref(props.selectedCategory);
|
||||||
const emit = defineEmits();
|
|
||||||
|
const emit = defineEmits(['update:data']);
|
||||||
const selectCategory = (cate) => {
|
const selectCategory = (cate) => {
|
||||||
selectedCategory.value = selectedCategory.value === cate ? null : cate;
|
selectedCategory.value = selectedCategory.value === cate ? null : cate;
|
||||||
emit('update:data', selectedCategory.value);
|
emit('update:data', selectedCategory.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(() => props.selectedCategory, (newVal) => {
|
||||||
|
selectedCategory.value = newVal;
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.cate-list {
|
.cate-list {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
|
|
||||||
li {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -114,7 +114,6 @@
|
|||||||
|
|
||||||
// 초기 데이터가 있을 경우, HTML 형식으로 삽입
|
// 초기 데이터가 있을 경우, HTML 형식으로 삽입
|
||||||
if (props.initialData) {
|
if (props.initialData) {
|
||||||
console.log(props.initialData);
|
|
||||||
quillInstance.setContents(JSON.parse(props.initialData));
|
quillInstance.setContents(JSON.parse(props.initialData));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +125,6 @@
|
|||||||
|
|
||||||
// 에디터의 텍스트가 변경될 때마다 이미지 처리
|
// 에디터의 텍스트가 변경될 때마다 이미지 처리
|
||||||
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
||||||
emit('update:data', quillInstance.getContents());
|
|
||||||
delta.ops.forEach(op => {
|
delta.ops.forEach(op => {
|
||||||
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
|
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
|
||||||
const imageUrl = op.insert.image; // 이미지 URL 추출
|
const imageUrl = op.insert.image; // 이미지 URL 추출
|
||||||
@ -135,7 +133,9 @@
|
|||||||
checkForDeletedImages(); // 삭제된 이미지 확인
|
checkForDeletedImages(); // 삭제된 이미지 확인
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
emit('update:data', quillInstance.getContents());
|
||||||
});
|
});
|
||||||
|
|
||||||
// 로컬 이미지 파일 선택
|
// 로컬 이미지 파일 선택
|
||||||
async function selectLocalImage() {
|
async function selectLocalImage() {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
@ -165,17 +165,43 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이미지 서버 업로드
|
// 이미지 서버 업로드
|
||||||
async function uploadImageToServer(formData) {
|
async function uploadImageToServer(formData) {
|
||||||
try {
|
try {
|
||||||
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
|
// Make the POST request to upload the image
|
||||||
const imageUrl = response.data.data;
|
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
|
||||||
return imageUrl; // 서버에서 받은 이미지 URL 반환
|
|
||||||
} catch (error) {
|
// Check if the response contains the expected data
|
||||||
toastStore.onToast('잠시후 다시 시도해주세요.', 'e');
|
if (response.data && response.data.data) {
|
||||||
throw error;
|
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() {
|
function checkForDeletedImages() {
|
||||||
@ -190,6 +216,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import 'quill/dist/quill.snow.css';
|
@import 'quill/dist/quill.snow.css';
|
||||||
.ql-editor {
|
.ql-editor {
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="postcode"
|
v-model="postcode"
|
||||||
placeholder="우편번호"
|
placeholder="우편번호"
|
||||||
|
disabled="true"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -26,6 +27,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="address"
|
v-model="address"
|
||||||
placeholder="기본주소"
|
placeholder="기본주소"
|
||||||
|
disabled="true"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,101 +14,98 @@
|
|||||||
:placeholder="title"
|
:placeholder="title"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:min="min"
|
:min="min"
|
||||||
|
autocomplete="off"
|
||||||
@focusout="$emit('focusout', modelValue)"
|
@focusout="$emit('focusout', modelValue)"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
|
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}을 확인해주세요.</div>
|
||||||
{{ title }}을 확인해주세요.
|
|
||||||
</div>
|
|
||||||
<!-- 카테고리 중복 -->
|
<!-- 카테고리 중복 -->
|
||||||
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">
|
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>
|
||||||
카테고리 중복입니다.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '라벨',
|
default: '라벨',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'nameplz',
|
default: 'nameplz',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isEssential: {
|
isEssential: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'text',
|
default: 'text',
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
maxlength: {
|
maxlength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 30,
|
default: 30,
|
||||||
},
|
},
|
||||||
isAlert: {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isCateAlert : {
|
isCateAlert: {
|
||||||
type :Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isLabel : {
|
isLabel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
min: {
|
min: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
required: false,
|
required: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits 정의
|
// Emits 정의
|
||||||
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
|
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) => {
|
watch(
|
||||||
emits('update:modelValue', newValue);
|
() => props.modelValue,
|
||||||
});
|
newValue => {
|
||||||
|
if (inputValue.value !== newValue) {
|
||||||
|
inputValue.value = newValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 초기값 동기화
|
const handleInput = event => {
|
||||||
watch(() => props.modelValue, (newValue) => {
|
const newValue = event.target.value.slice(0, props.maxlength);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (newValue.trim() !== '') {
|
||||||
|
emits('update:alert', false);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="mb-2" :class="isRow ? 'row' : ''">
|
<div class="mb-2" :class="isRow ? 'row' : ''">
|
||||||
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
|
<span v-if="isEssential" class="link-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div :class="isRow ? 'col-md-10' : 'col-md-12'" class="d-flex gap-2 align-items-center">
|
<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')">
|
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}" @blur="$emit('blur')">
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</h5>
|
</h5>
|
||||||
<p v-if="isProjectExpired" class="btn-icon btn-danger rounded-2"><i class='bx bx-power-off'></i></p>
|
<p v-if="isProjectExpired" class="btn-icon btn-danger rounded-2"><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" />
|
<EditBtn @click.stop="openEditModal" />
|
||||||
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
||||||
</div>
|
</div>
|
||||||
@ -128,6 +128,7 @@
|
|||||||
title="종료일"
|
title="종료일"
|
||||||
type="date"
|
type="date"
|
||||||
name="endDay"
|
name="endDay"
|
||||||
|
:min="todays"
|
||||||
:modelValue="selectedProject.PROJCTEND"
|
:modelValue="selectedProject.PROJCTEND"
|
||||||
@update:modelValue="selectedProject.PROJCTEND = $event"
|
@update:modelValue="selectedProject.PROJCTEND = $event"
|
||||||
/>
|
/>
|
||||||
@ -160,7 +161,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 UserList from '@c/user/UserList.vue';
|
||||||
import CenterModal from '@c/modal/CenterModal.vue';
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
@ -253,6 +254,12 @@ const isProjectCreator = computed(() => {
|
|||||||
return user.value?.id === props.projctCreatorId;
|
return user.value?.id === props.projctCreatorId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dayjs 인스턴스 가져오기
|
||||||
|
const dayjs = inject('dayjs');
|
||||||
|
|
||||||
|
// 오늘 날짜를 YYYY-MM-DD 형식으로 변환
|
||||||
|
const todays = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
||||||
const isProjectExpired = computed(() => {
|
const isProjectExpired = computed(() => {
|
||||||
if (!props.enddate) return false;
|
if (!props.enddate) return false;
|
||||||
@ -356,19 +363,6 @@ const hasChanges = computed(() => {
|
|||||||
selectedProject.value.PROJCTDES !== props.description ||
|
selectedProject.value.PROJCTDES !== props.description ||
|
||||||
selectedProject.value.PROJCTCOL !== props.projctCol;
|
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 = () => {
|
const handleUpdate = () => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
|
<div v-if="isOpen" class="vac-modal-dialog" @click.self="closeModal">
|
||||||
<div class="vac-modal-content p-5 modal-scroll">
|
<div class="vac-modal-content p-5 modal-scroll">
|
||||||
<h5 class="vac-modal-title">📅 내 연차 사용 내역</h5>
|
<h5 class="vac-modal-title">📅 내 연차 상세 내역</h5>
|
||||||
<button class="close-btn" @click="closeModal">✖</button>
|
<button class="close-btn" @click="closeModal">✖</button>
|
||||||
<!-- 연차 목록 -->
|
<!-- 연차 목록 -->
|
||||||
<div class="vac-modal-body" v-if="mergedVacations.length > 0">
|
<div class="vac-modal-body" v-if="mergedVacations.length > 0">
|
||||||
@ -26,8 +26,8 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -58,19 +58,15 @@ const emit = defineEmits(["close"]);
|
|||||||
// 사용한 휴가(선물,연차사용)
|
// 사용한 휴가(선물,연차사용)
|
||||||
let globalCounter = 0;
|
let globalCounter = 0;
|
||||||
const usedVacations = computed(() => {
|
const usedVacations = computed(() => {
|
||||||
const result = [];
|
return props.myVacations.flatMap((v) => {
|
||||||
props.myVacations.forEach((v) => {
|
|
||||||
const count = v.used_quota || 1;
|
const count = v.used_quota || 1;
|
||||||
for (let i = 0; i < count; i++) {
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
result.push({
|
...v,
|
||||||
...v,
|
category: "used",
|
||||||
category: "used",
|
code: v.LOCVACTYP,
|
||||||
code: v.LOCVACTYP,
|
_expandIndex: globalCounter++,
|
||||||
_expandIndex: globalCounter++,
|
}));
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return result;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 받은 휴가
|
// 받은 휴가
|
||||||
@ -114,7 +110,7 @@ const mergedVacations = computed(() => {
|
|||||||
|
|
||||||
// 모달 닫기
|
// 모달 닫기
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
emit("close");
|
emit("close");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -69,6 +69,7 @@
|
|||||||
:type="'date'"
|
:type="'date'"
|
||||||
name="endDay"
|
name="endDay"
|
||||||
:modelValue="endDay"
|
:modelValue="endDay"
|
||||||
|
:min = "today"
|
||||||
@update:modelValue="endDay = $event"
|
@update:modelValue="endDay = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -94,8 +94,7 @@
|
|||||||
:is-common="true"
|
:is-common="true"
|
||||||
:is-color="true"
|
:is-color="true"
|
||||||
:data="colorList"
|
:data="colorList"
|
||||||
@update:data="color = $event"
|
@update:data="handleColorUpdate"
|
||||||
@blur="checkColorDuplicate"
|
|
||||||
class="w-50"
|
class="w-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +135,7 @@
|
|||||||
@update:data="handleAddressUpdate"
|
@update:data="handleAddressUpdate"
|
||||||
@update:alert="addressAlert = $event"
|
@update:alert="addressAlert = $event"
|
||||||
:value="address"
|
:value="address"
|
||||||
|
:disabled="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserFormInput
|
<UserFormInput
|
||||||
@ -143,7 +143,7 @@
|
|||||||
name="phone"
|
name="phone"
|
||||||
:isEssential="true"
|
:isEssential="true"
|
||||||
:is-alert="phoneAlert"
|
:is-alert="phoneAlert"
|
||||||
@update:data="phone = $event"
|
@update:data="phone = $event.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1')"
|
||||||
@update:alert="phoneAlert = $event"
|
@update:alert="phoneAlert = $event"
|
||||||
@blur="checkPhoneDuplicate"
|
@blur="checkPhoneDuplicate"
|
||||||
:maxlength="11"
|
:maxlength="11"
|
||||||
@ -298,7 +298,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 색상 중복체크
|
// 색상 중복체크
|
||||||
const checkColorDuplicate = async () => {
|
const checkColorDuplicate = async () => {
|
||||||
const response = await $api.get(`/user/checkColor?memberCol=${color.value}`);
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
@ -366,4 +373,3 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card-body d-flex justify-content-center m-n5">
|
<div class="card-body d-flex justify-content-center m-n5">
|
||||||
<ul class="list-unstyled profile-list">
|
<ul class="profile-list">
|
||||||
<li
|
<li
|
||||||
v-for="(user, index) in sortedUserList"
|
v-for="(user, index) in sortedUserList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="{ disabled: user.disabled }"
|
class="profile-item"
|
||||||
|
:class="{ newRow: (index + 1) % 4 === 0 }"
|
||||||
@click="$emit('profileClick', user)"
|
@click="$emit('profileClick', user)"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
:aria-label="user.MEMBERSEQ"
|
:aria-label="user.MEMBERSEQ"
|
||||||
@ -69,12 +70,18 @@ nextTick(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sortedUserList = computed(() => {
|
const sortedUserList = computed(() => {
|
||||||
if (!employeeId.value) return userList.value;
|
if (!employeeId.value) return [];
|
||||||
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;
|
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) =>
|
const getUserProfileImage = (profilePath) =>
|
||||||
profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
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 profileSize = computed(() => {
|
||||||
const totalUsers = userList.value.length;
|
const totalUsers = userList.value.length;
|
||||||
|
|
||||||
if (windowWidth.value >= 1650) {
|
if (windowWidth.value >= 1850) {
|
||||||
if (totalUsers <= 10) return "68px";
|
if (totalUsers <= 10) return "80px";
|
||||||
if (totalUsers <= 15) return "55px";
|
if (totalUsers <= 15) return "60px";
|
||||||
return "45px";
|
return "45px";
|
||||||
} else if (windowWidth.value >= 1300) {
|
} else if (windowWidth.value >= 1500) {
|
||||||
if (totalUsers <= 10) return "45px";
|
if (totalUsers <= 10) return "60px";
|
||||||
if (totalUsers <= 15) return "40px";
|
if (totalUsers <= 15) return "40px";
|
||||||
return "30px";
|
return "30px";
|
||||||
} else if (windowWidth.value >= 1024) {
|
} else if (windowWidth.value >= 900) {
|
||||||
if (totalUsers <= 10) return "40px";
|
if (totalUsers <= 10) return "48px";
|
||||||
if (totalUsers <= 15) return "30px";
|
if (totalUsers <= 15) return "30px";
|
||||||
return "20px";
|
return "20px";
|
||||||
} else {
|
} else {
|
||||||
return "20px";
|
return "35px";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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" >
|
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" >
|
||||||
<h5 class="card-title mb-1">
|
<h5 class="card-title mb-1">
|
||||||
<div class="list-unstyled users-list d-flex align-items-center gap-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"
|
class="rounded-circle user-avatar border border-3 w-px-40"
|
||||||
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
||||||
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -17,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="add-btn d-flex align-items-center">
|
<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">
|
<div v-if="userStore.user.id === data.localVote.LOCVOTREG">
|
||||||
<button
|
<button
|
||||||
@ -26,8 +27,9 @@
|
|||||||
class="bx btn btn-danger"
|
class="bx btn btn-danger"
|
||||||
@click="endBtn(data.localVote.LOCVOTSEQ)"
|
@click="endBtn(data.localVote.LOCVOTSEQ)"
|
||||||
>종료</button>
|
>종료</button>
|
||||||
<DeleteBtn @click="voteDelete(data.localVote.LOCVOTSEQ)" />
|
<DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -36,21 +38,23 @@
|
|||||||
<h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}</h5>
|
<h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}</h5>
|
||||||
<small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
|
<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>
|
<small class="text-primary text-uppercase">투표 결과없음 (😂아무도 투표하지 않았습니다)</small>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<vote-card-check
|
<vote-card-check
|
||||||
v-if="data.yesVotetotal == 0"
|
v-if="yesVotetotal == 0 && !data.localVote.LOCVOTDDT"
|
||||||
@addContents="addContents"
|
@addContents="addContents"
|
||||||
@checkedNames="checkedNames"
|
@checkedNames="checkedNames"
|
||||||
:data="data.voteDetails"
|
:data="data.voteDetails"
|
||||||
:voteInfo="data.localVote"
|
:voteInfo="data.localVote"
|
||||||
:total="data.voteDetails.length "/>
|
:total="data.voteDetails.length "/>
|
||||||
|
|
||||||
|
<small v-if="yesVotetotal != 0 && !data.localVote.LOCVOTDDT">투표 완료 : 종료시 투표 결과가 나타납니다.</small>
|
||||||
|
|
||||||
<!-- 투표 결과 -->
|
<!-- 투표 결과 -->
|
||||||
<div v-if="data.localVote.LOCVOTDDT" class="mt-3">
|
<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>
|
</div>
|
||||||
<!-- 투표완/미완 인원 -->
|
<!-- 투표완/미완 인원 -->
|
||||||
<vote-user-list
|
<vote-user-list
|
||||||
@ -84,6 +88,13 @@ const props = defineProps({
|
|||||||
required: false,
|
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 baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|||||||
@ -10,27 +10,26 @@
|
|||||||
:selectedValues="checkedNames"
|
:selectedValues="checkedNames"
|
||||||
@update:selectedValues="updateCheckedNames"
|
@update:selectedValues="updateCheckedNames"
|
||||||
/>
|
/>
|
||||||
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1" class="d-flex align-items-center">
|
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1">
|
||||||
<div class="d-flex flex-column gap-2">
|
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start mt-2">
|
||||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start">
|
<div class="flex-grow-1 me-2 ">
|
||||||
<form-input
|
<form-input
|
||||||
class="flex-grow-1 me-2"
|
|
||||||
:title="'항목 ' + (index + data.length + 1)"
|
:title="'항목 ' + (index + data.length + 1)"
|
||||||
:name="'content' + index"
|
:name="'content' + index"
|
||||||
:is-essential="false"
|
:is-essential="false"
|
||||||
:is-alert="contentAlerts[index]"
|
:is-alert="contentAlerts[index]"
|
||||||
v-model="item.content"
|
v-model="item.content"
|
||||||
/>
|
/>
|
||||||
<link-input v-model="item.url" />
|
<link-input v-model="item.url" />
|
||||||
<delete-btn @click="removeItem(index)" class="ms-2" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 d-flex justify-content">
|
<delete-btn @click="removeItem(index)" />
|
||||||
<plus-btn @click="addItem" :disabled="total >= 10" class="mb-3" />
|
</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">
|
<button class="btn btn-primary btn-icon mb-3" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
|
||||||
<i class="bx bx-check"></i>
|
<i class="bx bx-check"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -89,7 +88,9 @@ const updateCheckedNames = (newValues) => {
|
|||||||
checkedNames.value = newValues;
|
checkedNames.value = newValues;
|
||||||
};
|
};
|
||||||
const selectVote = () =>{
|
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 handleChange = (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
let updatedValues = [];
|
let updatedValues = [];
|
||||||
|
|
||||||
// 체크박스일 때 여러 개 선택 가능
|
// 체크박스일 때 여러 개 선택 가능
|
||||||
if (props.multiIs === "1") {
|
if (props.multiIs === "1") {
|
||||||
updatedValues = event.target.checked
|
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) =>{
|
const addContents = (itemList ,voteId) =>{
|
||||||
emit('addContents',itemList ,voteId);
|
emit('addContents',itemList ,voteId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<ul class="d-flex p-0 mb-0">
|
||||||
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
<li class="d-flex">
|
||||||
<li v-for="char in koreanChars" :key="char" class="mt-2 me-2">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="alphabet-btn"
|
||||||
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
|
:class="{ active: selectedAl === char.CHARACTER_ }"
|
||||||
@click="selectAlphabet(char)"
|
@click="selectAlphabet(char.CHARACTER_)"
|
||||||
>
|
>
|
||||||
{{ char }}
|
{{ char.CHARACTER_ }} ({{ char.COUNT }})
|
||||||
</button>
|
</button>
|
||||||
|
<span v-if="index !== koreanChars.length - 1" class="divider">|</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
<ul class="d-flex p-0 mb-0">
|
||||||
<li v-for="char in englishChars" :key="char" class="mt-2 me-2">
|
<li v-for="(char, index) in englishChars" :key="char.CHARACTER_" class="d-flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="alphabet-btn"
|
||||||
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
|
:class="{ active: selectedAl === char.CHARACTER_ }"
|
||||||
@click="selectAlphabet(char)"
|
@click="selectAlphabet(char.CHARACTER_)"
|
||||||
>
|
>
|
||||||
{{ char }}
|
{{ char.CHARACTER_ }} ({{ char.COUNT }})
|
||||||
</button>
|
</button>
|
||||||
|
<span v-if="index !== englishChars.length - 1" class="divider">|</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
const koreanChars = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
|
const props = defineProps({
|
||||||
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'];
|
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 emit = defineEmits();
|
||||||
const selectAlphabet = (alphabet) => {
|
const selectAlphabet = (alphabet) => {
|
||||||
selectedAlphabet.value = selectedAlphabet.value === alphabet ? null : alphabet;
|
selectedAlphabet.value = selectedAlphabet.value === alphabet ? null : alphabet;
|
||||||
emit('update:data',selectedAlphabet.value);
|
emit('update:data', selectedAlphabet.value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.btn {
|
.alphabet-btn {
|
||||||
min-width: 56px;
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 70%;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.alphabet-btn:hover {
|
||||||
.alphabet-list {
|
color: #0d6efd;
|
||||||
overflow-x: scroll;
|
}
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
}
|
.alphabet-btn.active {
|
||||||
|
color: #0d6efd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
:titleValue="item.WRDDICTTL"
|
:titleValue="item.WRDDICTTL"
|
||||||
:contentValue="item.WRDDICCON"
|
:contentValue="item.WRDDICCON"
|
||||||
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
|
:isDisabled="userStore.user.role !== 'ROLE_ADMIN'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<input
|
<input
|
||||||
@ -65,7 +65,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -124,7 +125,7 @@ const editWord = (data) => {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.data === 1) {
|
if (res.data.data === 1) {
|
||||||
toastStore.onToast('✅ 용어가 수정되었습니다.', 's');
|
toastStore.onToast('용어가 수정되었습니다.', 's');
|
||||||
writeStore.closeAll();
|
writeStore.closeAll();
|
||||||
if (writeButton.value) {
|
if (writeButton.value) {
|
||||||
writeButton.value.resetButton();
|
writeButton.value.resetButton();
|
||||||
|
|||||||
@ -11,10 +11,11 @@
|
|||||||
@change="onChange"
|
@change="onChange"
|
||||||
:value="formValue"
|
:value="formValue"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
|
:is-essential="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 btn-margin" v-if="!isDisabled">
|
<div class="col-2 btn-margin" v-if="!isDisabled">
|
||||||
<PlusBtn @click="toggleInput" />
|
<PlusBtn @click="toggleInput"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -41,15 +42,11 @@
|
|||||||
:modelValue="titleValue"
|
:modelValue="titleValue"
|
||||||
@update:modelValue="wordTitle = $event"
|
@update:modelValue="wordTitle = $event"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
|
@keyup="ValidHandler('title')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<QEditor
|
<QEditor @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||||
@update:data="content = $event"
|
|
||||||
@update:imageUrls="imageUrls = $event"
|
|
||||||
:is-alert="wordContentAlert"
|
|
||||||
:initialData="contentValue"
|
|
||||||
/>
|
|
||||||
<div class="text-end mt-5">
|
<div class="text-end mt-5">
|
||||||
<button class="btn btn-primary" @click="saveWord">
|
<button class="btn btn-primary" @click="saveWord">
|
||||||
<i class="bx bx-check"></i>
|
<i class="bx bx-check"></i>
|
||||||
@ -59,153 +56,183 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed, ref, defineEmits } from 'vue';
|
import { defineProps, computed, ref, defineEmits } from 'vue';
|
||||||
|
|
||||||
import QEditor from '@/components/editor/QEditor.vue';
|
import QEditor from '@/components/editor/QEditor.vue';
|
||||||
import FormInput from '@/components/input/FormInput.vue';
|
import FormInput from '@/components/input/FormInput.vue';
|
||||||
import FormSelect from '@/components/input/FormSelect.vue';
|
import FormSelect from '@/components/input/FormSelect.vue';
|
||||||
import PlusBtn from '../button/PlusBtn.vue';
|
import PlusBtn from '../button/PlusBtn.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'addCategory', 'addWord']);
|
const emit = defineEmits(['close','addCategory','addWord']);
|
||||||
|
|
||||||
//용어제목
|
//용어제목
|
||||||
const wordTitle = ref('');
|
const wordTitle = ref('');
|
||||||
const addCategory = ref('');
|
const addCategory = ref('');
|
||||||
const content = ref('');
|
const content = ref('');
|
||||||
const imageUrls = ref([]);
|
const imageUrls = ref([]);
|
||||||
|
|
||||||
//용어 Vaildation용
|
//용어 Vaildation용
|
||||||
const wordTitleAlert = ref(false);
|
const wordTitleAlert = ref(false);
|
||||||
const wordContentAlert = ref(false);
|
const wordContentAlert = ref(false);
|
||||||
const addCategoryAlert = 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({
|
// 카테고리 입력 중복 ref
|
||||||
dataList: {
|
const categoryInputRef = ref(null);
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
NumValue: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
formValue: {
|
|
||||||
type: [String, Number],
|
|
||||||
},
|
|
||||||
titleValue: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
contentValue: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
isDisabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 카테고리 입력 창
|
const props = defineProps({
|
||||||
const showInput = ref(false);
|
dataList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
NumValue : {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
formValue : {
|
||||||
|
type:[String, Number]
|
||||||
|
},
|
||||||
|
titleValue : {
|
||||||
|
type:String,
|
||||||
|
},contentValue : {
|
||||||
|
type:String,
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 카테고리 입력 토글
|
// 카테고리 입력 창
|
||||||
const toggleInput = () => {
|
const showInput = ref(false);
|
||||||
showInput.value = !showInput.value;
|
|
||||||
|
// 카테고리 입력 토글
|
||||||
|
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
//용어 등록
|
// 카테고리 focusout 이벤트 핸들러 추가
|
||||||
const saveWord = () => {
|
const handleCategoryFocusout = (value) => {
|
||||||
//validation
|
const valueTrim = value.trim();
|
||||||
let computedTitleTrim;
|
|
||||||
|
|
||||||
if (computedTitle.value != undefined) {
|
const existingCategory = props.dataList.find(item => item.label === valueTrim);
|
||||||
computedTitleTrim = computedTitle.value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 용어 체크
|
// 카테고리 입력시 공백
|
||||||
if (computedTitleTrim == undefined || computedTitleTrim == '') {
|
if(valueTrim == ''){
|
||||||
wordTitleAlert.value = true;
|
addCategoryAlert.value = true;
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
wordTitleAlert.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 내용 확인
|
// 공백시 강제 focus
|
||||||
let inserts = [];
|
setTimeout(() => {
|
||||||
if (inserts.length === 0 && content.value?.ops?.length > 0) {
|
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
|
||||||
inserts = content.value.ops.map(op => (typeof op.insert === 'string' ? op.insert.trim() : op.insert));
|
if (inputElement) {
|
||||||
}
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// 내용 체크
|
|
||||||
if (content.value == '' || inserts.join('') === '') {
|
|
||||||
wordContentAlert.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wordData = {
|
}else if (existingCategory) {
|
||||||
id: props.NumValue || null,
|
addCategoryAlert.value = true;
|
||||||
title: computedTitle.value,
|
|
||||||
category: selectedCategory.value,
|
|
||||||
content: content.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('addWord', wordData, addCategory.value);
|
// 중복시 강제 focus
|
||||||
};
|
setTimeout(() => {
|
||||||
|
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// 카테고리 focusout 이벤트 핸들러 추가
|
} else {
|
||||||
const handleCategoryFocusout = value => {
|
addCategoryAlert.value = false;
|
||||||
const valueTrim = value.trim();
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dict-w {
|
.dict-w {
|
||||||
width: 83%;
|
width: 83%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.btn-margin {
|
.btn-margin {
|
||||||
margin-top: 2.5rem;
|
margin-top: 2.5rem
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,7 +10,15 @@
|
|||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="card-body">
|
<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
|
<FormFile
|
||||||
@ -43,7 +51,6 @@
|
|||||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||||
내용
|
내용
|
||||||
<span class="text-red">*</span>
|
<span class="text-red">*</span>
|
||||||
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<QEditor
|
<QEditor
|
||||||
@ -53,6 +60,7 @@
|
|||||||
:initialData="content"
|
:initialData="content"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="contentAlert" class="invalid-feedback d-block">내용을 확인해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 버튼 -->
|
<!-- 버튼 -->
|
||||||
@ -74,10 +82,15 @@
|
|||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormFile from '@c/input/FormFile.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 { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
|
// 공통
|
||||||
|
const $common = inject('common');
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const content = ref('');
|
const content = ref('');
|
||||||
@ -134,51 +147,18 @@
|
|||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 수정
|
// 유효성 확인
|
||||||
const updateBoard = async () => {
|
const checkValidation = () => {
|
||||||
// 유효성 검사
|
contentAlert.value = $common.isNotValidContent(content);
|
||||||
if (!title.value) {
|
titleAlert.value = $common.isNotValidInput(title.value);
|
||||||
titleAlert.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
titleAlert.value = false;
|
|
||||||
|
|
||||||
if (!content.value) {
|
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
||||||
contentAlert.value = true;
|
if (titleAlert.value) {
|
||||||
return;
|
title.value = '';
|
||||||
}
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
const fileArray = newFileFilter(attachFiles);
|
} else {
|
||||||
const formData = new FormData();
|
return false;
|
||||||
|
|
||||||
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('게시물 수정에 실패했습니다.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -219,6 +199,60 @@
|
|||||||
};
|
};
|
||||||
////////////////// fileSection[E] ////////////////////
|
////////////////// 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(() => {
|
onMounted(() => {
|
||||||
if (currentBoardId.value) {
|
if (currentBoardId.value) {
|
||||||
|
|||||||
@ -25,9 +25,13 @@
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
autocomplete="off"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
placeholder="비밀번호 입력"
|
placeholder="비밀번호 입력"
|
||||||
@input="password = password.replace(/\s/g, '')"
|
@input="
|
||||||
|
password = password.replace(/\s/g, '');
|
||||||
|
inputCheck();
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-primary" @click="submitPassword">확인</button>
|
<button class="btn btn-primary" @click="submitPassword">확인</button>
|
||||||
</div>
|
</div>
|
||||||
@ -88,6 +92,7 @@
|
|||||||
:unknown="unknown"
|
:unknown="unknown"
|
||||||
:commentAlert="commentAlert"
|
:commentAlert="commentAlert"
|
||||||
:passwordAlert="passwordAlert"
|
:passwordAlert="passwordAlert"
|
||||||
|
:maxLength="500"
|
||||||
@submitComment="handleCommentSubmit"
|
@submitComment="handleCommentSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -130,6 +135,7 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('');
|
const profileName = ref('');
|
||||||
@ -148,6 +154,7 @@
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const toastStore = useToastStore();
|
||||||
const currentBoardId = ref(Number(route.params.id));
|
const currentBoardId = ref(Number(route.params.id));
|
||||||
const unknown = computed(() => profileName.value === '익명');
|
const unknown = computed(() => profileName.value === '익명');
|
||||||
const currentUserId = computed(() => userStore.user.id); // 현재 로그인한 사용자 id
|
const currentUserId = computed(() => userStore.user.id); // 현재 로그인한 사용자 id
|
||||||
@ -221,6 +228,9 @@
|
|||||||
navigateLastPage: 1,
|
navigateLastPage: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inputCheck = () => {
|
||||||
|
passwordAlert.value = '';
|
||||||
|
};
|
||||||
// 게시물 상세 데이터 불러오기
|
// 게시물 상세 데이터 불러오기
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
try {
|
try {
|
||||||
@ -228,7 +238,6 @@
|
|||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
profileName.value = data.author || '익명';
|
profileName.value = data.author || '익명';
|
||||||
console.log(data.author);
|
|
||||||
authorId.value = data.authorId;
|
authorId.value = data.authorId;
|
||||||
boardTitle.value = data.title || '제목 없음';
|
boardTitle.value = data.title || '제목 없음';
|
||||||
boardContent.value = data.content || '';
|
boardContent.value = data.content || '';
|
||||||
@ -562,6 +571,7 @@
|
|||||||
const submitPassword = async () => {
|
const submitPassword = async () => {
|
||||||
if (!password.value.trim()) {
|
if (!password.value.trim()) {
|
||||||
passwordAlert.value = '비밀번호를 입력해주세요.';
|
passwordAlert.value = '비밀번호를 입력해주세요.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -585,18 +595,7 @@
|
|||||||
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.reponse && error.reponse.status === 401) passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
if (error.response && error.response.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 = '요청 중 알 수 없는 오류가 발생했습니다.';
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -658,7 +657,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.code === 200) {
|
if (response.data.code === 200) {
|
||||||
alert('게시물이 삭제되었습니다.');
|
toastStore.onToast('게시물이 삭제되었습니다.');
|
||||||
router.push({ name: 'BoardList' });
|
router.push({ name: 'BoardList' });
|
||||||
} else {
|
} else {
|
||||||
alert('삭제 실패: ' + response.data.message);
|
alert('삭제 실패: ' + response.data.message);
|
||||||
|
|||||||
@ -23,11 +23,7 @@
|
|||||||
<div class="mb-4 d-flex align-items-center">
|
<div class="mb-4 d-flex align-items-center">
|
||||||
<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="d-flex flex-wrap align-items-center mt-3 ms-1">
|
<div class="d-flex flex-wrap align-items-center mt-3 ms-1">
|
||||||
<div
|
<div v-for="(category, index) in categoryList" :key="index" class="form-check me-3">
|
||||||
v-for="(category, index) in categoryList"
|
|
||||||
:key="index"
|
|
||||||
class="form-check me-3"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -41,9 +37,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
|
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">카테고리를 선택해주세요.</div>
|
||||||
카테고리를 선택해주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
||||||
@ -52,6 +46,7 @@
|
|||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
name="pw"
|
name="pw"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="passwordAlert"
|
:is-alert="passwordAlert"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
@ -74,7 +69,11 @@
|
|||||||
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
||||||
|
|
||||||
<ul class="list-group mt-2" v-if="attachFiles.length">
|
<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 }}
|
{{ file.name }}
|
||||||
<button class="close-btn" @click="removeFile(index)">✖</button>
|
<button class="close-btn" @click="removeFile(index)">✖</button>
|
||||||
</li>
|
</li>
|
||||||
@ -82,15 +81,11 @@
|
|||||||
|
|
||||||
<!-- 내용 입력 (에디터) -->
|
<!-- 내용 입력 (에디터) -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="col-md-2 col-form-label">
|
<label class="col-md-2 col-form-label"> 내용 <span class="text-danger">*</span> </label>
|
||||||
내용 <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<QEditor @update:data="content = $event" />
|
<QEditor @update:data="content = $event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
|
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div>
|
||||||
내용을 입력해주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 d-flex justify-content-end">
|
<div class="mb-4 d-flex justify-content-end">
|
||||||
@ -104,162 +99,162 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
import SaveButton from '@c/button/SaveBtn.vue';
|
import SaveButton from '@c/button/SaveBtn.vue';
|
||||||
import BackButton from '@c/button/BackBtn.vue';
|
import BackButton from '@c/button/BackBtn.vue';
|
||||||
import { useToastStore } from '@s/toastStore';
|
import { useToastStore } from '@s/toastStore';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const categoryList = ref([]);
|
const categoryList = ref([]);
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const categoryValue = ref(null);
|
const categoryValue = ref(null);
|
||||||
const content = ref({ ops: [] });
|
const content = ref({ ops: [] });
|
||||||
const isFileValid = ref(true);
|
const isFileValid = ref(true);
|
||||||
|
|
||||||
const titleAlert = ref(false);
|
const titleAlert = ref(false);
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const contentAlert = ref(false);
|
const contentAlert = ref(false);
|
||||||
const categoryAlert = ref(false);
|
const categoryAlert = ref(false);
|
||||||
const attachFilesAlert = ref(false);
|
const attachFilesAlert = ref(false);
|
||||||
|
|
||||||
const attachFiles = ref([]);
|
const attachFiles = ref([]);
|
||||||
const maxFiles = 5;
|
const maxFiles = 5;
|
||||||
const maxSize = 10 * 1024 * 1024;
|
const maxSize = 10 * 1024 * 1024;
|
||||||
const fileError = ref('');
|
const fileError = ref('');
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('board/categories');
|
const response = await axios.get('board/categories');
|
||||||
categoryList.value = response.data.data;
|
categoryList.value = response.data.data;
|
||||||
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
||||||
if (freeCategory) {
|
if (freeCategory) {
|
||||||
categoryValue.value = freeCategory.CMNCODVAL;
|
categoryValue.value = freeCategory.CMNCODVAL;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('카테고리 불러오기 오류:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error('카테고리 불러오기 오류:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileCount = computed(() => attachFiles.value.length);
|
const fileCount = computed(() => attachFiles.value.length);
|
||||||
|
|
||||||
const handleFileUpload = (files) => {
|
const handleFileUpload = files => {
|
||||||
const validFiles = files.filter(file => file.size <= maxSize);
|
const validFiles = files.filter(file => file.size <= maxSize);
|
||||||
if (files.some(file => file.size > maxSize)) {
|
if (files.some(file => file.size > maxSize)) {
|
||||||
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
||||||
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fileError.value = '';
|
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFile = (index) => {
|
|
||||||
attachFiles.value.splice(index, 1);
|
|
||||||
if (attachFiles.value.length <= maxFiles) {
|
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
}
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(attachFiles, () => {
|
const removeFile = index => {
|
||||||
isFileValid.value = attachFiles.value.length <= maxFiles;
|
attachFiles.value.splice(index, 1);
|
||||||
});
|
if (attachFiles.value.length <= maxFiles) {
|
||||||
|
fileError.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateTitle = () => {
|
watch(attachFiles, () => {
|
||||||
titleAlert.value = title.value.trim().length === 0;
|
isFileValid.value = attachFiles.value.length <= maxFiles;
|
||||||
};
|
});
|
||||||
|
|
||||||
const validatePassword = () => {
|
const validateTitle = () => {
|
||||||
if (categoryValue.value === 300102) {
|
titleAlert.value = title.value.trim().length === 0;
|
||||||
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
};
|
||||||
passwordAlert.value = password.value.length === 0;
|
|
||||||
} else {
|
|
||||||
passwordAlert.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateContent = () => {
|
const validatePassword = () => {
|
||||||
if (!content.value?.ops?.length) {
|
if (categoryValue.value === 300102) {
|
||||||
contentAlert.value = true;
|
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
||||||
return;
|
passwordAlert.value = password.value.length === 0;
|
||||||
}
|
} else {
|
||||||
|
passwordAlert.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 이미지 포함 여부 확인
|
const validateContent = () => {
|
||||||
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
if (!content.value?.ops?.length) {
|
||||||
// 텍스트 포함 여부 확인
|
contentAlert.value = true;
|
||||||
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
return;
|
||||||
|
|
||||||
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
// 이미지 포함 여부 확인
|
||||||
goList();
|
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
||||||
} catch (error) {
|
// 텍스트 포함 여부 확인
|
||||||
console.error(error);
|
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
||||||
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 목록으로 이동 */
|
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
||||||
const goList = () => {
|
contentAlert.value = !(hasText || hasImage);
|
||||||
router.push('/board');
|
};
|
||||||
};
|
|
||||||
|
|
||||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
/** 글쓰기 */
|
||||||
watch(content, () => {
|
const write = async () => {
|
||||||
validateContent();
|
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>
|
</script>
|
||||||
|
|||||||
@ -142,28 +142,29 @@ const calendarOptions = reactive({
|
|||||||
datesSet: handleMonthChange,
|
datesSet: handleMonthChange,
|
||||||
events: calendarEvents,
|
events: calendarEvents,
|
||||||
});
|
});
|
||||||
// 캘린더 월 변경경
|
// 캘린더 월 변경
|
||||||
function handleMonthChange(viewInfo) {
|
function handleMonthChange(viewInfo) {
|
||||||
const currentDate = viewInfo.view.currentStart;
|
const currentDate = viewInfo.view.currentStart;
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||||
loadCalendarData(year, month);
|
loadCalendarData(year, month);
|
||||||
}
|
}
|
||||||
// 캘린더 클릭
|
// 캘린더 클릭
|
||||||
function handleDateClick(info) {
|
function handleDateClick(info) {
|
||||||
const clickedDateStr = info.dateStr;
|
const clickedDateStr = info.dateStr;
|
||||||
const clickedDate = info.date;
|
const clickedDate = info.date;
|
||||||
const todayStr = new Date().toISOString().split("T")[0];
|
const todayStr = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
clickedDate.getDay() === 0 ||
|
clickedDate.getDay() === 0 ||
|
||||||
clickedDate.getDay() === 6 ||
|
clickedDate.getDay() === 6 ||
|
||||||
holidayDates.value.has(clickedDateStr) ||
|
holidayDates.value.has(clickedDateStr) ||
|
||||||
clickedDateStr < todayStr
|
clickedDateStr < todayStr
|
||||||
){
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isMyVacation = myVacations.value.some(vac => {
|
const isMyVacation = myVacations.value.some(vac => {
|
||||||
const vacDate = vac.date ? String(vac.date).substring(0, 10) : "";
|
const vacDate = vac.date ? vac.date.substring(0, 10) : "";
|
||||||
return vacDate === clickedDateStr && !vac.receiverId;
|
return vacDate === clickedDateStr && !vac.receiverId;
|
||||||
});
|
});
|
||||||
if (isMyVacation) {
|
if (isMyVacation) {
|
||||||
@ -186,7 +187,6 @@ function handleDateClick(info) {
|
|||||||
selectedDates.value.set(clickedDateStr, type);
|
selectedDates.value.set(clickedDateStr, type);
|
||||||
halfDayType.value = null;
|
halfDayType.value = null;
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
// 날짜 선택 후 반차버튼 초기화
|
|
||||||
if (halfDayButtonsRef.value) {
|
if (halfDayButtonsRef.value) {
|
||||||
halfDayButtonsRef.value.resetHalfDay();
|
halfDayButtonsRef.value.resetHalfDay();
|
||||||
}
|
}
|
||||||
@ -266,15 +266,24 @@ const handleProfileClick = async (user) => {
|
|||||||
const fetchUserList = async () => {
|
const fetchUserList = async () => {
|
||||||
try {
|
try {
|
||||||
await userListStore.fetchUserList();
|
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) {
|
if (!userList.value.length) {
|
||||||
console.warn("📌 사용자 목록이 비어 있음!");
|
console.warn("📌 사용자 목록이 비어 있음!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 사용자별 색상 저장
|
||||||
userColors.value = {};
|
userColors.value = {};
|
||||||
userList.value.forEach((user) => {
|
userList.value.forEach((user) => {
|
||||||
userColors.value[user.MEMBERSEQ] = user.usercolor;
|
userColors.value[user.MEMBERSEQ] = user.usercolor;
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("📌 사용자 목록 불러오기 오류:", error);
|
console.error("📌 사용자 목록 불러오기 오류:", error);
|
||||||
}
|
}
|
||||||
@ -310,47 +319,57 @@ const filteredReceivedVacations = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* 휴가 저장 */
|
||||||
/* 휴가 변경사항 저장 */
|
// 휴가 변경사항 저장
|
||||||
async function saveVacationChanges() {
|
async function saveVacationChanges() {
|
||||||
if (!hasChanges.value) return;
|
if (!hasChanges.value) return;
|
||||||
const selectedDatesArray = Array.from(selectedDates.value);
|
const selectedDatesArray = Array.from(selectedDates.value);
|
||||||
const vacationsToAdd = selectedDatesArray
|
const vacationChangesByYear = selectedDatesArray.reduce((acc, [date, type]) => {
|
||||||
.filter(([date, type]) => type !== "delete")
|
const year = date.split("-")[0]; // YYYY-MM-DD에서 YYYY 추출
|
||||||
.filter(([date, type]) =>
|
if (!acc[year]) acc[year] = { add: [], delete: [] };
|
||||||
!myVacations.value.some(vac => vac.date && vac.date.startsWith(date)) ||
|
if (type !== "delete") {
|
||||||
myVacations.value.some(vac => vac.date && vac.date.startsWith(date) && vac.receiverId)
|
acc[year].add.push({ date, type });
|
||||||
)
|
|
||||||
.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();
|
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error("🚨 휴가 변경 저장 실패:", error);
|
console.error("🚨 휴가 변경 저장 실패:", error);
|
||||||
toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
|
toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
|
||||||
@ -363,15 +382,17 @@ async function fetchVacationHistory(year) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`vacation/history?year=${year}`);
|
const response = await axios.get(`vacation/history?year=${year}`);
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
myVacations.value = response.data.data.usedVacations || [];
|
const newVacations = response.data.data.usedVacations || [];
|
||||||
receivedVacations.value = response.data.data.receivedVacations || [];
|
|
||||||
} else {
|
const uniqueVacations = Array.from(
|
||||||
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
|
new Map([...myVacations.value, ...newVacations].map(v => [`${v.date}-${v.type}`, v]))
|
||||||
myVacations.value = [];
|
.values()
|
||||||
receivedVacations.value = [];
|
);
|
||||||
|
|
||||||
|
myVacations.value = uniqueVacations;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("🚨 연차 데이터 불러오기 실패:", error);
|
console.error(`🚨 ${year}년 휴가 데이터 불러오기 실패:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 모든 사원 연차 내역 및 그래프화
|
// 모든 사원 연차 내역 및 그래프화
|
||||||
@ -462,16 +483,20 @@ function toggleHalfDay(type) {
|
|||||||
/* 페이지 이동 시 변경 사항 확인 */
|
/* 페이지 이동 시 변경 사항 확인 */
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (hasChanges.value) {
|
if (hasChanges.value) {
|
||||||
|
console.log('휴가!!!!!');
|
||||||
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
|
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
return next(false);
|
return next(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
selectedDates.value.clear();
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("beforeunload", preventUnsavedChanges);
|
window.removeEventListener("beforeunload", preventUnsavedChanges);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
/* 새로고침 또는 페이지 종료 시 알림 */
|
||||||
function preventUnsavedChanges(event) {
|
function preventUnsavedChanges(event) {
|
||||||
if (hasChanges.value) {
|
if (hasChanges.value) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -480,8 +505,10 @@ function preventUnsavedChanges(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* watch */
|
/* watch */
|
||||||
watch(lastRemainingYear, async (newYear, oldYear) => {
|
watch(() => lastRemainingYear.value, async (newYear, oldYear) => {
|
||||||
await fetchVacationHistory(newYear);
|
if (newYear !== oldYear) {
|
||||||
|
await fetchVacationHistory(newYear);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// `selectedDates` 변경 시 반차 버튼 초기화
|
// `selectedDates` 변경 시 반차 버튼 초기화
|
||||||
watch(
|
watch(
|
||||||
@ -533,6 +560,9 @@ onMounted(async () => {
|
|||||||
altFormat: "F Y"
|
altFormat: "F Y"
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
onOpen: function() {
|
||||||
|
document.querySelector('.flatpickr-input').style.visibility = 'hidden';
|
||||||
|
},
|
||||||
onChange: function(selectedDatesArr, dateStr) {
|
onChange: function(selectedDatesArr, dateStr) {
|
||||||
// 선택한 달의 첫날로 달력을 이동
|
// 선택한 달의 첫날로 달력을 이동
|
||||||
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
|
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
|
||||||
@ -551,17 +581,17 @@ onMounted(async () => {
|
|||||||
if (titleEl) {
|
if (titleEl) {
|
||||||
titleEl.style.cursor = 'pointer';
|
titleEl.style.cursor = 'pointer';
|
||||||
titleEl.addEventListener('click', () => {
|
titleEl.addEventListener('click', () => {
|
||||||
const dpEl = calendarDatepicker.value;
|
const dpEl = calendarDatepicker.value;
|
||||||
dpEl.style.display = 'block';
|
dpEl.style.display = 'block';
|
||||||
dpEl.style.position = 'fixed';
|
dpEl.style.position = 'fixed';
|
||||||
dpEl.style.top = '25%';
|
dpEl.style.top = '25%';
|
||||||
dpEl.style.left = '50%';
|
dpEl.style.left = '50%';
|
||||||
dpEl.style.transform = 'translate(-50%, -50%)';
|
dpEl.style.transform = 'translate(-50%, -50%)';
|
||||||
dpEl.style.zIndex = '9999';
|
dpEl.style.zIndex = '9999';
|
||||||
dpEl.style.border = 'none';
|
dpEl.style.border = 'none';
|
||||||
dpEl.style.outline = 'none';
|
dpEl.style.outline = 'none';
|
||||||
dpEl.style.backgroundColor = 'transparent';
|
dpEl.style.backgroundColor = 'transparent';
|
||||||
fpInstance.open();
|
fpInstance.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -77,20 +77,21 @@ const changeCheck = () =>{
|
|||||||
getvoteList();
|
getvoteList();
|
||||||
}
|
}
|
||||||
//투표목록
|
//투표목록
|
||||||
const getvoteList = async () => {
|
const getvoteList = () => {
|
||||||
const response = await $api.get('vote/getVoteList',{
|
$api.get('vote/getVoteList',{
|
||||||
params:
|
//목록조회시 파라미터 전달
|
||||||
{
|
params:
|
||||||
page: currentPage.value
|
{
|
||||||
,voteset:voteset.value
|
page: currentPage.value
|
||||||
,myVote:ischeked.value ? '1':'0'
|
,voteset:voteset.value
|
||||||
}
|
,myVote:ischeked.value ? '1':'0'
|
||||||
});
|
}
|
||||||
if (response.data.status === "OK") {
|
}).then(res => {
|
||||||
PageData.value = response.data.data;
|
PageData.value = res.data.data;
|
||||||
voteListCardData.value = response.data.data.list;
|
voteListCardData.value = res.data.data.list;
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectHandler = () =>{
|
const selectHandler = () =>{
|
||||||
voteset.value = category.value;
|
voteset.value = category.value;
|
||||||
getvoteList();
|
getvoteList();
|
||||||
@ -148,14 +149,17 @@ const voteDelete =(id) =>{
|
|||||||
}
|
}
|
||||||
//랜덤 1위 뽑기
|
//랜덤 1위 뽑기
|
||||||
const randomList = (data,id) =>{
|
const randomList = (data,id) =>{
|
||||||
|
isLoading.value = false;
|
||||||
$api.post('vote/randomList',{
|
$api.post('vote/randomList',{
|
||||||
randomList :data
|
randomList :data
|
||||||
,voteid:id
|
,voteid:id
|
||||||
}).then((res)=>{
|
}).then((res)=>{
|
||||||
if(res.data.status === 'OK'){
|
if(res.data.status === 'OK'){
|
||||||
toastStore.onToast('랜덤뽑기 진행되었습니다.', 's');
|
toastStore.onToast('랜덤뽑기 진행되었습니다.', 's');
|
||||||
getvoteList();
|
setTimeout(() => {
|
||||||
}
|
getvoteList();
|
||||||
|
}, 2000); // 3000ms = 3초
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//수정
|
//수정
|
||||||
|
|||||||
@ -1,45 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<div class="card p-5">
|
<div >
|
||||||
<!-- 타이틀, 검색 -->
|
<!-- 타이틀, 검색 -->
|
||||||
<div class="row">
|
<SearchBar @update:data="search"/>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 단어 갯수, 작성하기 -->
|
<!-- 단어 갯수, 작성하기 -->
|
||||||
<div class="mt-4">
|
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
|
||||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
||||||
<div>
|
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" :indexCategory="indexCategory" :selectedAl="selectedAlphabet" />
|
||||||
<DictAlphabetFilter @update:data="handleSelectedAlphabetChange" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 카테고리 -->
|
<!-- 카테고리 -->
|
||||||
<div v-if="cateList.length" class="mt-5">
|
<div v-if="cateList.length">
|
||||||
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange"/>
|
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange" :showAll="true"/>
|
||||||
</div>
|
</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"/>
|
<DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 용어 리스트 -->
|
<!-- 용어 리스트 -->
|
||||||
<div class="mt-10">
|
<div >
|
||||||
<!-- 로딩 중일 때 -->
|
<!-- 로딩 중일 때 -->
|
||||||
<div v-if="loading">로딩 중...</div>
|
<LoadingSpinner v-if="loading"/>
|
||||||
|
|
||||||
<!-- 에러 메시지 -->
|
<!-- 에러 메시지 -->
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
<!-- 단어 목록 -->
|
<!-- 단어 목록 -->
|
||||||
<ul v-if="total > 0" class="px-0 list-unstyled">
|
<ul v-if="total > 0" class="px-0 list-unstyled">
|
||||||
<DictCard
|
<DictCard
|
||||||
@ -47,14 +30,12 @@
|
|||||||
:key="item.WRDDICSEQ"
|
:key="item.WRDDICSEQ"
|
||||||
:item="item"
|
:item="item"
|
||||||
v-model:cateList="cateList"
|
v-model:cateList="cateList"
|
||||||
@refreshWordList="getwordList"
|
@refreshWordList="refreshWordList"
|
||||||
@updateChecked="updateCheckedItems"
|
@updateChecked="updateCheckedItems"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -77,6 +58,7 @@
|
|||||||
import commonApi from '@/common/commonApi';
|
import commonApi from '@/common/commonApi';
|
||||||
import { useToastStore } from '@s/toastStore';
|
import { useToastStore } from '@s/toastStore';
|
||||||
import { useWriteVisibleStore } from '@s/writeVisible';
|
import { useWriteVisibleStore } from '@s/writeVisible';
|
||||||
|
import LoadingSpinner from "@v/LoadingPage.vue";
|
||||||
|
|
||||||
// 작성창 구분
|
// 작성창 구분
|
||||||
const writeStore = useWriteVisibleStore();
|
const writeStore = useWriteVisibleStore();
|
||||||
@ -116,11 +98,19 @@
|
|||||||
// 검색
|
// 검색
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
|
|
||||||
|
//검색 정렬
|
||||||
|
const indexCategory = ref([]);
|
||||||
|
|
||||||
// 데이터 로드
|
// 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getwordList();
|
getIndex();
|
||||||
|
writeStore.closeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshWordList = () => {
|
||||||
|
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
|
||||||
|
};
|
||||||
|
|
||||||
//용어 목록
|
//용어 목록
|
||||||
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
|
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
|
||||||
axios.get('worddict/getWordList',{
|
axios.get('worddict/getWordList',{
|
||||||
@ -144,6 +134,14 @@
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
//정렬 목록
|
||||||
|
const getIndex = () => {
|
||||||
|
axios.get('worddict/getIndexCategory').then(res=>{
|
||||||
|
if(res.data.status ="OK"){
|
||||||
|
indexCategory.value = res.data.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 검색
|
// 검색
|
||||||
const search = (e) => {
|
const search = (e) => {
|
||||||
@ -154,41 +152,49 @@
|
|||||||
// 알파벳 선택
|
// 알파벳 선택
|
||||||
const handleSelectedAlphabetChange = (newAlphabet) => {
|
const handleSelectedAlphabetChange = (newAlphabet) => {
|
||||||
selectedAlphabet.value = 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) => {
|
const handleSelectedCategoryChange = (category) => {
|
||||||
selectedCategory.value = 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) => {
|
const addWord = (wordData, data) => {
|
||||||
let category = null;
|
let category = null;
|
||||||
|
let newCodName = '';
|
||||||
// 카테고리 체크
|
// 카테고리 체크
|
||||||
const existingCategory = cateList.value.find(item => item.label === data.trim());
|
if(typeof(data) == 'number'){
|
||||||
|
category = data;
|
||||||
if (existingCategory) {
|
newCodName = '';
|
||||||
//카테고리 있을시 그냥 저장
|
}else{
|
||||||
category = existingCategory.label == '' ? wordData.category : existingCategory.value;
|
|
||||||
} else {
|
|
||||||
//카테고리 없을시 카테고리 와 용어 둘다 저장
|
|
||||||
const lastCategory = cateList.value[cateList.value.length - 1];
|
const lastCategory = cateList.value[cateList.value.length - 1];
|
||||||
category = lastCategory ? lastCategory.value + 1 : 600101;
|
category = lastCategory ? lastCategory.value + 1 : 600101;
|
||||||
|
newCodName = data;
|
||||||
}
|
}
|
||||||
sendWordRequest(category, wordData, data, !existingCategory);
|
sendWordRequest(category, wordData, newCodName);
|
||||||
};
|
};
|
||||||
|
const sendWordRequest = (category, wordData, data) => {
|
||||||
const sendWordRequest = (category, wordData, data, isNewCategory) => {
|
|
||||||
const payload = {
|
const payload = {
|
||||||
WRDDICCAT: category,
|
WRDDICCAT: category,
|
||||||
WRDDICTTL: wordData.title,
|
WRDDICTTL: wordData.title,
|
||||||
WRDDICCON: $common.deltaAsJson(wordData.content),
|
WRDDICCON: $common.deltaAsJson(wordData.content),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isNewCategory) {
|
|
||||||
payload.CMNCODNAM = data;
|
payload.CMNCODNAM = data;
|
||||||
axios.post('worddict/insertWord', payload).then(res => {
|
axios.post('worddict/insertWord', payload).then(res => {
|
||||||
if (res.data.status === 'OK') {
|
if (res.data.status === 'OK') {
|
||||||
@ -198,22 +204,14 @@
|
|||||||
writeButton.value.resetButton();
|
writeButton.value.resetButton();
|
||||||
}
|
}
|
||||||
getwordList();
|
getwordList();
|
||||||
const newCategory = { label: data, value: category };
|
getIndex();
|
||||||
cateList.value = [newCategory, ...cateList.value];
|
if(res.data.data == '2'){
|
||||||
}
|
const newCategory = { label: data, value: category };
|
||||||
});
|
cateList.value = [...cateList.value,newCategory];
|
||||||
} else {
|
|
||||||
axios.post('worddict/insertWord', payload).then(res => {
|
|
||||||
if (res.data.status === 'OK') {
|
|
||||||
toastStore.onToast('용어가 등록 되었습니다.', 's');
|
|
||||||
writeStore.closeAll();
|
|
||||||
if (writeButton.value) {
|
|
||||||
writeButton.value.resetButton();
|
|
||||||
}
|
}
|
||||||
getwordList();
|
selectedAlphabet.value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user