Compare commits
No commits in common. "main" and "250401_main_park" have entirely different histories.
main
...
250401_mai
@ -24,6 +24,7 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes new {
|
@keyframes new {
|
||||||
0% {
|
0% {
|
||||||
background-color: #ffcc00;
|
background-color: #ffcc00;
|
||||||
@ -42,6 +43,7 @@
|
|||||||
|
|
||||||
/* board end */
|
/* board end */
|
||||||
|
|
||||||
|
|
||||||
/* Qeditor */
|
/* Qeditor */
|
||||||
.ql-editor {
|
.ql-editor {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
@ -172,9 +174,6 @@
|
|||||||
.fc-toolbar-title {
|
.fc-toolbar-title {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.fc-today-button {
|
|
||||||
cursor: pointer !important;
|
|
||||||
}
|
|
||||||
/* 클릭 가능한 날짜 */
|
/* 클릭 가능한 날짜 */
|
||||||
.fc-daygrid-day.clickable {
|
.fc-daygrid-day.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -431,8 +430,8 @@
|
|||||||
/* 작은 화면에서 버튼 크기 조정 */
|
/* 작은 화면에서 버튼 크기 조정 */
|
||||||
@media (max-width: 1700px) {
|
@media (max-width: 1700px) {
|
||||||
.count-btn {
|
.count-btn {
|
||||||
width: 26px;
|
width: 28px;
|
||||||
height: 26px;
|
height: 28px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.count-container {
|
.count-container {
|
||||||
@ -443,7 +442,7 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.count-value {
|
.count-value {
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
.custom-button {
|
.custom-button {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
@ -454,10 +453,10 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.vac-modal-text {
|
.vac-modal-text {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.vac-modal-title {
|
.vac-modal-title {
|
||||||
font-size: 15px;
|
font-size: 17px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.vacation-item {
|
.vacation-item {
|
||||||
@ -466,8 +465,8 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.vac-btn {
|
.vac-btn {
|
||||||
width: 40px;
|
width: 50px;
|
||||||
height: 40px;
|
height: 50px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.vac-btn-success {
|
.vac-btn-success {
|
||||||
@ -498,12 +497,12 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.vac-btn {
|
.vac-btn {
|
||||||
width: 10px;
|
width: 40px;
|
||||||
height: 10px;
|
height: 40px;
|
||||||
font-size: 12px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.vac-btn-success {
|
.vac-btn-success {
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
@ -588,7 +587,7 @@
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-daygrid-day[data-has-commuters='true'] {
|
.fc-daygrid-day[data-has-commuters="true"] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -699,6 +698,7 @@
|
|||||||
/* Mobile end */
|
/* Mobile end */
|
||||||
/* media end */
|
/* media end */
|
||||||
|
|
||||||
|
|
||||||
/* BoardComment */
|
/* BoardComment */
|
||||||
|
|
||||||
.beforeRotate {
|
.beforeRotate {
|
||||||
@ -722,12 +722,6 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-time-input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 권한부여 */
|
/* 권한부여 */
|
||||||
.user-card-container {
|
.user-card-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -775,22 +769,22 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
transition: 0.4s;
|
transition: .4s;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
.slider:before {
|
.slider:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: "";
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
transition: 0.4s;
|
transition: .4s;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
input:checked + .slider {
|
input:checked + .slider {
|
||||||
background-color: #4caf50;
|
background-color: #4CAF50;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .slider:before {
|
input:checked + .slider:before {
|
||||||
@ -798,6 +792,7 @@ input:checked + .slider:before {
|
|||||||
}
|
}
|
||||||
/* 권한부여 끝 */
|
/* 권한부여 끝 */
|
||||||
|
|
||||||
|
|
||||||
/* toast */
|
/* toast */
|
||||||
|
|
||||||
.bs-toast {
|
.bs-toast {
|
||||||
@ -810,25 +805,3 @@ input:checked + .slider:before {
|
|||||||
.cursor-none{
|
.cursor-none{
|
||||||
cursor: none !important;
|
cursor: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-1 {
|
|
||||||
margin-left: 0.25rem !important;
|
|
||||||
}
|
|
||||||
.mr-1 {
|
|
||||||
margin-right: 0.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nickname-ellipsis {
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* font css */
|
|
||||||
.font-bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@ -65,8 +65,6 @@ const common = {
|
|||||||
callback = `${year}-${month}-${day}`;
|
callback = `${year}-${month}-${day}`;
|
||||||
} else if (type == 'MD') {
|
} else if (type == 'MD') {
|
||||||
callback = `${month}-${day}`;
|
callback = `${month}-${day}`;
|
||||||
} else if (type == 'T') {
|
|
||||||
callback = `${hours}:${minutes}`;
|
|
||||||
} else {
|
} else {
|
||||||
callback = `${year}-${month}-${day} ${hours}:${minutes}`;
|
callback = `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
@ -75,10 +73,10 @@ const common = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatDateTime(dateStr) {
|
formatDateTime(dateObj) {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateObj);
|
||||||
const dateCheck = date.getTime();
|
const dateCheck = date.getTime();
|
||||||
if (isNaN(dateCheck)) return dateStr;
|
if (isNaN(dateCheck)) return dateObj;
|
||||||
|
|
||||||
const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
||||||
return {
|
return {
|
||||||
@ -104,16 +102,6 @@ const common = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// 해당 날짜가 오늘인지 확인
|
|
||||||
isToday(dateStr) {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
const dateCheck = date.getTime();
|
|
||||||
if (isNaN(dateCheck)) return '날짜 타입 에러';
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
return date.toDateString() === today.toDateString();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 해당 월, 일에 맞는 목록 필터링
|
// 해당 월, 일에 맞는 목록 필터링
|
||||||
filterTargetByDate(target, key, month, day) {
|
filterTargetByDate(target, key, month, day) {
|
||||||
if (!Array.isArray(target) || target.length === 0) return [];
|
if (!Array.isArray(target) || target.length === 0) return [];
|
||||||
|
|||||||
@ -12,8 +12,6 @@
|
|||||||
:isLike="!isLike"
|
:isLike="!isLike"
|
||||||
:isCommentPassword="isCommentPassword"
|
:isCommentPassword="isCommentPassword"
|
||||||
:isCommentProfile="true"
|
:isCommentProfile="true"
|
||||||
:is-edit-pushed="isEditPushed"
|
|
||||||
:is-delete-pushed="isDeletePushed"
|
|
||||||
@editClick="handleEditClick"
|
@editClick="handleEditClick"
|
||||||
@deleteClick="$emit('deleteClick', comment)"
|
@deleteClick="$emit('deleteClick', comment)"
|
||||||
@updateReaction="handleUpdateReaction"
|
@updateReaction="handleUpdateReaction"
|
||||||
@ -37,7 +35,7 @@
|
|||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<template v-if="comment.isEditTextarea">
|
<template v-if="comment.isEditTextarea">
|
||||||
<textarea v-model="localEditedContent" class="form-control" maxLength="500"></textarea>
|
<textarea v-model="localEditedContent" class="form-control"></textarea>
|
||||||
<span v-if="editCommentAlert" class="invalid-feedback d-block text-start">{{ editCommentAlert }}</span>
|
<span v-if="editCommentAlert" class="invalid-feedback d-block text-start">{{ editCommentAlert }}</span>
|
||||||
<div class="mt-2 d-flex justify-content-end">
|
<div class="mt-2 d-flex justify-content-end">
|
||||||
<SaveBtn class="btn btn-primary" @click="submitEdit" :isEnabled="disabled"></SaveBtn>
|
<SaveBtn class="btn btn-primary" @click="submitEdit" :isEnabled="disabled"></SaveBtn>
|
||||||
@ -118,20 +116,9 @@
|
|||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
// isEditPushed: {
|
|
||||||
// type: Boolean,
|
|
||||||
// required: false,
|
|
||||||
// },
|
|
||||||
// isDeletePushed: {
|
|
||||||
// type: Boolean,
|
|
||||||
// required: false,
|
|
||||||
// },
|
|
||||||
editCommentAlert: String,
|
editCommentAlert: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEditPushed = ref(false);
|
|
||||||
const isDeletePushed = ref(false);
|
|
||||||
|
|
||||||
const displayName = computed(() => {
|
const displayName = computed(() => {
|
||||||
return props.nickname ? props.nickname : props.comment.author;
|
return props.nickname ? props.nickname : props.comment.author;
|
||||||
});
|
});
|
||||||
@ -184,24 +171,6 @@
|
|||||||
emit('submitPassword', props.comment, props.password);
|
emit('submitPassword', props.comment, props.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInject = inject('isBtnPushed');
|
|
||||||
|
|
||||||
// 수정, 삭제 버튼 활성화 상태값
|
|
||||||
watch(
|
|
||||||
() => handleInject.value,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
if (newValue.target == props.comment.commentId) {
|
|
||||||
isEditPushed.value = newValue.isEditPushed;
|
|
||||||
isDeletePushed.value = newValue.isDeletePushed;
|
|
||||||
} else {
|
|
||||||
isEditPushed.value = false;
|
|
||||||
isDeletePushed.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.comment.isEditTextarea,
|
() => props.comment.isEditTextarea,
|
||||||
newVal => {
|
newVal => {
|
||||||
|
|||||||
@ -41,8 +41,6 @@
|
|||||||
v-model="nickname"
|
v-model="nickname"
|
||||||
placeholder="닉네임"
|
placeholder="닉네임"
|
||||||
@input="clearAlert('nickname')"
|
@input="clearAlert('nickname')"
|
||||||
@keypress="noSpace"
|
|
||||||
:maxlength="6"
|
|
||||||
/>
|
/>
|
||||||
<!-- 닉네임 경고 메시지 -->
|
<!-- 닉네임 경고 메시지 -->
|
||||||
<div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0">
|
<div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0">
|
||||||
@ -111,10 +109,6 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const noSpace = (e) => {
|
|
||||||
if (e.key === ' ') e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const $common = inject('common');
|
const $common = inject('common');
|
||||||
const comment = ref('');
|
const comment = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|||||||
@ -13,8 +13,6 @@
|
|||||||
:currentPasswordCommentId="currentPasswordCommentId"
|
:currentPasswordCommentId="currentPasswordCommentId"
|
||||||
:password="password"
|
:password="password"
|
||||||
:editCommentAlert="editCommentAlert[comment.commentId]"
|
:editCommentAlert="editCommentAlert[comment.commentId]"
|
||||||
:is-edit-pushed="comment.isEditPushed"
|
|
||||||
:is-delete-pushed="comment.isDeletePushed"
|
|
||||||
@editClick="handleEditClick"
|
@editClick="handleEditClick"
|
||||||
@deleteClick="handleDeleteClick"
|
@deleteClick="handleDeleteClick"
|
||||||
@submitPassword="submitPassword"
|
@submitPassword="submitPassword"
|
||||||
@ -42,8 +40,6 @@
|
|||||||
:passwordCommentAlert="passwordCommentAlert"
|
:passwordCommentAlert="passwordCommentAlert"
|
||||||
:password="password"
|
:password="password"
|
||||||
:editCommentAlert="editCommentAlert[child.commentId]"
|
:editCommentAlert="editCommentAlert[child.commentId]"
|
||||||
:is-edit-pushed="child.isEditPushed"
|
|
||||||
:is-delete-pushed="child.isDeletePushed"
|
|
||||||
@editClick="handleReplyEditClick"
|
@editClick="handleReplyEditClick"
|
||||||
@deleteClick="$emit('deleteClick', child)"
|
@deleteClick="$emit('deleteClick', child)"
|
||||||
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent, child.commentId)"
|
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent, child.commentId)"
|
||||||
@ -63,7 +59,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, watch } from 'vue';
|
import { defineProps, defineEmits } from 'vue';
|
||||||
import BoardComment from './BoardComment.vue';
|
import BoardComment from './BoardComment.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<img
|
<img
|
||||||
:src="getProfileImage(profileImg)"
|
:src="getProfileImage(profileImg)"
|
||||||
alt="user"
|
alt="user"
|
||||||
class="rounded-circle profile-img"
|
class="rounded-circle"
|
||||||
@error="setDefaultImage($event)"
|
@error="setDefaultImage($event)"
|
||||||
@load="showImage($event)"
|
@load="showImage($event)"
|
||||||
/>
|
/>
|
||||||
@ -27,14 +27,8 @@
|
|||||||
<!-- 수정, 삭제 버튼 -->
|
<!-- 수정, 삭제 버튼 -->
|
||||||
<template v-if="!isDeletedComment && (unknown || isCommentAuthor || isAuthor)">
|
<template v-if="!isDeletedComment && (unknown || isCommentAuthor || isAuthor)">
|
||||||
<div class="float-end ms-1">
|
<div class="float-end ms-1">
|
||||||
<slot name="gobackBtn"></slot>
|
<EditButton @click.stop="editClick" />
|
||||||
<EditButton @click.stop="editClick" :is-pushed="isEditPushed" />
|
<DeleteButton :class="'ms-1'" @click.stop="deleteClick" />
|
||||||
<DeleteButton :class="'ms-1'" @click.stop="deleteClick" :is-pushed="isDeletePushed" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="float-end ms-1">
|
|
||||||
<slot name="gobackBtn"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -113,14 +107,6 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isEditPushed: {
|
|
||||||
type: Boolean,
|
|
||||||
require: false,
|
|
||||||
},
|
|
||||||
isDeletePushed: {
|
|
||||||
type: Boolean,
|
|
||||||
require: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
|
const emit = defineEmits(['updateReaction', 'editClick', 'deleteClick']);
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comment: {
|
comment: {
|
||||||
@ -64,11 +64,6 @@
|
|||||||
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);
|
||||||
|
|
||||||
watch([() => props.likeClicked, () => props.dislikeClicked], ([likeNewval, dislikeNewval]) => {
|
|
||||||
likeClicked.value = likeNewval;
|
|
||||||
dislikeClicked.value = dislikeNewval;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
const isLike = !likeClicked.value;
|
const isLike = !likeClicked.value;
|
||||||
const isDislike = false;
|
const isDislike = false;
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="btn btn-label-primary btn-icon" :class="{ active: props.isPushed }">
|
<button class="btn btn-label-primary btn-icon">
|
||||||
<i class="bx bx-trash"></i>
|
<i class='bx bx-trash' ></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script>
|
||||||
const props = defineProps({
|
export default {
|
||||||
isPushed: {
|
name: 'DeleteButton',
|
||||||
type: Boolean,
|
methods: {
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="btn btn-label-primary btn-icon" :class="{ active: props.isPushed }" @click="toggleText">
|
<button class="btn btn-label-primary btn-icon" @click="toggleText">
|
||||||
<i :class="buttonClass"></i>
|
<i :class="buttonClass"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, defineEmits, watchEffect } from 'vue';
|
import { ref, watch, defineProps, defineEmits, watchEffect } from 'vue';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isToggleEnabled: {
|
isToggleEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -14,22 +14,18 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isPushed: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['click']);
|
const emit = defineEmits(["click"]);
|
||||||
const buttonClass = ref('bx bx-edit-alt');
|
const buttonClass = ref('bx bx-edit-alt');
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
buttonClass.value = props.isActive ? 'bx bx-x' : 'bx bx-edit-alt';
|
buttonClass.value = props.isActive ? 'bx bx-x' : 'bx bx-edit-alt';
|
||||||
});
|
});
|
||||||
const toggleText = event => {
|
const toggleText = (event) => {
|
||||||
// 이벤트 객체를 매개변수로 받아옵니다
|
// 이벤트 객체를 매개변수로 받아옵니다
|
||||||
if (props.isToggleEnabled) {
|
if (props.isToggleEnabled) {
|
||||||
buttonClass.value = buttonClass.value === 'bx bx-edit-alt' ? 'bx bx-x' : 'bx bx-edit-alt';
|
buttonClass.value = buttonClass.value === 'bx bx-edit-alt' ? 'bx bx-x' : 'bx bx-edit-alt';
|
||||||
}
|
}
|
||||||
emit('click', event); // 이벤트 객체를 같이 전달
|
emit("click", event); // 이벤트 객체를 같이 전달
|
||||||
};
|
};
|
||||||
const resetButton = () => {
|
const resetButton = () => {
|
||||||
buttonClass.value = 'bx bx-edit-alt';
|
buttonClass.value = 'bx bx-edit-alt';
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 뒤로가기 -->
|
<!-- 뒤로가기 -->
|
||||||
<button
|
<button
|
||||||
v-if="canGoBack"
|
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
:disabled="!canGoBack"
|
:disabled="!canGoBack"
|
||||||
:class="{ 'shifted': showButton }"
|
:class="{ 'shifted': showButton }"
|
||||||
@ -27,7 +26,7 @@ const showButton = ref(false);
|
|||||||
const canGoBack = ref(false);
|
const canGoBack = ref(false);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loginPagePath = "/login"; // 로그인 페이지 기본 경로
|
const loginPage = "/login"; // 로그인 페이지 경로
|
||||||
|
|
||||||
// 스크롤 이벤트 핸들러
|
// 스크롤 이벤트 핸들러
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -51,21 +50,7 @@ const updateCanGoBack = () => {
|
|||||||
const historyBack = router.options.history.state.back;
|
const historyBack = router.options.history.state.back;
|
||||||
const previousPage = document.referrer;
|
const previousPage = document.referrer;
|
||||||
|
|
||||||
// URL에서 경로만 추출하는 함수
|
canGoBack.value = !!historyBack && historyBack !== loginPage && !previousPage.includes(loginPage);
|
||||||
const getPath = (url) => {
|
|
||||||
try {
|
|
||||||
return new URL(url, window.location.origin).pathname; // 쿼리 제거
|
|
||||||
} catch {
|
|
||||||
return ""; // 잘못된 URL 처리
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const previousPath = getPath(previousPage);
|
|
||||||
|
|
||||||
// 뒤로 갈 수 있는 조건:
|
|
||||||
canGoBack.value = !!historyBack
|
|
||||||
&& getPath(historyBack) !== loginPagePath
|
|
||||||
&& !previousPath.startsWith(loginPagePath);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 마운트 시 한 번 실행
|
// 마운트 시 한 번 실행
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated', 'projectChangeComplete', 'update:pendingProjectChange']);
|
const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated', 'projectChangeComplete']);
|
||||||
|
|
||||||
const workTime = ref(null);
|
const workTime = ref(null);
|
||||||
const leaveTime = ref(null)
|
const leaveTime = ref(null)
|
||||||
@ -171,6 +171,7 @@ const setWorkTime = async () => {
|
|||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
todayCommuterInfo();
|
todayCommuterInfo();
|
||||||
|
|
||||||
emit('workTimeUpdated', true);
|
emit('workTimeUpdated', true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -181,8 +182,7 @@ const setLeaveTime = async () => {
|
|||||||
// 현재 위치 주소 가져오기
|
// 현재 위치 주소 가져오기
|
||||||
const address = await getLocation();
|
const address = await getLocation();
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
if (!address && !leaveTime.value) {
|
|
||||||
// 주소를 가져오지 못했을 때도 계속 진행할지 사용자에게 확인
|
// 주소를 가져오지 못했을 때도 계속 진행할지 사용자에게 확인
|
||||||
if (!confirm('위치 정보를 가져오지 못했습니다. 위치 없이 퇴근 처리하시겠습니까?')) {
|
if (!confirm('위치 정보를 가져오지 못했습니다. 위치 없이 퇴근 처리하시겠습니까?')) {
|
||||||
return;
|
return;
|
||||||
@ -198,7 +198,6 @@ const setLeaveTime = async () => {
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
todayCommuterInfo();
|
todayCommuterInfo();
|
||||||
emit('leaveTimeUpdated');
|
emit('leaveTimeUpdated');
|
||||||
emit('update:pendingProjectChange', null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -210,6 +209,10 @@ watch(() => props.userId, async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => props.checkedInProject, () => {
|
||||||
|
// 프로젝트가 변경되면 필요한 처리 수행
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await todayCommuterInfo();
|
await todayCommuterInfo();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
|
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle object-fit-cover" @error="$event.target.src = '/img/icons/icon.png'"/>
|
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||||
<p class="mt-2 fw-bold">
|
<p class="mt-2 fw-bold">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</p>
|
</p>
|
||||||
@ -13,7 +13,6 @@
|
|||||||
:userId="user.id"
|
:userId="user.id"
|
||||||
:checkedInProject="checkedInProject || {}"
|
:checkedInProject="checkedInProject || {}"
|
||||||
:pendingProjectChange="pendingProjectChange"
|
:pendingProjectChange="pendingProjectChange"
|
||||||
@update:pendingProjectChange="pendingProjectChange = $event"
|
|
||||||
@workTimeUpdated="handleWorkTimeUpdate"
|
@workTimeUpdated="handleWorkTimeUpdate"
|
||||||
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||||
ref="workTimeComponentRef"
|
ref="workTimeComponentRef"
|
||||||
@ -60,7 +59,7 @@
|
|||||||
<div class="row my-2 d-flex align-items-center">
|
<div class="row my-2 d-flex align-items-center">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
class="me-2 w-px-50 h-px-50 rounded-circle object-fit-cover"
|
class="rounded-circle me-2 w-px-50 h-px-50"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'">
|
@error="$event.target.src = '/img/icons/icon.png'">
|
||||||
|
|
||||||
<span class="fw-bold">{{ commuter.memberName }}</span>
|
<span class="fw-bold">{{ commuter.memberName }}</span>
|
||||||
@ -179,34 +178,8 @@ const handleWorkTimeUpdate = () => {
|
|||||||
loadCommuters();
|
loadCommuters();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLeaveTimeUpdate = async () => {
|
const handleLeaveTimeUpdate = () => {
|
||||||
await todaysCommuter(); // 최신 출근자 목록 다시 로드
|
todaysCommuter();
|
||||||
|
|
||||||
// 현재 사용자의 퇴근 기록이 null인지 확인
|
|
||||||
const currentUserCommuter = commuters.value.find(c => c.MEMBERSEQ === user.value.id);
|
|
||||||
if (currentUserCommuter && !currentUserCommuter.COMMUTLVE) {
|
|
||||||
await projectStore.getMemberProjects();
|
|
||||||
|
|
||||||
if (projectStore.activeMemberProjectList.length > 0) {
|
|
||||||
const previousProject = projectStore.activeMemberProjectList.find(
|
|
||||||
p => commuters.value.some(c => c.MEMBERSEQ === user.value.id && c.PROJCTLVE === p.PROJCTSEQ)
|
|
||||||
) || projectStore.activeMemberProjectList[0]; // 이전 출근 프로젝트가 없으면 첫 번째 프로젝트
|
|
||||||
|
|
||||||
if (previousProject) {
|
|
||||||
selectedProject.value = previousProject.PROJCTSEQ;
|
|
||||||
projectStore.setSelectedProject(previousProject);
|
|
||||||
} else if (projectStore.activeProjectList.length > 0) {
|
|
||||||
selectedProject.value = projectStore.activeProjectList[0].PROJCTSEQ;
|
|
||||||
projectStore.setSelectedProject(projectStore.activeProjectList[0]);
|
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
projectStore.setSelectedProject(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
projectStore.setSelectedProject(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 프로젝트 드롭 이벤트 핸들러 (ProjectList 컴포넌트에서 전달받음)
|
// 프로젝트 드롭 이벤트 핸들러 (ProjectList 컴포넌트에서 전달받음)
|
||||||
@ -233,11 +206,6 @@ const handleProjectDrop = ({ event, targetProject }) => {
|
|||||||
? { ...commuter, PROJCTNAM: targetProject.PROJCTNAM, PROJCTLVE: targetProject.PROJCTSEQ }
|
? { ...commuter, PROJCTNAM: targetProject.PROJCTNAM, PROJCTLVE: targetProject.PROJCTSEQ }
|
||||||
: commuter
|
: commuter
|
||||||
);
|
);
|
||||||
|
|
||||||
// 드롭 후 CommuterBtn 컴포넌트의 상태를 업데이트 (출근/퇴근 버튼 상태 변경)
|
|
||||||
if (workTimeComponentRef.value && workTimeComponentRef.value.fetchWorkTime) {
|
|
||||||
workTimeComponentRef.value.fetchWorkTime();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 오늘 출근 모든 사용자 조회
|
// 오늘 출근 모든 사용자 조회
|
||||||
@ -399,7 +367,7 @@ const loadCommuters = async () => {
|
|||||||
// 프로필 이미지 생성
|
// 프로필 이미지 생성
|
||||||
const profileImg = document.createElement('img');
|
const profileImg = document.createElement('img');
|
||||||
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
||||||
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto object-fit-cover';
|
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto';
|
||||||
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
|
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
|
||||||
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
|
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
|
||||||
|
|
||||||
@ -463,17 +431,12 @@ watch(() => projectStore.selectedProject, (newProject) => {
|
|||||||
if (newProject) {
|
if (newProject) {
|
||||||
selectedProject.value = newProject.PROJCTSEQ;
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
checkedInProject.value = newProject;
|
checkedInProject.value = newProject;
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
checkedInProject.value = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 모달 닫기
|
// 모달 닫기
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
isModalOpen.value = false;
|
isModalOpen.value = false;
|
||||||
|
|
||||||
visiblePopover.value = { type: null, index: null };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// MapPopover에서 visible 상태 변경 이벤트 처리
|
// MapPopover에서 visible 상태 변경 이벤트 처리
|
||||||
@ -502,11 +465,10 @@ onMounted(async () => {
|
|||||||
await todaysCommuter();
|
await todaysCommuter();
|
||||||
|
|
||||||
// 저장된 선택 프로젝트 가져오기
|
// 저장된 선택 프로젝트 가져오기
|
||||||
if (projectStore.activeMemberProjectList.length > 0) {
|
const storedProject = projectStore.getSelectedProject();
|
||||||
const initialProject = projectStore.getSelectedProject() || projectStore.activeMemberProjectList[0];
|
if (storedProject) {
|
||||||
selectedProject.value = initialProject?.PROJCTSEQ || null;
|
selectedProject.value = storedProject.PROJCTSEQ;
|
||||||
projectStore.setSelectedProject(initialProject);
|
checkedInProject.value = storedProject;
|
||||||
checkedInProject.value = initialProject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datePickerStore.initDatePicker(
|
datePickerStore.initDatePicker(
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
<img
|
<img
|
||||||
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
alt="User Profile"
|
alt="User Profile"
|
||||||
class="rounded-circle object-fit-cover"
|
class="rounded-circle"
|
||||||
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
||||||
:draggable="isCurrentUser(commuter)"
|
:draggable="isCurrentUser(commuter)"
|
||||||
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
||||||
|
|||||||
@ -41,7 +41,6 @@
|
|||||||
|
|
||||||
<button class="ql-link">Link</button>
|
<button class="ql-link">Link</button>
|
||||||
<button class="ql-image">Image</button>
|
<button class="ql-image">Image</button>
|
||||||
<button class="ql-video">Video</button>
|
|
||||||
<button class="ql-blockquote">Blockquote</button>
|
<button class="ql-blockquote">Blockquote</button>
|
||||||
<button class="ql-code-block">Code Block</button>
|
<button class="ql-code-block">Code Block</button>
|
||||||
</div>
|
</div>
|
||||||
@ -55,11 +54,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Quill from 'quill';
|
import Quill from 'quill';
|
||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import $api from '@api';
|
|
||||||
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
||||||
import { useToastStore } from '@s/toastStore';
|
import $api from '@api';
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isAlert: {
|
isAlert: {
|
||||||
@ -135,38 +131,6 @@
|
|||||||
initCheckImageIndex();
|
initCheckImageIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 영상 넣기
|
|
||||||
quillInstance.getModule('toolbar').addHandler('video', () => {
|
|
||||||
const url = prompt('YouTube 영상 URL을 입력하세요:');
|
|
||||||
let src = '';
|
|
||||||
if (!url || url.trim() == '') return;
|
|
||||||
|
|
||||||
// 일반 youtube url
|
|
||||||
if (url.indexOf('watch?v=') !== -1) {
|
|
||||||
src = url.replace('watch?v=', 'embed/');
|
|
||||||
|
|
||||||
// youtu.be 단축 URL (ex : https://youtu.be/CfiojceAaeQ?si=G7eM56sdDjIEw-Tz)
|
|
||||||
} else if (url.indexOf('youtu.be/') !== -1) {
|
|
||||||
const videoId = url.split('youtu.be/')[1].split('?')[0];
|
|
||||||
src = `https://www.youtube.com/embed/${videoId}`;
|
|
||||||
|
|
||||||
// iframe 주소
|
|
||||||
} else if (url.indexOf('<iframe') !== -1) {
|
|
||||||
// DOMParser를 사용하여 embeded url만 추출
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(url, 'text/html');
|
|
||||||
const iframeEL = doc.querySelector('iframe');
|
|
||||||
src = iframeEL.getAttribute('src');
|
|
||||||
} else {
|
|
||||||
toastStore.onToast('지원하는 영상 타입 아님', 'e');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = quillInstance.getSelection().index;
|
|
||||||
quillInstance.insertEmbed(index, 'video', src);
|
|
||||||
quillInstance.setSelection(index + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 이미지 업로드 기능 처리
|
// 이미지 업로드 기능 처리
|
||||||
let imageUrls = new Set(); // 업로드된 이미지 URL을 추적
|
let imageUrls = new Set(); // 업로드된 이미지 URL을 추적
|
||||||
quillInstance.getModule('toolbar').addHandler('image', () => {
|
quillInstance.getModule('toolbar').addHandler('image', () => {
|
||||||
|
|||||||
@ -4,19 +4,12 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
<span v-if="isEssential" class="link-danger">*</span>
|
<span v-if="isEssential" class="link-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div :class="isRow ? 'col-md-10' : 'col-md-12'">
|
<div :class="isRow ? 'col-md-10' : 'col-md-12'" class="d-flex gap-2 align-items-center">
|
||||||
<div 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 v-if="isColor && (!data || data.length === 0)" class="form-select" disabled>
|
|
||||||
<option>사용가능한 컬러가 없습니다</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- 데이터가 있는 경우 원래 select 표시 -->
|
|
||||||
<select v-else class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}" @blur="$emit('blur')">
|
|
||||||
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
|
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
|
||||||
{{ isCommon ? item.label : item }}
|
{{ isCommon ? item.label : item }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div v-if="isBtn" class="ms-2">
|
<div v-if="isBtn" class="ms-2">
|
||||||
<slot name="append"></slot>
|
<slot name="append"></slot>
|
||||||
</div>
|
</div>
|
||||||
@ -32,9 +25,7 @@
|
|||||||
:src="`/img/mbti/${selected.toLowerCase()}.png`"
|
:src="`/img/mbti/${selected.toLowerCase()}.png`"
|
||||||
alt="MBTI image"/>
|
alt="MBTI image"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isAlert" class="invalid-feedback">{{ title }}을 확인해주세요.</div>
|
||||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}를 확인해주세요.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
:value="computedValue"
|
:value="computedValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="placeholder ? placeholder : title"
|
:placeholder="title"
|
||||||
@blur="$emit('blur')"
|
@blur="$emit('blur')"
|
||||||
/>
|
/>
|
||||||
<span class="input-group-text">@ localhost.co.kr</span>
|
<span class="input-group-text">@ localhost.co.kr</span>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
:value="computedValue"
|
:value="computedValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="placeholder ? placeholder : title"
|
:placeholder="title"
|
||||||
@blur="$emit('blur')"
|
@blur="$emit('blur')"
|
||||||
@click="handleDateClick"
|
@click="handleDateClick"
|
||||||
ref="inputElement"
|
ref="inputElement"
|
||||||
@ -89,10 +89,6 @@
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
||||||
|
|||||||
@ -12,43 +12,26 @@
|
|||||||
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 날짜 -->
|
<!-- 날짜 -->
|
||||||
<div class="row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
|
||||||
<i class="bx bx-calendar"></i>
|
<i class="bx bx-calendar"></i>
|
||||||
<div class="ms-2">날짜</div>
|
<div class="ms-2">날짜</div>
|
||||||
|
<div class="ms-12">{{ strdate }} ~ {{ enddate }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 col-md-10">
|
|
||||||
{{ strdate }} ~ {{ enddate }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 참여자 -->
|
<!-- 참여자 -->
|
||||||
<div class="row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
|
||||||
<i class="bx bxs-user"></i>
|
<i class="bx bxs-user"></i>
|
||||||
<div class="ms-2">참여자</div>
|
<div class="ms-2">참여자</div>
|
||||||
|
<UserList ref="userListRef" :projctSeq="projctSeq" :showOnlyActive="true" class="ms-8 mb-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 col-md-10">
|
|
||||||
<UserList ref="userListRef" :projctSeq="projctSeq" :showOnlyActive="true" class="mb-0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 설명 -->
|
<!-- 설명 -->
|
||||||
<div class="row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
|
||||||
<i class="bx bx-detail"></i>
|
<i class="bx bx-detail"></i>
|
||||||
<div class="ms-2">설명</div>
|
<div class="ms-2">설명</div>
|
||||||
|
<div class="ms-12">{{ description }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 col-md-10">
|
|
||||||
{{ description || '-' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 주소 -->
|
<!-- 주소 -->
|
||||||
<div class="row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
|
||||||
<MapPopover
|
<MapPopover
|
||||||
:address="address"
|
:address="address"
|
||||||
:is-visible="isMapVisible"
|
:is-visible="isMapVisible"
|
||||||
@ -61,13 +44,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MapPopover>
|
</MapPopover>
|
||||||
|
<div class="ms-12">
|
||||||
|
{{ address }} {{ addressdtail }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 col-md-10 d-flex justify-content-between align-items-center">
|
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal"><i class='bx bx-child'></i></button>
|
||||||
<div>{{ address }} {{ addressdtail }}</div>
|
|
||||||
<button type="button" class="btn text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">
|
|
||||||
<i class='bx bx-child'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -541,19 +521,17 @@ const handleUpdate = async () => {
|
|||||||
|
|
||||||
// 프로젝트 삭제
|
// 프로젝트 삭제
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (confirm('프로젝트를 삭제하시겠습니까?')) {
|
|
||||||
$api.patch('project/delete', {
|
$api.patch('project/delete', {
|
||||||
projctSeq: props.projctSeq,
|
projctSeq: props.projctSeq,
|
||||||
projctCol: props.projctCol,
|
projctCol: props.projctCol,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toastStore.onToast('프로젝트가 삭제되었습니다.', 's');
|
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||||
projectStore.getProjectList();
|
projectStore.getProjectList();
|
||||||
projectStore.getMemberProjects();
|
projectStore.getMemberProjects();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 컴포넌트 마운트 시 실행
|
// 컴포넌트 마운트 시 실행
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
|
|
||||||
<div class="card text-center h-100">
|
|
||||||
<!-- 더보기 버튼 -->
|
|
||||||
<div class="d-flex">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'BoardList', query: { type: selectedBoard } }"
|
|
||||||
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
|
|
||||||
>
|
|
||||||
more
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- 모달 본문 -->
|
|
||||||
<div class="modal-body">
|
|
||||||
<!-- 탭 버튼 영역 -->
|
|
||||||
<div class="btn-group mb-5" role="group">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
:class="selectedBoard === 'notices' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="changeBoard('notices')"
|
|
||||||
>
|
|
||||||
공지
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
:class="selectedBoard === 'general' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="changeBoard('general')"
|
|
||||||
>
|
|
||||||
자유
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
:class="selectedBoard === 'anonymous' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="changeBoard('anonymous')"
|
|
||||||
>
|
|
||||||
익명
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<!-- 게시글 미리보기 테이블 -->
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<!-- 익명게시판은 '닉네임', 나머지는 '작성자' -->
|
|
||||||
<th class="text-start">
|
|
||||||
<div class="ms-4">
|
|
||||||
{{ selectedBoard === 'anonymous' ? '닉네임' : '작성자' }}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="text-start" style="width: 65%;">
|
|
||||||
<div class="ms-4">
|
|
||||||
제목
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="post in currentList"
|
|
||||||
:key="post.id"
|
|
||||||
style="cursor: pointer;"
|
|
||||||
@click="goDetail(post.id, selectedBoard)"
|
|
||||||
>
|
|
||||||
<td class="text-start nickname-ellipsis small">
|
|
||||||
<div class="ms-4">
|
|
||||||
{{ selectedBoard === 'anonymous' ? post.nickname : post.author }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-start fs-6">
|
|
||||||
<div class="ms-4">
|
|
||||||
{{ truncateTitle(post.title) }}
|
|
||||||
<span v-if="post.commentCount" class="text-danger ml-1 small">
|
|
||||||
[{{ post.commentCount }}]
|
|
||||||
</span>
|
|
||||||
<i v-if="post.img" class="bi bi-image mx-1 small"></i>
|
|
||||||
<i
|
|
||||||
v-if="post.hasAttachment.length > 0"
|
|
||||||
class="bi bi-paperclip ml-1 small"
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<div class="text-muted small small">
|
|
||||||
{{ post.date }}
|
|
||||||
<span class="ms-1">
|
|
||||||
<i class="fa-regular fa-eye small me-1"></i>{{post.views}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="currentList.length === 0">
|
|
||||||
<td colspan="3" class="text-center text-muted">게시물이 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import axios from '@api';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import isToday from 'dayjs/plugin/isToday';
|
|
||||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
|
||||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
|
||||||
|
|
||||||
dayjs.extend(isToday);
|
|
||||||
dayjs.extend(isYesterday);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 모달이 항상 보이도록 초기값 true
|
|
||||||
const isModalOpen = ref(true);
|
|
||||||
|
|
||||||
// 현재 선택된 게시판 타입: 'notices', 'general', 'anonymous'
|
|
||||||
const selectedBoard = ref('notices');
|
|
||||||
|
|
||||||
// 각 게시판 미리보기 데이터 배열
|
|
||||||
const noticeList = ref([]);
|
|
||||||
const freeList = ref([]);
|
|
||||||
const anonymousList = ref([]);
|
|
||||||
|
|
||||||
// 선택된 게시판에 따른 미리보기 목록 computed
|
|
||||||
const currentList = computed(() => {
|
|
||||||
if (selectedBoard.value === 'notices') return noticeList.value;
|
|
||||||
if (selectedBoard.value === 'general') return freeList.value;
|
|
||||||
if (selectedBoard.value === 'anonymous') return anonymousList.value;
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 날짜 포맷 함수: 오늘이면 HH:mm, 아니면 YYYY-MM-DD
|
|
||||||
const formatDate = dateString => {
|
|
||||||
const date = dayjs(dateString);
|
|
||||||
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
|
||||||
const truncateTitle = title => {
|
|
||||||
return title.length > 7 ? title.slice(0, 7) + '...' : title;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 공지사항 데이터 로드 (최대 5개)
|
|
||||||
const fetchNoticePosts = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get('board/notices', { params: { size: 8 } });
|
|
||||||
if (data?.data) {
|
|
||||||
noticeList.value = data.data.map(post => ({
|
|
||||||
id: post.id,
|
|
||||||
title: post.title,
|
|
||||||
date: formatDate(post.date),
|
|
||||||
rawDate: post.date,
|
|
||||||
views: post.cnt || 0,
|
|
||||||
commentCount: post.commentCount,
|
|
||||||
img: post.firstImageUrl,
|
|
||||||
author: post.author || '관리자',
|
|
||||||
nickname: post.nickname || '관리자',
|
|
||||||
hasAttachment: post.hasAttachment, // 첨부파일 유무
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// board/general 게시글 로드 후 자유게시판과 익명게시판으로 분리 (최대 10개 조회 → 각각 최대 5개)
|
|
||||||
const fetchGeneralPosts = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get('board/general', { params: { size: 16 } });
|
|
||||||
if (data?.data && data.data.list) {
|
|
||||||
const freePosts = [];
|
|
||||||
const anonymousPosts = [];
|
|
||||||
data.data.list.forEach(post => {
|
|
||||||
if (post.nickname) {
|
|
||||||
// 닉네임이 있으면 익명게시판 데이터
|
|
||||||
anonymousPosts.push({
|
|
||||||
id: post.id,
|
|
||||||
title: post.title,
|
|
||||||
date: formatDate(post.date),
|
|
||||||
img: post.firstImageUrl,
|
|
||||||
rawDate: post.date,
|
|
||||||
views: post.cnt || 0,
|
|
||||||
commentCount: post.commentCount,
|
|
||||||
nickname: post.nickname,
|
|
||||||
hasAttachment: post.hasAttachment, // 첨부파일 유무
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 닉네임이 없으면 자유게시판 데이터
|
|
||||||
freePosts.push({
|
|
||||||
id: post.id,
|
|
||||||
title: post.title,
|
|
||||||
date: formatDate(post.date),
|
|
||||||
rawDate: post.date,
|
|
||||||
views: post.cnt || 0,
|
|
||||||
img: post.firstImageUrl,
|
|
||||||
commentCount: post.commentCount,
|
|
||||||
author: post.author || '익명',
|
|
||||||
hasAttachment: post.hasAttachment, // 첨부파일 유무
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
freeList.value = freePosts.slice(0, 8);
|
|
||||||
anonymousList.value = anonymousPosts.slice(0, 8);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 탭 변경 함수
|
|
||||||
const changeBoard = type => {
|
|
||||||
selectedBoard.value = type;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 상세 페이지 이동 (게시판 타입 전달)
|
|
||||||
const goDetail = (id, boardType) => {
|
|
||||||
router.push({ name: 'BoardDetail', params: { id }, query: { type: boardType } });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달이 열릴 때 데이터 로드
|
|
||||||
fetchNoticePosts();
|
|
||||||
fetchGeneralPosts();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.table > :not(caption) > * > * {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -28,30 +28,10 @@
|
|||||||
placeholder="장소"
|
placeholder="장소"
|
||||||
v-model="eventPlace"
|
v-model="eventPlace"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
@input="handleChangeInput"
|
|
||||||
/>
|
/>
|
||||||
<span v-if="noInputAlert" class="invalid-feedback d-block" style="padding-left: 5px">{{ noInputAlert }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div @click="focusPicker">
|
<input type="time" class="form-control form-control-sm py-1" style="height: 25px; font-size: 12px" v-model="eventTime" />
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
class="form-control form-control-sm py-1"
|
|
||||||
style="height: 0%; font-size: 12px"
|
|
||||||
v-model="eventTime"
|
|
||||||
@input="handleChangeInput2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref="timeInput"
|
|
||||||
type="time"
|
|
||||||
class="hidden-time-input"
|
|
||||||
style="height: 0%; font-size: 12px"
|
|
||||||
v-model="eventTime"
|
|
||||||
@input="handleChangeInput2"
|
|
||||||
/>
|
|
||||||
<span v-if="noInputAlert2" class="invalid-feedback d-block" style="padding-left: 5px">{{ noInputAlert2 }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<button class="btn btn-primary btn-sm py-1" style="font-size: 12px; height: 25px; line-height: 1" @click="handleSubmit">
|
<button class="btn btn-primary btn-sm py-1" style="font-size: 12px; height: 25px; line-height: 1" @click="handleSubmit">
|
||||||
@ -90,16 +70,6 @@
|
|||||||
const selectedEventType = ref(null);
|
const selectedEventType = ref(null);
|
||||||
const eventPlace = ref('');
|
const eventPlace = ref('');
|
||||||
const eventTime = ref('');
|
const eventTime = ref('');
|
||||||
const noInputAlert = ref(null);
|
|
||||||
const noInputAlert2 = ref(null);
|
|
||||||
const timeInput = ref(null);
|
|
||||||
|
|
||||||
const focusPicker = () => {
|
|
||||||
if (timeInput.value) {
|
|
||||||
timeInput.value.showPicker(); // 달력 자동 열기 (일부 브라우저에서 지원)
|
|
||||||
timeInput.value.focus(); // 포커스 이동
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventTypes = [
|
const eventTypes = [
|
||||||
{ type: 'birthdayParty', code: '300203', title: '생일파티' },
|
{ type: 'birthdayParty', code: '300203', title: '생일파티' },
|
||||||
@ -109,6 +79,8 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const getEventTitle = type => {
|
const getEventTitle = type => {
|
||||||
|
console.log('type: ', type);
|
||||||
|
console.log('event.type: ', eventTypes);
|
||||||
return eventTypes.find(event => event.code === type)?.title || '';
|
return eventTypes.find(event => event.code === type)?.title || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,50 +99,22 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedEventType.value = event.code;
|
selectedEventType.value = event.code;
|
||||||
noInputAlert.value = '';
|
|
||||||
noInputAlert2.value = '';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (isValid()) {
|
if (!eventPlace.value || !eventTime.value) {
|
||||||
|
alert('장소와 시간을 모두 입력해주세요');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit('insert', {
|
emit('insert', {
|
||||||
date: props.selectedDate,
|
date: props.selectedDate,
|
||||||
code: selectedEventType.value,
|
code: selectedEventType.value,
|
||||||
title: getEventTitle(selectedEventType.value),
|
title: getEventTitle(selectedEventType.value),
|
||||||
place: eventPlace.value.trim(),
|
place: eventPlace.value,
|
||||||
time: eventTime.value,
|
time: eventTime.value,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 유효성 검사
|
|
||||||
const isValid = () => {
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!eventPlace.value.trim()) {
|
|
||||||
noInputAlert.value = '장소를 입력해 주세요';
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
noInputAlert.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eventTime.value) {
|
|
||||||
noInputAlert2.value = '시간을 입력해 주세요';
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
noInputAlert2.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeInput = () => {
|
|
||||||
noInputAlert.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeInput2 = () => {
|
|
||||||
noInputAlert2.value = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ps-2" style="font-size: 13px">
|
|
||||||
<span class="d-flex align-items-center g-2 font_767"><i class="bx bxs-map pe-1"></i>{{ place }}</span>
|
|
||||||
<span class="d-flex align-items-center g-2 font_767"
|
|
||||||
><i class="bx bxs-time-five pe-1"></i>{{ $common.dateFormatter(time, 'T') }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
place: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.font_767 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -5,23 +5,16 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<img
|
<img
|
||||||
v-if="user"
|
v-if="user"
|
||||||
:src="`${profileImgUrl}profile/${user.profile}`"
|
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||||
alt="Profile Image"
|
alt="Profile Image"
|
||||||
class="w-px-50 h-px-50 rounded-circle profile-img"
|
class="w-px-50 h-px-50 rounded-circle"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
/>
|
/>
|
||||||
<p class="mt-2 fw-bold">
|
<p class="mt-2 fw-bold">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CommuterBtn
|
<CommuterBtn :userId="user.id" :checkedInProject="checkedInProject || {}" ref="workTimeComponentRef" />
|
||||||
ref="workTimeComponentRef"
|
|
||||||
:userId="user.id"
|
|
||||||
:checkedInProject="checkedInProject || {}"
|
|
||||||
:pendingProjectChange="pendingProjectChange"
|
|
||||||
@update:pendingProjectChange="pendingProjectChange = $event"
|
|
||||||
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MainEventList
|
<MainEventList
|
||||||
:categoryList="categoryList"
|
:categoryList="categoryList"
|
||||||
@ -32,7 +25,6 @@
|
|||||||
:dinnerList="dinnerList"
|
:dinnerList="dinnerList"
|
||||||
:teaTimeList="teaTimeList"
|
:teaTimeList="teaTimeList"
|
||||||
:workShopList="workShopList"
|
:workShopList="workShopList"
|
||||||
@handle-click-vacation="handleClickVacation"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,7 +40,6 @@
|
|||||||
class="flatpickr-calendar-only"
|
class="flatpickr-calendar-only"
|
||||||
>
|
>
|
||||||
</full-calendar>
|
</full-calendar>
|
||||||
<input ref="calendarDatepicker" type="text" class="d-none" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,44 +60,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject, onMounted, reactive, ref, watch, nextTick } from 'vue';
|
|
||||||
import { fetchHolidays } from '@c/calendar/holiday';
|
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
|
||||||
import { useProjectStore } from '@/stores/useProjectStore';
|
|
||||||
import { useToastStore } from '@s/toastStore';
|
|
||||||
import { useWeatherStore } from '@/stores/useWeatherStore';
|
|
||||||
import { useDatePicker } from '@/stores/useDatePicker';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import router from '@/router';
|
|
||||||
import FullCalendar from '@fullcalendar/vue3';
|
import FullCalendar from '@fullcalendar/vue3';
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
import { inject, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import MainEventList from '@c/main/MainEventList.vue';
|
|
||||||
import EventModal from '@c/main/EventModal.vue';
|
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
import 'flatpickr/dist/flatpickr.min.css';
|
import 'flatpickr/dist/flatpickr.min.css';
|
||||||
import '@/assets/css/app-calendar.css';
|
import '@/assets/css/app-calendar.css';
|
||||||
|
import { fetchHolidays } from '@c/calendar/holiday';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
|
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||||
|
import MainEventList from '@c/main/MainEventList.vue';
|
||||||
|
import EventModal from '@c/main/EventModal.vue';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
|
||||||
const baseUrl = import.meta.env.VITE_DOMAIN;
|
const baseUrl = import.meta.env.VITE_DOMAIN;
|
||||||
const profileImgUrl = import.meta.env.VITE_SERVER_IMG_URL;
|
|
||||||
const user = ref({});
|
const user = ref({});
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
const weatherStore = useWeatherStore();
|
|
||||||
const datePickerStore = useDatePicker();
|
|
||||||
const { dailyWeatherList } = storeToRefs(weatherStore);
|
|
||||||
|
|
||||||
const dayjs = inject('dayjs');
|
const dayjs = inject('dayjs');
|
||||||
const fullCalendarRef = ref(null);
|
const fullCalendarRef = ref(null);
|
||||||
const workTimeComponentRef = ref(null);
|
const workTimeComponentRef = ref(null);
|
||||||
const calendarEvents = ref([]);
|
const calendarEvents = ref([]);
|
||||||
const calendarDatepicker = ref(null);
|
|
||||||
//const dailyWeatherList = ref([]);
|
|
||||||
|
|
||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
const checkedInProject = ref(null);
|
const checkedInProject = ref(null);
|
||||||
const pendingProjectChange = ref(null);
|
|
||||||
|
|
||||||
// 이벤트 모달 관련
|
// 이벤트 모달 관련
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
@ -121,6 +101,24 @@
|
|||||||
const pressTimer = ref(null);
|
const pressTimer = ref(null);
|
||||||
const longPressDelay = 500; // 0.5초
|
const longPressDelay = 500; // 0.5초
|
||||||
|
|
||||||
|
// // 출퇴근 컴포넌트 이벤트 핸들러
|
||||||
|
// const handleWorkTimeUpdate = () => {
|
||||||
|
// todaysCommuter();
|
||||||
|
// //loadCommuters();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleLeaveTimeUpdate = () => {
|
||||||
|
// todaysCommuter();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 오늘 출근 모든 사용자 조회
|
||||||
|
// const todaysCommuter = async () => {
|
||||||
|
// const res = await $api.get(`commuters/todays`);
|
||||||
|
// if (res.status === 200) {
|
||||||
|
// commuters.value = res.data.data;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
/************* category ***************/
|
/************* category ***************/
|
||||||
|
|
||||||
// 이벤트 카테고리 데이터 로딩
|
// 이벤트 카테고리 데이터 로딩
|
||||||
@ -154,7 +152,7 @@
|
|||||||
|
|
||||||
// 기존의 공휴일 이벤트는 유지
|
// 기존의 공휴일 이벤트는 유지
|
||||||
const holidayEvents = calendarEvents.value.filter(event => event.classNames?.includes('holiday-event'));
|
const holidayEvents = calendarEvents.value.filter(event => event.classNames?.includes('holiday-event'));
|
||||||
calendarEvents.value = [...holidayEvents, ...dailyWeatherList.value];
|
calendarEvents.value = [...holidayEvents];
|
||||||
|
|
||||||
// 생일자
|
// 생일자
|
||||||
if (res?.memberBirthdayList?.length) {
|
if (res?.memberBirthdayList?.length) {
|
||||||
@ -176,7 +174,7 @@
|
|||||||
monthBirthdayPartyList.value = [];
|
monthBirthdayPartyList.value = [];
|
||||||
monthDinnerList.value = [];
|
monthDinnerList.value = [];
|
||||||
monthTeaTimeList.value = [];
|
monthTeaTimeList.value = [];
|
||||||
monthWorkShopList.value = [];
|
monthTeaTimeList.value = [];
|
||||||
|
|
||||||
if (res?.eventList?.length) {
|
if (res?.eventList?.length) {
|
||||||
res.eventList.forEach(item => {
|
res.eventList.forEach(item => {
|
||||||
@ -312,7 +310,7 @@
|
|||||||
// 날짜 선택 가능 여부를 확인하는 공통 함수
|
// 날짜 선택 가능 여부를 확인하는 공통 함수
|
||||||
const isSelectableDate = date => {
|
const isSelectableDate = date => {
|
||||||
const checkDate = dayjs(date);
|
const checkDate = dayjs(date);
|
||||||
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6; // 주말체크
|
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6;
|
||||||
// 공휴일 체크
|
// 공휴일 체크
|
||||||
const isHoliday = calendarEvents.value.some(
|
const isHoliday = calendarEvents.value.some(
|
||||||
event =>
|
event =>
|
||||||
@ -330,37 +328,17 @@
|
|||||||
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
|
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
|
||||||
if (!isSelectableDate(cellDate)) {
|
if (!isSelectableDate(cellDate)) {
|
||||||
classes.push('fc-day-sat-sun');
|
classes.push('fc-day-sat-sun');
|
||||||
} else {
|
|
||||||
// 선택 가능한 날짜 포인터 클래스 추가
|
|
||||||
classes.push('clickable');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 날짜 클릭 이벤트 핸들러
|
// 날짜 클릭 이벤트 핸들러
|
||||||
let todayEL = null;
|
|
||||||
const handleDateClick = info => {
|
const handleDateClick = info => {
|
||||||
if (isSelectableDate(info.date)) {
|
|
||||||
if ($common.isToday(info.date)) {
|
|
||||||
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
|
|
||||||
todayEL = info.dayEl;
|
|
||||||
todayEL.classList.remove('fc-day-today');
|
|
||||||
} else if (todayEL) {
|
|
||||||
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
|
|
||||||
todayEL.classList.add('fc-day-today');
|
|
||||||
todayEL = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { month, day } = $common.formatDateTime(new Date(info.dateStr));
|
const { month, day } = $common.formatDateTime(new Date(info.dateStr));
|
||||||
useFilterEventList(month, day);
|
useFilterEventList(month, day);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 오늘 날짜 노란색 배경 복구
|
|
||||||
const colorToday = e => {
|
|
||||||
if (todayEL != null && !todayEL.classList.contains('fc-day-today')) todayEL.classList.add('fc-day-today');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 이벤트 모달 핸들러
|
// 이벤트 모달 핸들러
|
||||||
const handleMouseDown = (date, jsEvent) => {
|
const handleMouseDown = (date, jsEvent) => {
|
||||||
if (showModal.value) showModal.value = false;
|
if (showModal.value) showModal.value = false;
|
||||||
@ -408,7 +386,7 @@
|
|||||||
param.append('day', day);
|
param.append('day', day);
|
||||||
|
|
||||||
await fetchEventList(param);
|
await fetchEventList(param);
|
||||||
useFilterEventList(month, day);
|
useFilterEventList(month, year);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이벤트 추가 api
|
// 이벤트 추가 api
|
||||||
@ -430,7 +408,7 @@
|
|||||||
param.append('day', day);
|
param.append('day', day);
|
||||||
|
|
||||||
await fetchEventList(param);
|
await fetchEventList(param);
|
||||||
useFilterEventList(month, day);
|
useFilterEventList(month, year);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이벤트 선택 핸들러
|
// 이벤트 선택 핸들러
|
||||||
@ -439,13 +417,11 @@
|
|||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이벤트 추가 핸들러
|
|
||||||
const handleEventInsert = data => {
|
const handleEventInsert = data => {
|
||||||
insertEvent(data.date, data.code, data.title, data.place, data.time);
|
insertEvent(data.date, data.code, data.title, data.place, data.time);
|
||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이벤트 삭제 핸들러
|
|
||||||
const handleEventDelete = data => {
|
const handleEventDelete = data => {
|
||||||
toggleEvent(data.date, data.code, data.title);
|
toggleEvent(data.date, data.code, data.title);
|
||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
@ -518,13 +494,10 @@
|
|||||||
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
|
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
|
||||||
dateClick: handleDateClick,
|
dateClick: handleDateClick,
|
||||||
dayCellDidMount: arg => {
|
dayCellDidMount: arg => {
|
||||||
// 날씨 정보 업데이트
|
|
||||||
addWeatherInfo(arg);
|
|
||||||
const dateCell = arg.el;
|
const dateCell = arg.el;
|
||||||
|
|
||||||
// 마우스 홀드시 이벤트 모달
|
// 마우스 홀드시 이벤트 모달
|
||||||
dateCell.addEventListener('mousedown', e => {
|
dateCell.addEventListener('mousedown', e => {
|
||||||
if (!isSelectableDate(arg.date)) return; // 공휴일 제외
|
|
||||||
const date = $common.dateFormatter(arg.date, 'YMD');
|
const date = $common.dateFormatter(arg.date, 'YMD');
|
||||||
handleMouseDown(date, e);
|
handleMouseDown(date, e);
|
||||||
});
|
});
|
||||||
@ -551,53 +524,6 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 날짜 정보 업데이트
|
|
||||||
const addWeatherInfo = arg => {
|
|
||||||
const dateStr = $common.dateFormatter(arg.date, 'YMD');
|
|
||||||
// 해당 셀의 날짜와 일치하는 데이터
|
|
||||||
const theDayWeatherInfo = dailyWeatherList.value.find(weather => weather.date === dateStr);
|
|
||||||
const dayTopEl = arg.el.querySelector('.fc-daygrid-day-top');
|
|
||||||
const isWeatherInfoExist = dayTopEl.getElementsByClassName('weather-icon').length > 0; // 중복 방지
|
|
||||||
|
|
||||||
if (theDayWeatherInfo && !isWeatherInfoExist) {
|
|
||||||
let weatherIconUrl = `https://openweathermap.org/img/wn/${theDayWeatherInfo.icon}.png`;
|
|
||||||
if (theDayWeatherInfo.icon === '01d' || theDayWeatherInfo.icon === '01n') {
|
|
||||||
weatherIconUrl = '/img/icons/sunny-custom.png';
|
|
||||||
}
|
|
||||||
// 날씨 이미지 세팅
|
|
||||||
const weatherEl = document.createElement('img');
|
|
||||||
weatherEl.src = weatherIconUrl;
|
|
||||||
weatherEl.alt = theDayWeatherInfo.description;
|
|
||||||
weatherEl.className = 'weather-icon';
|
|
||||||
weatherEl.style.width = '28px';
|
|
||||||
weatherEl.style.height = '28px';
|
|
||||||
|
|
||||||
// 해당 셀에 이미지 넣기
|
|
||||||
dayTopEl.classList.add('align-items-center');
|
|
||||||
dayTopEl.prepend(weatherEl); // 이상하게 가장 앞에 넣어야 일자 뒤에 나옴 reverse 옵션이 있는듯
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 날씨 데이터 변경 감지하여 날씨 정보 업데이트
|
|
||||||
watch(dailyWeatherList, async () => {
|
|
||||||
await nextTick(); // DOM이 업데이트된 후 실행
|
|
||||||
document.querySelectorAll('.fc-daygrid-day').forEach(dayCell => {
|
|
||||||
addWeatherInfo({
|
|
||||||
el: dayCell,
|
|
||||||
date: dayCell.dataset.date,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleWheelEvent = e => {
|
|
||||||
handleCloseModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 선택한 날의 이벤트 중에 휴가자 항목을 누를때 휴가페이지 이동
|
|
||||||
const handleClickVacation = () => {
|
|
||||||
router.push('/vacation');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
||||||
watch(
|
watch(
|
||||||
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
|
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
|
||||||
@ -606,55 +532,6 @@
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// selectbox 프로젝트 선택 변경 사항 감지
|
|
||||||
watch(
|
|
||||||
() => projectStore.selectedProject,
|
|
||||||
newProject => {
|
|
||||||
if (newProject) {
|
|
||||||
selectedProject.value = newProject.PROJCTSEQ;
|
|
||||||
checkedInProject.value = newProject;
|
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
checkedInProject.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleLeaveTimeUpdate = async event => {
|
|
||||||
const memberSeq = user.value.id;
|
|
||||||
if (!memberSeq) return;
|
|
||||||
|
|
||||||
// 현 사용자 프로젝트 퇴근기록 확인
|
|
||||||
const { data } = await $api.post('main/getUserLeaveRecord', {
|
|
||||||
memberSeq: memberSeq,
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = data?.data;
|
|
||||||
if (res && !res?.COMMUTLVE) {
|
|
||||||
await projectStore.getMemberProjects();
|
|
||||||
|
|
||||||
if (projectStore.activeMemberProjectList.length > 0) {
|
|
||||||
const previousProject =
|
|
||||||
projectStore.activeMemberProjectList.find(p => res.MEMBERSEQ === user.value.id && res.PROJCTLVE === p.PROJCTSEQ) ||
|
|
||||||
projectStore.activeMemberProjectList[0]; // 이전 출근 프로젝트가 없으면 첫 번째 프로젝트
|
|
||||||
|
|
||||||
if (previousProject) {
|
|
||||||
selectedProject.value = previousProject.PROJCTSEQ;
|
|
||||||
projectStore.setSelectedProject(previousProject);
|
|
||||||
} else if (projectStore.activeProjectList.length > 0) {
|
|
||||||
selectedProject.value = projectStore.activeProjectList[0].PROJCTSEQ;
|
|
||||||
projectStore.setSelectedProject(projectStore.activeProjectList[0]);
|
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
projectStore.setSelectedProject(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedProject.value = null;
|
|
||||||
projectStore.setSelectedProject(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
user.value = userStore.user;
|
user.value = userStore.user;
|
||||||
@ -678,47 +555,31 @@
|
|||||||
await fetchEventList(param);
|
await fetchEventList(param);
|
||||||
useFilterEventList(month, day);
|
useFilterEventList(month, day);
|
||||||
|
|
||||||
// 스크롤 감지 이벤트 리스너
|
// 이벤트모달 외부 클릭 감지
|
||||||
window.addEventListener('wheel', handleWheelEvent);
|
// document.addEventListener('click', e => {
|
||||||
window.addEventListener('click', colorToday);
|
// if (showModal.value && !e.target.closest('.event-modal') && !e.target.closest('.fc-daygrid-day')) {
|
||||||
|
// showModal.value = false;
|
||||||
datePickerStore.initDatePicker(fullCalendarRef, async (year, month, options) => {
|
// }
|
||||||
// 데이터 다시 불러오기
|
// });
|
||||||
await fetchData();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style>
|
||||||
::v-deep(.fc-h-event) {
|
.fc-h-event {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.event-modal) {
|
.event-modal {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.event-icon-select:hover) {
|
.event-icon-select:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 이벤트 모달 노출 시 텍스트 선택 방지 */
|
/* 이벤트 모달 노출 시 텍스트 선택 방지 */
|
||||||
::v-deep(.fc-daygrid-day) {
|
.fc-daygrid-day {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.fc-daygrid-day-events) {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
/* align-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center !important; */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 공휴일만 가로로 넗게 나오게 */
|
|
||||||
::v-deep(.fc-daygrid-event-harness:has(.holiday-event)) {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,8 +10,6 @@
|
|||||||
(category.CMNCODVAL === 300205 && teaTimeList?.length) ||
|
(category.CMNCODVAL === 300205 && teaTimeList?.length) ||
|
||||||
(category.CMNCODVAL === 300206 && workShopList?.length)
|
(category.CMNCODVAL === 300206 && workShopList?.length)
|
||||||
"
|
"
|
||||||
@click="category.CMNCODVAL == 300202 ? $emit('handleClickVacation') : ''"
|
|
||||||
:class="category.CMNCODVAL == 300202 ? 'pointer' : ''"
|
|
||||||
class="border border-2 mt-3 card p-2"
|
class="border border-2 mt-3 card p-2"
|
||||||
>
|
>
|
||||||
<div class="row g-2 position-relative">
|
<div class="row g-2 position-relative">
|
||||||
@ -24,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 mx-0 px-0 d-flex align-items-center">
|
<div class="col-9 mx-0 px-0">
|
||||||
<template v-if="category.CMNCODVAL === 300201">
|
<template v-if="category.CMNCODVAL === 300201">
|
||||||
<MainMemberProfile :members="birthdayList" :baseUrl="baseUrl" />
|
<MainMemberProfile :members="birthdayList" :baseUrl="baseUrl" />
|
||||||
</template>
|
</template>
|
||||||
@ -32,16 +30,36 @@
|
|||||||
<MainMemberProfile :members="vacationList" :baseUrl="baseUrl" />
|
<MainMemberProfile :members="vacationList" :baseUrl="baseUrl" />
|
||||||
</template>
|
</template>
|
||||||
<template v-if="category.CMNCODVAL === 300203">
|
<template v-if="category.CMNCODVAL === 300203">
|
||||||
<MainEventBoard :place="birthdayPartyList[0].LOCEVTPLC" :time="birthdayPartyList[0].LOCEVTTME" />
|
<div>
|
||||||
|
{{ birthdayPartyList[0].LOCEVTPLC }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $common.dateFormatter(birthdayPartyList[0].LOCEVTTME) }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="category.CMNCODVAL === 300204">
|
<template v-if="category.CMNCODVAL === 300204">
|
||||||
<MainEventBoard :place="dinnerList[0].LOCEVTPLC" :time="dinnerList[0].LOCEVTTME" />
|
<div>
|
||||||
|
{{ dinnerList[0].LOCEVTPLC }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $common.dateFormatter(dinnerList[0].LOCEVTTME) }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="category.CMNCODVAL === 300205">
|
<template v-if="category.CMNCODVAL === 300205">
|
||||||
<MainEventBoard :place="teaTimeList[0].LOCEVTPLC" :time="teaTimeList[0].LOCEVTTME" />
|
<div>
|
||||||
|
{{ teaTimeList[0].LOCEVTPLC }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $common.dateFormatter(teaTimeList[0].LOCEVTTME) }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="category.CMNCODVAL === 300206">
|
<template v-if="category.CMNCODVAL === 300206">
|
||||||
<MainEventBoard :place="workShopList[0].LOCEVTPLC" :time="workShopList[0].LOCEVTTME" />
|
<div>
|
||||||
|
{{ workShopList[0].LOCEVTPLC }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $common.dateFormatter(workShopList[0].LOCEVTTME) }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +71,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits } from 'vue';
|
import { defineEmits } from 'vue';
|
||||||
import MainMemberProfile from '@c/main/MainMemberProfile.vue';
|
import MainMemberProfile from '@c/main/MainMemberProfile.vue';
|
||||||
import MainEventBoard from '@c/main/MainEventBoard.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
@ -102,36 +119,4 @@
|
|||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['handleClickVacation']);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.event-board {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
min-width: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
color: #333;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ms-2" style="flex: 1">
|
<div class="ms-2">
|
||||||
<ul class="row gx-1 mb-0 list-inline">
|
<ul class="row gx-1 mb-0 list-inline d-flex align-items-center">
|
||||||
<li class="col-4 me-0" v-for="(member, index) in members" :key="index">
|
<li class="col-4 me-0" v-for="(member, index) in members" :key="index">
|
||||||
<div class="ratio ratio-1x1 mb-0">
|
<div class="ratio ratio-1x1 mb-0 profile-list">
|
||||||
<img
|
<img
|
||||||
:src="`${profileImgUrl}profile/${member.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${member.MEMBERPRF}`"
|
||||||
:style="`border-color: ${member.usercolor} !important;`"
|
:style="`border-color: ${member.usercolor} !important;`"
|
||||||
alt="User Profile"
|
alt="User Profile"
|
||||||
class="rounded-circle border border-2 profile-img"
|
class="rounded-circle border border-2"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -16,8 +16,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script>
|
||||||
const props = defineProps({
|
export default {
|
||||||
|
props: {
|
||||||
members: {
|
members: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@ -26,7 +27,6 @@
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
};
|
||||||
const profileImgUrl = import.meta.env.VITE_SERVER_IMG_URL;
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,244 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
|
|
||||||
<div class="card h-100">
|
|
||||||
<!-- 더보기 버튼-->
|
|
||||||
<div class="d-flex">
|
|
||||||
<router-link
|
|
||||||
to="/voteboard"
|
|
||||||
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
|
|
||||||
>
|
|
||||||
more
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="card-header d-flex justify-content-between">
|
|
||||||
<div class="card-title mb-0">
|
|
||||||
<h5 class="mb-1 me-2">투표진행</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" v-if="voteListData.length > 0">
|
|
||||||
<ul class="p-0 m-0">
|
|
||||||
<li class="d-flex mb-1" v-for="item in voteListData" :key="item.LOCVOTSEQ">
|
|
||||||
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
|
|
||||||
<div class="me-2 mb-3">
|
|
||||||
<div class="text-muted small">{{ item.localVote.formatted_LOCVOTRDT }}</div>
|
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
|
||||||
<div class="avatar flex-shrink-0 me-1">
|
|
||||||
<img
|
|
||||||
style="cursor: auto;"
|
|
||||||
class="rounded-circle user-avatar object-fit-cover"
|
|
||||||
:src="getProfileImage(item.localVote.MEMBERPRF)"
|
|
||||||
alt="최초 작성자"
|
|
||||||
:style="{ borderColor: item.localVote.usercolor }"
|
|
||||||
@error="setDefaultImage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-event ps-1" style="cursor: pointer;" @click.stop="openModal(item.localVote.LOCVOTSEQ)" >
|
|
||||||
<div class="timeline-header ">
|
|
||||||
<small ><strong>{{ truncateTitle(item.localVote.LOCVOTTTL) }}</strong></small>
|
|
||||||
</div>
|
|
||||||
<small class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0"
|
|
||||||
:style="{ color: getDaysAgo(item.localVote.formatted_LOCVOTEDT) == '금일 종료' ? 'red' : '' }">
|
|
||||||
⏰{{getDaysAgo(item.localVote.formatted_LOCVOTEDT)}}({{item.localVote.total_voted}}/{{ item.localVote.total_votable }})
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" v-else>
|
|
||||||
진행중인 투표가 없습니다.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!--투표 모달 -->
|
|
||||||
<CenterModal :display="isModalOpen" @close="closeModal">
|
|
||||||
<template #title> 투표 하기 </template>
|
|
||||||
<template #body>
|
|
||||||
<div>
|
|
||||||
<vote-list
|
|
||||||
:key="voteListKey"
|
|
||||||
:data="selectVoteDate"
|
|
||||||
@checkedNames="checkedNames"
|
|
||||||
@addContents="addContents"
|
|
||||||
@endVoteId="endVoteId"
|
|
||||||
@voteDelete="voteDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<BackButton @click="closeModal" />
|
|
||||||
</template>
|
|
||||||
</CenterModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import router from '@/router';
|
|
||||||
import $api from '@api';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import CenterModal from '@c/modal/CenterModal.vue';
|
|
||||||
import BackButton from '@c/button/BackBtn.vue';
|
|
||||||
import voteList from '@c/voteboard/voteCardList.vue';
|
|
||||||
import { useToastStore } from '@s/toastStore';
|
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const voteset = ref(0);
|
|
||||||
const voteListData= ref([]);
|
|
||||||
const voteListKey = ref(0); //초기화
|
|
||||||
// 로그 모달 상태
|
|
||||||
const isModalOpen = ref(false);
|
|
||||||
const selectVoteDate = ref([]);
|
|
||||||
// 로그 모달 열기
|
|
||||||
const openModal = async (id) => {
|
|
||||||
isModalOpen.value = true;
|
|
||||||
if(id){
|
|
||||||
const selectData = voteListData.value.filter((item) => item.localVote.LOCVOTSEQ === id);
|
|
||||||
selectVoteDate.value = selectData;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 로그 모달 닫기
|
|
||||||
const closeModal = () => {
|
|
||||||
isModalOpen.value = false;
|
|
||||||
voteListKey.value++;
|
|
||||||
};
|
|
||||||
// 프로필 이미지
|
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
|
||||||
const defaultProfile = "/img/icons/icon.png";
|
|
||||||
const getProfileImage = (profilePath) => {
|
|
||||||
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
|
||||||
};
|
|
||||||
const setDefaultImage = (event) => {
|
|
||||||
event.target.src = defaultProfile;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getvoteList();
|
|
||||||
});
|
|
||||||
|
|
||||||
//투표목록
|
|
||||||
const getvoteList = () => {
|
|
||||||
$api.get('vote/getVoteList',{
|
|
||||||
params:
|
|
||||||
{
|
|
||||||
page: 1
|
|
||||||
,voteset:'2' //투표중
|
|
||||||
,myVote:'2' //내가 안한 투표
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
voteListData.value = res.data.data.list;
|
|
||||||
voteListData.value = res.data.data.list.slice(0, 6);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
//투표하기
|
|
||||||
const checkedNames = (numList) => {
|
|
||||||
$api.post('vote/insertCheckedNums',{
|
|
||||||
checkedList :numList
|
|
||||||
,votenum : numList[0].LOCVOTSEQ
|
|
||||||
}).then((res)=>{
|
|
||||||
if(res.data.status === 'OK'){
|
|
||||||
toastStore.onToast('투표가 완료되었습니다.', 's');
|
|
||||||
isModalOpen.value = false;
|
|
||||||
getvoteList();
|
|
||||||
voteListKey.value++;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
//투표항목추가
|
|
||||||
const addContents = (itemList, voteId) => {
|
|
||||||
$api.post('vote/insertWord',{
|
|
||||||
itemList :itemList
|
|
||||||
,voteId :voteId
|
|
||||||
}).then((res)=>{
|
|
||||||
if(res.data.status === 'OK'){
|
|
||||||
toastStore.onToast('항목이 등록되었습니다.', 's');
|
|
||||||
getvoteList();
|
|
||||||
const updatedVote = selectVoteDate.value.find(vote => vote.localVote.LOCVOTSEQ === voteId);
|
|
||||||
if (updatedVote) {
|
|
||||||
if (!updatedVote.voteDetails) {
|
|
||||||
updatedVote.voteDetails = [];
|
|
||||||
}
|
|
||||||
const maxSeq = updatedVote.voteDetails.reduce((max, item) => {
|
|
||||||
return item.VOTDETSEQ > max ? item.VOTDETSEQ : max;
|
|
||||||
}, 0);
|
|
||||||
// 새 항목을 voteDetails에 추가
|
|
||||||
itemList.forEach(item => {
|
|
||||||
updatedVote.voteDetails.push({
|
|
||||||
VOTDETSEQ: maxSeq + 1,
|
|
||||||
LOCVOTSEQ: voteId,
|
|
||||||
LOCVOTCON: item.content,
|
|
||||||
LOCVOTLIK: item.url,
|
|
||||||
VOTE_COUNT: 0,
|
|
||||||
yesvote: 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
//투표종료
|
|
||||||
const endVoteId = (endVoteId) => {
|
|
||||||
if(confirm('투표를 종료하시겠습니까?')){
|
|
||||||
$api.patch('vote/updateEndData',{
|
|
||||||
endVoteId :endVoteId
|
|
||||||
}).then((res)=>{
|
|
||||||
if(res.data.status === 'OK'){
|
|
||||||
getvoteList();
|
|
||||||
isModalOpen.value = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//투표 삭제
|
|
||||||
const voteDelete =(id) =>{
|
|
||||||
if(confirm('투표를 삭제하시겠습니까?')){
|
|
||||||
$api.patch('vote/updateDeleteData',{
|
|
||||||
deleteVoteId :id
|
|
||||||
}).then((res)=>{
|
|
||||||
if(res.data.status === 'OK'){
|
|
||||||
toastStore.onToast('투표가 삭제되었습니다.', 's');
|
|
||||||
getvoteList();
|
|
||||||
isModalOpen.value = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
|
||||||
const truncateTitle = title => {
|
|
||||||
return title.length > 10 ? title.slice(0, 10) + '...' : title;
|
|
||||||
};
|
|
||||||
|
|
||||||
//투표이동
|
|
||||||
const goVoteList = () =>{
|
|
||||||
router.push({
|
|
||||||
path: '/voteboard',
|
|
||||||
query: {
|
|
||||||
voteset: '2' //투표중
|
|
||||||
,myVote:'2' //내가 안한 투표
|
|
||||||
,id:id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//종료 ㅇㅇ 일 전
|
|
||||||
const getDaysAgo = (dateString) => {
|
|
||||||
const inputDate = new Date(dateString); // 문자열을 Date 객체로 변환
|
|
||||||
const today = new Date(); // 현재 날짜 가져오기
|
|
||||||
const input = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
|
|
||||||
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
||||||
const timeDiff = now - input;
|
|
||||||
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
|
||||||
// 오늘 날짜인 경우
|
|
||||||
if (dayDiff === 0) return "금일 종료";
|
|
||||||
|
|
||||||
return `종료 ${Math.abs(dayDiff)}일 전`;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.user-avatar {
|
|
||||||
border: 3px solid;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-6">
|
|
||||||
<div class="card h-100">
|
|
||||||
<!-- 더보기 버튼 -->
|
|
||||||
<div class="d-flex ">
|
|
||||||
<router-link
|
|
||||||
to="/wordDict"
|
|
||||||
class="btn btn-primary mr-1 pe-1 ps-1 ms-auto my-auto h-50"
|
|
||||||
>
|
|
||||||
more
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="card-header d-flex justify-content-between">
|
|
||||||
<div class="card-title mb-0">
|
|
||||||
<h5 class="mb-1 me-2">용어집</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" v-if="wordList.length > 0">
|
|
||||||
<ul class="p-0 m-0" v-for="item in wordList" :key="item.WRDDICSEQ">
|
|
||||||
<li class="d-flex align-items-center mb-1">
|
|
||||||
<!-- 프로필 이미지 -->
|
|
||||||
<div class="avatar flex-shrink-0 me-2 d-flex align-items-center">
|
|
||||||
<img
|
|
||||||
class="rounded-circle user-avatar object-fit-cover"
|
|
||||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
|
||||||
alt="최종 작성자"
|
|
||||||
:style="{ borderColor: item.lastEditor.color }"
|
|
||||||
@error="setDefaultImage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 텍스트 영역 -->
|
|
||||||
<div class="timeline-event ps-1 flex-grow-1" style="cursor: pointer;" @click="goWordList(item.WRDDICCAT,item.WRDDICTTL)">
|
|
||||||
<div class="timeline-header">
|
|
||||||
<small class="text-primary text-uppercase">{{ item.category }}</small>
|
|
||||||
</div>
|
|
||||||
<h6 class="my-50 d-flex align-items-center">
|
|
||||||
{{ truncateTitle(item.WRDDICTTL) }}
|
|
||||||
</h6>
|
|
||||||
<div class="text-muted small">{{$common.dateFormatter(item.lastEditor.updatedAt)}}</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" v-else >
|
|
||||||
등록된 용어가 없습니다.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import axios from '@api';
|
|
||||||
import { getCurrentInstance, onMounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import commonApi from '@/common/commonApi';
|
|
||||||
|
|
||||||
const { appContext } = getCurrentInstance();
|
|
||||||
const $common = appContext.config.globalProperties.$common;
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getwordList();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 프로필 이미지
|
|
||||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, '');
|
|
||||||
const defaultProfile = "/img/icons/icon.png";
|
|
||||||
const getProfileImage = (profilePath) => {
|
|
||||||
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
|
||||||
};
|
|
||||||
const setDefaultImage = (event) => {
|
|
||||||
event.target.src = defaultProfile;
|
|
||||||
};
|
|
||||||
const wordList = ref([]);
|
|
||||||
//용어 목록
|
|
||||||
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
|
|
||||||
axios.get('worddict/getWordList',{
|
|
||||||
params: {
|
|
||||||
searchKeyword : searchKeyword,
|
|
||||||
indexKeyword :indexKeyword,
|
|
||||||
category : category,
|
|
||||||
pageNum:6
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
wordList.value = res.data.data.data;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
//초성 /알파벳 변환
|
|
||||||
const getFirstCharacter = (char) => {
|
|
||||||
const CHOSUNG_LIST = [
|
|
||||||
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
|
|
||||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!char || char.length === 0) return '';
|
|
||||||
|
|
||||||
const code = char.charCodeAt(0);
|
|
||||||
|
|
||||||
// 한글 범위 (가~힣) → 초성 변환
|
|
||||||
if (code >= 0xAC00 && code <= 0xD7A3) {
|
|
||||||
const index = Math.floor((code - 0xAC00) / (21 * 28));
|
|
||||||
return CHOSUNG_LIST[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 영어 소문자 → 대문자로 변환
|
|
||||||
if (char.match(/[a-zA-Z]/)) {
|
|
||||||
return char.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기타 문자 (숫자, 특수문자) 그대로 반환
|
|
||||||
return char;
|
|
||||||
};
|
|
||||||
//용어집 이동
|
|
||||||
const goWordList = (category, indexKeyword) => {
|
|
||||||
const firstChar = getFirstCharacter(indexKeyword[0]); // 첫 글자 변환
|
|
||||||
router.push({
|
|
||||||
path: '/wordDict',
|
|
||||||
query: {
|
|
||||||
indexKeyword: firstChar,
|
|
||||||
category: category,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
|
||||||
const truncateTitle = title => {
|
|
||||||
return title.length > 25 ? title.slice(0, 25) + '...' : title;
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.user-avatar {
|
|
||||||
border: 3px solid;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -13,27 +13,19 @@
|
|||||||
<img
|
<img
|
||||||
:src="`${imgURL}profile/${member.MEMBERPRF}`"
|
:src="`${imgURL}profile/${member.MEMBERPRF}`"
|
||||||
alt="Profile Image"
|
alt="Profile Image"
|
||||||
class="img-thumbnail mx-auto d-block"
|
class="img-fluid mx-auto d-block"
|
||||||
style="max-height: 140px"
|
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
|
<!-- 날짜 -->
|
||||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<div class="font-bold">{{ member.MEMBERNAM }}</div>
|
<div class="">{{ member.MEMBERNAM }}</div>
|
||||||
<div class="ms-2">
|
|
||||||
({{ member.MBTI }})
|
|
||||||
<img
|
|
||||||
role="img"
|
|
||||||
class="w-px-30 h-px-40"
|
|
||||||
:src="`/img/mbti/${member.MBTI.toLowerCase()}.png`"
|
|
||||||
alt="MBTI image"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 참여자 -->
|
||||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<i class="bx bx-id-card"></i>
|
<i class="bx bxs-envelope"></i>
|
||||||
<div class="ms-2">{{ member.MEMBERIDS }}</div>
|
<div class="ms-2">{{ member.MEMBERIDS }}@local-host.co.kr</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<i class="bx bxs-phone"></i>
|
<i class="bx bxs-phone"></i>
|
||||||
@ -41,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||||
<i class="bx bx-calendar"></i>
|
<i class="bx bx-calendar"></i>
|
||||||
<div class="ms-2">{{ $common.dateFormatter(member.MEMBERCDT) }}</div>
|
<div class="ms-2">{{ $common.dateFormatter(member.MEMBERRDT) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 d-flex align-items-center">
|
<div class="col-2 d-flex align-items-center">
|
||||||
@ -50,15 +42,14 @@
|
|||||||
<label class="switch"
|
<label class="switch"
|
||||||
><input
|
><input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="member.checked"
|
:checked="checked"
|
||||||
@click="handleRegisterMember($event, member)" />
|
@change="handleRegisterMember(member.MEMBERSEQ)" /><span class="slider round"></span
|
||||||
<span class="slider round"></span
|
|
||||||
></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-close btn-close-sm"
|
class="btn-close btn-close-sm"
|
||||||
style="position: absolute; top: 10px; right: 10px"
|
style="position: absolute; top: 10px; right: 10px"
|
||||||
@click="handleRejectMember(member)"
|
@click="handleRejectMember(member.MEMBERSEQ)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,37 +68,30 @@
|
|||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
|
||||||
const memberList = ref([]);
|
const memberList = ref([]);
|
||||||
|
const checked = ref(false);
|
||||||
const toast = useToastStore();
|
const toast = useToastStore();
|
||||||
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
|
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
|
||||||
|
|
||||||
// 조회 api
|
// 조회 api
|
||||||
const fetchRegisterMemberList = async () => {
|
const fetchRegisterMemberList = async () => {
|
||||||
const { data } = await $api.get('main/registerMemberList');
|
const { data } = await $api.get('main/registerMemberList');
|
||||||
if (data?.data) {
|
if (data?.data) memberList.value = data.data;
|
||||||
memberList.value = data.data.map(member => ({
|
|
||||||
...member,
|
|
||||||
checked: false, // 각 항목에 checked 값 설정
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 사원 등록 api
|
// 사원 등록 api
|
||||||
const handleRegisterMember = async (e, member) => {
|
const handleRegisterMember = async memberSeq => {
|
||||||
e.preventDefault();
|
const { data } = await $api.post('main/registerMember', { memberSeq: memberSeq });
|
||||||
|
|
||||||
const { data } = await $api.post('main/registerMember', { memberSeq: member.MEMBERSEQ });
|
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
member.checked = true;
|
|
||||||
toast.onToast(data.data, 's');
|
toast.onToast(data.data, 's');
|
||||||
fetchRegisterMemberList();
|
fetchRegisterMemberList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 사원 등록 미승인 api
|
// 사원 등록 미승인 api
|
||||||
const handleRejectMember = async member => {
|
const handleRejectMember = async memberSeq => {
|
||||||
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
|
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
|
||||||
|
|
||||||
const { data } = await $api.post('main/rejectMember', { memberSeq: member.MEMBERSEQ });
|
const { data } = await $api.post('main/rejectMember', { memberSeq: memberSeq });
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
toast.onToast(data.data, 's');
|
toast.onToast(data.data, 's');
|
||||||
fetchRegisterMemberList();
|
fetchRegisterMemberList();
|
||||||
|
|||||||
@ -56,7 +56,6 @@
|
|||||||
:value="color"
|
:value="color"
|
||||||
:data="colorList"
|
:data="colorList"
|
||||||
@update:data="color = $event"
|
@update:data="color = $event"
|
||||||
:is-alert="colorAlert"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mb-2 row">
|
<div class="mb-2 row">
|
||||||
@ -167,11 +166,9 @@
|
|||||||
const endDay = ref('');
|
const endDay = ref('');
|
||||||
const description = ref('');
|
const description = ref('');
|
||||||
const nameAlert = ref(false);
|
const nameAlert = ref(false);
|
||||||
const colorAlert = ref(false);
|
|
||||||
const addressAlert = ref(false);
|
const addressAlert = ref(false);
|
||||||
const startDayAlert = ref(false);
|
const startDayAlert = ref(false);
|
||||||
|
|
||||||
|
|
||||||
const startDateInput = ref(null);
|
const startDateInput = ref(null);
|
||||||
const endDateInput = ref(null);
|
const endDateInput = ref(null);
|
||||||
|
|
||||||
@ -260,9 +257,7 @@
|
|||||||
const formReset = () => {
|
const formReset = () => {
|
||||||
|
|
||||||
name.value = '';
|
name.value = '';
|
||||||
if (colorList.value && colorList.value.length > 0) {
|
|
||||||
color.value = colorList.value[0].value;
|
color.value = colorList.value[0].value;
|
||||||
}
|
|
||||||
addressData.value = {
|
addressData.value = {
|
||||||
postcode: '',
|
postcode: '',
|
||||||
address: '',
|
address: '',
|
||||||
@ -322,11 +317,7 @@
|
|||||||
startDayAlert.value = startDay.value.trim() === '';
|
startDayAlert.value = startDay.value.trim() === '';
|
||||||
addressAlert.value = addressData.value.address.trim() === '';
|
addressAlert.value = addressData.value.address.trim() === '';
|
||||||
|
|
||||||
if (!colorList.value || colorList.value.length === 0) {
|
if (nameAlert.value || startDayAlert.value || addressAlert.value) {
|
||||||
colorAlert.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nameAlert.value || startDayAlert.value || addressAlert.value || colorAlert.value) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
for="profilePic"
|
for="profilePic"
|
||||||
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
||||||
id="profileLabel"
|
id="profileLabel"
|
||||||
style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat; background-size: cover;"
|
style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@ -25,7 +25,6 @@
|
|||||||
@update:alert="idAlert = $event"
|
@update:alert="idAlert = $event"
|
||||||
@blur="checkIdDuplicate"
|
@blur="checkIdDuplicate"
|
||||||
:value="id"
|
:value="id"
|
||||||
@keypress="noSpace"
|
|
||||||
/>
|
/>
|
||||||
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
||||||
|
|
||||||
@ -38,7 +37,6 @@
|
|||||||
@update:data="password = $event"
|
@update:data="password = $event"
|
||||||
@update:alert="passwordAlert = $event"
|
@update:alert="passwordAlert = $event"
|
||||||
:value="password"
|
:value="password"
|
||||||
@keypress="noSpace"
|
|
||||||
/>
|
/>
|
||||||
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
||||||
|
|
||||||
@ -51,7 +49,6 @@
|
|||||||
@update:data="passwordcheck = $event"
|
@update:data="passwordcheck = $event"
|
||||||
@update:alert="passwordcheckAlert = $event"
|
@update:alert="passwordcheckAlert = $event"
|
||||||
:value="passwordcheck"
|
:value="passwordcheck"
|
||||||
@keypress="noSpace"
|
|
||||||
/>
|
/>
|
||||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||||
|
|
||||||
@ -85,7 +82,6 @@
|
|||||||
@update:data="name = $event"
|
@update:data="name = $event"
|
||||||
@update:alert="nameAlert = $event"
|
@update:alert="nameAlert = $event"
|
||||||
:value="name"
|
:value="name"
|
||||||
@keypress="noSpace"
|
|
||||||
class="me-2 w-50"
|
class="me-2 w-50"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -99,7 +95,6 @@
|
|||||||
:is-color="true"
|
:is-color="true"
|
||||||
:data="colorList"
|
:data="colorList"
|
||||||
@update:data="handleColorUpdate"
|
@update:data="handleColorUpdate"
|
||||||
:is-alert="colorAlert"
|
|
||||||
class="w-50"
|
class="w-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -148,12 +143,13 @@
|
|||||||
name="phone"
|
name="phone"
|
||||||
:isEssential="true"
|
:isEssential="true"
|
||||||
:is-alert="phoneAlert"
|
:is-alert="phoneAlert"
|
||||||
|
@update:data="phone = $event"
|
||||||
@update:alert="phoneAlert = $event"
|
@update:alert="phoneAlert = $event"
|
||||||
@blur="checkPhoneDuplicate"
|
@blur="checkPhoneDuplicate"
|
||||||
:maxlength="11"
|
:maxlength="11"
|
||||||
:value="phone"
|
:value="phone"
|
||||||
@keypress="onlyNumber"
|
@keypress="onlyNumber"
|
||||||
@input="inputEvent"
|
|
||||||
/>
|
/>
|
||||||
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
|
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
|
||||||
|
|
||||||
@ -209,7 +205,6 @@
|
|||||||
const passwordcheckErrorAlert = ref(false); // 비밀번호 확인 오류 메시지
|
const passwordcheckErrorAlert = ref(false); // 비밀번호 확인 오류 메시지
|
||||||
const pwhintResAlert = ref(false);
|
const pwhintResAlert = ref(false);
|
||||||
const nameAlert = ref(false);
|
const nameAlert = ref(false);
|
||||||
const colorAlert = ref(false);
|
|
||||||
const birthAlert = ref(false);
|
const birthAlert = ref(false);
|
||||||
const addressAlert = ref(false);
|
const addressAlert = ref(false);
|
||||||
const phoneAlert = ref(false);
|
const phoneAlert = ref(false);
|
||||||
@ -218,10 +213,6 @@
|
|||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
const noSpace = (e) => {
|
|
||||||
if (e.key === ' ') e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 프로필 체크
|
// 프로필 체크
|
||||||
const profileValid = (size, type) => {
|
const profileValid = (size, type) => {
|
||||||
const maxSize = 5 * 1024 * 1024;
|
const maxSize = 5 * 1024 * 1024;
|
||||||
@ -335,12 +326,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputEvent = (e) => {
|
|
||||||
const newValue = e.target.value.replace(/\D/g, ''); // 숫자만 남김
|
|
||||||
e.target.value = newValue; // 입력 필드 즉시 반영
|
|
||||||
phone.value = newValue; // Vue 반응형 상태 업데이트
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(id, (newValue) => {
|
watch(id, (newValue) => {
|
||||||
if (newValue && newValue.length >= 4) {
|
if (newValue && newValue.length >= 4) {
|
||||||
idError.value = '';
|
idError.value = '';
|
||||||
@ -352,7 +337,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(password, (newValue) => {
|
watch(password, (newValue) => {
|
||||||
|
|
||||||
if (newValue && newValue.length >= 4) {
|
if (newValue && newValue.length >= 4) {
|
||||||
passwordErrorAlert.value = false;
|
passwordErrorAlert.value = false;
|
||||||
passwordError.value = '';
|
passwordError.value = '';
|
||||||
@ -375,6 +359,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 회원가입
|
// 회원가입
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await checkColorDuplicate();
|
await checkColorDuplicate();
|
||||||
@ -388,10 +373,6 @@
|
|||||||
addressAlert.value = address.value.trim() === '';
|
addressAlert.value = address.value.trim() === '';
|
||||||
phoneAlert.value = phone.value.trim() === '';
|
phoneAlert.value = phone.value.trim() === '';
|
||||||
|
|
||||||
if (!colorList.value || colorList.value.length === 0) {
|
|
||||||
colorAlert.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 아이디 길이 체크
|
// 아이디 길이 체크
|
||||||
if (id.value && id.value.length < 4) {
|
if (id.value && id.value.length < 4) {
|
||||||
idErrorAlert.value = true;
|
idErrorAlert.value = true;
|
||||||
@ -405,10 +386,8 @@
|
|||||||
} else {
|
} else {
|
||||||
passwordError.value = '';
|
passwordError.value = '';
|
||||||
}
|
}
|
||||||
const phoneRegex = /^010\d{8}$/;
|
|
||||||
const isFormatValid = phoneRegex.test(phone.value);
|
|
||||||
|
|
||||||
if (!/^\d+$/.test(phone.value) || !isFormatValid) {
|
if (!/^\d+$/.test(phone.value)) {
|
||||||
phoneAlert.value = true;
|
phoneAlert.value = true;
|
||||||
} else {
|
} else {
|
||||||
phoneAlert.value = false;
|
phoneAlert.value = false;
|
||||||
@ -423,7 +402,6 @@
|
|||||||
profilAlert.value = false;
|
profilAlert.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
profilAlert.value ||
|
profilAlert.value ||
|
||||||
idAlert.value ||
|
idAlert.value ||
|
||||||
@ -438,20 +416,19 @@
|
|||||||
addressAlert.value ||
|
addressAlert.value ||
|
||||||
phoneAlert.value ||
|
phoneAlert.value ||
|
||||||
phoneErrorAlert.value ||
|
phoneErrorAlert.value ||
|
||||||
colorAlert.value ||
|
|
||||||
colorErrorAlert.value
|
colorErrorAlert.value
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('memberIds', id.value.trim());
|
formData.append('memberIds', id.value);
|
||||||
formData.append('memberPwd', password.value.trim());
|
formData.append('memberPwd', password.value);
|
||||||
formData.append('memberPwh', pwhint.value);
|
formData.append('memberPwh', pwhint.value);
|
||||||
formData.append('memberPwr', pwhintRes.value.trim());
|
formData.append('memberPwr', pwhintRes.value);
|
||||||
formData.append('memberNam', name.value.trim());
|
formData.append('memberNam', name.value);
|
||||||
formData.append('memberArr', address.value);
|
formData.append('memberArr', address.value);
|
||||||
formData.append('memberDtl', detailAddress.value.trim());
|
formData.append('memberDtl', detailAddress.value);
|
||||||
formData.append('memberZip', postcode.value);
|
formData.append('memberZip', postcode.value);
|
||||||
formData.append('memberBth', birth.value);
|
formData.append('memberBth', birth.value);
|
||||||
formData.append('memberTel', phone.value);
|
formData.append('memberTel', phone.value);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul v-if="displayedUserList && displayedUserList.length > 0" class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
|
<ul class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
|
||||||
<li
|
<li
|
||||||
v-for="(user, index) in displayedUserList"
|
v-for="(user, index) in displayedUserList"
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -14,7 +14,7 @@
|
|||||||
:data-bs-original-title="getTooltipTitle(user)"
|
:data-bs-original-title="getTooltipTitle(user)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="user-avatar border border-3 rounded-circle object-fit-cover"
|
class="rounded-circle user-avatar border border-3"
|
||||||
:class="{ 'grayscaleImg': isUserDisabled(user) }"
|
:class="{ 'grayscaleImg': isUserDisabled(user) }"
|
||||||
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
||||||
:style="`border-color: ${user.usercolor} !important;`"
|
:style="`border-color: ${user.usercolor} !important;`"
|
||||||
@ -23,12 +23,12 @@
|
|||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<span v-else >-</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, nextTick, computed, watch } from 'vue';
|
import { onMounted, ref, nextTick, computed, watch } from 'vue';
|
||||||
import { useUserStore } from '@s/userList';
|
import { useUserStore } from '@s/userList';
|
||||||
|
import { useProjectStore } from '@s/useProjectStore';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
import { useToastStore } from "@s/toastStore";
|
import { useToastStore } from "@s/toastStore";
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
class="start-50 translate-middle crown-icon"
|
class="start-50 translate-middle crown-icon"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
class="rounded-circle object-fit-cover"
|
class="rounded-circle profile-img"
|
||||||
:src="getUserProfileImage(user.MEMBERPRF)"
|
:src="getUserProfileImage(user.MEMBERPRF)"
|
||||||
alt="user"
|
alt="user"
|
||||||
:style="getDynamicStyle(user)"
|
:style="getDynamicStyle(user)"
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<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">
|
||||||
<img
|
<img
|
||||||
class="object-fit-cover rounded-circle user-avatar border border-3 w-px-40 h-px-40"
|
class="rounded-circle user-avatar border border-3 w-px-40 h-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'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
<h5 class="mb-0">{{ data.localVote.LOCVOTTTL }}
|
<h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}
|
||||||
<i v-if="yesVotetotal != '0'" class="bx bxs-check-circle link-success"></i>
|
<i v-if="yesVotetotal != '0'" class="bx bxs-check-circle link-success"></i>
|
||||||
</h5>
|
</h5>
|
||||||
<small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
|
<small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="d-flex flex-wrap align-items-center">
|
||||||
<ul class="list-unstyled users-list d-flex align-items-center avatar-group">
|
<ul class="list-unstyled users-list d-flex align-items-center avatar-group m-0 me-2">
|
||||||
<vote-complete-user-list-card :data="data"/>
|
<vote-complete-user-list-card :data="data"/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
:aria-label="data.MEMBERSEQ"
|
:aria-label="data.MEMBERSEQ"
|
||||||
:data-bs-original-title="getTooltipTitle(data)">
|
:data-bs-original-title="getTooltipTitle(data)">
|
||||||
<img
|
<img
|
||||||
class="rounded-circle user-avatar border border-3 object-fit-cover"
|
class="rounded-circle user-avatar border border-3"
|
||||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||||
:style="`border-color: ${data.usercolor} !important; width: 90%; height: 90%;`"
|
:style="`border-color: ${data.usercolor} !important;`"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="d-flex flex-wrap align-items-center">
|
||||||
<ul class="list-unstyled users-list d-flex align-items-center avatar-group ">
|
<ul class="list-unstyled users-list d-flex align-items-center avatar-group m-0 me-2">
|
||||||
<vote-in-complete-user-list-card :data="data" />
|
<vote-in-complete-user-list-card :data="data" />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
:aria-label="data.MEMBERSEQ"
|
:aria-label="data.MEMBERSEQ"
|
||||||
:data-bs-original-title="getTooltipTitle(data)">
|
:data-bs-original-title="getTooltipTitle(data)">
|
||||||
<img
|
<img
|
||||||
class="rounded-circle user-avatar border border-3 object-fit-cover"
|
class="rounded-circle user-avatar border border-3"
|
||||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||||
:style="`border-color: ${data.usercolor} !important; width: 90%; height: 90%;`"
|
:style="`border-color: ${data.usercolor} !important;`"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="d-flex align-items-start mt-3">
|
<div class="d-flex align-items-start mt-3">
|
||||||
<!--투표한 사람 목록 -->
|
<!--투표한 사람 목록 -->
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<i class='bx bxs-user-check link-info fa-2x'></i>
|
<i class='bx bxs-user-check link-info fa-3x'></i>
|
||||||
<vote-complete-user-list
|
<vote-complete-user-list
|
||||||
v-for="(item, index) in voetedUsers"
|
v-for="(item, index) in voetedUsers"
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 투표안한 사람 목록 -->
|
<!-- 투표안한 사람 목록 -->
|
||||||
<div class="d-flex align-items-center gap-2 ms-auto flex-wrap">
|
<div class="d-flex align-items-center gap-2 ms-auto flex-wrap">
|
||||||
<i class='bx bxs-user-x link-danger fa-2x'></i>
|
<i class='bx bxs-user-x link-danger fa-3x'></i>
|
||||||
<vote-in-complete-user-list
|
<vote-in-complete-user-list
|
||||||
v-for="(item, index) in noVoetedUsers"
|
v-for="(item, index) in noVoetedUsers"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="card p-4 mb-2">
|
<li class="card p-5 mb-2">
|
||||||
<DictWrite
|
<DictWrite
|
||||||
v-if="writeStore.isItemActive(item.WRDDICSEQ)"
|
v-if="writeStore.isItemActive(item.WRDDICSEQ)"
|
||||||
@close="writeStore.closeAll();"
|
@close="writeStore.closeAll();"
|
||||||
@ -16,9 +16,8 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="btn btn-primary pe-none m-2"
|
<span class="btn btn-primary pe-none">{{ item.category }}</span>
|
||||||
style="writing-mode: horizontal-tb;">{{ item.category }}</span>
|
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
|
||||||
{{ item.WRDDICTTL }}
|
|
||||||
</div>
|
</div>
|
||||||
<EditBtn
|
<EditBtn
|
||||||
@click="toggleEdit"
|
@click="toggleEdit"
|
||||||
@ -32,7 +31,7 @@
|
|||||||
<div class="d-flex flex-wrap align-items-center me-4">
|
<div class="d-flex flex-wrap align-items-center me-4">
|
||||||
<div class="avatar me-2">
|
<div class="avatar me-2">
|
||||||
<img
|
<img
|
||||||
class="rounded-circle user-avatar object-fit-cover"
|
class="rounded-circle user-avatar"
|
||||||
:src="getProfileImage(item.author.profileImage)"
|
:src="getProfileImage(item.author.profileImage)"
|
||||||
alt="최초 작성자"
|
alt="최초 작성자"
|
||||||
:style="{ borderColor: item.author.color }"
|
:style="{ borderColor: item.author.color }"
|
||||||
@ -51,7 +50,7 @@
|
|||||||
>
|
>
|
||||||
<div class="avatar me-2">
|
<div class="avatar me-2">
|
||||||
<img
|
<img
|
||||||
class="rounded-circle user-avatar object-fit-cover"
|
class="rounded-circle user-avatar"
|
||||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||||
alt="최근 작성자"
|
alt="최근 작성자"
|
||||||
:style="{ borderColor: item.lastEditor.color }"
|
:style="{ borderColor: item.lastEditor.color }"
|
||||||
@ -164,6 +163,7 @@ const toggleEdit = async () => {
|
|||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
|
padding: 0.1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn {
|
.edit-btn {
|
||||||
@ -178,21 +178,4 @@ const toggleEdit = async () => {
|
|||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
--bs-form-check-bg: #fff;
|
--bs-form-check-bg: #fff;
|
||||||
}
|
}
|
||||||
.btn.btn-primary {
|
|
||||||
writing-mode: horizontal-tb;
|
|
||||||
}
|
|
||||||
.dict-content-wrap {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: normal;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.dict-content-wrap * {
|
|
||||||
max-width: 100% !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: normal !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
@keyup="ValidHandler('title')"
|
@keyup="ValidHandler('title')"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<QEditor class="q-editor-container" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
<QEditor class="" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @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" :disabled="titleValue ? !changed : false">
|
<button class="btn btn-primary" @click="saveWord">
|
||||||
<i class="bx bx-check"></i>
|
<i class="bx bx-check"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -67,6 +67,39 @@ import FormSelect from '@/components/input/FormSelect.vue';
|
|||||||
import PlusBtn from '../button/PlusBtn.vue';
|
import PlusBtn from '../button/PlusBtn.vue';
|
||||||
import EditBtn from '../button/EditBtn.vue';
|
import EditBtn from '../button/EditBtn.vue';
|
||||||
import { useWriteVisibleStore } from '@s/writeVisible';
|
import { useWriteVisibleStore } from '@s/writeVisible';
|
||||||
|
|
||||||
|
const writeStore = useWriteVisibleStore();
|
||||||
|
|
||||||
|
const emit = defineEmits(['close','addCategory','addWord', 'toggleEdit']);
|
||||||
|
|
||||||
|
//용어제목
|
||||||
|
const wordTitle = ref('');
|
||||||
|
const addCategory = ref('');
|
||||||
|
const content = ref('');
|
||||||
|
const imageUrls = ref([]);
|
||||||
|
|
||||||
|
//용어 Vaildation용
|
||||||
|
const wordTitleAlert = ref(false);
|
||||||
|
const wordContentAlert = ref(false);
|
||||||
|
const addCategoryAlert = ref(false);
|
||||||
|
|
||||||
|
//선택 카테고리
|
||||||
|
const selectCategory = ref('');
|
||||||
|
|
||||||
|
// 제목 상태
|
||||||
|
const computedTitle = computed(() =>
|
||||||
|
wordTitle.value === '' ? props.titleValue : wordTitle.value
|
||||||
|
);
|
||||||
|
|
||||||
|
// 카테고리 상태
|
||||||
|
const selectedCategory = computed(() =>
|
||||||
|
selectCategory.value === '' ? props.formValue : selectCategory.value
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
// 카테고리 입력 중복 ref
|
||||||
|
const categoryInputRef = ref(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dataList: {
|
dataList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -93,38 +126,6 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const writeStore = useWriteVisibleStore();
|
|
||||||
|
|
||||||
const emit = defineEmits(['close','addCategory','addWord', 'toggleEdit']);
|
|
||||||
|
|
||||||
//용어제목
|
|
||||||
const wordTitle = ref('');
|
|
||||||
const addCategory = ref('');
|
|
||||||
const content = ref('');
|
|
||||||
const imageUrls = ref([]);
|
|
||||||
//용어 Vaildation용
|
|
||||||
const wordTitleAlert = ref(false);
|
|
||||||
const wordContentAlert = ref(false);
|
|
||||||
const addCategoryAlert = ref(false);
|
|
||||||
const changed = ref(false);
|
|
||||||
//선택 카테고리
|
|
||||||
const selectCategory = ref('');
|
|
||||||
|
|
||||||
// 제목 상태
|
|
||||||
const computedTitle = computed(() =>
|
|
||||||
wordTitle.value === '' ? props.titleValue : wordTitle.value
|
|
||||||
);
|
|
||||||
|
|
||||||
// 카테고리 상태
|
|
||||||
const selectedCategory = computed(() =>
|
|
||||||
selectCategory.value === '' ? props.formValue : selectCategory.value
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
// 카테고리 입력 중복 ref
|
|
||||||
const categoryInputRef = ref(null);
|
|
||||||
|
|
||||||
|
|
||||||
// 카테고리 입력 창
|
// 카테고리 입력 창
|
||||||
const showInput = ref(false);
|
const showInput = ref(false);
|
||||||
|
|
||||||
@ -135,7 +136,6 @@ const toggleInput = () => {
|
|||||||
|
|
||||||
const onChange = (newValue) => {
|
const onChange = (newValue) => {
|
||||||
selectCategory.value = newValue.target.value;
|
selectCategory.value = newValue.target.value;
|
||||||
changed.value = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ValidHandler = (field) => {
|
const ValidHandler = (field) => {
|
||||||
@ -144,22 +144,11 @@ const ValidHandler = (field) => {
|
|||||||
}
|
}
|
||||||
if(field == 'content'){
|
if(field == 'content'){
|
||||||
wordContentAlert.value = false;
|
wordContentAlert.value = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
const handleContentUpdate = (newContent) => {
|
const handleContentUpdate = (newContent) => {
|
||||||
content.value = newContent;
|
content.value = newContent;
|
||||||
|
|
||||||
const oldContent = typeof props.contentValue === 'string'? JSON.parse(props.contentValue) : props.contentValue;
|
|
||||||
const newContentOps = newContent?.ops || [];
|
|
||||||
|
|
||||||
const oldContentJson = JSON.stringify(oldContent);
|
|
||||||
const newContentJson = JSON.stringify(newContentOps);
|
|
||||||
|
|
||||||
// 기존 데이터와 새 데이터가 다를 경우 changed = true;
|
|
||||||
changed.value = oldContentJson !== newContentJson;
|
|
||||||
|
|
||||||
ValidHandler("content"); // 유효성 검사 실행
|
ValidHandler("content"); // 유효성 검사 실행
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -232,11 +221,3 @@ const handleCategoryFocusout = (value) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.q-editor-container * {
|
|
||||||
max-width: 100% !important;
|
|
||||||
word-break: break-all !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
white-space: normal !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -80,12 +80,12 @@
|
|||||||
<div class="text-truncate">Authorization</div>
|
<div class="text-truncate">Authorization</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item" :class="$route.path.includes('/people') ? 'active' : ''">
|
<!-- <li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
||||||
<RouterLink class="menu-link" to="/people"> <i class="bi "></i>
|
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
||||||
<i class="menu-icon icon-base bi bi-people-fill"></i>
|
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||||
<div class="text-truncate">people</div>
|
<div class="text-truncate">Sample</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li> -->
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
<!-- / Menu -->
|
<!-- / Menu -->
|
||||||
@ -94,7 +94,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
|
||||||
|
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||||
|
|||||||
@ -6,22 +6,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 날씨 정보 영역 -->
|
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
||||||
<div class="navbar-nav align-items-center">
|
|
||||||
<div class="d-flex align-items-center weather-box">
|
|
||||||
<img v-if="weather.icon" :src="customIconUrl" :alt="weather.description" :class="customIconClass" />
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<span class="weather-desc">{{ weather.description }}</span>
|
|
||||||
<span class="weather-temp" v-if="weatherReady">
|
|
||||||
최저 {{ weather.tempMin }}° / 최고 {{ weather.tempMax }}°
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center ms-auto" id="navbar-collapse">
|
|
||||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||||
<select class="form-select py-1 cursor-pointer" id="name" v-model="selectedProject" @change="updateSelectedProject">
|
<select class="form-select py-1" id="name" v-model="selectedProject" @change="updateSelectedProject">
|
||||||
<!-- 내가 참여하고 있는 프로젝트 그룹 -->
|
<!-- 내가 참여하고 있는 프로젝트 그룹 -->
|
||||||
<option v-for="item in myActiveProjects" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
<option v-for="item in myActiveProjects" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
||||||
{{ item.PROJCTNAM }}
|
{{ item.PROJCTNAM }}
|
||||||
@ -33,7 +20,10 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<i class="cursor-pointer p-2"></i>
|
<!-- <button class="btn p-1" @click="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button> -->
|
||||||
|
<!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
|
||||||
|
|
||||||
|
<i class="bx bx-bell bx-md bx-log-out cursor-pointer p-3" @click="handleLogout"></i>
|
||||||
|
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
||||||
@ -46,26 +36,125 @@
|
|||||||
>
|
>
|
||||||
<span class="position-relative">
|
<span class="position-relative">
|
||||||
<i class="bx bx-bell bx-md"></i>
|
<i class="bx bx-bell bx-md"></i>
|
||||||
<!-- 알림이 있을 경우에만 뱃지를 표시 -->
|
<span class="badge rounded-pill bg-danger badge-dot badge-notifications border"></span>
|
||||||
<span
|
|
||||||
v-if="notificationCount > 0"
|
|
||||||
class="badge rounded-pill bg-danger badge-dot badge-notifications border"
|
|
||||||
></span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end p-0">
|
<ul class="dropdown-menu dropdown-menu-end p-0">
|
||||||
<li class="dropdown-notifications-list scrollable-container p-3">
|
<li class="dropdown-menu-header border-bottom">
|
||||||
<!-- 알림이 없으면 "알림이 없습니다." 메시지 표시 -->
|
<div class="dropdown-header d-flex align-items-center py-3">
|
||||||
<div v-if="notificationCount === 0">
|
<h6 class="mb-0 me-auto">Notification</h6>
|
||||||
알림이 없습니다.
|
<div class="d-flex align-items-center h6 mb-0">
|
||||||
|
<span class="badge bg-label-primary me-2">8 New</span>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="dropdown-notifications-all p-2"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
title="Mark all as read"
|
||||||
|
><i class="bx bx-envelope-open text-heading"></i
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-notifications-list scrollable-container">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item list-group-item-action dropdown-notifications-item">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<img src="/img/avatars/1.png" class="rounded-circle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="small mb-0">Congratulation Lettie 🎉</h6>
|
||||||
|
<small class="mb-1 d-block text-body">Won the monthly best seller gold badge</small>
|
||||||
|
<small class="text-muted">1h ago</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 dropdown-notifications-actions">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-read"
|
||||||
|
><span class="badge badge-dot"></span
|
||||||
|
></a>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-archive"
|
||||||
|
><span class="bx bx-x"></span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item list-group-item-action dropdown-notifications-item">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<span class="avatar-initial rounded-circle bg-label-danger">CF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="small mb-0">Charles Franklin</h6>
|
||||||
|
<small class="mb-1 d-block text-body">Accepted your connection</small>
|
||||||
|
<small class="text-muted">12hr ago</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 dropdown-notifications-actions">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-read"
|
||||||
|
><span class="badge badge-dot"></span
|
||||||
|
></a>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-archive"
|
||||||
|
><span class="bx bx-x"></span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<img src="/img/avatars/2.png" class="rounded-circle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="small mb-0">New Message ✉️</h6>
|
||||||
|
<small class="mb-1 d-block text-body">You have new message from Natalie</small>
|
||||||
|
<small class="text-muted">1h ago</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 dropdown-notifications-actions">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-read"
|
||||||
|
><span class="badge badge-dot"></span
|
||||||
|
></a>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-archive"
|
||||||
|
><span class="bx bx-x"></span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item list-group-item-action dropdown-notifications-item">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<span class="avatar-initial rounded-circle bg-label-success"
|
||||||
|
><i class="bx bx-cart"></i
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="small mb-0">Whoo! You have new order 🛒</h6>
|
||||||
|
<small class="mb-1 d-block text-body">ACME Inc. made new order $1,154</small>
|
||||||
|
<small class="text-muted">1 day ago</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 dropdown-notifications-actions">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-read"
|
||||||
|
><span class="badge badge-dot"></span
|
||||||
|
></a>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-notifications-archive"
|
||||||
|
><span class="bx bx-x"></span
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 알림이 있을 때 목록 렌더링-->
|
|
||||||
<div v-else>
|
|
||||||
<ul>
|
|
||||||
<li v-for="notification in notifications" :key="notification.id">
|
|
||||||
{{ notification.text }}
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="border-top">
|
||||||
|
<div class="d-grid p-4">
|
||||||
|
<a class="btn btn-primary btn-sm d-flex" href="javascript:void(0);">
|
||||||
|
<small class="align-middle">View all notifications</small>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -78,15 +167,33 @@
|
|||||||
v-if="user"
|
v-if="user"
|
||||||
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||||
alt="Profile Image"
|
alt="Profile Image"
|
||||||
class="w-px-40 h-px-40 rounded-circle border border-3 object-fit-cover"
|
class="w-px-40 h-px-40 rounded-circle border border-3"
|
||||||
:style="`border-color: ${user.usercolor} !important;`"
|
:style="`border-color: ${user.usercolor} !important;`"
|
||||||
@error="$event.target.src = '/img/icons/icon.png'"
|
@error="$event.target.src = '/img/icons/icon.png'"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="javascript:void(0)" @click="goToMyPage">
|
<a class="dropdown-item" href="pages-account-settings-account.html">
|
||||||
<i class="bx bx-user bx-md me-3"></i><span>My Page</span>
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="avatar avatar-online">
|
||||||
|
<img src="/img/avatars/1.png" class="w-px-40 h-auto rounded-circle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-0">John Doe</h6>
|
||||||
|
<small class="text-muted">Admin</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-divider my-1"></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-profile-user.html">
|
||||||
|
<i class="bx bx-user bx-md me-3"></i><span>My Profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -94,6 +201,28 @@
|
|||||||
<i class="bx bx-cog bx-md me-3"></i><span>Settings</span>
|
<i class="bx bx-cog bx-md me-3"></i><span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-account-settings-billing.html">
|
||||||
|
<span class="d-flex align-items-center align-middle">
|
||||||
|
<i class="flex-shrink-0 bx bx-credit-card bx-md me-3"></i
|
||||||
|
><span class="flex-grow-1 align-middle">Billing Plan</span>
|
||||||
|
<span class="flex-shrink-0 badge rounded-pill bg-danger">4</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-divider my-1"></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-pricing.html">
|
||||||
|
<i class="bx bx-dollar bx-md me-3"></i><span>Pricing</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="pages-faq.html">
|
||||||
|
<i class="bx bx-help-circle bx-md me-3"></i><span>FAQ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-divider my-1"></div>
|
<div class="dropdown-divider my-1"></div>
|
||||||
</li>
|
</li>
|
||||||
@ -104,19 +233,24 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<!--/ User -->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Small Screens -->
|
||||||
|
<div class="navbar-search-wrapper search-input-wrapper d-none">
|
||||||
|
<input type="text" class="form-control search-input container-xxl border-0" placeholder="Search..." aria-label="Search..." />
|
||||||
|
<i class="bx bx-x bx-md search-toggler cursor-pointer"></i>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from '@s/useAuthStore';
|
import { useAuthStore } from '@s/useAuthStore';
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import { useProjectStore } from '@/stores/useProjectStore';
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useThemeStore } from '@s/darkmode';
|
import { useThemeStore } from '@s/darkmode';
|
||||||
import { useWeatherStore } from '@/stores/useWeatherStore';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import axios from '@api';
|
|
||||||
|
|
||||||
const baseUrl = import.meta.env.VITE_SERVER;
|
const baseUrl = import.meta.env.VITE_SERVER;
|
||||||
|
|
||||||
@ -124,24 +258,9 @@
|
|||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
|
||||||
const weatherStore = useWeatherStore();
|
|
||||||
|
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
const weather = ref({});
|
|
||||||
const dailyWeatherList = ref([]);
|
|
||||||
const notifications = ref([]);
|
|
||||||
const notificationCount = ref(0);
|
|
||||||
|
|
||||||
const weatherReady = computed(() => {
|
|
||||||
return (
|
|
||||||
weather.value &&
|
|
||||||
weather.value.tempMin !== null &&
|
|
||||||
weather.value.tempMax !== null &&
|
|
||||||
!!weather.value.description
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 내가 참여하고 있는 진행 중인 프로젝트 목록
|
// 내가 참여하고 있는 진행 중인 프로젝트 목록
|
||||||
const myActiveProjects = computed(() => {
|
const myActiveProjects = computed(() => {
|
||||||
@ -164,7 +283,9 @@
|
|||||||
if (!selectedProject.value) return;
|
if (!selectedProject.value) return;
|
||||||
|
|
||||||
// 모든 진행 중인 프로젝트 리스트에서 선택된 프로젝트 찾기
|
// 모든 진행 중인 프로젝트 리스트에서 선택된 프로젝트 찾기
|
||||||
let selected = projectStore.activeProjectList.find(project => project.PROJCTSEQ === selectedProject.value);
|
let selected = projectStore.activeProjectList.find(
|
||||||
|
project => project.PROJCTSEQ === selectedProject.value
|
||||||
|
);
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
projectStore.setSelectedProject(selected);
|
projectStore.setSelectedProject(selected);
|
||||||
@ -172,40 +293,15 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 선택된 프로젝트 변경 감지
|
// 선택된 프로젝트 변경 감지
|
||||||
watch(
|
watch(() => projectStore.selectedProject, (newProject) => {
|
||||||
() => projectStore.selectedProject,
|
|
||||||
newProject => {
|
|
||||||
if (newProject) {
|
if (newProject) {
|
||||||
selectedProject.value = newProject.PROJCTSEQ;
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const customIconUrl = computed(() => {
|
|
||||||
if (weather.value.icon === '01d' || weather.value.icon === '01n') {
|
|
||||||
return '/img/icons/sunny-custom.png';
|
|
||||||
}
|
|
||||||
return `https://openweathermap.org/img/wn/${weather.value.icon}@2x.png`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const customIconClass = computed(() => {
|
|
||||||
if (weather.value.icon === '01d' || weather.value.icon === '01n') {
|
|
||||||
return 'custom-sunny-icon';
|
|
||||||
}
|
|
||||||
return 'weather-icon';
|
|
||||||
});
|
|
||||||
|
|
||||||
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await authStore.logout();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToMyPage = () => {
|
|
||||||
router.push('/mypage');
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// if (isDarkMode) {
|
// if (isDarkMode) {
|
||||||
// switchToDarkMode();
|
// switchToDarkMode();
|
||||||
@ -227,59 +323,14 @@
|
|||||||
projectStore.setSelectedProject(firstProject);
|
projectStore.setSelectedProject(firstProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로그인 페이지가 아닐 때만 날씨 정보를 가져오도록
|
|
||||||
if (route.name !== 'login' && route.name !== undefined) {
|
|
||||||
// 날씨 정보 갱신
|
|
||||||
await weatherStore.getWeatherInfoWithCache();
|
|
||||||
weather.value = weatherStore.weather; // 오늘 날씨
|
|
||||||
dailyWeatherList.value = weatherStore.dailyWeatherList; // 주간 날씨
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await authStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.weather-icon {
|
|
||||||
width: 40%;
|
|
||||||
height: 40%;
|
|
||||||
}
|
|
||||||
.weather-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.weather-temp {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #888;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
/* .weather-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-width: 3000px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
} */
|
|
||||||
.custom-sunny-icon {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
object-fit: contain;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.weather-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 160px; /* 필요시 */
|
|
||||||
}
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.custom-sunny-icon {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.custom-sunny-icon {
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
38
src/main.js
38
src/main.js
@ -1,29 +1,29 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia'
|
||||||
import piniaPersist from 'pinia-plugin-persist';
|
import piniaPersist from 'pinia-plugin-persist'
|
||||||
import App from './App.vue';
|
import App from './App.vue'
|
||||||
import router from '@/router';
|
import router from '@/router'
|
||||||
import dayjs from '@p/dayjs';
|
import dayjs from '@p/dayjs'
|
||||||
import ToastModal from '@c/modal/ToastModal.vue';
|
import ToastModal from '@c/modal/ToastModal.vue';
|
||||||
import common from '@/common/common.js';
|
import common from '@/common/common.js'
|
||||||
import { useKakao } from 'vue3-kakao-maps/@utils';
|
import { useKakao } from 'vue3-kakao-maps/@utils'
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPersist);
|
pinia.use(piniaPersist)
|
||||||
|
|
||||||
const kakaoApiKey = import.meta.env.VITE_KAKAO_MAP_KEY;
|
const kakaoApiKey = import.meta.env.VITE_KAKAO_MAP_KEY;
|
||||||
|
|
||||||
useKakao(kakaoApiKey, ['services']);
|
useKakao(kakaoApiKey, ['services'])
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App)
|
||||||
app.use(router).use(pinia).use(common).use(dayjs).component('ToastModal', ToastModal);
|
app.use(router)
|
||||||
|
.use(pinia)
|
||||||
|
.use(common)
|
||||||
|
.use(dayjs)
|
||||||
|
.component('ToastModal',ToastModal)
|
||||||
|
.mount('#app')
|
||||||
|
|
||||||
// 라우트 로딩 후 앱 마우트
|
if (import.meta.env.MODE === "prod") {
|
||||||
router.isReady().then(() => {
|
|
||||||
app.mount('#app');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (import.meta.env.MODE === 'prod') {
|
|
||||||
const console = window.console || {};
|
const console = window.console || {};
|
||||||
console.log = function no_console() { }; // console log 막기
|
console.log = function no_console() { }; // console log 막기
|
||||||
console.warn = function no_console() { }; // console warning 막기
|
console.warn = function no_console() { }; // console warning 막기
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: () => import('@v/MainView.vue'),
|
component: () => import('@v/MainView.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/board',
|
path: '/board',
|
||||||
@ -22,7 +22,6 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'write',
|
path: 'write',
|
||||||
name: 'BoardWrite',
|
|
||||||
component: () => import('@v/board/BoardWrite.vue'),
|
component: () => import('@v/board/BoardWrite.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -37,17 +36,10 @@ const routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/mypage',
|
|
||||||
name: 'MyPage',
|
|
||||||
component: () => import('@v/mypage/MyPage.vue'),
|
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/wordDict',
|
path: '/wordDict',
|
||||||
name: 'WordDict',
|
|
||||||
component: () => import('@v/wordDict/wordDict.vue'),
|
component: () => import('@v/wordDict/wordDict.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@ -69,52 +61,39 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/vacation',
|
path: '/vacation',
|
||||||
name: 'Vacation',
|
|
||||||
component: () => import('@v/vacation/VacationManagement.vue'),
|
component: () => import('@v/vacation/VacationManagement.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/voteboard',
|
path: '/voteboard',
|
||||||
name: 'VoteBoard',
|
|
||||||
component: () => import('@v/voteboard/TheVoteBoard.vue'),
|
component: () => import('@v/voteboard/TheVoteBoard.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'VoteBoardList',
|
|
||||||
component: () => import('@v/voteboard/voteBoardList.vue'),
|
component: () => import('@v/voteboard/voteBoardList.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'write',
|
path: 'write',
|
||||||
name: 'VoteboardWrite',
|
|
||||||
component: () => import('@v/voteboard/voteboardWrite.vue'),
|
component: () => import('@v/voteboard/voteboardWrite.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/projectlist',
|
path: '/projectlist',
|
||||||
name: 'Projectlist',
|
|
||||||
component: () => import('@v/projectlist/TheProjectList.vue'),
|
component: () => import('@v/projectlist/TheProjectList.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/commuters',
|
path: '/commuters',
|
||||||
name: 'Commuters',
|
|
||||||
component: () => import('@v/commuters/TheCommuters.vue'),
|
component: () => import('@v/commuters/TheCommuters.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/authorization',
|
path: '/authorization',
|
||||||
name: 'Authorization',
|
|
||||||
component: () => import('@v/admin/TheAuthorization.vue'),
|
component: () => import('@v/admin/TheAuthorization.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/people',
|
|
||||||
name: 'people',
|
|
||||||
component: () => import('@v/people/PeopleList.vue'),
|
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/error/400',
|
path: '/error/400',
|
||||||
name: 'Error400',
|
name: 'Error400',
|
||||||
@ -154,7 +133,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
|
|
||||||
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
||||||
if (to.path === '/authorization' && userId !== allowedUserId) {
|
if (to.path === '/authorization' && userId !== allowedUserId) {
|
||||||
return next();
|
return next('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
||||||
|
|||||||
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
작성자 : 서지희
|
|
||||||
작성일 : 2025-04-04
|
|
||||||
수정일 : 2025-04-07
|
|
||||||
설명 : 위치 기반으로 날씨를 조회하고, 10분 단위 캐시로 저장합니다.
|
|
||||||
*/
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import $api from '@api';
|
|
||||||
|
|
||||||
export const useWeatherStore = defineStore('weather', () => {
|
|
||||||
const weather = ref({
|
|
||||||
icon: '',
|
|
||||||
description: '',
|
|
||||||
tempMin: null,
|
|
||||||
tempMax: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dailyWeatherList = ref([]);
|
|
||||||
|
|
||||||
const getWeatherInfo = async () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
navigator.geolocation.getCurrentPosition(async position => {
|
|
||||||
const lat = position.coords.latitude;
|
|
||||||
const lon = position.coords.longitude;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await $api.get(`/weather`, {
|
|
||||||
params: { lat, lon },
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res?.data?.data) return;
|
|
||||||
|
|
||||||
const resData = res.data.data;
|
|
||||||
const raw = resData.weatherInfo;
|
|
||||||
const data = JSON.parse(raw);
|
|
||||||
if (!data || !Array.isArray(data.list)) {
|
|
||||||
console.error('날씨 데이터 형식 오류');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검은색 태양 아이콘 변경
|
|
||||||
dailyWeatherList.value = resData.dailyWeatherList.map(w => {
|
|
||||||
return {
|
|
||||||
...w,
|
|
||||||
icon: w.icon.replace(/n$/, 'd'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const todayStr = now.toISOString().split('T')[0];
|
|
||||||
const nowTime = now.getTime();
|
|
||||||
|
|
||||||
const todayList = data.list.filter(item => item.dt_txt.startsWith(todayStr));
|
|
||||||
if (todayList.length > 0) {
|
|
||||||
const minTemp = Math.min(...todayList.map(i => i.main.temp_min));
|
|
||||||
const maxTemp = Math.max(...todayList.map(i => i.main.temp_max));
|
|
||||||
weather.value.tempMin = Math.round(minTemp);
|
|
||||||
weather.value.tempMax = Math.round(maxTemp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closest = data.list.reduce((prev, curr) => {
|
|
||||||
const prevDiff = Math.abs(new Date(prev.dt_txt).getTime() - nowTime);
|
|
||||||
const currDiff = Math.abs(new Date(curr.dt_txt).getTime() - nowTime);
|
|
||||||
return currDiff < prevDiff ? curr : prev;
|
|
||||||
});
|
|
||||||
|
|
||||||
weather.value.icon = closest.weather[0].icon.replace(/n$/, 'd');
|
|
||||||
weather.value.description = closest.weather[0].description;
|
|
||||||
|
|
||||||
resolve({ weather: weather.value, dailyWeatherList: dailyWeatherList.value });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('날씨 정보 가져오기 실패:', e);
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}, reject);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWeatherInfoWithCache = async () => {
|
|
||||||
const now = new Date();
|
|
||||||
const pad = n => String(n).padStart(2, '0');
|
|
||||||
const key = `weather_${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(Math.floor(now.getMinutes() / 10) * 10)}`;
|
|
||||||
const cached = localStorage.getItem(key);
|
|
||||||
if (cached) {
|
|
||||||
const parsed = JSON.parse(cached);
|
|
||||||
weather.value = parsed.weather;
|
|
||||||
dailyWeatherList.value = parsed.dailyWeatherList;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { weather: w, dailyWeatherList: d } = await getWeatherInfo();
|
|
||||||
|
|
||||||
// 기존 캐시 삭제
|
|
||||||
Object.keys(localStorage).forEach(k => {
|
|
||||||
if (k.startsWith('weather_')) localStorage.removeItem(k);
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(key, JSON.stringify({ weather: w, dailyWeatherList: d }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('날씨 API 호출 실패, 캐시 fallback 시도 중...');
|
|
||||||
const oldKey = Object.keys(localStorage)
|
|
||||||
.filter(k => k.startsWith('weather_'))
|
|
||||||
.sort()
|
|
||||||
.pop();
|
|
||||||
if (oldKey) {
|
|
||||||
const fallback = JSON.parse(localStorage.getItem(oldKey));
|
|
||||||
weather.value = fallback.weather;
|
|
||||||
dailyWeatherList.value = fallback.dailyWeatherList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
weather,
|
|
||||||
dailyWeatherList,
|
|
||||||
getWeatherInfo,
|
|
||||||
getWeatherInfoWithCache,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@ -2,37 +2,27 @@
|
|||||||
<div class="container-xxl flex-grow-1 container-p-y pb-0">
|
<div class="container-xxl flex-grow-1 container-p-y pb-0">
|
||||||
<MainEventCalendar />
|
<MainEventCalendar />
|
||||||
<MemberManagement v-if="isAdmin" />
|
<MemberManagement v-if="isAdmin" />
|
||||||
<div class="row g-4 mt-2">
|
|
||||||
<!-- 게시판 -->
|
|
||||||
<BoardMain />
|
|
||||||
<!-- 용어집 -->
|
|
||||||
<main-word-dict />
|
|
||||||
<!-- 투표 -->
|
|
||||||
<main-vote />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import MainEventCalendar from '@/components/main/MainEventCalendar.vue';
|
import MainEventCalendar from '@/components/main/MainEventCalendar.vue';
|
||||||
import MemberManagement from '@/components/main/MemberManagement.vue';
|
import MemberManagement from '@/components/main/MemberManagement.vue';
|
||||||
import MainWordDict from '@c/main/MainWordDict.vue';
|
|
||||||
import BoardMain from '@c/main/BoardMain.vue';
|
|
||||||
import MainVote from '@c/main/MainVote.vue';
|
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import { onMounted, ref } from 'vue';
|
import { inject, onMounted, ref } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const user = ref();
|
||||||
const isAdmin = ref(false);
|
const isAdmin = ref(false);
|
||||||
|
|
||||||
const checkAdmin = async user => {
|
const checkAdmin = user => {
|
||||||
const { data } = await $api.post('user/authCheck', { memberId: user.loginId });
|
return user?.value?.role === 'ROLE_ADMIN' ? true : false;
|
||||||
return data.data === 'ROLE_ADMIN' ? true : false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
isAdmin.value = await checkAdmin(userStore.user);
|
user.value = userStore.user;
|
||||||
|
isAdmin.value = await checkAdmin(user);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -6,23 +6,14 @@
|
|||||||
<div class="user-card-container">
|
<div class="user-card-container">
|
||||||
<div v-for="user in users" :key="user.id" class="user-card">
|
<div v-for="user in users" :key="user.id" class="user-card">
|
||||||
<!-- 프로필 사진 -->
|
<!-- 프로필 사진 -->
|
||||||
<img
|
<img :src="getProfileImage(user.photo)" class="profile-img" alt="프로필 사진" @error="setDefaultImage" />
|
||||||
:src="getProfileImage(user.photo)"
|
|
||||||
class="user-avatar2"
|
|
||||||
alt="프로필 사진"
|
|
||||||
@error="setDefaultImage"
|
|
||||||
/>
|
|
||||||
<!-- 사용자 정보 -->
|
<!-- 사용자 정보 -->
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<h5>{{ user.name }}</h5>
|
<h5>{{ user.name }}</h5>
|
||||||
</div>
|
</div>
|
||||||
<!-- 권한 토글 버튼 (기본 동작 막고 클릭시 직접 토글 처리) -->
|
<!-- 권한 토글 버튼 -->
|
||||||
<label class="switch me-0">
|
<label class="switch me-0">
|
||||||
<input
|
<input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" />
|
||||||
type="checkbox"
|
|
||||||
:checked="user.isAdmin"
|
|
||||||
@click="handleToggle($event, user)"
|
|
||||||
/>
|
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -41,17 +32,20 @@ const users = ref([]);
|
|||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
||||||
const defaultProfile = "/img/icons/icon.png";
|
const defaultProfile = "/img/icons/icon.png";
|
||||||
const allowedUserId = 1; // 특정 ID (필요에 따라 변경)
|
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||||
|
|
||||||
// 사용자 목록 가져오기
|
// 사용자 목록 가져오기
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('admin/users'); // API 경로 확인
|
const response = await axios.get('admin/users'); // API 경로 확인 필요
|
||||||
|
|
||||||
|
// API 응답 구조 확인 후 데이터가 배열인지 체크
|
||||||
if (!response.data || !Array.isArray(response.data.data)) {
|
if (!response.data || !Array.isArray(response.data.data)) {
|
||||||
throw new Error("올바른 데이터 형식이 아닙니다.");
|
throw new Error("올바른 데이터 형식이 아닙니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MEMBERSEQ가 1이 아닌 회원만 필터링하여 데이터 매핑
|
||||||
users.value = response.data.data
|
users.value = response.data.data
|
||||||
.filter(user => user.MEMBERSEQ !== allowedUserId)
|
.filter(user => user.MEMBERSEQ !== allowedUserId) // MEMBERSEQ가 1이면 제외
|
||||||
.map(user => ({
|
.map(user => ({
|
||||||
id: user.MEMBERSEQ,
|
id: user.MEMBERSEQ,
|
||||||
name: user.MEMBERNAM,
|
name: user.MEMBERNAM,
|
||||||
@ -59,6 +53,7 @@ try {
|
|||||||
color: user.MEMBERCOL,
|
color: user.MEMBERCOL,
|
||||||
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
||||||
}
|
}
|
||||||
@ -74,31 +69,22 @@ function setDefaultImage(event) {
|
|||||||
event.target.src = defaultProfile;
|
event.target.src = defaultProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 권한 토글 시 기본 동작 막고 직접 제어하는 함수
|
// 관리자 권한 토글 함수
|
||||||
async function handleToggle(event, user) {
|
async function toggleAdmin(user) {
|
||||||
// Prevent the default checkbox toggle behavior
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// 저장: 현재 상태를 기반으로 변경 요청 (체크박스는 아직 변하지 않았음)
|
|
||||||
const originalState = user.isAdmin;
|
|
||||||
const newState = !originalState;
|
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: originalState ? 'MEMBER' : 'ADMIN'
|
role: user.isAdmin ? 'MEMBER' : 'ADMIN'
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.put('admin/role', requestData);
|
const response = await axios.put('admin/role', requestData);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// 서버 요청 성공 시에만 상태를 업데이트
|
user.isAdmin = !user.isAdmin;
|
||||||
user.isAdmin = newState;
|
|
||||||
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('권한 변경 실패');
|
throw new Error('권한 변경 실패');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 에러 발생 시 상태를 변경하지 않음
|
|
||||||
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,14 +93,4 @@ onMounted(fetchUsers);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.user-avatar2 {
|
|
||||||
width: 160px;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
margin: 1rem auto 0 auto;
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const content = ref({ ops: [] });
|
const content = ref('');
|
||||||
const autoIncrement = ref(0);
|
const autoIncrement = ref(0);
|
||||||
|
|
||||||
// 경고 상태
|
// 경고 상태
|
||||||
@ -130,9 +130,10 @@
|
|||||||
// 최초 업데이트 감지 여부
|
// 최초 업데이트 감지 여부
|
||||||
const isFirstContentUpdate = ref(true);
|
const isFirstContentUpdate = ref(true);
|
||||||
|
|
||||||
// 에디터 데이터 업데이트 시 처리 (최초 데이터 저장)
|
// 에디터에서 데이터 업데이트 시
|
||||||
const handleEditorDataUpdate = data => {
|
const handleEditorDataUpdate = data => {
|
||||||
content.value = data;
|
content.value = data;
|
||||||
|
|
||||||
if (isFirstContentUpdate.value) {
|
if (isFirstContentUpdate.value) {
|
||||||
originalContent.value = structuredClone(data);
|
originalContent.value = structuredClone(data);
|
||||||
isFirstContentUpdate.value = false;
|
isFirstContentUpdate.value = false;
|
||||||
@ -140,28 +141,23 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// isDeltaChanged 함수 수정 (내장 diff 대신 텍스트, 이미지, 비디오 각각을 직접 비교)
|
|
||||||
function isDeltaChanged(current, original) {
|
function isDeltaChanged(current, original) {
|
||||||
const Delta = Quill.import('delta');
|
const Delta = Quill.import('delta');
|
||||||
const currentDelta = new Delta(current || []);
|
const currentDelta = new Delta(current || []);
|
||||||
const originalDelta = new Delta(original || []);
|
const originalDelta = new Delta(original || []);
|
||||||
|
|
||||||
// 텍스트 추출
|
const diff = originalDelta.diff(currentDelta);
|
||||||
|
if (!diff || diff.ops.length === 0) return false;
|
||||||
|
|
||||||
|
// 텍스트만 비교해서 완전 동일한지 확인
|
||||||
const getPlainText = delta =>
|
const getPlainText = delta =>
|
||||||
(delta.ops || [])
|
(delta.ops || [])
|
||||||
.filter(op => typeof op.insert === 'string')
|
.filter(op => typeof op.insert === 'string')
|
||||||
.map(op => op.insert)
|
.map(op => op.insert)
|
||||||
.join('');
|
.join('');
|
||||||
// 이미지 URL 추출
|
|
||||||
const getImages = delta =>
|
const getImages = delta =>
|
||||||
(delta.ops || [])
|
(delta.ops || []).filter(op => typeof op.insert === 'object' && op.insert.image).map(op => op.insert.image);
|
||||||
.filter(op => typeof op.insert === 'object' && op.insert.image)
|
|
||||||
.map(op => op.insert.image);
|
|
||||||
// 비디오 URL 추출
|
|
||||||
const getVideos = delta =>
|
|
||||||
(delta.ops || [])
|
|
||||||
.filter(op => typeof op.insert === 'object' && op.insert.video)
|
|
||||||
.map(op => op.insert.video);
|
|
||||||
|
|
||||||
const textCurrent = getPlainText(currentDelta);
|
const textCurrent = getPlainText(currentDelta);
|
||||||
const textOriginal = getPlainText(originalDelta);
|
const textOriginal = getPlainText(originalDelta);
|
||||||
@ -169,36 +165,36 @@
|
|||||||
const imgsCurrent = getImages(currentDelta);
|
const imgsCurrent = getImages(currentDelta);
|
||||||
const imgsOriginal = getImages(originalDelta);
|
const imgsOriginal = getImages(originalDelta);
|
||||||
|
|
||||||
const vidsCurrent = getVideos(currentDelta);
|
|
||||||
const vidsOriginal = getVideos(originalDelta);
|
|
||||||
|
|
||||||
const textEqual = textCurrent === textOriginal;
|
const textEqual = textCurrent === textOriginal;
|
||||||
const imageEqual = imgsCurrent.length === imgsOriginal.length && imgsCurrent.every((val, idx) => val === imgsOriginal[idx]);
|
const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal);
|
||||||
const videoEqual = vidsCurrent.length === vidsOriginal.length && vidsCurrent.every((val, idx) => val === vidsOriginal[idx]);
|
|
||||||
|
|
||||||
return !(textEqual && imageEqual && videoEqual);
|
return !(textEqual && imageEqual); // 둘 다 같아야 false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 게시물 변경 여부 계산
|
|
||||||
const isChanged = computed(() => {
|
const isChanged = computed(() => {
|
||||||
if (!contentInitialized.value) return false;
|
if (!contentInitialized.value) return false;
|
||||||
const isTitleChanged = title.value !== originalTitle.value;
|
const isTitleChanged = title.value !== originalTitle.value;
|
||||||
const isContentChanged = isDeltaChanged(content.value, originalContent.value);
|
const isContentChanged = isDeltaChanged(content.value, originalContent.value);
|
||||||
const isFilesChanged =
|
const isFilesChanged =
|
||||||
attachFiles.value.some(f => !f.id) || // 신규 파일 존재
|
attachFiles.value.some(f => !f.id) || // id 없는 새 파일이 있는 경우
|
||||||
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
||||||
!isSameFiles(
|
!isSameFiles(
|
||||||
attachFiles.value.filter(f => f.id), // 기존 파일만 비교
|
attachFiles.value.filter(f => f.id), // 기존 파일(id 있는 것만)
|
||||||
originalFiles.value
|
originalFiles.value,
|
||||||
);
|
);
|
||||||
return isTitleChanged || isContentChanged || isFilesChanged;
|
return isTitleChanged || isContentChanged || isFilesChanged;
|
||||||
});
|
});
|
||||||
|
watch(isChanged, val => {
|
||||||
|
console.log('🔄 isChanged changed:', val);
|
||||||
|
});
|
||||||
|
|
||||||
// 파일 비교 함수
|
// 파일 비교 함수
|
||||||
function isSameFiles(current, original) {
|
function isSameFiles(current, original) {
|
||||||
if (current.length !== original.length) return false;
|
if (current.length !== original.length) return false;
|
||||||
|
|
||||||
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
||||||
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
return sortedCurrent.every((file, idx) => {
|
return sortedCurrent.every((file, idx) => {
|
||||||
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
|
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
|
||||||
});
|
});
|
||||||
@ -206,24 +202,31 @@
|
|||||||
|
|
||||||
// 게시물 데이터 로드
|
// 게시물 데이터 로드
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
|
// 수정 데이터 전송
|
||||||
let password = accessStore.password;
|
let password = accessStore.password;
|
||||||
const params = {
|
const params = {
|
||||||
password: `${password}` || '',
|
password: `${password}` || '',
|
||||||
};
|
};
|
||||||
|
//const response = await axios.get(`board/${currentBoardId.value}`);
|
||||||
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
||||||
|
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
|
//toastStore.onToast(data.message, 'e');
|
||||||
alert(data.message, 'e');
|
alert(data.message, 'e');
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const boardData = data.data;
|
const boardData = data.data;
|
||||||
|
// 기존 첨부파일 추가
|
||||||
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
||||||
const formatted = addDisplayFileName([...boardData.attachments]);
|
const formatted = addDisplayFileName([...boardData.attachments]);
|
||||||
attachFiles.value = formatted;
|
attachFiles.value = formatted;
|
||||||
originalFiles.value = formatted;
|
originalFiles.value = formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 데이터 설정
|
||||||
title.value = boardData.title || '제목 없음';
|
title.value = boardData.title || '제목 없음';
|
||||||
content.value = boardData.content || { ops: [] };
|
content.value = boardData.content || '내용 없음';
|
||||||
originalTitle.value = title.value;
|
originalTitle.value = title.value;
|
||||||
originalContent.value = structuredClone(boardData.content);
|
originalContent.value = structuredClone(boardData.content);
|
||||||
contentInitialized.value = true;
|
contentInitialized.value = true;
|
||||||
@ -242,34 +245,38 @@
|
|||||||
const addDisplayFileName = fileInfos =>
|
const addDisplayFileName = fileInfos =>
|
||||||
fileInfos.map(file => ({
|
fileInfos.map(file => ({
|
||||||
...file,
|
...file,
|
||||||
name: `${file.originalName}.${file.extension}`
|
name: `${file.originalName}.${file.extension}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 상세 페이지 이동
|
// 상세 페이지로 이동
|
||||||
const goList = () => {
|
const goList = () => {
|
||||||
accessStore.$reset();
|
accessStore.$reset();
|
||||||
|
|
||||||
|
// 목록으로 바로 이동시 필터 유지
|
||||||
|
// const getFilter = localStorage.getItem(`boardList_${currentBoardId.value}`);
|
||||||
|
// if (getFilter) {
|
||||||
|
// router.push({
|
||||||
|
// path: '/board',
|
||||||
|
// query: JSON.parse(getFilter),
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// router.push('/board');
|
||||||
|
// }
|
||||||
|
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전 페이지 이동
|
// 전 페이지로 이동
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
accessStore.$reset();
|
accessStore.$reset();
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 로컬 유효성 검사 함수 (에디터 내용: 텍스트, 이미지, 비디오 중 하나라도 있으면 유효)
|
// 유효성 확인
|
||||||
const isNotValidContent = delta => {
|
|
||||||
if (!delta?.ops?.length) return true;
|
|
||||||
const hasText = delta.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
|
||||||
const hasImage = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
|
||||||
const hasVideo = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.video);
|
|
||||||
return !(hasText || hasImage || hasVideo);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 유효성 확인 함수
|
|
||||||
const checkValidation = () => {
|
const checkValidation = () => {
|
||||||
contentAlert.value = isNotValidContent(content.value);
|
contentAlert.value = $common.isNotValidContent(content);
|
||||||
titleAlert.value = $common.isNotValidInput(title.value);
|
titleAlert.value = $common.isNotValidInput(title.value);
|
||||||
|
|
||||||
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
||||||
if (titleAlert.value) {
|
if (titleAlert.value) {
|
||||||
title.value = '';
|
title.value = '';
|
||||||
@ -285,6 +292,7 @@
|
|||||||
|
|
||||||
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;
|
||||||
@ -295,11 +303,13 @@
|
|||||||
}
|
}
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
|
|
||||||
autoIncrement.value++;
|
autoIncrement.value++;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFile = (index, file) => {
|
const removeFile = (index, file) => {
|
||||||
if (file.id) delFileIdx.value.push(file.id);
|
if (file.id) delFileIdx.value.push(file.id);
|
||||||
|
|
||||||
attachFiles.value.splice(index, 1);
|
attachFiles.value.splice(index, 1);
|
||||||
if (attachFiles.value.length <= maxFiles) {
|
if (attachFiles.value.length <= maxFiles) {
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
@ -317,41 +327,55 @@
|
|||||||
};
|
};
|
||||||
////////////////// fileSection[E] ////////////////////
|
////////////////// fileSection[E] ////////////////////
|
||||||
|
|
||||||
/** content 변경 감지 (deep 옵션 추가) */
|
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
contentAlert.value = isNotValidContent(content.value);
|
contentAlert.value = $common.isNotValidContent(content);
|
||||||
}, { deep: true });
|
});
|
||||||
|
|
||||||
// 글 제목 유효성 검사
|
// 글 제목 유효성
|
||||||
const validateTitle = () => {
|
const validateTitle = () => {
|
||||||
titleAlert.value = title.value.trim().length === 0;
|
titleAlert.value = title.value.trim().length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 수정 함수
|
// 게시물 수정
|
||||||
const updateBoard = async () => {
|
const updateBoard = async () => {
|
||||||
if (checkValidation()) return;
|
if (checkValidation()) return;
|
||||||
|
|
||||||
|
// 수정 데이터 전송
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value.trim(),
|
LOCBRDTTL: title.value.trim(),
|
||||||
LOCBRDCON: JSON.stringify(content.value),
|
LOCBRDCON: JSON.stringify(content.value),
|
||||||
LOCBRDSEQ: currentBoardId.value
|
LOCBRDSEQ: currentBoardId.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 업로드 된 첨부파일의 삭제목록
|
||||||
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
||||||
boardData.delFileIdx = [...delFileIdx.value];
|
boardData.delFileIdx = [...delFileIdx.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 에디터에 업로드 된 이미지 인덱스 목록
|
||||||
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
||||||
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 삭제할 에디터 이미지 인덱스
|
||||||
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
||||||
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileArray = newFileFilter(attachFiles);
|
const fileArray = newFileFilter(attachFiles);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// formData에 boardData 추가
|
||||||
Object.entries(boardData).forEach(([key, value]) => {
|
Object.entries(boardData).forEach(([key, value]) => {
|
||||||
formData.append(key, value);
|
formData.append(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// formData에 새로 추가한 파일 추가
|
||||||
fileArray.forEach((file, idx) => {
|
fileArray.forEach((file, idx) => {
|
||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
||||||
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
||||||
<th style="width: 10%" class="text-strat fw-bold">작성자</th>
|
<th style="width: 10%" class="text-center fw-bold">작성자</th>
|
||||||
<th style="width: 15%" class="text-center fw-bold">작성일</th>
|
<th style="width: 15%" class="text-center fw-bold">작성일</th>
|
||||||
<th style="width: 9%" class="text-center fw-bold">조회수</th>
|
<th style="width: 9%" class="text-center fw-bold">조회수</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -63,9 +63,9 @@
|
|||||||
>
|
>
|
||||||
<td class="text-center">공지</td>
|
<td class="text-center">공지</td>
|
||||||
<td class="cursor-pointer">
|
<td class="cursor-pointer">
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="me-1">📌</span>
|
<span class="me-1">📌</span>
|
||||||
<span>{{ truncateTitle(notice.title) }}</span>
|
<span>{{ notice.title }}</span>
|
||||||
|
|
||||||
<span v-if="notice.commentCount" class="text-danger fw-bold mx-1">
|
<span v-if="notice.commentCount" class="text-danger fw-bold mx-1">
|
||||||
[ {{ notice.commentCount }} ]
|
[ {{ notice.commentCount }} ]
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<span v-if="isNewPost(notice.rawDate)" class="box-new badge text-white ms-2 fs-tiny"> N </span>
|
<span v-if="isNewPost(notice.rawDate)" class="box-new badge text-white ms-2 fs-tiny"> N </span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-start">{{ notice.author }}</td>
|
<td class="text-center">{{ notice.author }}</td>
|
||||||
<td class="text-center">{{ notice.date }}</td>
|
<td class="text-center">{{ notice.date }}</td>
|
||||||
<td class="text-center">{{ notice.views }}</td>
|
<td class="text-center">{{ notice.views }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -94,9 +94,9 @@
|
|||||||
>
|
>
|
||||||
<td class="text-center">{{ post.id }}</td>
|
<td class="text-center">{{ post.id }}</td>
|
||||||
<td class="cursor-pointer">
|
<td class="cursor-pointer">
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
{{ truncateTitle(post.title) }}
|
{{ post.title }}
|
||||||
<span v-if="post.commentCount" class="comment-count text-danger">[ {{ post.commentCount }} ]</span>
|
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
|
||||||
<i v-if="post.img" class="bi bi-image mx-1"></i>
|
<i v-if="post.img" class="bi bi-image mx-1"></i>
|
||||||
<i
|
<i
|
||||||
v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0"
|
v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0"
|
||||||
@ -105,7 +105,7 @@
|
|||||||
<span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span>
|
<span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-start nickname-ellipsis">{{ post.nickname ? post.nickname : post.author }}</td>
|
<td class="text-center">{{ post.nickname ? post.nickname : post.author }}</td>
|
||||||
<td class="text-center">{{ post.date }}</td>
|
<td class="text-center">{{ post.date }}</td>
|
||||||
<td class="text-center">{{ post.views }}</td>
|
<td class="text-center">{{ post.views }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -177,10 +177,6 @@
|
|||||||
router.push({ name: 'BoardDetail', params: { id: id } });
|
router.push({ name: 'BoardDetail', params: { id: id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const truncateTitle = title => {
|
|
||||||
return title.length > 19 ? title.slice(0, 19) + '...' : title;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 로컬 스토리지 필터 저장
|
// 로컬 스토리지 필터 저장
|
||||||
const saveFilterToStorage = seq => {
|
const saveFilterToStorage = seq => {
|
||||||
const query = {
|
const query = {
|
||||||
@ -190,9 +186,9 @@
|
|||||||
searchText: searchText.value,
|
searchText: searchText.value,
|
||||||
showNotice: showNotices.value,
|
showNotice: showNotices.value,
|
||||||
};
|
};
|
||||||
//localStorage.removeItem
|
|
||||||
// 목록으로 바로 보낼때 필터 유지값
|
// 목록으로 바로 보낼때 필터 유지값
|
||||||
localStorage.setItem(`boardList_${seq}`, JSON.stringify(query));
|
//localStorage.setItem(`boardList_${seq}`, JSON.stringify(query));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 스토리지 초기화
|
// 스토리지 초기화
|
||||||
|
|||||||
@ -17,18 +17,9 @@
|
|||||||
:date="formattedBoardDate"
|
:date="formattedBoardDate"
|
||||||
:isLike="false"
|
:isLike="false"
|
||||||
:isAuthor="isAuthor"
|
:isAuthor="isAuthor"
|
||||||
:is-edit-pushed="isEditPushed"
|
|
||||||
:is-delete-pushed="isDeletePushed"
|
|
||||||
@editClick="editClick"
|
@editClick="editClick"
|
||||||
@deleteClick="deleteClick"
|
@deleteClick="deleteClick"
|
||||||
>
|
/>
|
||||||
<!-- 목록으로 버튼 -->
|
|
||||||
<template #gobackBtn>
|
|
||||||
<button class="btn btn-label-primary btn-icon me-1" @click="goList">
|
|
||||||
<i class="bx bx-left-arrow-alt"></i>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</BoardProfile>
|
|
||||||
|
|
||||||
<!-- 비밀번호 입력창 (익명일 경우) -->
|
<!-- 비밀번호 입력창 (익명일 경우) -->
|
||||||
<div v-if="isPassword && unknown" class="mt-3 w-px-200 ms-auto">
|
<div v-if="isPassword && unknown" class="mt-3 w-px-200 ms-auto">
|
||||||
@ -89,7 +80,7 @@
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- 좋아요 버튼 -->
|
<!-- 좋아요 버튼 -->
|
||||||
<div v-if="!type" class="row justify-content-center my-10">
|
<div v-if="unknown || authorId" class="row justify-content-center my-10">
|
||||||
<BoardRecommendBtn
|
<BoardRecommendBtn
|
||||||
:bigBtn="true"
|
:bigBtn="true"
|
||||||
:boardId="currentBoardId"
|
:boardId="currentBoardId"
|
||||||
@ -101,7 +92,7 @@
|
|||||||
@updateReaction="handleUpdateReaction"
|
@updateReaction="handleUpdateReaction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!type">
|
<div v-if="unknown || authorId" >
|
||||||
<!-- 댓글 입력 영역 -->
|
<!-- 댓글 입력 영역 -->
|
||||||
<BoardCommentArea
|
<BoardCommentArea
|
||||||
:profileName="profileName"
|
:profileName="profileName"
|
||||||
@ -115,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 댓글 목록 -->
|
<!-- 댓글 목록 -->
|
||||||
<div v-if="!type" class="card-footer">
|
<div v-if="unknown || authorId" class="card-footer">
|
||||||
<BoardCommentList
|
<BoardCommentList
|
||||||
:unknown="unknown"
|
:unknown="unknown"
|
||||||
:comments="commentsWithAuthStatus"
|
:comments="commentsWithAuthStatus"
|
||||||
@ -151,7 +142,7 @@
|
|||||||
import BoardCommentList from '@c/board/BoardCommentList.vue';
|
import BoardCommentList from '@c/board/BoardCommentList.vue';
|
||||||
import BoardRecommendBtn from '@c/button/BoardRecommendBtn.vue';
|
import BoardRecommendBtn from '@c/button/BoardRecommendBtn.vue';
|
||||||
import Pagination from '@c/pagination/Pagination.vue';
|
import Pagination from '@c/pagination/Pagination.vue';
|
||||||
import { ref, onMounted, computed, inject, provide } from 'vue';
|
import { ref, onMounted, computed, inject } 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 { useToastStore } from '@s/toastStore';
|
||||||
@ -174,8 +165,6 @@
|
|||||||
const attachment = ref(false);
|
const attachment = ref(false);
|
||||||
const comments = ref([]);
|
const comments = ref([]);
|
||||||
const profileImg = ref('');
|
const profileImg = ref('');
|
||||||
const isEditPushed = ref(false);
|
|
||||||
const isDeletePushed = ref(false);
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -241,7 +230,6 @@
|
|||||||
const isDeleted = ref(true);
|
const isDeleted = ref(true);
|
||||||
const commentAlert = ref('');
|
const commentAlert = ref('');
|
||||||
const boardPasswordAlert = ref('');
|
const boardPasswordAlert = ref('');
|
||||||
const type = ref('');
|
|
||||||
|
|
||||||
const updatePassword = newPassword => {
|
const updatePassword = newPassword => {
|
||||||
password.value = newPassword;
|
password.value = newPassword;
|
||||||
@ -273,7 +261,6 @@
|
|||||||
const boardData = data.data;
|
const boardData = data.data;
|
||||||
profileName.value = boardData.author || '익명';
|
profileName.value = boardData.author || '익명';
|
||||||
authorId.value = boardData.authorId;
|
authorId.value = boardData.authorId;
|
||||||
type.value = boardData.type === '300103';
|
|
||||||
boardTitle.value = boardData.title || '제목 없음';
|
boardTitle.value = boardData.title || '제목 없음';
|
||||||
boardContent.value = boardData.content || '';
|
boardContent.value = boardData.content || '';
|
||||||
profileImg.value = boardData.profileImg || '';
|
profileImg.value = boardData.profileImg || '';
|
||||||
@ -330,9 +317,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
fetchComments(pagination.value.currentPage);
|
fetchComments(pagination.value.currentPage);
|
||||||
closeAllEditTextareas();
|
|
||||||
closeAllPasswordAreas();
|
|
||||||
activeCommentBtnClass();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 댓글 목록 조회
|
// 댓글 목록 조회
|
||||||
@ -458,7 +442,6 @@
|
|||||||
passwordAlert.value = '';
|
passwordAlert.value = '';
|
||||||
commentAlert.value = '';
|
commentAlert.value = '';
|
||||||
await fetchComments();
|
await fetchComments();
|
||||||
activeCommentBtnClass();
|
|
||||||
} else {
|
} else {
|
||||||
alert('댓글 작성을 실패했습니다.');
|
alert('댓글 작성을 실패했습니다.');
|
||||||
}
|
}
|
||||||
@ -490,9 +473,6 @@
|
|||||||
const isUnknown = unknown?.unknown ?? false;
|
const isUnknown = unknown?.unknown ?? false;
|
||||||
|
|
||||||
if (isUnknown) {
|
if (isUnknown) {
|
||||||
closeAllEditTextareas();
|
|
||||||
closeAllPasswordAreas();
|
|
||||||
activeCommentBtnClass();
|
|
||||||
togglePassword('edit');
|
togglePassword('edit');
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
|
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
|
||||||
@ -504,9 +484,6 @@
|
|||||||
const isUnknown = unknown?.unknown ?? false;
|
const isUnknown = unknown?.unknown ?? false;
|
||||||
|
|
||||||
if (isUnknown) {
|
if (isUnknown) {
|
||||||
closeAllEditTextareas();
|
|
||||||
closeAllPasswordAreas();
|
|
||||||
activeCommentBtnClass();
|
|
||||||
togglePassword('delete');
|
togglePassword('delete');
|
||||||
} else {
|
} else {
|
||||||
deletePost();
|
deletePost();
|
||||||
@ -526,36 +503,11 @@
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnState = ref({});
|
|
||||||
provide('isBtnPushed', btnState);
|
|
||||||
|
|
||||||
const activeCommentBtnClass = (targetComment = null, type = 3) => {
|
|
||||||
const target = targetComment?.commentId;
|
|
||||||
let editPush = false;
|
|
||||||
let deletePush = false;
|
|
||||||
|
|
||||||
if (targetComment) {
|
|
||||||
if (type == 1) {
|
|
||||||
editPush = true;
|
|
||||||
deletePush = false;
|
|
||||||
} else if (type == 2) {
|
|
||||||
editPush = false;
|
|
||||||
deletePush = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btnState.value = {
|
|
||||||
target: target,
|
|
||||||
isEditPushed: editPush,
|
|
||||||
isDeletePushed: deletePush,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// 댓글 수정 클릭 시 이벤트(대댓글 포함)
|
// 댓글 수정 클릭 시 이벤트(대댓글 포함)
|
||||||
const editComment = comment => {
|
const editComment = comment => {
|
||||||
acitveButtonType(); //게시글 버튼 클릭 클래스 제거
|
|
||||||
|
|
||||||
password.value = '';
|
password.value = '';
|
||||||
passwordCommentAlert.value = '';
|
passwordCommentAlert.value = '';
|
||||||
|
//currentPasswordCommentId.value = null;
|
||||||
isPassword.value = false; // 상단 프로필 비밀번호
|
isPassword.value = false; // 상단 프로필 비밀번호
|
||||||
|
|
||||||
const targetComment = findCommentById(comment.commentId, comments.value);
|
const targetComment = findCommentById(comment.commentId, comments.value);
|
||||||
@ -568,20 +520,27 @@
|
|||||||
if (isMyComment) {
|
if (isMyComment) {
|
||||||
if (targetComment.isEditTextarea) {
|
if (targetComment.isEditTextarea) {
|
||||||
// 수정창이 열려 있는 상태에서 다시 수정 버튼을 누르면 초기화
|
// 수정창이 열려 있는 상태에서 다시 수정 버튼을 누르면 초기화
|
||||||
|
|
||||||
targetComment.isEditTextarea = false;
|
targetComment.isEditTextarea = false;
|
||||||
|
|
||||||
currentPasswordCommentId.value = comment.commentId;
|
currentPasswordCommentId.value = comment.commentId;
|
||||||
activeCommentBtnClass(targetComment, 3);
|
|
||||||
} else {
|
} else {
|
||||||
closeAllEditTextareas(); // 다른 모든 댓글의 수정창 닫기
|
// 다른 모든 댓글의 수정창 닫기
|
||||||
currentPasswordCommentId.value = null; // 현재 댓글만 수정 모드 활성화
|
closeAllEditTextareas();
|
||||||
targetComment.isEditTextarea = true; // 선택 버튼 활성화 상태 제어
|
currentPasswordCommentId.value = null;
|
||||||
activeCommentBtnClass(targetComment, 1);
|
// 현재 댓글만 수정 모드 활성화
|
||||||
|
targetComment.isEditTextarea = true;
|
||||||
}
|
}
|
||||||
} else if (isAnonymous) {
|
} else if (isAnonymous) {
|
||||||
if (currentPasswordCommentId.value === comment.commentId) {
|
if (currentPasswordCommentId.value === comment.commentId) {
|
||||||
toggleCommentPassword(comment, 'edit'); // 이미 비밀번호 입력 중이면 유지
|
// 이미 비밀번호 입력 중이면 유지
|
||||||
|
toggleCommentPassword(comment, 'edit');
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
closeAllEditTextareas(); // 다른 모든 댓글의 수정창 닫기
|
// 다른 모든 댓글의 수정창 닫기
|
||||||
|
closeAllEditTextareas();
|
||||||
|
|
||||||
|
// 비밀번호 입력
|
||||||
targetComment.isEditTextarea = false;
|
targetComment.isEditTextarea = false;
|
||||||
toggleCommentPassword(comment, 'edit');
|
toggleCommentPassword(comment, 'edit');
|
||||||
}
|
}
|
||||||
@ -590,28 +549,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 댓글 삭제 버튼 클릭
|
|
||||||
const deleteComment = async comment => {
|
|
||||||
acitveButtonType(); //게시글 버튼 클릭 클래스 제거
|
|
||||||
closeAllEditTextareas();
|
|
||||||
const isMyComment = comment.authorId === currentUserId.value;
|
|
||||||
|
|
||||||
// 익명인 경우
|
|
||||||
if (unknown.value && !isMyComment) {
|
|
||||||
// 수정 에디터 열려있을때
|
|
||||||
if (comment.isEditTextarea) {
|
|
||||||
comment.isEditTextarea = false;
|
|
||||||
comment.isCommentPassword = true;
|
|
||||||
toggleCommentPassword(comment, 'delete');
|
|
||||||
} else {
|
|
||||||
activeCommentBtnClass(comment, 3);
|
|
||||||
toggleCommentPassword(comment, 'delete');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
deleteReplyComment(comment);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모든 댓글의 수정 창 닫기
|
// 모든 댓글의 수정 창 닫기
|
||||||
const closeAllEditTextareas = () => {
|
const closeAllEditTextareas = () => {
|
||||||
comments.value.forEach(comment => {
|
comments.value.forEach(comment => {
|
||||||
@ -629,20 +566,30 @@
|
|||||||
passwordCommentAlert.value = '';
|
passwordCommentAlert.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 댓글 삭제 버튼 클릭
|
||||||
|
const deleteComment = async comment => {
|
||||||
|
const isMyComment = comment.authorId === currentUserId.value;
|
||||||
|
|
||||||
|
if (unknown.value && !isMyComment) {
|
||||||
|
if (comment.isEditTextarea) {
|
||||||
|
comment.isEditTextarea = false;
|
||||||
|
comment.isCommentPassword = true;
|
||||||
|
toggleCommentPassword(comment, 'delete');
|
||||||
|
} else {
|
||||||
|
toggleCommentPassword(comment, 'delete');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deleteReplyComment(comment);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 익명 댓글 비밀번호 창 토글
|
// 익명 댓글 비밀번호 창 토글
|
||||||
const toggleCommentPassword = (comment, button) => {
|
const toggleCommentPassword = (comment, button) => {
|
||||||
if (lastCommentClickedButton.value === button && currentPasswordCommentId.value === comment.commentId) {
|
if (lastCommentClickedButton.value === button && currentPasswordCommentId.value === comment.commentId) {
|
||||||
currentPasswordCommentId.value = null; // 비밀번호 창 닫기
|
currentPasswordCommentId.value = null; // 비밀번호 창 닫기
|
||||||
password.value = '';
|
password.value = '';
|
||||||
passwordCommentAlert.value = '';
|
passwordCommentAlert.value = '';
|
||||||
activeCommentBtnClass(comment, 3);
|
|
||||||
} else {
|
} else {
|
||||||
if (button == 'edit') {
|
|
||||||
activeCommentBtnClass(comment, 1);
|
|
||||||
} else if (button == 'delete') {
|
|
||||||
activeCommentBtnClass(comment, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPasswordCommentId.value = comment.commentId; // 비밀번호 창 열기
|
currentPasswordCommentId.value = comment.commentId; // 비밀번호 창 열기
|
||||||
password.value = '';
|
password.value = '';
|
||||||
passwordCommentAlert.value = '';
|
passwordCommentAlert.value = '';
|
||||||
@ -659,48 +606,17 @@
|
|||||||
isPassword.value = false;
|
isPassword.value = false;
|
||||||
boardPasswordAlert.value = '';
|
boardPasswordAlert.value = '';
|
||||||
password.value = '';
|
password.value = '';
|
||||||
acitveButtonType();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closeAllPasswordAreas();
|
closeAllPasswordAreas();
|
||||||
if (lastClickedButton.value === button) {
|
if (lastClickedButton.value === button) {
|
||||||
isPassword.value = !isPassword.value;
|
isPassword.value = !isPassword.value;
|
||||||
boardPasswordAlert.value = '';
|
boardPasswordAlert.value = '';
|
||||||
acitveButtonType();
|
|
||||||
} else {
|
} else {
|
||||||
isPassword.value = true;
|
isPassword.value = true;
|
||||||
|
|
||||||
if (button == 'edit') {
|
|
||||||
acitveButtonType(1);
|
|
||||||
} else if (button == 'delete') {
|
|
||||||
acitveButtonType(2);
|
|
||||||
} else {
|
|
||||||
acitveButtonType();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 선택한 게시글 버튼 핸들링(수정, 삭제 버튼)
|
lastClickedButton.value = button;
|
||||||
const acitveButtonType = type => {
|
|
||||||
// 수정
|
|
||||||
if (type == 1) {
|
|
||||||
isEditPushed.value = true;
|
|
||||||
isDeletePushed.value = false;
|
|
||||||
lastClickedButton.value = 'edit';
|
|
||||||
|
|
||||||
// 삭제
|
|
||||||
} else if (type == 2) {
|
|
||||||
isEditPushed.value = false;
|
|
||||||
isDeletePushed.value = true;
|
|
||||||
lastClickedButton.value = 'delete';
|
|
||||||
|
|
||||||
// 비활성화
|
|
||||||
} else {
|
|
||||||
isEditPushed.value = false;
|
|
||||||
isDeletePushed.value = false;
|
|
||||||
lastClickedButton.value = '';
|
|
||||||
isPassword.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시글 비밀번호 제출
|
// 게시글 비밀번호 제출
|
||||||
@ -820,7 +736,6 @@
|
|||||||
if (response.data.code === 200) {
|
if (response.data.code === 200) {
|
||||||
await fetchComments(pagination.value.currentPage);
|
await fetchComments(pagination.value.currentPage);
|
||||||
closeAllPasswordAreas();
|
closeAllPasswordAreas();
|
||||||
activeCommentBtnClass();
|
|
||||||
|
|
||||||
if (targetComment) {
|
if (targetComment) {
|
||||||
// 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
// 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
||||||
@ -849,7 +764,6 @@
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
togglePassword('close');
|
togglePassword('close');
|
||||||
fetchComments(pagination.value.currentPage);
|
fetchComments(pagination.value.currentPage);
|
||||||
activeCommentBtnClass();
|
|
||||||
return;
|
return;
|
||||||
// const targetComment = findCommentById(comment.commentId, comments.value);
|
// const targetComment = findCommentById(comment.commentId, comments.value);
|
||||||
|
|
||||||
@ -891,16 +805,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시글 목록 이동 버튼
|
|
||||||
const goList = () => {
|
|
||||||
// 목록으로 바로 이동시 필터 유지
|
|
||||||
const getFilter = localStorage.getItem(`boardList_${currentBoardId.value}`);
|
|
||||||
router.push({
|
|
||||||
name: 'BoardList',
|
|
||||||
query: getFilter ? JSON.parse(getFilter) : '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 댓글 삭제 (대댓글 포함)
|
// 댓글 삭제 (대댓글 포함)
|
||||||
const handleCommentDeleted = deletedCommentId => {
|
const handleCommentDeleted = deletedCommentId => {
|
||||||
// 댓글 삭제
|
// 댓글 삭제
|
||||||
@ -930,14 +834,9 @@
|
|||||||
|
|
||||||
const formattedBoardDate = computed(() => formattedDate(date.value));
|
const formattedBoardDate = computed(() => formattedDate(date.value));
|
||||||
|
|
||||||
const scrollToTop = () => {
|
|
||||||
window.scrollTo({ top: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 컴포넌트 마운트 시 데이터 로드
|
// 컴포넌트 마운트 시 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchBoardDetails();
|
fetchBoardDetails();
|
||||||
fetchComments();
|
fetchComments();
|
||||||
scrollToTop();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -37,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,8 +50,6 @@
|
|||||||
v-model="nickname"
|
v-model="nickname"
|
||||||
@update:alert="nicknameAlert = $event"
|
@update:alert="nicknameAlert = $event"
|
||||||
@input="validateNickname"
|
@input="validateNickname"
|
||||||
@keypress="noSpace"
|
|
||||||
:maxlength="6"
|
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
@ -103,14 +99,11 @@
|
|||||||
@update:deleteImgIndexList="handleDeleteEditorImg"
|
@update:deleteImgIndexList="handleDeleteEditorImg"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
<BackButton @click="goList" />
|
<BackButton @click="goList" />
|
||||||
<!-- 저장 버튼은 항상 활성화 -->
|
|
||||||
<SaveButton @click="write" :isEnabled="isFileValid" />
|
<SaveButton @click="write" :isEnabled="isFileValid" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -120,7 +113,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, 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';
|
||||||
@ -153,10 +146,6 @@
|
|||||||
const editorUploadedImgList = ref([]);
|
const editorUploadedImgList = ref([]);
|
||||||
const editorDeleteImgList = ref([]);
|
const editorDeleteImgList = ref([]);
|
||||||
|
|
||||||
const noSpace = (e) => {
|
|
||||||
if (e.key === ' ') e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
const response = await axios.get('board/categories');
|
const response = await axios.get('board/categories');
|
||||||
categoryList.value = response.data.data;
|
categoryList.value = response.data.data;
|
||||||
@ -174,12 +163,10 @@
|
|||||||
|
|
||||||
const fileCount = computed(() => attachFiles.value.length);
|
const fileCount = computed(() => attachFiles.value.length);
|
||||||
|
|
||||||
// 업데이트된 에디터 이미지 목록 업데이트
|
|
||||||
const handleUpdateEditorImg = item => {
|
const handleUpdateEditorImg = item => {
|
||||||
editorUploadedImgList.value = item;
|
editorUploadedImgList.value = item;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 삭제된 에디터 이미지 목록 업데이트
|
|
||||||
const handleDeleteEditorImg = item => {
|
const handleDeleteEditorImg = item => {
|
||||||
editorDeleteImgList.value = item;
|
editorDeleteImgList.value = item;
|
||||||
};
|
};
|
||||||
@ -194,8 +181,10 @@
|
|||||||
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
|
|
||||||
autoIncrement.value++;
|
autoIncrement.value++;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -217,8 +206,7 @@
|
|||||||
|
|
||||||
const validateNickname = () => {
|
const validateNickname = () => {
|
||||||
if (categoryValue.value === 300102) {
|
if (categoryValue.value === 300102) {
|
||||||
nickname.value = nickname.value.replace(/\s/g, ''); // 공백 제거
|
nicknameAlert.value = nickname.value.trim().length === 0;
|
||||||
nicknameAlert.value = nickname.value.length === 0;
|
|
||||||
} else {
|
} else {
|
||||||
nicknameAlert.value = false;
|
nicknameAlert.value = false;
|
||||||
}
|
}
|
||||||
@ -233,28 +221,19 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* validateContent:
|
|
||||||
* - 내용이 없으면 contentAlert를 true로 설정
|
|
||||||
* - 텍스트, 이미지, 비디오 중 하나라도 존재하면 유효한 콘텐츠로 판단
|
|
||||||
*/
|
|
||||||
const validateContent = () => {
|
const validateContent = () => {
|
||||||
if (!content.value?.ops?.length) {
|
if (!content.value?.ops?.length) {
|
||||||
contentAlert.value = true;
|
contentAlert.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasText = content.value.ops.some(
|
// 이미지 포함 여부 확인
|
||||||
op => typeof op.insert === 'string' && op.insert.trim().length > 0
|
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
||||||
);
|
// 텍스트 포함 여부 확인
|
||||||
const hasImage = content.value.ops.some(
|
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
||||||
op => op.insert && typeof op.insert === 'object' && op.insert.image
|
|
||||||
);
|
|
||||||
const hasVideo = content.value.ops.some(
|
|
||||||
op => op.insert && typeof op.insert === 'object' && op.insert.video
|
|
||||||
);
|
|
||||||
|
|
||||||
contentAlert.value = !(hasText || hasImage || hasVideo);
|
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
||||||
|
contentAlert.value = !(hasText || hasImage);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 글쓰기 */
|
/** 글쓰기 */
|
||||||
@ -278,7 +257,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value.trim(),
|
LOCBRDTTL: title.value,
|
||||||
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
||||||
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
|
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
|
||||||
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
||||||
@ -308,10 +287,10 @@
|
|||||||
formData.append('CMNFLEORG', fileNameWithoutExt);
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
formData.append('CMNFLESIZ', file.size);
|
formData.append('CMNFLESIZ', file.size);
|
||||||
formData.append('file', file);
|
formData.append('file', file); // 📌 실제 파일 추가
|
||||||
|
|
||||||
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,8 +306,8 @@
|
|||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** content 변경 감지 (deep 옵션 추가) */
|
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
validateContent();
|
validateContent();
|
||||||
}, { deep: true });
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,331 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
|
||||||
<div class="card shadow-sm rounded-lg p-6 max-w-2xl mx-auto">
|
|
||||||
<h3 class="text-2xl font-semibold mb-3 text-center">마이 페이지</h3>
|
|
||||||
<form @submit.prevent="handleSubmit">
|
|
||||||
<div class="text-center">
|
|
||||||
<label
|
|
||||||
for="profilePic"
|
|
||||||
class="rounded-circle cursor-pointer"
|
|
||||||
id="profileLabel"
|
|
||||||
:style="profilePreviewStyle"
|
|
||||||
></label>
|
|
||||||
<input type="file" id="profilePic" class="d-none object-fit-cover" @change="profileUpload" />
|
|
||||||
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-12">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="w-50 me-2">
|
|
||||||
<UserFormInput
|
|
||||||
title="입사일"
|
|
||||||
name="entryDate"
|
|
||||||
type="date"
|
|
||||||
:value="form.entryDate"
|
|
||||||
@update:data="form.entryDate = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex flex-column w-50">
|
|
||||||
<FormSelect
|
|
||||||
title="컬러"
|
|
||||||
name="color"
|
|
||||||
:is-row="false"
|
|
||||||
:is-label="true"
|
|
||||||
:is-common="true"
|
|
||||||
:is-color="true"
|
|
||||||
:data="colorList"
|
|
||||||
:value="form.color"
|
|
||||||
@update:data="handleColorUpdate"
|
|
||||||
/>
|
|
||||||
<span v-if="colorDuplicated" class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
이미 사용 중인 컬러입니다.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex">
|
|
||||||
<UserFormInput title="생년월일" name="birth" type="date"
|
|
||||||
:value="form.birth" @update:data="form.birth = $event" class="me-2 w-50" />
|
|
||||||
<FormSelect title="MBTI" name="mbti" :is-row="false" :is-label="true"
|
|
||||||
:is-common="true" :is-mbti="true" :data="mbtiList"
|
|
||||||
:value="form.mbti" @update:data="form.mbti = $event" class="w-50" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ArrInput title="주소" name="address" v-model="form.address" :disabled="true" />
|
|
||||||
|
|
||||||
<UserFormInput title="전화번호" name="phone" :value="form.phone"
|
|
||||||
@update:data="form.phone = $event" @blur="checkPhoneDuplicateAndFormat"
|
|
||||||
:maxlength="11" @keypress="onlyNumber" />
|
|
||||||
<span v-if="phoneFormatError" class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
전화번호 형식이 올바르지 않습니다.
|
|
||||||
</span>
|
|
||||||
<span v-if="phoneDuplicated" class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
이미 사용 중인 전화번호입니다.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- 기존 비밀번호 입력 -->
|
|
||||||
<UserFormInput title="비밀번호 재설정" placeholder="기존 비밀번호를 입력하세요" name="currentPw" type="password"
|
|
||||||
:value="password.current" @update:data="password.current = $event"
|
|
||||||
@blur="checkCurrentPassword" @keypress="noSpace" />
|
|
||||||
<span v-if="passwordError" class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
비밀번호가 일치하지 않습니다.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- 비밀번호 재설정 -->
|
|
||||||
<div v-if="showResetPw">
|
|
||||||
<UserFormInput title="새 비밀번호" name="newPw" type="password"
|
|
||||||
:value="password.new" @update:data="password.new = $event" @keypress="noSpace" />
|
|
||||||
<span v-if="password.new && password.new.length < 4"
|
|
||||||
class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
새 비밀번호는 최소 4자리 이상이어야 합니다.
|
|
||||||
</span>
|
|
||||||
<span v-if="password.new === password.current"
|
|
||||||
class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
기존 비밀번호와 다르게 설정해주세요.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<UserFormInput title="비밀번호 확인" name="confirmPw" type="password"
|
|
||||||
:value="password.confirm" @update:data="password.confirm = $event" @keypress="noSpace" />
|
|
||||||
<span v-if="password.confirm && password.confirm !== password.new"
|
|
||||||
class="text-danger invalid-feedback mt-1 d-block">
|
|
||||||
새 비밀번호와 일치하지 않습니다.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mt-2">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
||||||
:disabled="!canResetPassword"
|
|
||||||
@click="handlePasswordReset">
|
|
||||||
비밀번호 변경
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex mt-5">
|
|
||||||
<button type="submit" class="btn btn-primary w-100"
|
|
||||||
:disabled="!isChanged || phoneDuplicated || phoneFormatError || colorDuplicated">
|
|
||||||
정보 수정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, watch} from 'vue';
|
|
||||||
import $api from '@api';
|
|
||||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
|
||||||
import FormSelect from '@c/input/FormSelect.vue';
|
|
||||||
import ArrInput from '@c/input/ArrInput.vue';
|
|
||||||
import { useToastStore } from '@s/toastStore';
|
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
|
||||||
|
|
||||||
const noSpace = (e) => {
|
|
||||||
if (e.key === ' ') e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
entryDate: '', birth: '', phone: '', color: '', mbti: '',
|
|
||||||
address: { address: '', detailAddress: '', postcode: '' },
|
|
||||||
id: ''
|
|
||||||
});
|
|
||||||
const originalData = ref({});
|
|
||||||
const profile = ref('');
|
|
||||||
const uploadedFile = ref(null);
|
|
||||||
const profileChanged = ref(false);
|
|
||||||
const profilerr = ref('');
|
|
||||||
const currentBlobUrl = ref('');
|
|
||||||
const colorDuplicated = ref(false);
|
|
||||||
const phoneDuplicated = ref(false);
|
|
||||||
const mbtiList = ref([]);
|
|
||||||
const colorList = ref([]);
|
|
||||||
|
|
||||||
const password = ref({ current: '', new: '', confirm: '' });
|
|
||||||
const passwordError = ref(false);
|
|
||||||
const phoneFormatError = ref(false);
|
|
||||||
const showResetPw = ref(false);
|
|
||||||
|
|
||||||
const canResetPassword = computed(() => {
|
|
||||||
return (
|
|
||||||
password.value.new.length >= 4 &&
|
|
||||||
password.value.new !== password.value.current &&
|
|
||||||
password.value.new === password.value.confirm
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.value.address.detailAddress,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal !== newVal.trim()) {
|
|
||||||
form.value.address.detailAddress = newVal.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const isChanged = computed(() => {
|
|
||||||
const f = form.value;
|
|
||||||
const o = originalData.value;
|
|
||||||
return (
|
|
||||||
f.entryDate !== o.entryDate || f.birth !== o.birth || f.phone !== o.phone ||
|
|
||||||
f.color !== o.color || f.mbti !== o.mbti || profileChanged.value ||
|
|
||||||
f.address.address !== o.address.address ||
|
|
||||||
f.address.detailAddress !== o.address.detailAddress ||
|
|
||||||
f.address.postcode !== o.address.postcode
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
|
||||||
const defaultProfile = "/img/icons/icon.png";
|
|
||||||
const getProfileImageUrl = (profilePath) =>
|
|
||||||
profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
|
||||||
const profilePreviewStyle = computed(() => ({
|
|
||||||
width: '100px',
|
|
||||||
height: '100px',
|
|
||||||
backgroundImage: `url(${profile.value})`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center'
|
|
||||||
}));
|
|
||||||
|
|
||||||
const profileUpload = (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
if (file.size > 5 * 1024 * 1024 || !['image/jpeg', 'image/png'].includes(file.type)) {
|
|
||||||
profilerr.value = '5MB 이하의 JPG/PNG 파일만 업로드 가능합니다.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
profilerr.value = '';
|
|
||||||
if (currentBlobUrl.value) URL.revokeObjectURL(currentBlobUrl.value);
|
|
||||||
uploadedFile.value = file;
|
|
||||||
const newBlobUrl = URL.createObjectURL(file);
|
|
||||||
profile.value = newBlobUrl;
|
|
||||||
currentBlobUrl.value = newBlobUrl;
|
|
||||||
profileChanged.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onlyNumber = (e) => {
|
|
||||||
if (!/[0-9]/.test(e.key)) e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkPhoneDuplicateAndFormat = async () => {
|
|
||||||
const currentPhone = form.value.phone.trim();
|
|
||||||
|
|
||||||
// 형식 검사 (010으로 시작 + 숫자 8자리)
|
|
||||||
const phoneRegex = /^010\d{8}$/;
|
|
||||||
const isFormatValid = phoneRegex.test(currentPhone);
|
|
||||||
|
|
||||||
phoneFormatError.value = !isFormatValid;
|
|
||||||
|
|
||||||
// 중복 체크는 형식이 맞을 때만 수행
|
|
||||||
if (isFormatValid) {
|
|
||||||
const response = await $api.get('/user/checkPhone', {
|
|
||||||
params: { memberTel: currentPhone },
|
|
||||||
});
|
|
||||||
|
|
||||||
// true면 사용 가능하다는 의미니까 → 중복 아님
|
|
||||||
// false면 중복된 번호라는 의미니까 → 중복됨
|
|
||||||
phoneDuplicated.value = currentPhone !== originalData.value.phone && !response.data.data;
|
|
||||||
} else {
|
|
||||||
// 형식이 맞지 않으면 중복 여부는 무시 (false로 초기화)
|
|
||||||
phoneDuplicated.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColorUpdate = async (colorVal) => {
|
|
||||||
form.value.color = colorVal;
|
|
||||||
colorDuplicated.value = colorVal !== originalData.value.color &&
|
|
||||||
(await $api.get('/user/checkColor', { params: { memberCol: colorVal } })).data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkCurrentPassword = async () => {
|
|
||||||
if (!password.value.current) return;
|
|
||||||
const res = await $api.post('/user/checkPassword', {
|
|
||||||
id: form.value.id,
|
|
||||||
password: password.value.current
|
|
||||||
});
|
|
||||||
passwordError.value = res.data.data;
|
|
||||||
showResetPw.value = !res.data.data;
|
|
||||||
};
|
|
||||||
const handlePasswordReset = async () => {
|
|
||||||
const res = await $api.patch('/user/pwNew', {
|
|
||||||
id: form.value.id,
|
|
||||||
password: password.value.new
|
|
||||||
});
|
|
||||||
if (res.data.data) {
|
|
||||||
toastStore.onToast('비밀번호가 변경되었습니다.', 's');
|
|
||||||
password.value = { current: '', new: '', confirm: '' };
|
|
||||||
showResetPw.value = false;
|
|
||||||
passwordError.value = false;
|
|
||||||
} else {
|
|
||||||
toastStore.onToast('비밀번호 변경 실패', 'e');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (isoDate) => isoDate?.split('T')[0] || '';
|
|
||||||
|
|
||||||
const loadInitialData = async () => {
|
|
||||||
const user = (await $api.get('/user/userInfo')).data.data;
|
|
||||||
const serverColors = (await $api.get('/user/color', { params: { type: 'YON' } })).data.data.map(c => ({
|
|
||||||
value: c.CMNCODVAL, label: c.CMNCODNAM
|
|
||||||
}));
|
|
||||||
const matchedColor = serverColors.find(c => c.label === user.usercolor);
|
|
||||||
const colorCode = matchedColor ? matchedColor.value : user.color;
|
|
||||||
colorList.value = serverColors.some(c => c.value === colorCode)
|
|
||||||
? serverColors
|
|
||||||
: [{ value: colorCode, label: user.usercolor }, ...serverColors];
|
|
||||||
|
|
||||||
const initData = {
|
|
||||||
id: user.loginId,
|
|
||||||
entryDate: formatDate(user.isCdt),
|
|
||||||
birth: formatDate(user.birth),
|
|
||||||
phone: user.phone || '',
|
|
||||||
color: colorCode,
|
|
||||||
mbti: user.mbit || '',
|
|
||||||
address: {
|
|
||||||
address: user.address || '',
|
|
||||||
detailAddress: user.addressDetail || '',
|
|
||||||
postcode: user.zipcode || ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
form.value = { ...initData };
|
|
||||||
originalData.value = { ...initData };
|
|
||||||
profile.value = getProfileImageUrl(user.profile);
|
|
||||||
profileChanged.value = false;
|
|
||||||
|
|
||||||
const mbtiRes = await $api.get('/user/mbti');
|
|
||||||
mbtiList.value = mbtiRes.data.data.map(m => ({ value: m.CMNCODVAL, label: m.CMNCODNAM }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const formData = new FormData();
|
|
||||||
Object.entries(form.value).forEach(([k, v]) => {
|
|
||||||
if (typeof v === 'object') {
|
|
||||||
formData.append('address', v.address);
|
|
||||||
formData.append('detailAddress', v.detailAddress);
|
|
||||||
formData.append('postcode', v.postcode);
|
|
||||||
} else {
|
|
||||||
formData.append(k, v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (uploadedFile.value) formData.append('profileFile', uploadedFile.value);
|
|
||||||
|
|
||||||
if (form.value.color !== originalData.value.color) {
|
|
||||||
if (form.value.color) await $api.patch('/user/updateColorYon', { color: form.value.color, type: 'YON' });
|
|
||||||
if (originalData.value.color) await $api.patch('/user/updateColorChange', { color: originalData.value.color, type: 'YON' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await $api.patch('/user/updateInfo', formData, { isFormData: true });
|
|
||||||
originalData.value = { ...form.value };
|
|
||||||
profileChanged.value = false;
|
|
||||||
location.reload();
|
|
||||||
toastStore.onToast('정보가 수정되었습니다.', 's');
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => loadInitialData());
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
@ -1,318 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
|
||||||
<div class="card">
|
|
||||||
<!-- 사원 목록이 없을 경우 표시 -->
|
|
||||||
<div v-if="allUserList.length === 0" class="text-center my-4">
|
|
||||||
<p class="text-muted">등록된 사원이 없습니다.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 사원 카드 리스트 영역 -->
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="card-list">
|
|
||||||
<div
|
|
||||||
v-for="(person, index) in allUserList"
|
|
||||||
:key="index"
|
|
||||||
class="person-card"
|
|
||||||
@click="openModal(person)"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
class="rounded-circle user-avatar pointer"
|
|
||||||
:src="getProfileImage(person.MEMBERPRF)"
|
|
||||||
:style="{ borderColor: person.usercolor }"
|
|
||||||
@error="setDefaultImage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="person-name">{{ person.MEMBERNAM }}</h3>
|
|
||||||
<p class="person-email">{{ person.MEMBERIDS }}@local-host.co.kr</p>
|
|
||||||
<p class="person-phone">{{ person.MEMBERTEL }}</p>
|
|
||||||
<small>
|
|
||||||
{{ person.MEMBERARR }} {{ person.MEMBERDTL }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 상세보기 Modal -->
|
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<button class="close-btn" @click="closeModal">×</button>
|
|
||||||
<div class="modal-body">
|
|
||||||
<img
|
|
||||||
class="user-avatar2"
|
|
||||||
:src="getProfileImage(selectedPerson.MEMBERPRF)"
|
|
||||||
:style="{ borderColor: selectedPerson.usercolor }"
|
|
||||||
@error="setDefaultImage"
|
|
||||||
/>
|
|
||||||
<h4>{{ selectedPerson.MEMBERNAM }}</h4>
|
|
||||||
<p>{{ selectedPerson.MEMBERIDS }}@local-host.co.kr</p>
|
|
||||||
<p>{{ selectedPerson.MEMBERTEL }}</p>
|
|
||||||
<p>{{ selectedPerson.MEMBERARR }} {{ selectedPerson.MEMBERDTL }}</p>
|
|
||||||
<hr />
|
|
||||||
<!-- 추가 정보: 사용자가 속한 프로젝트 목록 -->
|
|
||||||
<h5>참여 프로젝트</h5>
|
|
||||||
<div v-if="memberProjects.length > 0" class="project-list-container">
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="(project, idx) in memberProjects"
|
|
||||||
:key="idx"
|
|
||||||
class="project-item"
|
|
||||||
>
|
|
||||||
<span class="project-name">{{ project.PROJCTNAM }}</span>
|
|
||||||
<span class="project-period">
|
|
||||||
<!-- projectEndDate가 있는 경우 -->
|
|
||||||
<!-- <template v-if="project.projectEndDate"> -->
|
|
||||||
{{ project.userStartDate ? project.userStartDate : project.projectStartDate }} ~
|
|
||||||
{{ project.userEndDate ? project.userEndDate : project.projectEndDate }}
|
|
||||||
<!-- </template> -->
|
|
||||||
<!-- 없으면 종료일 표시 안함 -->
|
|
||||||
<!-- <template v-else>
|
|
||||||
{{ project.userStartDate ? project.userStartDate : project.projectStartDate }} ~
|
|
||||||
</template> -->
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<p>참여중인 프로젝트가 없습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from '@api' // API 호출용 Axios 인스턴스
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import SearchBar from '@c/search/SearchBar.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PeopleList',
|
|
||||||
components: { SearchBar },
|
|
||||||
setup() {
|
|
||||||
const allUserList = ref([]) // 전체 사원 목록
|
|
||||||
const user = ref({}) // 현재 로그인한 사용자 (필요 시 사용)
|
|
||||||
const showModal = ref(false) // 모달 표시 여부
|
|
||||||
const selectedPerson = ref({})// 모달에 표시할 선택된 사원 정보
|
|
||||||
const memberProjects = ref([])// 선택된 사원의 프로젝트 목록
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('user/allUserList')
|
|
||||||
allUserList.value = response.data.data.allUserList
|
|
||||||
user.value = response.data.data.user
|
|
||||||
} catch (error) {
|
|
||||||
console.error('사원 목록 조회 실패:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, '')
|
|
||||||
const defaultProfile = '/img/icons/icon.png'
|
|
||||||
|
|
||||||
const getProfileImage = (profilePath) => {
|
|
||||||
return profilePath && profilePath.trim()
|
|
||||||
? `${baseUrl}upload/img/profile/${profilePath}`
|
|
||||||
: defaultProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDefaultImage = (event) => {
|
|
||||||
event.target.src = defaultProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
// 참여 프로젝트 조회 API
|
|
||||||
const fetchMemberProjects = async (memberSeq) => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(`project/people/${memberSeq}`)
|
|
||||||
memberProjects.value = res.data.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('프로젝트 조회 실패:', error)
|
|
||||||
memberProjects.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = (person) => {
|
|
||||||
selectedPerson.value = person
|
|
||||||
fetchMemberProjects(person.MEMBERSEQ)
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allUserList,
|
|
||||||
user,
|
|
||||||
showModal,
|
|
||||||
selectedPerson,
|
|
||||||
memberProjects,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
getProfileImage,
|
|
||||||
defaultProfile,
|
|
||||||
setDefaultImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.container-xxl {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-card {
|
|
||||||
width: 280px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
transition: box-shadow 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-card:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-card .card-header {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 160px;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
display: block;
|
|
||||||
margin: 1rem auto 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar2 {
|
|
||||||
width: 160px;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
margin: 1rem auto 0 auto;
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-card .card-body {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-name {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.person-email,
|
|
||||||
.person-phone {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 모달 스타일 */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 111%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
position: relative;
|
|
||||||
width: 400px;
|
|
||||||
background: #fff;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
animation: slideDown 0.3s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-img {
|
|
||||||
width: 50%;
|
|
||||||
height: 50%;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 프로젝트 리스트 스타일 */
|
|
||||||
.project-list-container {
|
|
||||||
max-height: 200px; /* 필요에 따라 높이 조절 */
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
list-style: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-period {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #888;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideDown {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-15%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -586,45 +586,30 @@ function updateCalendarEvents() {
|
|||||||
const todayElement = document.querySelector(`.fc-daygrid-day[data-date="${todayStr}"]`);
|
const todayElement = document.querySelector(`.fc-daygrid-day[data-date="${todayStr}"]`);
|
||||||
|
|
||||||
if (todayElement) {
|
if (todayElement) {
|
||||||
|
// 오늘 날짜가 선택된 경우 배경 제거
|
||||||
if (selectedDates.value.has(todayStr)) {
|
if (selectedDates.value.has(todayStr)) {
|
||||||
todayElement.style.backgroundColor = 'transparent'; // 노란 배경 제거
|
todayElement.classList.remove("fc-day-today"); // 기본 스타일 제거
|
||||||
todayElement.classList.add("selected-event");
|
todayElement.classList.add("selected-event"); // 선택된 날짜 스타일 적용
|
||||||
|
|
||||||
// 기존 오버레이 제거
|
// 🔹 오전 반차일 경우 'half-day-am' 클래스 추가
|
||||||
const existingOverlay = todayElement.querySelector('.half-day-overlay');
|
if (selectedDates.value.get(todayStr) === "700101") {
|
||||||
if (existingOverlay) {
|
todayElement.classList.add("half-day-am");
|
||||||
todayElement.removeChild(existingOverlay);
|
todayElement.classList.remove("half-day-pm");
|
||||||
}
|
}
|
||||||
|
// 🔹 오후 반차일 경우 'half-day-pm' 클래스 추가
|
||||||
const overlay = document.createElement('div');
|
else if (selectedDates.value.get(todayStr) === "700102") {
|
||||||
overlay.classList.add('half-day-overlay');
|
todayElement.classList.add("half-day-pm");
|
||||||
|
todayElement.classList.remove("half-day-am");
|
||||||
const type = selectedDates.value.get(todayStr);
|
|
||||||
if (type === "700101") {
|
|
||||||
overlay.classList.add('am');
|
|
||||||
} else if (type === "700102") {
|
|
||||||
overlay.classList.add('pm');
|
|
||||||
} else {
|
} else {
|
||||||
// 전체 연차는 배경 전체 덮기
|
todayElement.classList.remove("half-day-am", "half-day-pm");
|
||||||
overlay.style.width = '100%';
|
|
||||||
overlay.style.borderRadius = '4px';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
todayElement.appendChild(overlay);
|
|
||||||
} else {
|
} else {
|
||||||
// 선택 해제 시 원상복구
|
todayElement.classList.add("fc-day-today"); // 기본 스타일 복원
|
||||||
todayElement.classList.remove("selected-event", "half-day-am", "half-day-pm");
|
todayElement.classList.remove("selected-event", "half-day-am", "half-day-pm"); // 선택된 상태 해제
|
||||||
todayElement.style.backgroundColor = ''; // 배경 복원
|
|
||||||
|
|
||||||
const existingOverlay = todayElement.querySelector('.half-day-overlay');
|
|
||||||
if (existingOverlay) {
|
|
||||||
todayElement.removeChild(existingOverlay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 휴가 종류에 따른 클래스명
|
// 휴가 종류에 따른 클래스명
|
||||||
const getVacationTypeClass = (type) => {
|
const getVacationTypeClass = (type) => {
|
||||||
if (type === "700101") return "half-day-am";
|
if (type === "700101") return "half-day-am";
|
||||||
|
|||||||
@ -57,7 +57,7 @@ import $api from '@api';
|
|||||||
import Quill from 'quill';
|
import Quill from 'quill';
|
||||||
import WriteBtn from '@c/button/WriteBtn.vue';
|
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||||
import voteList from '@c/voteboard/voteCardList.vue';
|
import voteList from '@c/voteboard/voteCardList.vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const category = ref('0');
|
const category = ref('0');
|
||||||
@ -71,14 +71,8 @@ const voteset = ref(0);
|
|||||||
const ischeked = ref(false);
|
const ischeked = ref(false);
|
||||||
const selectedVote = ref({}); // 선택된 투표 데이터
|
const selectedVote = ref({}); // 선택된 투표 데이터
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
|
||||||
const myVote = ref('2');
|
onMounted(async () => {
|
||||||
onMounted(() => {
|
|
||||||
const maincvoteset = route.query.voteset || '0';
|
|
||||||
const maincmyVote = route.query.myVote || '0';
|
|
||||||
voteset.value =maincvoteset;
|
|
||||||
category.value = maincvoteset;
|
|
||||||
myVote.value = maincmyVote;
|
|
||||||
getvoteList();
|
getvoteList();
|
||||||
});
|
});
|
||||||
//글작성
|
//글작성
|
||||||
@ -93,16 +87,16 @@ const changeCheck = () =>{
|
|||||||
//투표목록
|
//투표목록
|
||||||
const getvoteList = () => {
|
const getvoteList = () => {
|
||||||
$api.get('vote/getVoteList',{
|
$api.get('vote/getVoteList',{
|
||||||
|
//목록조회시 파라미터 전달
|
||||||
params:
|
params:
|
||||||
{
|
{
|
||||||
page: currentPage.value
|
page: currentPage.value
|
||||||
,voteset:voteset.value
|
,voteset:voteset.value
|
||||||
,myVote:myVote.value == '2' ? myVote.value : ischeked.value ? '1':'0'
|
,myVote:ischeked.value ? '1':'0'
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
PageData.value = res.data.data;
|
PageData.value = res.data.data;
|
||||||
voteListCardData.value = res.data.data.list;
|
voteListCardData.value = res.data.data.list;
|
||||||
myVote.value = '';
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -138,7 +132,6 @@ const checkedNames = (numList) => {
|
|||||||
}
|
}
|
||||||
//투표종료
|
//투표종료
|
||||||
const endVoteId = (endVoteId) => {
|
const endVoteId = (endVoteId) => {
|
||||||
if(confirm('투표를 종료하시겠습니까?')){
|
|
||||||
$api.patch('vote/updateEndData',{
|
$api.patch('vote/updateEndData',{
|
||||||
endVoteId :endVoteId
|
endVoteId :endVoteId
|
||||||
}).then((res)=>{
|
}).then((res)=>{
|
||||||
@ -147,14 +140,12 @@ const endVoteId = (endVoteId) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//기한 지난 투표 종료
|
//기한 지난 투표 종료
|
||||||
const voteEnded = async (id) =>{
|
const voteEnded = async (id) =>{
|
||||||
await endVoteId(id.id);
|
await endVoteId(id.id);
|
||||||
}
|
}
|
||||||
//투표 삭제
|
//투표 삭제
|
||||||
const voteDelete =(id) =>{
|
const voteDelete =(id) =>{
|
||||||
if(confirm('투표를 삭제하시겠습니까?')){
|
|
||||||
$api.patch('vote/updateDeleteData',{
|
$api.patch('vote/updateDeleteData',{
|
||||||
deleteVoteId :id
|
deleteVoteId :id
|
||||||
}).then((res)=>{
|
}).then((res)=>{
|
||||||
@ -164,7 +155,6 @@ const voteDelete =(id) =>{
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//랜덤 1위 뽑기
|
//랜덤 1위 뽑기
|
||||||
const randomList = (data,id) =>{
|
const randomList = (data,id) =>{
|
||||||
$api.post('vote/randomList',{
|
$api.post('vote/randomList',{
|
||||||
|
|||||||
@ -244,14 +244,4 @@ const goList = () => {
|
|||||||
.item-input {
|
.item-input {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
.hidden-date-input {
|
|
||||||
display: block; /* 한 줄 차지 */
|
|
||||||
margin-top: 19.5px; /* form-input과 붙게 조정 */
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none; /* 사용자 클릭 못하게 */
|
|
||||||
position: absolute; /* 시각적으로 띄워두기 */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<!-- 단어 갯수, 작성하기 -->
|
<!-- 단어 갯수, 작성하기 -->
|
||||||
<!-- 왼쪽 사이드바 -->
|
<!-- 왼쪽 사이드바 -->
|
||||||
<div v-if="cateList.length>0" class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
<div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
||||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
||||||
:isActive="writeStore.activeItemId === 999999"/>
|
:isActive="writeStore.activeItemId === 999999"/>
|
||||||
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
||||||
@ -17,10 +17,6 @@
|
|||||||
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange" :showAll="true" :selectedCategory="selectedCategory" />
|
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange" :showAll="true" :selectedCategory="selectedCategory" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
|
||||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
|
||||||
:isActive="writeStore.activeItemId === 999999"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 용어 리스트 컨텐츠 -->
|
<!-- 용어 리스트 컨텐츠 -->
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
@ -33,9 +29,8 @@
|
|||||||
<!-- 에러 메시지 -->
|
<!-- 에러 메시지 -->
|
||||||
<div v-if="error" class="fw-bold text-danger">{{ error }}</div>
|
<div v-if="error" class="fw-bold text-danger">{{ error }}</div>
|
||||||
<!-- 단어 목록 -->
|
<!-- 단어 목록 -->
|
||||||
<ul v-if="total > 0" class="ms-3 list-unstyled">
|
<ul v-if="total > 0" class="ms-3 list-unstyled" style="overflow-x: hidden; word-wrap: break-word;">
|
||||||
<DictCard
|
<DictCard
|
||||||
class="q-editor-container"
|
|
||||||
v-for="item in wordList"
|
v-for="item in wordList"
|
||||||
:key="item.WRDDICSEQ"
|
:key="item.WRDDICSEQ"
|
||||||
:item="item"
|
:item="item"
|
||||||
@ -71,9 +66,6 @@ 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";
|
import LoadingSpinner from "@v/LoadingPage.vue";
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
// 작성창 구분
|
// 작성창 구분
|
||||||
const writeStore = useWriteVisibleStore();
|
const writeStore = useWriteVisibleStore();
|
||||||
@ -119,13 +111,6 @@ import { useRoute } from 'vue-router';
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getIndex();
|
getIndex();
|
||||||
writeStore.closeAll();
|
writeStore.closeAll();
|
||||||
const mainindexKeyword = route.query.indexKeyword || '';
|
|
||||||
const maincategory = route.query.category || '';
|
|
||||||
selectedAlphabet.value = mainindexKeyword;
|
|
||||||
selectedCategory.value = maincategory;
|
|
||||||
if(mainindexKeyword){
|
|
||||||
getwordList('', selectedAlphabet.value, selectedCategory.value );
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshWordList = (category) => {
|
const refreshWordList = (category) => {
|
||||||
@ -147,7 +132,7 @@ import { useRoute } from 'vue-router';
|
|||||||
// 용어 목록 저장
|
// 용어 목록 저장
|
||||||
wordList.value = res.data.data.data;
|
wordList.value = res.data.data.data;
|
||||||
// 총 개수 저장
|
// 총 개수 저장
|
||||||
total.value = res.data.data.data.length;
|
total.value = res.data.data.total;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('데이터 로드 오류:', err);
|
console.error('데이터 로드 오류:', err);
|
||||||
@ -228,7 +213,6 @@ import { useRoute } from 'vue-router';
|
|||||||
sendWordRequest(category, wordData, newCodName);
|
sendWordRequest(category, wordData, newCodName);
|
||||||
};
|
};
|
||||||
const sendWordRequest = (category, wordData, data) => {
|
const sendWordRequest = (category, wordData, data) => {
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
WRDDICCAT: category,
|
WRDDICCAT: category,
|
||||||
WRDDICTTL: wordData.title,
|
WRDDICTTL: wordData.title,
|
||||||
@ -243,15 +227,13 @@ import { useRoute } from 'vue-router';
|
|||||||
writeButton.value.resetButton();
|
writeButton.value.resetButton();
|
||||||
}
|
}
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
const firstChar = getFirstCharacter(wordData.title[0]); // 첫 글자 변환
|
|
||||||
selectedAlphabet.value = firstChar;
|
|
||||||
|
|
||||||
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
|
getwordList(searchText.value, selectedAlphabet.value, selectedCategory.value);
|
||||||
getIndex();
|
getIndex();
|
||||||
if(res.data.data == '2'){
|
if(res.data.data == '2'){
|
||||||
const newCategory = { label: data, value: category };
|
const newCategory = { label: data, value: category };
|
||||||
cateList.value = [...cateList.value,newCategory];
|
cateList.value = [...cateList.value,newCategory];
|
||||||
}
|
}
|
||||||
|
selectedAlphabet.value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -293,32 +275,6 @@ import { useRoute } from 'vue-router';
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//초성 /알파벳 변환
|
|
||||||
const getFirstCharacter = (char) => {
|
|
||||||
const CHOSUNG_LIST = [
|
|
||||||
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
|
|
||||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!char || char.length === 0) return '';
|
|
||||||
|
|
||||||
const code = char.charCodeAt(0);
|
|
||||||
|
|
||||||
// 한글 범위 (가~힣) → 초성 변환
|
|
||||||
if (code >= 0xAC00 && code <= 0xD7A3) {
|
|
||||||
const index = Math.floor((code - 0xAC00) / (21 * 28));
|
|
||||||
return CHOSUNG_LIST[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 영어 소문자 → 대문자로 변환
|
|
||||||
if (char.match(/[a-zA-Z]/)) {
|
|
||||||
return char.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기타 문자 (숫자, 특수문자) 그대로 반환
|
|
||||||
return char;
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -336,11 +292,4 @@ import { useRoute } from 'vue-router';
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-editor-container {
|
|
||||||
max-width: 100%; /* 영역이 넘치지 않게 */
|
|
||||||
overflow: auto; /* 넘치는 내용은 스크롤로 처리 */
|
|
||||||
word-wrap: break-word; /* 긴 단어는 자동으로 줄바꿈 */
|
|
||||||
white-space: normal; /* 내용이 길어지면 자동으로 줄바꿈 */
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user