Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
This commit is contained in:
commit
9e4207de95
@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html
|
<html
|
||||||
lang=""
|
lang=""
|
||||||
class="light-style layout-navbar-fixed layout-menu-fixed layout-compact"
|
class="light-style layout-navbar-fixed layout-menu-fixed layout-compact scrollbar-none"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
data-theme="theme-default"
|
data-theme="theme-default"
|
||||||
data-assets-path="/"
|
data-assets-path="/"
|
||||||
|
|||||||
8467
package-lock.json
generated
8467
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
|||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tinymce/tinymce-vue": "^5.1.1",
|
"@tinymce/tinymce-vue": "^5.1.1",
|
||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
|
"@vueuse/core": "^13.0.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
height: 8px !important;
|
height: 8px !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-size: 0px !important;
|
font-size: 0px !important;
|
||||||
|
margin-left: -0.5% !important;
|
||||||
}
|
}
|
||||||
/* 오후 반차 그래프 (오른쪽 절반) */
|
/* 오후 반차 그래프 (오른쪽 절반) */
|
||||||
.fc-daygrid-event.half-day-pm {
|
.fc-daygrid-event.half-day-pm {
|
||||||
@ -24,6 +25,7 @@
|
|||||||
margin-left: auto !important;
|
margin-left: auto !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-size: 0px !important;
|
font-size: 0px !important;
|
||||||
|
margin-right: -0.5% !important;
|
||||||
}
|
}
|
||||||
/* 연차 그래프 (풀) */
|
/* 연차 그래프 (풀) */
|
||||||
.fc-daygrid-event.full-day {
|
.fc-daygrid-event.full-day {
|
||||||
@ -69,7 +71,7 @@ background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */
|
|||||||
.fc-day-sat-sun {
|
.fc-day-sat-sun {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
/* 과거 날짜 (오늘 이전) */
|
/* 과거 날짜 (오늘 -7일일) */
|
||||||
.fc-daygrid-day.past {
|
.fc-daygrid-day.past {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
@ -423,3 +425,50 @@ cursor: not-allowed !important;
|
|||||||
background-color: #ddd !important;
|
background-color: #ddd !important;
|
||||||
}
|
}
|
||||||
/* project list end */
|
/* project list end */
|
||||||
|
|
||||||
|
/* commuters project list */
|
||||||
|
.commuter-list {
|
||||||
|
max-height: 358px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
/* commuters project list end */
|
||||||
|
|
||||||
|
/* Scroll Button */
|
||||||
|
|
||||||
|
.scroll-top-btn {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: opacity 0.4s ease, visibility 0.4s ease, transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-top-btn.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-top-btn.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll Button end */
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
|
||||||
|
/* 가로모드 모바일 디바이스 (가로 해상도가 768px 보다 작은 화면에 적용) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
aside {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 세로모드 모바일 디바이스 (가로 해상도가 576px 보다 작은 화면에 적용) */
|
||||||
|
@media (max-width: 575px) { }
|
||||||
|
|
||||||
|
/* Mobile end */
|
||||||
@ -124,12 +124,21 @@ const common = {
|
|||||||
* @param { String } profileImg
|
* @param { String } profileImg
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getProfileImage(profileImg) {
|
getProfileImage(profileImg, isAnonymous = false) {
|
||||||
let profileImgUrl = '/img/icons/icon.png'; // 기본 프로필 이미지 경로
|
const defaultProfileImg = '/img/icons/icon.png'; // 기본 프로필 이미지 경로
|
||||||
|
const anonymousImg = '/img/avatars/default-Profile.jpg'; // 익명 이미지
|
||||||
|
let profileImgUrl = isAnonymous ? anonymousImg : defaultProfileImg;
|
||||||
const UserProfile = `${import.meta.env.VITE_SERVER}upload/img/profile/${profileImg}`;
|
const UserProfile = `${import.meta.env.VITE_SERVER}upload/img/profile/${profileImg}`;
|
||||||
|
|
||||||
return !profileImg || profileImg === '' ? profileImgUrl : UserProfile;
|
return !profileImg || profileImg === '' ? profileImgUrl : UserProfile;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setDefaultImage(event, deafultImg = '/img/icons/icon.png') {
|
||||||
|
return (event.target.src = deafultImg);
|
||||||
|
},
|
||||||
|
showImage(event) {
|
||||||
|
return (event.target.style.visibility = 'visible');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -35,6 +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"></textarea>
|
<textarea v-model="localEditedContent" class="form-control"></textarea>
|
||||||
|
<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"></SaveBtn>
|
<SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn>
|
||||||
</div>
|
</div>
|
||||||
@ -60,7 +61,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, computed, watch } from 'vue';
|
import { defineProps, defineEmits, ref, computed, watch, inject } from 'vue';
|
||||||
import BoardProfile from './BoardProfile.vue';
|
import BoardProfile from './BoardProfile.vue';
|
||||||
import BoardCommentArea from './BoardCommentArea.vue';
|
import BoardCommentArea from './BoardCommentArea.vue';
|
||||||
import PlusButton from '../button/PlusBtn.vue';
|
import PlusButton from '../button/PlusBtn.vue';
|
||||||
@ -109,6 +110,7 @@
|
|||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
editCommentAlert: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
// emits 정의
|
// emits 정의
|
||||||
@ -121,6 +123,7 @@
|
|||||||
'submitEdit',
|
'submitEdit',
|
||||||
'cancelEdit',
|
'cancelEdit',
|
||||||
'update:password',
|
'update:password',
|
||||||
|
'inputDetector',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filterInput = event => {
|
const filterInput = event => {
|
||||||
@ -165,6 +168,14 @@
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// text 변화 감지하여 부모에게 전달
|
||||||
|
watch(
|
||||||
|
() => localEditedContent.value,
|
||||||
|
newVal => {
|
||||||
|
emit('inputDetector');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// watch(() => props.comment.isDeleted, () => {
|
// watch(() => props.comment.isDeleted, () => {
|
||||||
// console.log("BoardComment - isDeleted 상태 변경됨:", newVal);
|
// console.log("BoardComment - isDeleted 상태 변경됨:", newVal);
|
||||||
|
|
||||||
|
|||||||
@ -12,50 +12,62 @@
|
|||||||
<!-- 텍스트박스 -->
|
<!-- 텍스트박스 -->
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control"
|
class="form-control mb-2"
|
||||||
placeholder="댓글 달기"
|
placeholder="댓글 달기"
|
||||||
rows="3"
|
rows="3"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
v-model="comment"
|
v-model="comment"
|
||||||
@input="alertTextHandler"
|
@input="alertTextHandler"
|
||||||
></textarea>
|
></textarea>
|
||||||
<span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span>
|
<span v-if="commentAlert" class="invalid-feedback d-inline text-start ms-2 mb-2">{{ commentAlert }}</span>
|
||||||
<span v-else class="invalid-feedback d-block text-start ms-2">{{ textAlert }}</span>
|
<span v-else class="invalid-feedback d-inline text-start ms-2">{{ textAlert }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 옵션 및 버튼 섹션 -->
|
<!-- 옵션 및 버튼 섹션 -->
|
||||||
<div class="d-flex justify-content-between flex-wrap mt-4">
|
<div class="d-flex justify-content-between mt-1">
|
||||||
<div class="d-flex flex-wrap align-items-center">
|
<div class="row g-2">
|
||||||
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
<div class="d-flex flex-wrap align-items-center mb-2">
|
||||||
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
|
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
||||||
<input class="form-check-input" type="checkbox" :id="`checkboxAnnonymous${commnetId}`" v-model="isCheck" />
|
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4 d-flex align-items-center">
|
||||||
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
|
|
||||||
<template v-if="isCheck">
|
|
||||||
<div class="d-flex align-items-center flex-grow-1">
|
|
||||||
<label class="form-label mb-0 me-3" for="basic-default-password">비밀번호</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
class="form-check-input me-2"
|
||||||
id="basic-default-password"
|
type="checkbox"
|
||||||
class="form-control flex-grow-1"
|
:id="`checkboxAnnonymous${commnetId}`"
|
||||||
autocomplete="new-password"
|
v-model="isCheck"
|
||||||
v-model="password"
|
@change="pwd2AlertHandler"
|
||||||
placeholder="비밀번호 입력"
|
|
||||||
@input="passwordAlertTextHandler"
|
|
||||||
/>
|
/>
|
||||||
|
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
|
||||||
|
<template v-if="isCheck">
|
||||||
|
<div class="d-flex align-items-center col">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="basic-default-password"
|
||||||
|
class="form-control w-80"
|
||||||
|
autocomplete="new-password"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="비밀번호 입력"
|
||||||
|
@input="passwordAlertTextHandler"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div style="width: 70px"></div>
|
||||||
|
<div class="col">
|
||||||
|
<span v-if="passwordAlert" class="invalid-feedback d-inline">{{ passwordAlert }}</span>
|
||||||
|
<span v-else class="invalid-feedback d-inline">{{ passwordAlert2 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 답변 쓰기 버튼 -->
|
<!-- 답변 쓰기 버튼 -->
|
||||||
<div class="ms-auto mt-3 mt-md-0">
|
<div class="ms-auto mt-3 mt-md-0">
|
||||||
<SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
|
<SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
|
|
||||||
<span v-else class="invalid-feedback d-block text-start ms-2">{{ passwordAlert2 }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +148,11 @@
|
|||||||
resetCommentForm();
|
resetCommentForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 비밀번호 경고 초기화
|
||||||
|
const pwd2AlertHandler = () => {
|
||||||
|
if (isCheck.value === false) passwordAlert2.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
// 입력 필드 리셋 함수 추가
|
// 입력 필드 리셋 함수 추가
|
||||||
const resetCommentForm = () => {
|
const resetCommentForm = () => {
|
||||||
comment.value = '';
|
comment.value = '';
|
||||||
|
|||||||
@ -11,14 +11,16 @@
|
|||||||
:passwordCommentAlert="passwordCommentAlert || ''"
|
:passwordCommentAlert="passwordCommentAlert || ''"
|
||||||
:currentPasswordCommentId="currentPasswordCommentId"
|
:currentPasswordCommentId="currentPasswordCommentId"
|
||||||
:password="password"
|
:password="password"
|
||||||
|
:editCommentAlert="editCommentAlert[comment.commentId]"
|
||||||
@editClick="handleEditClick"
|
@editClick="handleEditClick"
|
||||||
@deleteClick="handleDeleteClick"
|
@deleteClick="handleDeleteClick"
|
||||||
@submitPassword="submitPassword"
|
@submitPassword="submitPassword"
|
||||||
@submitComment="submitComment"
|
@submitComment="submitComment"
|
||||||
@submitEdit="handleSubmitEdit"
|
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
|
||||||
@cancelEdit="handleCancelEdit"
|
@cancelEdit="handleCancelEdit"
|
||||||
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
|
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
|
||||||
@update:password="updatePassword"
|
@update:password="updatePassword"
|
||||||
|
@inputDetector="$emit('inputDetector')"
|
||||||
>
|
>
|
||||||
<!-- 대댓글 -->
|
<!-- 대댓글 -->
|
||||||
<template #reply>
|
<template #reply>
|
||||||
@ -35,14 +37,16 @@
|
|||||||
:currentPasswordCommentId="currentPasswordCommentId"
|
:currentPasswordCommentId="currentPasswordCommentId"
|
||||||
:passwordCommentAlert="passwordCommentAlert"
|
:passwordCommentAlert="passwordCommentAlert"
|
||||||
:password="password"
|
:password="password"
|
||||||
|
:editCommentAlert="editCommentAlert[child.commentId]"
|
||||||
@editClick="handleReplyEditClick"
|
@editClick="handleReplyEditClick"
|
||||||
@deleteClick="$emit('deleteClick', child)"
|
@deleteClick="$emit('deleteClick', child)"
|
||||||
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
|
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent, child.commentId)"
|
||||||
@cancelEdit="$emit('cancelEdit', child)"
|
@cancelEdit="$emit('cancelEdit', child)"
|
||||||
@submitComment="submitComment"
|
@submitComment="submitComment"
|
||||||
@updateReaction="handleUpdateReaction"
|
@updateReaction="handleUpdateReaction"
|
||||||
@submitPassword="$emit('submitPassword', child, password)"
|
@submitPassword="$emit('submitPassword', child, password)"
|
||||||
@update:password="$emit('update:password', $event)"
|
@update:password="$emit('update:password', $event)"
|
||||||
|
@inputDetector="$emit('inputDetector')"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -95,6 +99,7 @@
|
|||||||
index: {
|
index: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
editCommentAlert: Object,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@ -106,6 +111,7 @@
|
|||||||
'clearPassword',
|
'clearPassword',
|
||||||
'submitEdit',
|
'submitEdit',
|
||||||
'update:password',
|
'update:password',
|
||||||
|
'inputDetector',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const submitComment = replyData => {
|
const submitComment = replyData => {
|
||||||
|
|||||||
@ -2,7 +2,13 @@
|
|||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar me-2">
|
<div class="avatar me-2">
|
||||||
<img :src="getProfileImage(profileImg)" alt="Avatar" class="rounded-circle" />
|
<img
|
||||||
|
:src="getProfileImage(profileImg)"
|
||||||
|
alt="user"
|
||||||
|
class="rounded-circle"
|
||||||
|
@error="setDefaultImage($event)"
|
||||||
|
@load="showImage($event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="me-2">
|
<div class="me-2">
|
||||||
@ -120,6 +126,14 @@
|
|||||||
|
|
||||||
// 프로필 이미지 경로 설정
|
// 프로필 이미지 경로 설정
|
||||||
const getProfileImage = profileImg => {
|
const getProfileImage = profileImg => {
|
||||||
return $common.getProfileImage(profileImg);
|
return $common.getProfileImage(profileImg, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDefaultImage = e => {
|
||||||
|
return $common.setDefaultImage(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showImage = e => {
|
||||||
|
return $common.showImage(e);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
32
src/components/button/ScrollTopButton.vue
Normal file
32
src/components/button/ScrollTopButton.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="scrollToTop"
|
||||||
|
class="scroll-top-btn rounded-pill btn-icon btn-primary position-fixed shadow z-5 border-0"
|
||||||
|
:class="{ 'visible': showButton, 'hidden': !showButton }"
|
||||||
|
>
|
||||||
|
<i class='bx bx-chevron-up'></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const showButton = ref(false);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
showButton.value = window.scrollY > 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
|
||||||
<div class="card app-calendar-wrapper">
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-3 border-end text-center">
|
|
||||||
<div class="card-body pb-0">
|
|
||||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-auto rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
|
||||||
<p class="mt-2">
|
|
||||||
{{ user.name }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="row g-0">
|
|
||||||
<div class="col-6 pe-1">
|
|
||||||
<p>출근시간</p>
|
|
||||||
<button class="btn btn-outline-primary border-3 w-100 py-0">
|
|
||||||
<i class='bx bx-run fs-2'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6 ps-1">
|
|
||||||
<p>퇴근시간</p>
|
|
||||||
<button class="btn btn-outline-secondary border-3 w-100 py-0">
|
|
||||||
<i class='bx bxs-door-open fs-2'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-for="post in project" :key="post.PROJCTSEQ" class="border border-2 mt-3" :style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`">
|
|
||||||
{{ post.PROJCTNAM }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col app-calendar-content">
|
|
||||||
<div class="card shadow-none border-0">
|
|
||||||
<div class="card-body pb-0">
|
|
||||||
<full-calendar
|
|
||||||
ref="fullCalendarRef"
|
|
||||||
:events="calendarEvents"
|
|
||||||
:options="calendarOptions"
|
|
||||||
defaultView="dayGridMonth"
|
|
||||||
class="flatpickr-calendar-only"
|
|
||||||
>
|
|
||||||
</full-calendar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<center-modal :display="isModalVisible" @close="isModalVisible = $event">
|
|
||||||
<template #title> Add Event </template>
|
|
||||||
<template #body>
|
|
||||||
<FormInput
|
|
||||||
title="이벤트 제목"
|
|
||||||
name="event"
|
|
||||||
:is-essential="true"
|
|
||||||
:is-alert="eventAlert"
|
|
||||||
@update:data="eventTitle = $event"
|
|
||||||
/>
|
|
||||||
<FormInput
|
|
||||||
title="이벤트 날짜"
|
|
||||||
type="date"
|
|
||||||
name="eventDate"
|
|
||||||
:is-essential="true"
|
|
||||||
:is-alert="eventDateAlert"
|
|
||||||
@update:data="eventDate = $event"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<button @click="addEvent">추가</button>
|
|
||||||
</template>
|
|
||||||
</center-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import FullCalendar from '@fullcalendar/vue3';
|
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
|
||||||
import CenterModal from '@c/modal/CenterModal.vue';
|
|
||||||
import { inject, onMounted, reactive, ref, watch } from 'vue';
|
|
||||||
import $api from '@api';
|
|
||||||
import { isEmpty } from '@/common/utils';
|
|
||||||
import FormInput from '../input/FormInput.vue';
|
|
||||||
import 'flatpickr/dist/flatpickr.min.css';
|
|
||||||
import '@/assets/css/app-calendar.css';
|
|
||||||
import { fetchHolidays } from '@c/calendar/holiday';
|
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
|
||||||
import { useProjectStore } from '@/stores/useProjectStore';
|
|
||||||
|
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
|
||||||
const user = ref({});
|
|
||||||
const project = ref({});
|
|
||||||
const userStore = useUserInfoStore();
|
|
||||||
const projectStore = useProjectStore();
|
|
||||||
|
|
||||||
const dayjs = inject('dayjs');
|
|
||||||
const fullCalendarRef = ref(null);
|
|
||||||
const calendarEvents = ref([]);
|
|
||||||
const isModalVisible = ref(false);
|
|
||||||
const eventAlert = ref(false);
|
|
||||||
const eventDateAlert = ref(false);
|
|
||||||
const eventTitle = ref('');
|
|
||||||
const eventDate = ref('');
|
|
||||||
const selectedDate = ref(null);
|
|
||||||
|
|
||||||
|
|
||||||
// 날짜 선택 핸들러
|
|
||||||
const handleDateSelect = (selectedDates) => {
|
|
||||||
if (selectedDates.length > 0) {
|
|
||||||
// 선택된 첫 번째 날짜를 YYYY-MM-DD 형식으로 변환
|
|
||||||
const selectedDate = dayjs(selectedDates[0]).format('YYYY-MM-DD');
|
|
||||||
eventDate.value = selectedDate;
|
|
||||||
showModal(); // 모달 표시
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 캘린더 데이터 가져오기
|
|
||||||
const fetchData = async () => {
|
|
||||||
// FullCalendar API 인스턴스 가져오기
|
|
||||||
const calendarApi = fullCalendarRef.value?.getApi();
|
|
||||||
if (!calendarApi) return;
|
|
||||||
|
|
||||||
// 현재 표시된 달력의 연도, 월 추출
|
|
||||||
const date = calendarApi.currentData.viewTitle;
|
|
||||||
const dateArr = date.split(' ');
|
|
||||||
let currentYear = dateArr[0].trim();
|
|
||||||
let currentMonth = dateArr[1].trim();
|
|
||||||
const regex = /\D/g;
|
|
||||||
// 숫자가 아닌 문자 제거 후 정수로 변환
|
|
||||||
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
|
||||||
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 현재 표시 중인 월의 공휴일 정보 가져오기
|
|
||||||
const holidayEvents = await fetchHolidays(currentYear, String(currentMonth).padStart(2, '0'));
|
|
||||||
// 기존 이벤트에서 공휴일 이벤트를 제외한 이벤트만 필터링
|
|
||||||
const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event'));
|
|
||||||
// 필터링된 이벤트와 새로 가져온 공휴일 이벤트 병합
|
|
||||||
calendarEvents.value = [...existingEvents, ...holidayEvents];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('공휴일 정보 로딩 실패:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 캘린더 이동 함수 (이전, 다음, 오늘)
|
|
||||||
const moveCalendar = async (value = 0) => {
|
|
||||||
const calendarApi = fullCalendarRef.value?.getApi();
|
|
||||||
|
|
||||||
if (value === 1) {
|
|
||||||
calendarApi.prev(); // 이전 달로 이동
|
|
||||||
} else if (value === 2) {
|
|
||||||
calendarApi.next(); // 다음 달로 이동
|
|
||||||
} else if (value === 3) {
|
|
||||||
calendarApi.today(); // 오늘 날짜로 이동
|
|
||||||
}
|
|
||||||
|
|
||||||
// 캘린더 이동 후 데이터 다시 가져오기
|
|
||||||
await fetchData();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달 표시 함수
|
|
||||||
const showModal = () => {
|
|
||||||
isModalVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달 닫기 함수
|
|
||||||
const closeModal = () => {
|
|
||||||
isModalVisible.value = false;
|
|
||||||
// 입력 필드 초기화
|
|
||||||
eventTitle.value = '';
|
|
||||||
eventDate.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 이벤트 추가 함수
|
|
||||||
const addEvent = () => {
|
|
||||||
// 이벤트 유효성 검사
|
|
||||||
if (!checkEvent()) {
|
|
||||||
// 유효성 검사 통과 시 이벤트 추가
|
|
||||||
calendarEvents.value.push({
|
|
||||||
title: eventTitle.value,
|
|
||||||
start: eventDate.value,
|
|
||||||
backgroundColor: '#4CAF50' // 일반 이벤트 색상
|
|
||||||
});
|
|
||||||
closeModal(); // 모달 닫기
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 이벤트 유효성 검사 함수
|
|
||||||
const checkEvent = () => {
|
|
||||||
// 제목과 날짜가 비어있는지 확인
|
|
||||||
eventAlert.value = isEmpty(eventTitle.value);
|
|
||||||
eventDateAlert.value = isEmpty(eventDate.value);
|
|
||||||
// 하나라도 비어있으면 true 반환 (유효성 검사 실패)
|
|
||||||
return eventAlert.value || eventDateAlert.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 캘린더 옵션 설정
|
|
||||||
const calendarOptions = reactive({
|
|
||||||
plugins: [dayGridPlugin, interactionPlugin], // 사용할 플러그인
|
|
||||||
initialView: 'dayGridMonth', // 초기 뷰 (월간)
|
|
||||||
headerToolbar: { // 상단 툴바 구성
|
|
||||||
left: 'today', // 왼쪽: 오늘 버튼
|
|
||||||
center: 'title', // 중앙: 제목(연월)
|
|
||||||
right: 'prev,next', // 오른쪽: 이전/다음 버튼
|
|
||||||
},
|
|
||||||
locale: 'kr', // 한국어 지역화
|
|
||||||
events: calendarEvents, // 표시할 이벤트 데이터
|
|
||||||
eventOrder: 'sortIdx', // 이벤트 정렬 기준
|
|
||||||
selectable: true, // 날짜 선택 가능 여부
|
|
||||||
dateClick: handleDateSelect, // 날짜 클릭 이벤트 핸들러
|
|
||||||
droppable: false, // 드래그 앤 드롭 비활성화
|
|
||||||
eventDisplay: 'block', // 이벤트 표시 방식
|
|
||||||
|
|
||||||
// 커스텀 버튼 정의
|
|
||||||
customButtons: {
|
|
||||||
prev: {
|
|
||||||
text: 'PREV', // 이전 버튼 텍스트
|
|
||||||
click: () => moveCalendar(1), // 클릭 시 이전 달로 이동
|
|
||||||
},
|
|
||||||
today: {
|
|
||||||
text: 'TODAY', // 오늘 버튼 텍스트
|
|
||||||
click: () => moveCalendar(3), // 클릭 시 오늘로 이동
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
text: 'NEXT', // 다음 버튼 텍스트
|
|
||||||
click: () => moveCalendar(2), // 클릭 시 다음 달로 이동
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
|
||||||
watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => {
|
|
||||||
await fetchData();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
console.log(project)
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData();
|
|
||||||
await userStore.userInfo();
|
|
||||||
user.value = userStore.user;
|
|
||||||
await projectStore.getProjectList();
|
|
||||||
project.value = projectStore.projectList;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
176
src/components/commuters/CommuterBtn.vue
Normal file
176
src/components/commuters/CommuterBtn.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-6 pe-1">
|
||||||
|
<button
|
||||||
|
class="btn border-3 w-100 py-0 h-px-50"
|
||||||
|
:class="workTime ? 'p-0 btn-primary pe-none' : 'btn-outline-primary'"
|
||||||
|
@click="setWorkTime"
|
||||||
|
>
|
||||||
|
<i v-if="!workTime" class="bx bx-run fs-2"></i>
|
||||||
|
<span v-if="workTime" class="ql-size-12px">{{ workTime }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 ps-1">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50"
|
||||||
|
@click="setLeaveTime"
|
||||||
|
>
|
||||||
|
<i v-if="!leaveTime" class='bx bxs-door-open fs-2'></i>
|
||||||
|
<span v-if="leaveTime" class="ql-size-12px">{{ leaveTime }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, onMounted, watch } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
import { useGeolocation } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
userId: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
checkedInProject: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated']);
|
||||||
|
|
||||||
|
const workTime = ref(null);
|
||||||
|
const leaveTime = ref(null)
|
||||||
|
const userLocation = ref(null);
|
||||||
|
|
||||||
|
// 위치 정보 가져오기 설정
|
||||||
|
const { coords, isSupported, error } = useGeolocation({
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 주소 변환 함수
|
||||||
|
const getAddress = (lat, lng) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const geocoder = new kakao.maps.services.Geocoder();
|
||||||
|
const coord = new kakao.maps.LatLng(lat, lng);
|
||||||
|
|
||||||
|
geocoder.coord2Address(coord.getLng(), coord.getLat(), (result, status) => {
|
||||||
|
if (status === kakao.maps.services.Status.OK) {
|
||||||
|
const address = result[0].address.address_name;
|
||||||
|
resolve(address);
|
||||||
|
} else {
|
||||||
|
reject('주소를 가져올 수 없습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 위치 정보 가져오기 함수
|
||||||
|
const getLocation = async () => {
|
||||||
|
if (!isSupported.value) {
|
||||||
|
alert('브라우저가 위치 정보를 지원하지 않습니다.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
alert(`위치 정보를 가져오는데 실패했습니다: ${error.value.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coords.value) {
|
||||||
|
userLocation.value = {
|
||||||
|
lat: coords.value.latitude,
|
||||||
|
lng: coords.value.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const address = await getAddress(coords.value.latitude, coords.value.longitude);
|
||||||
|
return address;
|
||||||
|
} catch (error) {
|
||||||
|
alert(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 오늘 사용자의 출근 정보 조회
|
||||||
|
const todayCommuterInfo = async () => {
|
||||||
|
if (!props.userId) return;
|
||||||
|
|
||||||
|
const res = await $api.get(`commuters/today/${props.userId}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
const commuterInfo = res.data.data[0];
|
||||||
|
|
||||||
|
if (commuterInfo) {
|
||||||
|
workTime.value = commuterInfo.COMMUTCMT;
|
||||||
|
leaveTime.value = commuterInfo.COMMUTLVE;
|
||||||
|
|
||||||
|
// 부모 컴포넌트에 상태 업데이트 알림
|
||||||
|
emit('workTimeUpdated', workTime.value);
|
||||||
|
emit('leaveTimeUpdated', leaveTime.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 출근 시간
|
||||||
|
const setWorkTime = async () => {
|
||||||
|
// 이미 출근 시간이 설정된 경우 중복 실행 방지
|
||||||
|
if (workTime.value) return;
|
||||||
|
|
||||||
|
// 현재 위치 주소 가져오기
|
||||||
|
const address = await getLocation();
|
||||||
|
|
||||||
|
$api.post('commuters/insert', {
|
||||||
|
memberSeq: props.userId,
|
||||||
|
projctSeq: props.checkedInProject.PROJCTSEQ,
|
||||||
|
commutLvt: null,
|
||||||
|
commutArr: address,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
todayCommuterInfo();
|
||||||
|
|
||||||
|
emit('workTimeUpdated', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 퇴근
|
||||||
|
const setLeaveTime = () => {
|
||||||
|
$api.patch('commuters/updateLve', {
|
||||||
|
memberSeq: props.userId,
|
||||||
|
commutLve: leaveTime.value || null,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
todayCommuterInfo();
|
||||||
|
// 부모 컴포넌트에 업데이트 이벤트 발생
|
||||||
|
emit('leaveTimeUpdated', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// props 변경 감지
|
||||||
|
watch(() => props.userId, async () => {
|
||||||
|
if (props.userId) {
|
||||||
|
await todayCommuterInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.checkedInProject, () => {
|
||||||
|
// 프로젝트가 변경되면 필요한 처리 수행
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await todayCommuterInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 외부에서 접근할 메서드 노출
|
||||||
|
defineExpose({
|
||||||
|
todayCommuterInfo,
|
||||||
|
workTime,
|
||||||
|
leaveTime
|
||||||
|
});
|
||||||
|
</script>
|
||||||
411
src/components/commuters/CommuterCalendar.vue
Normal file
411
src/components/commuters/CommuterCalendar.vue
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl flex-grow-1 container-p-y pb-0">
|
||||||
|
<div class="card app-calendar-wrapper">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-3 border-end text-center">
|
||||||
|
<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" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||||
|
<p class="mt-2 fw-bold">
|
||||||
|
{{ user.name }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CommuterBtn
|
||||||
|
:userId="user.id"
|
||||||
|
:checkedInProject="checkedInProject || {}"
|
||||||
|
@workTimeUpdated="handleWorkTimeUpdate"
|
||||||
|
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||||
|
ref="workTimeComponentRef"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommuterProjectList
|
||||||
|
:project="project"
|
||||||
|
:commuters="commuters"
|
||||||
|
:baseUrl="baseUrl"
|
||||||
|
:user="user"
|
||||||
|
:selectedProject="selectedProject"
|
||||||
|
:checkedInProject="checkedInProject"
|
||||||
|
@drop="handleProjectDrop"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col app-calendar-content">
|
||||||
|
<div class="card shadow-none border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<full-calendar
|
||||||
|
ref="fullCalendarRef"
|
||||||
|
:events="calendarEvents"
|
||||||
|
:options="calendarOptions"
|
||||||
|
defaultView="dayGridMonth"
|
||||||
|
class="flatpickr-calendar-only"
|
||||||
|
>
|
||||||
|
</full-calendar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title>
|
||||||
|
{{ eventDate }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedDateCommuters.length > 0">
|
||||||
|
<div v-for="(commuter, index) in selectedDateCommuters" :key="index">
|
||||||
|
<div class="d-flex align-items-center my-2">
|
||||||
|
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
|
class="rounded-circle me-2 w-px-50 h-px-50"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'">
|
||||||
|
|
||||||
|
<span class="text-white fw-bold rounded py-1 px-3" :style="`background: ${commuter.projctcolor} !important;`">{{ commuter.memberName }}</span>
|
||||||
|
|
||||||
|
<div class="ms-auto text-start fw-bold">
|
||||||
|
{{ commuter.COMMUTCMT }} ~ {{ commuter.COMMUTLVE || "00:00:00" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<BackBtn @click="closeModal" />
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FullCalendar from '@fullcalendar/vue3';
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
|
import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
import 'flatpickr/dist/flatpickr.min.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 CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
|
||||||
|
import BackBtn from '@c/button/BackBtn.vue';
|
||||||
|
|
||||||
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
|
const user = ref({});
|
||||||
|
const project = ref({});
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
|
||||||
|
const dayjs = inject('dayjs');
|
||||||
|
const fullCalendarRef = ref(null);
|
||||||
|
const workTimeComponentRef = ref(null);
|
||||||
|
const calendarEvents = ref([]);
|
||||||
|
const eventDate = ref('');
|
||||||
|
|
||||||
|
const selectedProject = ref(null);
|
||||||
|
const checkedInProject = ref(null);
|
||||||
|
|
||||||
|
// 모달 상태
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
|
||||||
|
const commuters = ref([]);
|
||||||
|
const monthlyCommuters = ref([]);
|
||||||
|
|
||||||
|
|
||||||
|
// 출퇴근 컴포넌트 이벤트 핸들러
|
||||||
|
const handleWorkTimeUpdate = () => {
|
||||||
|
todaysCommuter();
|
||||||
|
loadCommuters();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveTimeUpdate = () => {
|
||||||
|
todaysCommuter();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 드롭 이벤트 핸들러 (ProjectList 컴포넌트에서 전달받음)
|
||||||
|
const handleProjectDrop = ({ event, targetProject }) => {
|
||||||
|
// 드래그된 프로젝트 데이터 가져오기
|
||||||
|
const draggedProjectData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||||
|
|
||||||
|
// 드래그한 프로젝트와 드롭한 프로젝트가 같으면 아무 동작 안 함
|
||||||
|
if (draggedProjectData.PROJCTSEQ === targetProject.PROJCTSEQ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 프로젝트 변경
|
||||||
|
checkedInProject.value = targetProject;
|
||||||
|
projectStore.setSelectedProject(targetProject);
|
||||||
|
|
||||||
|
// select 값도 변경
|
||||||
|
selectedProject.value = targetProject.PROJCTSEQ;
|
||||||
|
|
||||||
|
$api.patch('commuters/update', {
|
||||||
|
projctSeq: targetProject.PROJCTSEQ,
|
||||||
|
memberSeq: user.value.id,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
todaysCommuter();
|
||||||
|
loadCommuters();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 오늘 출근 모든 사용자 조회
|
||||||
|
const todaysCommuter = async () => {
|
||||||
|
const res = await $api.get(`commuters/todays`);
|
||||||
|
if (res.status === 200 ) {
|
||||||
|
commuters.value = res.data.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 캘린더 데이터 가져오기
|
||||||
|
const fetchData = async () => {
|
||||||
|
// FullCalendar API 인스턴스 가져오기
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
if (!calendarApi) return;
|
||||||
|
|
||||||
|
// 현재 표시된 달력의 연도, 월 추출
|
||||||
|
const date = calendarApi.currentData.viewTitle;
|
||||||
|
const dateArr = date.split(' ');
|
||||||
|
let currentYear = dateArr[0].trim();
|
||||||
|
let currentMonth = dateArr[1].trim();
|
||||||
|
const regex = /\D/g;
|
||||||
|
// 숫자가 아닌 문자 제거 후 정수로 변환
|
||||||
|
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
||||||
|
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 현재 표시 중인 월의 공휴일 정보 가져오기
|
||||||
|
const holidayEvents = await fetchHolidays(currentYear, String(currentMonth).padStart(2, '0'));
|
||||||
|
// 기존 이벤트에서 공휴일 이벤트를 제외한 이벤트만 필터링
|
||||||
|
const existingEvents = calendarEvents.value.filter(event => !event.classNames?.includes('holiday-event'));
|
||||||
|
// 필터링된 이벤트와 새로 가져온 공휴일 이벤트 병합
|
||||||
|
calendarEvents.value = [...existingEvents, ...holidayEvents];
|
||||||
|
|
||||||
|
// 출근 정보
|
||||||
|
await loadCommuters();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('공휴일 정보 로딩 실패:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 캘린더 이동 함수 (이전, 다음, 오늘)
|
||||||
|
const moveCalendar = async (value = 0) => {
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
|
||||||
|
if (value === 1) {
|
||||||
|
calendarApi.prev(); // 이전 달로 이동
|
||||||
|
} else if (value === 2) {
|
||||||
|
calendarApi.next(); // 다음 달로 이동
|
||||||
|
} else if (value === 3) {
|
||||||
|
calendarApi.today(); // 오늘 날짜로 이동
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 선택 가능 여부를 확인하는 공통 함수
|
||||||
|
const isSelectableDate = (date) => {
|
||||||
|
const checkDate = dayjs(date);
|
||||||
|
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6;
|
||||||
|
// 공휴일 체크
|
||||||
|
const isHoliday = calendarEvents.value.some(event =>
|
||||||
|
event.classNames?.includes('holiday-event') &&
|
||||||
|
dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD')
|
||||||
|
);
|
||||||
|
|
||||||
|
return !isWeekend && !isHoliday;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 날짜 클릭 이벤트 함수
|
||||||
|
let todayElement = null;
|
||||||
|
const handleDateClick = (info) => {
|
||||||
|
const clickedDate = dayjs(info.date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 클릭한 날짜에 월별 출근 정보가 있는지 확인
|
||||||
|
const dateCommuters = monthlyCommuters.value.filter(commuter =>
|
||||||
|
commuter.COMMUTDAY === clickedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
// 출근 기록이 있는 경우에만 모달 열기
|
||||||
|
if (dateCommuters.length > 0) {
|
||||||
|
eventDate.value = clickedDate;
|
||||||
|
isModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelectableDate(info.date)) {
|
||||||
|
const isToday = dayjs(info.date).isSame(dayjs(), 'day');
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
|
||||||
|
todayElement = info.dayEl;
|
||||||
|
todayElement.classList.remove('fc-day-today');
|
||||||
|
} else if (todayElement) {
|
||||||
|
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
|
||||||
|
todayElement.classList.add('fc-day-today');
|
||||||
|
todayElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 바깥 클릭 시 todayElement 클래스 복구
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (todayElement && !event.target.closest('.fc-daygrid-day')) {
|
||||||
|
todayElement.classList.add('fc-day-today');
|
||||||
|
todayElement = null;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// 날짜 셀 클래스 추가 함수
|
||||||
|
const getCellClassNames = (arg) => {
|
||||||
|
const cellDate = dayjs(arg.date);
|
||||||
|
const classes = [];
|
||||||
|
|
||||||
|
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
|
||||||
|
if (!isSelectableDate(cellDate)) {
|
||||||
|
classes.push('fc-day-sat-sun');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 현재 달의 모든 출근 정보 조회
|
||||||
|
const loadCommuters = async () => {
|
||||||
|
const calendarApi = fullCalendarRef.value?.getApi();
|
||||||
|
if (!calendarApi) return;
|
||||||
|
const date = calendarApi.currentData.viewTitle;
|
||||||
|
const dateArr = date.split(' ');
|
||||||
|
let currentYear = dateArr[0].trim();
|
||||||
|
let currentMonth = dateArr[1].trim();
|
||||||
|
const regex = /\D/g;
|
||||||
|
currentYear = parseInt(currentYear.replace(regex, ''), 10);
|
||||||
|
currentMonth = parseInt(currentMonth.replace(regex, ''), 10);
|
||||||
|
|
||||||
|
const res = await $api.get('commuters/month', {
|
||||||
|
params: {
|
||||||
|
year: currentYear,
|
||||||
|
month: currentMonth
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
// 월별 출근 정보 저장
|
||||||
|
monthlyCommuters.value = res.data.data;
|
||||||
|
|
||||||
|
document.querySelectorAll('.fc-daygrid-day-events img.rounded-circle').forEach(img => {
|
||||||
|
img.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
monthlyCommuters.value.forEach(commuter => {
|
||||||
|
const date = commuter.COMMUTDAY;
|
||||||
|
const dateCell = document.querySelector(`.fc-day[data-date="${date}"]`) ||
|
||||||
|
document.querySelector(`.fc-daygrid-day[data-date="${date}"]`);
|
||||||
|
if (dateCell) {
|
||||||
|
const dayEvents = dateCell.querySelector('.fc-daygrid-day-events');
|
||||||
|
if (dayEvents) {
|
||||||
|
dayEvents.classList.add('text-center');
|
||||||
|
// 프로필 이미지 생성
|
||||||
|
const profileImg = document.createElement('img');
|
||||||
|
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';
|
||||||
|
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
|
||||||
|
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
|
||||||
|
|
||||||
|
dayEvents.appendChild(profileImg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 캘린더 옵션 설정
|
||||||
|
const calendarOptions = reactive({
|
||||||
|
plugins: [dayGridPlugin, interactionPlugin],
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'prev,next',
|
||||||
|
},
|
||||||
|
locale: 'kr',
|
||||||
|
events: calendarEvents,
|
||||||
|
eventOrder: 'sortIdx',
|
||||||
|
contentHeight:"auto",
|
||||||
|
// eventContent: calendarCommuter,
|
||||||
|
// 날짜 선택 관련 옵션 수정
|
||||||
|
selectable: true,
|
||||||
|
selectAllow: (selectInfo) => isSelectableDate(selectInfo.start),
|
||||||
|
dateClick: handleDateClick,
|
||||||
|
dayCellClassNames: getCellClassNames,
|
||||||
|
|
||||||
|
// 날짜 클릭 비활성화를 위한 추가 설정
|
||||||
|
unselectAuto: true,
|
||||||
|
droppable: false,
|
||||||
|
eventDisplay: 'block',
|
||||||
|
|
||||||
|
// 커스텀 버튼 정의
|
||||||
|
customButtons: {
|
||||||
|
prev: {
|
||||||
|
text: 'PREV',
|
||||||
|
click: () => moveCalendar(1),
|
||||||
|
},
|
||||||
|
today: {
|
||||||
|
text: 'TODAY',
|
||||||
|
click: () => moveCalendar(3),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
text: 'NEXT',
|
||||||
|
click: () => moveCalendar(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
||||||
|
watch(() => fullCalendarRef.value?.getApi().currentData.viewTitle, async () => {
|
||||||
|
await fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// selectedProject 변경 감지
|
||||||
|
watch(() => projectStore.selectedProject, (newProject) => {
|
||||||
|
if (newProject) {
|
||||||
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
|
checkedInProject.value = newProject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 모달 열기
|
||||||
|
const openModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 닫기
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDateCommuters = computed(() => {
|
||||||
|
return monthlyCommuters.value.filter(commuter =>
|
||||||
|
commuter.COMMUTDAY === eventDate.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
await userStore.userInfo();
|
||||||
|
user.value = userStore.user;
|
||||||
|
await projectStore.getProjectList('', '', 'true');
|
||||||
|
project.value = projectStore.projectList;
|
||||||
|
|
||||||
|
await todaysCommuter();
|
||||||
|
|
||||||
|
// 저장된 선택 프로젝트 가져오기
|
||||||
|
const storedProject = projectStore.getSelectedProject();
|
||||||
|
if (storedProject) {
|
||||||
|
selectedProject.value = storedProject.PROJCTSEQ;
|
||||||
|
checkedInProject.value = storedProject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
82
src/components/commuters/CommuterProjectList.vue
Normal file
82
src/components/commuters/CommuterProjectList.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="commuter-list">
|
||||||
|
<div v-for="post in project" :key="post.PROJCTSEQ"
|
||||||
|
class="border border-2 mt-3 card p-2"
|
||||||
|
:style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`"
|
||||||
|
@dragover="allowDrop($event)"
|
||||||
|
@drop="handleDrop($event, post)">
|
||||||
|
<p class="mb-1">
|
||||||
|
{{ post.PROJCTNAM }}
|
||||||
|
</p>
|
||||||
|
<div class="row gx-2">
|
||||||
|
<div v-for="commuter in commuters.filter(c => c.PROJCTNAM === post.PROJCTNAM)" :key="commuter.COMMUTCMT" class="col-4">
|
||||||
|
<div class="ratio ratio-1x1">
|
||||||
|
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||||
|
alt="User Profile"
|
||||||
|
class="rounded-circle"
|
||||||
|
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
||||||
|
:draggable="isCurrentUser(commuter)"
|
||||||
|
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
||||||
|
@error="$event.target.src = '/img/icons/icon.png'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
commuters: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
selectedProject: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
checkedInProject: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['drop', 'update:selectedProject', 'update:checkedInProject']);
|
||||||
|
|
||||||
|
// 현재 사용자 확인
|
||||||
|
const isCurrentUser = (commuter) => {
|
||||||
|
return props.user && commuter && commuter.MEMBERSEQ === props.user.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드래그 시작 이벤트 핸들러
|
||||||
|
const dragStart = (event, project) => {
|
||||||
|
// 드래그 데이터 설정
|
||||||
|
event.dataTransfer.setData('application/json', JSON.stringify(project));
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드래그 오버 드롭 허용
|
||||||
|
const allowDrop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 드롭
|
||||||
|
const handleDrop = (event, targetProject) => {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('drop', { event, targetProject });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -128,7 +128,7 @@
|
|||||||
title="종료일"
|
title="종료일"
|
||||||
type="date"
|
type="date"
|
||||||
name="endDay"
|
name="endDay"
|
||||||
:min="todays"
|
:min="selectedProject.PROJCTSTR"
|
||||||
:modelValue="selectedProject.PROJCTEND"
|
:modelValue="selectedProject.PROJCTEND"
|
||||||
@update:modelValue="selectedProject.PROJCTEND = $event"
|
@update:modelValue="selectedProject.PROJCTEND = $event"
|
||||||
/>
|
/>
|
||||||
@ -161,7 +161,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, onMounted, ref, computed, watch, inject } from 'vue';
|
import { defineProps, onMounted, ref, computed, watch } from 'vue';
|
||||||
import UserList from '@c/user/UserList.vue';
|
import UserList from '@c/user/UserList.vue';
|
||||||
import CenterModal from '@c/modal/CenterModal.vue';
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
@ -177,10 +177,12 @@ import ArrInput from '@c/input/ArrInput.vue';
|
|||||||
import { useToastStore } from '@s/toastStore';
|
import { useToastStore } from '@s/toastStore';
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import commonApi from '@/common/commonApi';
|
import commonApi from '@/common/commonApi';
|
||||||
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
|
|
||||||
// 스토어
|
// 스토어
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -254,11 +256,6 @@ const isProjectCreator = computed(() => {
|
|||||||
return user.value?.id === props.projctCreatorId;
|
return user.value?.id === props.projctCreatorId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// dayjs 인스턴스 가져오기
|
|
||||||
const dayjs = inject('dayjs');
|
|
||||||
|
|
||||||
// 오늘 날짜를 YYYY-MM-DD 형식으로 변환
|
|
||||||
const todays = dayjs().format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
||||||
const isProjectExpired = computed(() => {
|
const isProjectExpired = computed(() => {
|
||||||
@ -330,6 +327,19 @@ const closeModal = () => {
|
|||||||
|
|
||||||
// 수정 모달 열기
|
// 수정 모달 열기
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
|
selectedProject.value = {
|
||||||
|
PROJCTSEQ: props.projctSeq,
|
||||||
|
PROJCTNAM: props.title,
|
||||||
|
PROJCTSTR: props.strdate,
|
||||||
|
PROJCTEND: props.enddate,
|
||||||
|
PROJCTZIP: props.addressZip,
|
||||||
|
PROJCTARR: props.address,
|
||||||
|
PROJCTDTL: props.addressdtail,
|
||||||
|
PROJCTDES: props.description,
|
||||||
|
PROJCTCOL: props.projctCol,
|
||||||
|
projctcolor: props.projctColor,
|
||||||
|
};
|
||||||
|
|
||||||
isEditModalOpen.value = true;
|
isEditModalOpen.value = true;
|
||||||
originalColor.value = props.projctCol;
|
originalColor.value = props.projctCol;
|
||||||
};
|
};
|
||||||
@ -364,6 +374,18 @@ const hasChanges = computed(() => {
|
|||||||
selectedProject.value.PROJCTCOL !== props.projctCol;
|
selectedProject.value.PROJCTCOL !== props.projctCol;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 시작일 또는 종료일이 변경될 때 종료일의 최소값을 설정
|
||||||
|
watch(
|
||||||
|
() => selectedProject.value.PROJCTSTR, // 시작일 (strdate)
|
||||||
|
(newStartDate) => {
|
||||||
|
if (newStartDate && new Date(newStartDate) > new Date(selectedProject.value.PROJCTEND)) {
|
||||||
|
// 시작일이 종료일보다 크면 종료일을 시작일로 설정
|
||||||
|
selectedProject.value.PROJCTEND = newStartDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// 프로젝트 수정
|
// 프로젝트 수정
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
|
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
|
||||||
@ -393,7 +415,7 @@ const handleUpdate = () => {
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
// 상위 컴포넌트에 업데이트 알림
|
|
||||||
emit('update');
|
emit('update');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -448,7 +470,8 @@ const handleDelete = () => {
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||||
location.reload()
|
projectStore.getProjectList();
|
||||||
|
projectStore.getMemberProjects();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
:type="'date'"
|
:type="'date'"
|
||||||
name="endDay"
|
name="endDay"
|
||||||
:modelValue="endDay"
|
:modelValue="endDay"
|
||||||
:min = "today"
|
:min = "startDay"
|
||||||
@update:modelValue="endDay = $event"
|
@update:modelValue="endDay = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -207,19 +207,14 @@
|
|||||||
// 등록 :: 주소 업데이트 핸들러
|
// 등록 :: 주소 업데이트 핸들러
|
||||||
const handleAddressUpdate = (data) => {
|
const handleAddressUpdate = (data) => {
|
||||||
addressData.value = data;
|
addressData.value = data;
|
||||||
};
|
} ;
|
||||||
|
|
||||||
// 종료일이 시작일보다 이전 날짜라면 종료일을 시작일로 맞추기
|
// 시작일이 종료일보다 크면 종료일 리셋
|
||||||
watch([startDay, endDay], () => {
|
watch(startDay, (newStartDate) => {
|
||||||
if (startDay.value && endDay.value) {
|
if (new Date(newStartDate) > new Date(endDay.value)) {
|
||||||
const start = new Date(startDay.value);
|
endDay.value = '';
|
||||||
const end = new Date(endDay.value);
|
|
||||||
|
|
||||||
if (end < start) {
|
|
||||||
endDay.value = startDay.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, { flush: 'post' });
|
});
|
||||||
|
|
||||||
// 프로젝트 등록
|
// 프로젝트 등록
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
@ -247,6 +242,8 @@
|
|||||||
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||||
closeCreateModal();
|
closeCreateModal();
|
||||||
getProjectList();
|
getProjectList();
|
||||||
|
projectStore.getMemberProjects();
|
||||||
|
formReset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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(public/img/avatars/default-Profile.jpg); background-repeat: no-repeat;"
|
style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@ -53,14 +53,14 @@
|
|||||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||||
|
|
||||||
<FormSelect
|
<FormSelect
|
||||||
title="비밀번호 힌트"
|
title="비밀번호 힌트"
|
||||||
name="pwhint"
|
name="pwhint"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-row="false"
|
:is-row="false"
|
||||||
:is-label="true"
|
:is-label="true"
|
||||||
:is-common="true"
|
:is-common="true"
|
||||||
:data="pwhintList"
|
:data="pwhintList"
|
||||||
@update:data="pwhint = $event"
|
@update:data="pwhint = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserFormInput
|
<UserFormInput
|
||||||
@ -143,11 +143,13 @@
|
|||||||
name="phone"
|
name="phone"
|
||||||
:isEssential="true"
|
:isEssential="true"
|
||||||
:is-alert="phoneAlert"
|
:is-alert="phoneAlert"
|
||||||
@update:data="phone = $event.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1')"
|
@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"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
|
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
|
||||||
|
|
||||||
@ -160,9 +162,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
import commonApi from '@/common/commonApi'
|
import commonApi from '@/common/commonApi';
|
||||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
import UserFormInput from '@c/input/UserFormInput.vue';
|
||||||
import FormSelect from '@c/input/FormSelect.vue';
|
import FormSelect from '@c/input/FormSelect.vue';
|
||||||
import ArrInput from '@c/input/ArrInput.vue';
|
import ArrInput from '@c/input/ArrInput.vue';
|
||||||
@ -185,13 +187,13 @@
|
|||||||
const birth = ref('');
|
const birth = ref('');
|
||||||
const address = ref('');
|
const address = ref('');
|
||||||
const detailAddress = ref('');
|
const detailAddress = ref('');
|
||||||
const postcode = ref(''); // 우편번호
|
const postcode = ref(''); // 우편번호
|
||||||
const phone = ref('');
|
const phone = ref('');
|
||||||
const phoneError = ref('');
|
const phoneError = ref('');
|
||||||
const color = ref(''); // 선택된 color
|
const color = ref(''); // 선택된 color
|
||||||
const colorError = ref('');
|
const colorError = ref('');
|
||||||
const mbti = ref(''); // 선택된 MBTI
|
const mbti = ref(''); // 선택된 MBTI
|
||||||
const pwhint = ref(''); // 선택된 pwhint
|
const pwhint = ref(''); // 선택된 pwhint
|
||||||
|
|
||||||
const profilAlert = ref(false);
|
const profilAlert = ref(false);
|
||||||
const idAlert = ref(false);
|
const idAlert = ref(false);
|
||||||
@ -209,7 +211,6 @@
|
|||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
|
|
||||||
// 프로필 체크
|
// 프로필 체크
|
||||||
const profileValid = (size, type) => {
|
const profileValid = (size, type) => {
|
||||||
const maxSize = 5 * 1024 * 1024;
|
const maxSize = 5 * 1024 * 1024;
|
||||||
@ -238,7 +239,7 @@
|
|||||||
// 사이즈, 파일 타입 안 맞으면 기본 이미지
|
// 사이즈, 파일 타입 안 맞으면 기본 이미지
|
||||||
if (!profileValid(file.size, file.type)) {
|
if (!profileValid(file.size, file.type)) {
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
|
profileLabel.style.backgroundImage = 'url("img/avatars/default-Profile.jpg")';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,16 +276,17 @@
|
|||||||
|
|
||||||
// 컬러, mbti, 비밀번호 힌트 목록 불러오기
|
// 컬러, mbti, 비밀번호 힌트 목록 불러오기
|
||||||
const { colorList, mbtiList, pwhintList } = commonApi({
|
const { colorList, mbtiList, pwhintList } = commonApi({
|
||||||
loadColor: true, colorType: 'YON',
|
loadColor: true,
|
||||||
|
colorType: 'YON',
|
||||||
loadMbti: true,
|
loadMbti: true,
|
||||||
loadPwhint: true,
|
loadPwhint: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 주소 업데이트 핸들러
|
// 주소 업데이트 핸들러
|
||||||
const handleAddressUpdate = (addressData) => {
|
const handleAddressUpdate = addressData => {
|
||||||
address.value = addressData.address;
|
address.value = addressData.address;
|
||||||
detailAddress.value = addressData.detailAddress;
|
detailAddress.value = addressData.detailAddress;
|
||||||
postcode.value = addressData.postcode; // 우편번호
|
postcode.value = addressData.postcode; // 우편번호
|
||||||
};
|
};
|
||||||
|
|
||||||
// 비밀번호 확인 체크
|
// 비밀번호 확인 체크
|
||||||
@ -311,7 +313,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColorUpdate = async (newColor) => {
|
const handleColorUpdate = async newColor => {
|
||||||
color.value = newColor;
|
color.value = newColor;
|
||||||
colorError.value = '';
|
colorError.value = '';
|
||||||
colorErrorAlert.value = false;
|
colorErrorAlert.value = false;
|
||||||
@ -319,10 +321,16 @@
|
|||||||
await checkColorDuplicate();
|
await checkColorDuplicate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onlyNumber = (event) => {
|
||||||
|
// 숫자가 아니면 입력 차단
|
||||||
|
if (!/^[0-9]$/.test(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// 회원가입
|
// 회원가입
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
await checkColorDuplicate();
|
await checkColorDuplicate();
|
||||||
|
|
||||||
idAlert.value = id.value.trim() === '';
|
idAlert.value = id.value.trim() === '';
|
||||||
@ -334,6 +342,12 @@
|
|||||||
addressAlert.value = address.value.trim() === '';
|
addressAlert.value = address.value.trim() === '';
|
||||||
phoneAlert.value = phone.value.trim() === '';
|
phoneAlert.value = phone.value.trim() === '';
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(phone.value)) {
|
||||||
|
phoneAlert.value = true;
|
||||||
|
} else {
|
||||||
|
phoneAlert.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 프로필 이미지 체크
|
// 프로필 이미지 체크
|
||||||
if (!profile.value) {
|
if (!profile.value) {
|
||||||
profilerr.value = '프로필 이미지를 선택해주세요.';
|
profilerr.value = '프로필 이미지를 선택해주세요.';
|
||||||
@ -343,9 +357,21 @@
|
|||||||
profilAlert.value = false;
|
profilAlert.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profilAlert.value || idAlert.value || idErrorAlert.value || passwordAlert.value || passwordcheckAlert.value ||
|
if (
|
||||||
passwordcheckErrorAlert.value || pwhintResAlert.value || nameAlert.value || birthAlert.value ||
|
profilAlert.value ||
|
||||||
addressAlert.value || phoneAlert.value || phoneErrorAlert.value || colorErrorAlert.value) {
|
idAlert.value ||
|
||||||
|
idErrorAlert.value ||
|
||||||
|
passwordAlert.value ||
|
||||||
|
passwordcheckAlert.value ||
|
||||||
|
passwordcheckErrorAlert.value ||
|
||||||
|
pwhintResAlert.value ||
|
||||||
|
nameAlert.value ||
|
||||||
|
birthAlert.value ||
|
||||||
|
addressAlert.value ||
|
||||||
|
phoneAlert.value ||
|
||||||
|
phoneErrorAlert.value ||
|
||||||
|
colorErrorAlert.value
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +390,7 @@
|
|||||||
formData.append('memberMbt', mbti.value);
|
formData.append('memberMbt', mbti.value);
|
||||||
formData.append('memberPrf', profile.value);
|
formData.append('memberPrf', profile.value);
|
||||||
|
|
||||||
const response = await $api.post('/user/join', formData, { isFormData : true });
|
const response = await $api.post('/user/join', formData, { isFormData: true });
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
|
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
|
||||||
@ -372,4 +398,3 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, nextTick, computed } from 'vue';
|
import { onMounted, ref, nextTick, computed } 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';
|
||||||
|
|
||||||
const emit = defineEmits(['user-list-update']);
|
const emit = defineEmits(['user-list-update']);
|
||||||
@ -106,7 +107,7 @@ const isUserDisabled = (user) => {
|
|||||||
// 클릭 시 활성화/비활성화 및 DB 업데이트
|
// 클릭 시 활성화/비활성화 및 DB 업데이트
|
||||||
// showOnlyActive가 true일 때는 toggleDisable 함수가 실행되지 않음
|
// showOnlyActive가 true일 때는 toggleDisable 함수가 실행되지 않음
|
||||||
const toggleDisable = async (index) => {
|
const toggleDisable = async (index) => {
|
||||||
if (props.showOnlyActive) return; // showOnlyActive가 true이면 함수 실행 중지
|
if (props.showOnlyActive) return;
|
||||||
|
|
||||||
const user = displayedUserList.value[index];
|
const user = displayedUserList.value[index];
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -125,6 +126,11 @@ const toggleDisable = async (index) => {
|
|||||||
if (originalIndex !== -1) {
|
if (originalIndex !== -1) {
|
||||||
userList.value[originalIndex].PROJCTYON = newParticipationStatus ? '0' : '1';
|
userList.value[originalIndex].PROJCTYON = newParticipationStatus ? '0' : '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 변경 후 프로젝트 목록 새로고침
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
await projectStore.getProjectList('', '', 'true');
|
||||||
|
await projectStore.getMemberProjects();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 원래 userList에서 해당 사용자를 찾아 업데이트
|
// 원래 userList에서 해당 사용자를 찾아 업데이트
|
||||||
|
|||||||
@ -87,27 +87,6 @@ profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}
|
|||||||
const setDefaultImage = (event) => (event.target.src = defaultProfile);
|
const setDefaultImage = (event) => (event.target.src = defaultProfile);
|
||||||
const showImage = (event) => (event.target.style.visibility = "visible");
|
const showImage = (event) => (event.target.style.visibility = "visible");
|
||||||
|
|
||||||
// 화면 크기에 따라 프로필 크기 동적 조정
|
|
||||||
const profileSize = computed(() => {
|
|
||||||
const totalUsers = userList.value.length;
|
|
||||||
|
|
||||||
if (windowWidth.value >= 1850) {
|
|
||||||
if (totalUsers <= 10) return "80px";
|
|
||||||
if (totalUsers <= 15) return "60px";
|
|
||||||
return "45px";
|
|
||||||
} else if (windowWidth.value >= 1500) {
|
|
||||||
if (totalUsers <= 10) return "60px";
|
|
||||||
if (totalUsers <= 15) return "40px";
|
|
||||||
return "30px";
|
|
||||||
} else if (windowWidth.value >= 900) {
|
|
||||||
if (totalUsers <= 10) return "48px";
|
|
||||||
if (totalUsers <= 15) return "30px";
|
|
||||||
return "20px";
|
|
||||||
} else {
|
|
||||||
return "35px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDynamicStyle = (user) => ({
|
const getDynamicStyle = (user) => ({
|
||||||
borderWidth: "3px",
|
borderWidth: "3px",
|
||||||
borderColor: user.usercolor || "#ccc",
|
borderColor: user.usercolor || "#ccc",
|
||||||
|
|||||||
@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
<!-- Drag Target Area To SlideIn Menu On Small Screens -->
|
<!-- Drag Target Area To SlideIn Menu On Small Screens -->
|
||||||
<div class="drag-target"></div>
|
<div class="drag-target"></div>
|
||||||
|
|
||||||
|
<ScrollTopButton />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -32,6 +34,7 @@
|
|||||||
import TheChat from './TheChat.vue';
|
import TheChat from './TheChat.vue';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { wait } from '@/common/utils';
|
import { wait } from '@/common/utils';
|
||||||
|
import ScrollTopButton from '@c/button/ScrollTopButton.vue';
|
||||||
|
|
||||||
window.isDarkStyle = window.Helpers.isDarkStyle();
|
window.isDarkStyle = window.Helpers.isDarkStyle();
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const sendMessage = () => {
|
|||||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 1000;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,17 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="content-footer footer bg-footer-theme">
|
<footer class="content-footer footer bg-footer-theme">
|
||||||
<div class="container-xxl">
|
<div class="container-xxl">
|
||||||
<div class="footer-container d-flex align-items-center justify-content-between py-4 flex-md-row flex-column">
|
|
||||||
<div class="text-body">
|
|
||||||
©2024
|
|
||||||
<!-- <script>
|
|
||||||
document.write(new Date().getFullYear())
|
|
||||||
</script> -->
|
|
||||||
, made with ❤️ by
|
|
||||||
<a href="https://themeselection.com/" target="_blank" class="footer-link">ThemeSelection</a>
|
|
||||||
</div>
|
|
||||||
<div class="d-none d-lg-inline-block"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -74,6 +74,12 @@
|
|||||||
<div class="text-truncate">Commuters</div>
|
<div class="text-truncate">Commuters</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="userId === allowedUserId" class="menu-item" :class="$route.path.includes('/authorization') ? 'active' : ''">
|
||||||
|
<RouterLink class="menu-link" to="/authorization">
|
||||||
|
<i class="menu-icon icon-base bx bx-user-check"></i>
|
||||||
|
<div class="text-truncate">Authorization</div>
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
<!-- <li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
<!-- <li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
||||||
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
||||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||||
@ -86,7 +92,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import router from '@/router';
|
import { computed } from "vue";
|
||||||
|
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||||
|
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
const allowedUserId = 26; // 특정 ID (변경필요!!)
|
||||||
|
|
||||||
|
const userId = computed(() => userStore.user?.id ?? null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@ -8,6 +8,17 @@
|
|||||||
|
|
||||||
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
<div class="navbar-nav-right d-flex align-items-center" 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" id="name" v-model="selectedProject" @change="updateSelectedProject">
|
||||||
|
<!-- 내가 참여하고 있는 프로젝트 그룹 -->
|
||||||
|
<option v-for="item in myProjects" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
||||||
|
{{ item.PROJCTNAM }}
|
||||||
|
</option>
|
||||||
|
<!-- 전체 프로젝트 그룹 -->
|
||||||
|
<option v-for="item in otherProjects" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
||||||
|
{{ item.PROJCTNAM }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- <button class="btn p-1" @click="switchToLightMode"><i class="bx bxs-sun link-warning"></i></button> -->
|
<!-- <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> -->
|
<!-- <button class="btn p-1" @click="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
|
||||||
|
|
||||||
@ -234,36 +245,95 @@
|
|||||||
<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 { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useThemeStore } from '@s/darkmode';
|
import { useThemeStore } from '@s/darkmode';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
|
||||||
const user = ref(null);
|
|
||||||
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
|
||||||
const baseUrl = import.meta.env.VITE_SERVER;
|
const baseUrl = import.meta.env.VITE_SERVER;
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
const user = ref(null);
|
||||||
|
const selectedProject = ref(null);
|
||||||
|
|
||||||
|
// 내가 참여하고 있는 프로젝트 목록
|
||||||
|
const myProjects = computed(() => {
|
||||||
|
return projectStore.memberProjectList || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 내가 참여하고 있지 않은 프로젝트 목록
|
||||||
|
const otherProjects = computed(() => {
|
||||||
|
if (!projectStore.projectList || !projectStore.memberProjectList) return [];
|
||||||
|
|
||||||
|
// 내 프로젝트 ID 목록
|
||||||
|
const myProjectIds = projectStore.memberProjectList.map(p => p.PROJCTSEQ);
|
||||||
|
|
||||||
|
// 내 프로젝트가 아닌 프로젝트만 필터링
|
||||||
|
return projectStore.projectList.filter(p => !myProjectIds.includes(p.PROJCTSEQ));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 프로젝트 선택 변경 시 스토어에 저장
|
||||||
|
const updateSelectedProject = () => {
|
||||||
|
if (!selectedProject.value) return;
|
||||||
|
|
||||||
|
// 전체 프로젝트 리스트에서 선택된 프로젝트 찾기
|
||||||
|
const selected = projectStore.projectList.find(
|
||||||
|
project => project.PROJCTSEQ === selectedProject.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
projectStore.setSelectedProject(selected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 선택된 프로젝트 변경 감지
|
||||||
|
watch(() => projectStore.selectedProject, (newProject) => {
|
||||||
|
if (newProject) {
|
||||||
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isDarkMode) {
|
// if (isDarkMode) {
|
||||||
switchToDarkMode();
|
// switchToDarkMode();
|
||||||
} else {
|
// } else {
|
||||||
switchToLightMode();
|
// switchToLightMode();
|
||||||
}
|
// }
|
||||||
|
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
user.value = userStore.user;
|
user.value = userStore.user;
|
||||||
|
|
||||||
|
await projectStore.getProjectList('', '', 'true');
|
||||||
|
|
||||||
|
// 사용자가 참여하고 있는 프로젝트 목록
|
||||||
|
await projectStore.getMemberProjects();
|
||||||
|
|
||||||
|
// 저장된 선택 프로젝트
|
||||||
|
const storedProject = projectStore.getSelectedProject();
|
||||||
|
if (storedProject) {
|
||||||
|
selectedProject.value = storedProject.PROJCTSEQ;
|
||||||
|
} else if (projectStore.memberProjectList.length > 0) {
|
||||||
|
// 저장된 선택 프로젝트가 없으면 첫 번째 참여 프로젝트 선택
|
||||||
|
selectedProject.value = projectStore.memberProjectList[0].PROJCTSEQ;
|
||||||
|
projectStore.setSelectedProject(projectStore.memberProjectList[0]);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await authStore.logout();
|
await authStore.logout();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { useAuthStore } from '@s/useAuthStore';
|
import { useAuthStore } from '@s/useAuthStore';
|
||||||
|
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||||
|
|
||||||
// 초기 렌더링 속도를 위해 지연 로딩 사용
|
// 초기 렌더링 속도를 위해 지연 로딩 사용
|
||||||
const routes = [
|
const routes = [
|
||||||
@ -85,8 +85,9 @@ const routes = [
|
|||||||
component: () => import('@v/commuters/TheCommuters.vue'),
|
component: () => import('@v/commuters/TheCommuters.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sample',
|
path: '/authorization',
|
||||||
component: () => import('@c/calendar/SampleCalendar.vue'),
|
component: () => import('@v/admin/TheAuthorization.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:anything(.*)",
|
path: "/:anything(.*)",
|
||||||
@ -102,38 +103,27 @@ const router = createRouter({
|
|||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
await authStore.checkAuthStatus(); // 로그인 상태 확인
|
await authStore.checkAuthStatus(); // 로그인 상태 확인
|
||||||
|
const allowedUserId = 26; // 특정 ID (변경필요!!)
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
const userId = userStore.user?.id ?? null;
|
||||||
|
|
||||||
|
// 로그인이 필요한 페이지인데 로그인되지 않은 경우 → 로그인 페이지로 이동
|
||||||
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||||
// 로그인이 필요한 페이지인데 로그인되지 않은 경우 → 로그인 페이지로 이동
|
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||||
next({ name: 'Login' });
|
|
||||||
} else if (to.meta.requiresGuest && authStore.isAuthenticated) {
|
|
||||||
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
|
||||||
next({ name: 'Home' });
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
||||||
|
if (to.path === "/authorization" && userId !== allowedUserId) {
|
||||||
|
return next("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
||||||
|
if (to.meta.requiresGuest && authStore.isAuthenticated) {
|
||||||
|
return next({ name: 'Home' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 조건을 통과하면 정상적으로 이동
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
// 최초 앱 로드 시 인증 상태 체크
|
|
||||||
await authStore.checkAuthStatus()
|
|
||||||
|
|
||||||
// 현재 라우트에 인증이 필요한지 확인
|
|
||||||
const requiresAuth = to.meta.requiresAuth === true
|
|
||||||
|
|
||||||
if (requiresAuth && !authStore.isAuthenticated) {
|
|
||||||
// 인증되지 않은 사용자를 로그인 페이지로 리다이렉트
|
|
||||||
// 원래 가려던 페이지를 쿼리 파라미터로 전달
|
|
||||||
next({
|
|
||||||
name: 'Login',
|
|
||||||
query: { redirect: to.fullPath }
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@ -6,22 +6,96 @@
|
|||||||
설명 : 프로젝트 목록
|
설명 : 프로젝트 목록
|
||||||
*/
|
*/
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
|
||||||
export const useProjectStore = defineStore('project', () => {
|
export const useProjectStore = defineStore('project', () => {
|
||||||
const projectList = ref([]);
|
const projectList = ref([]);
|
||||||
|
const memberProjectList = ref([]);
|
||||||
|
const selectedProject = ref(null);
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
|
||||||
const getProjectList = async (searchText = '', selectedYear = '') => {
|
// 전체 프로젝트 가져오기
|
||||||
|
const getProjectList = async (searchText = '', selectedYear = '', excludeEnded = '') => {
|
||||||
const res = await $api.get('project/select', {
|
const res = await $api.get('project/select', {
|
||||||
params: {
|
params: {
|
||||||
searchKeyword: searchText || '',
|
searchKeyword: searchText || '',
|
||||||
category: selectedYear || '',
|
category: selectedYear || '',
|
||||||
|
excludeEnded: excludeEnded
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
projectList.value = res.data.data.projectList;
|
projectList.value = res.data.data.projectList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 사용자가 속한 프로젝트 목록 가져오기
|
||||||
|
const getMemberProjects = async () => {
|
||||||
|
if (!userStore.user) return; // 로그인한 사용자 확인
|
||||||
|
|
||||||
return { projectList, getProjectList };
|
const res = await $api.get(`project/${userStore.user.id}`);
|
||||||
|
memberProjectList.value = res.data.data;
|
||||||
|
|
||||||
|
if (memberProjectList.value.length > 0 && !selectedProject.value) {
|
||||||
|
setSelectedProject(memberProjectList.value[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSelectedProject = (project) => {
|
||||||
|
selectedProject.value = project ? { ...project } : null;
|
||||||
|
if (project) {
|
||||||
|
localStorage.setItem('selectedProject', JSON.stringify(project));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('selectedProject');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedProject = () => {
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
const storedProject = localStorage.getItem('selectedProject');
|
||||||
|
if (storedProject) {
|
||||||
|
selectedProject.value = JSON.parse(storedProject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedProject.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 리스트가 변경될 때 자동으로 반응
|
||||||
|
watch(projectList, (newList) => {
|
||||||
|
// 선택된 프로젝트가 없고 목록이 있는 경우
|
||||||
|
if (!selectedProject.value && newList.length > 0) {
|
||||||
|
// 사용자가 속한 프로젝트가 있는지 먼저 확인
|
||||||
|
if (memberProjectList.value.length > 0) {
|
||||||
|
setSelectedProject(memberProjectList.value[0]);
|
||||||
|
} else {
|
||||||
|
setSelectedProject(newList[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(memberProjectList, (newList) => {
|
||||||
|
if (newList.length > 0) {
|
||||||
|
// 현재 선택된 프로젝트가 없는 경우 첫 번째 항목 선택
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
setSelectedProject(newList[0]);
|
||||||
|
} else {
|
||||||
|
// 선택된 프로젝트가 있는 경우 목록에 있는지 확인
|
||||||
|
const exists = newList.some(project => project.PROJCTSEQ === selectedProject.value.PROJCTSEQ);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
setSelectedProject(newList[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectList,
|
||||||
|
selectedProject,
|
||||||
|
getProjectList,
|
||||||
|
memberProjectList,
|
||||||
|
getMemberProjects,
|
||||||
|
setSelectedProject,
|
||||||
|
getSelectedProject
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
162
src/views/admin/TheAuthorization.vue
Normal file
162
src/views/admin/TheAuthorization.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container text-center flex-grow-1 container-p-y">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex flex-column">
|
||||||
|
<h3>관리자 권한 부여</h3>
|
||||||
|
<div class="user-card-container">
|
||||||
|
<div v-for="user in users" :key="user.id" class="user-card">
|
||||||
|
<!-- 프로필 사진 -->
|
||||||
|
<img :src="getProfileImage(user.photo)" class="profile-img" alt="프로필 사진" @error="setDefaultImage" />
|
||||||
|
<!-- 사용자 정보 -->
|
||||||
|
<div class="user-info">
|
||||||
|
<h5>{{ user.name }}</h5>
|
||||||
|
</div>
|
||||||
|
<!-- 권한 토글 버튼 -->
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import axios from '@api';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
|
||||||
|
const users = ref([]);
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
||||||
|
const defaultProfile = "/img/icons/icon.png";
|
||||||
|
|
||||||
|
// 사용자 목록 가져오기
|
||||||
|
async function fetchUsers() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('admin/users'); // API 경로 확인 필요
|
||||||
|
|
||||||
|
// API 응답 구조 확인 후 데이터가 배열인지 체크
|
||||||
|
if (!response.data || !Array.isArray(response.data.data)) {
|
||||||
|
throw new Error("올바른 데이터 형식이 아닙니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 매핑 (올바른 형식으로 변환)
|
||||||
|
users.value = response.data.data.map(user => ({
|
||||||
|
id: user.MEMBERSEQ,
|
||||||
|
name: user.MEMBERNAM,
|
||||||
|
photo: user.MEMBERPRF ? `${baseUrl}upload/img/profile/${user.MEMBERPRF}` : defaultProfile,
|
||||||
|
color: user.MEMBERCOL,
|
||||||
|
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사용자 목록을 불러오는 중 오류 발생:', error);
|
||||||
|
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로필 이미지 가져오기
|
||||||
|
function getProfileImage(photo) {
|
||||||
|
return photo || defaultProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 로드 오류 시 기본 이미지 설정
|
||||||
|
function setDefaultImage(event) {
|
||||||
|
event.target.src = defaultProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 관리자 권한 토글 함수
|
||||||
|
async function toggleAdmin(user) {
|
||||||
|
const requestData = {
|
||||||
|
id: user.id,
|
||||||
|
role: user.isAdmin ? 'MEMBER' : 'ADMIN'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios.put('admin/role', requestData); // API 경로 수정
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
user.isAdmin = !user.isAdmin;
|
||||||
|
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
||||||
|
} else {
|
||||||
|
throw new Error('권한 변경 실패');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('권한 변경 중 오류 발생:', error);
|
||||||
|
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchUsers);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.user-card {
|
||||||
|
width: 200px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.profile-img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -291,5 +291,7 @@ onMounted(() => {
|
|||||||
color: #ff5733;
|
color: #ff5733;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
:passwordCommentAlert="passwordCommentAlert"
|
:passwordCommentAlert="passwordCommentAlert"
|
||||||
:currentPasswordCommentId="currentPasswordCommentId"
|
:currentPasswordCommentId="currentPasswordCommentId"
|
||||||
:password="password"
|
:password="password"
|
||||||
|
:editCommentAlert="editCommentAlert"
|
||||||
@editClick="editComment"
|
@editClick="editComment"
|
||||||
@deleteClick="deleteComment"
|
@deleteClick="deleteComment"
|
||||||
@updateReaction="handleCommentReaction"
|
@updateReaction="handleCommentReaction"
|
||||||
@ -118,6 +119,7 @@
|
|||||||
@cancelEdit="handleCancelEdit"
|
@cancelEdit="handleCancelEdit"
|
||||||
@submitEdit="handleSubmitEdit"
|
@submitEdit="handleSubmitEdit"
|
||||||
@update:password="updatePassword"
|
@update:password="updatePassword"
|
||||||
|
@inputDetector="inputDetector"
|
||||||
/>
|
/>
|
||||||
<Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
|
<Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
|
||||||
</div>
|
</div>
|
||||||
@ -133,11 +135,13 @@
|
|||||||
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 } 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';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
|
const $common = inject('common');
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('');
|
const profileName = ref('');
|
||||||
const boardTitle = ref('제목 없음');
|
const boardTitle = ref('제목 없음');
|
||||||
@ -161,6 +165,7 @@
|
|||||||
const unknown = computed(() => profileName.value === '익명');
|
const unknown = computed(() => profileName.value === '익명');
|
||||||
const currentUserId = computed(() => userStore?.user?.id); // 현재 로그인한 사용자 id
|
const currentUserId = computed(() => userStore?.user?.id); // 현재 로그인한 사용자 id
|
||||||
const authorId = ref(''); // 작성자 id
|
const authorId = ref(''); // 작성자 id
|
||||||
|
const editCommentAlert = ref({}); //댓글, 대댓글 오류 메세지 객체
|
||||||
|
|
||||||
const isAuthor = computed(() => currentUserId.value === authorId.value);
|
const isAuthor = computed(() => currentUserId.value === authorId.value);
|
||||||
const commentsWithAuthStatus = computed(() => {
|
const commentsWithAuthStatus = computed(() => {
|
||||||
@ -234,6 +239,7 @@
|
|||||||
const inputCheck = () => {
|
const inputCheck = () => {
|
||||||
passwordAlert.value = '';
|
passwordAlert.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 상세 데이터 불러오기
|
// 게시물 상세 데이터 불러오기
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||||
@ -437,6 +443,11 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 댓글, 대댓글 오류 메세지 초기화
|
||||||
|
const inputDetector = () => {
|
||||||
|
editCommentAlert.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
// 게시글 수정 버튼 클릭
|
// 게시글 수정 버튼 클릭
|
||||||
const editClick = unknown => {
|
const editClick = unknown => {
|
||||||
const isUnknown = unknown?.unknown ?? false;
|
const isUnknown = unknown?.unknown ?? false;
|
||||||
@ -706,10 +717,10 @@
|
|||||||
|
|
||||||
if (targetComment) {
|
if (targetComment) {
|
||||||
// console.log('타겟',targetComment)
|
// console.log('타겟',targetComment)
|
||||||
// ✅ 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
// 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
||||||
targetComment.content = '댓글이 삭제되었습니다.';
|
targetComment.content = '댓글이 삭제되었습니다.';
|
||||||
targetComment.author = '알 수 없음'; // 익명 처리
|
targetComment.author = '알 수 없음'; // 익명 처리
|
||||||
targetComment.isDeleted = true; // ✅ 삭제 상태를 추가
|
targetComment.isDeleted = true; // 삭제 상태를 추가
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toastStore.onToast('댓글 삭제에 실패했습니다.', 'e');
|
toastStore.onToast('댓글 삭제에 실패했습니다.', 'e');
|
||||||
@ -721,6 +732,7 @@
|
|||||||
|
|
||||||
// 댓글 수정 확인
|
// 댓글 수정 확인
|
||||||
const handleSubmitEdit = async (comment, editedContent) => {
|
const handleSubmitEdit = async (comment, editedContent) => {
|
||||||
|
if (!checkValidation(comment, editedContent)) return;
|
||||||
togglePassword();
|
togglePassword();
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`board/comment/${comment.commentId}`, {
|
const response = await axios.put(`board/comment/${comment.commentId}`, {
|
||||||
@ -744,6 +756,15 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 유효성 체크하여 해당 댓글, 대댓글에 오류메세지 전달.
|
||||||
|
const checkValidation = (comment, content) => {
|
||||||
|
if (!$common.isNotEmpty(content)) {
|
||||||
|
editCommentAlert.value[comment.commentId] = '내용을 입력하세요';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// 댓글 수정 취소 (대댓글 포함)
|
// 댓글 수정 취소 (대댓글 포함)
|
||||||
const handleCancelEdit = comment => {
|
const handleCancelEdit = comment => {
|
||||||
const targetComment = findCommentById(comment.commentId, comments.value);
|
const targetComment = findCommentById(comment.commentId, comments.value);
|
||||||
@ -799,8 +820,23 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.board-content img {
|
.board-content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-content img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-content table {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,5 +3,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CommuteCalendar from '@c/commuters/CommuteCalendar.vue';
|
import CommuteCalendar from '@c/commuters/CommuterCalendar.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -152,23 +152,30 @@ function handleMonthChange(viewInfo) {
|
|||||||
loadCalendarData(year, month);
|
loadCalendarData(year, month);
|
||||||
}
|
}
|
||||||
// 캘린더 클릭
|
// 캘린더 클릭
|
||||||
|
// 캘린더 클릭
|
||||||
function handleDateClick(info) {
|
function handleDateClick(info) {
|
||||||
const clickedDateStr = info.dateStr;
|
const clickedDateStr = info.dateStr;
|
||||||
const clickedDate = info.date;
|
const clickedDate = info.date;
|
||||||
const todayStr = new Date().toISOString().split("T")[0];
|
const todayStr = new Date().toISOString().split("T")[0];
|
||||||
|
const todayObj = new Date(todayStr);
|
||||||
|
const oneWeekAgoObj = new Date(todayObj);
|
||||||
|
oneWeekAgoObj.setDate(todayObj.getDate() - 8); // 오늘 기준 7일 전 날짜
|
||||||
|
|
||||||
|
// 주말(토, 일) 또는 공휴일 또는 오늘 -7일 날짜 → 클릭 불가능
|
||||||
if (
|
if (
|
||||||
clickedDate.getDay() === 0 ||
|
clickedDate.getDay() === 0 || // 일요일
|
||||||
clickedDate.getDay() === 6 ||
|
clickedDate.getDay() === 6 || // 토요일
|
||||||
holidayDates.value.has(clickedDateStr) ||
|
holidayDates.value.has(clickedDateStr) || // 공휴일
|
||||||
clickedDateStr < todayStr
|
clickedDateStr <= oneWeekAgoObj.toISOString().split("T")[0] // 오늘 -7일 날짜 포함 과거 날짜 클릭 방지
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMyVacation = myVacations.value.some(vac => {
|
const isMyVacation = myVacations.value.some(vac => {
|
||||||
const vacDate = vac.date ? vac.date.substring(0, 10) : "";
|
const vacDate = vac.date ? vac.date.substring(0, 10) : "";
|
||||||
return vacDate === clickedDateStr && !vac.receiverId;
|
return vacDate === clickedDateStr && !vac.receiverId;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMyVacation) {
|
if (isMyVacation) {
|
||||||
if (selectedDates.value.get(clickedDateStr) === "delete") {
|
if (selectedDates.value.get(clickedDateStr) === "delete") {
|
||||||
selectedDates.value.delete(clickedDateStr);
|
selectedDates.value.delete(clickedDateStr);
|
||||||
@ -178,45 +185,55 @@ function handleDateClick(info) {
|
|||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedDates.value.has(clickedDateStr)) {
|
if (selectedDates.value.has(clickedDateStr)) {
|
||||||
selectedDates.value.delete(clickedDateStr);
|
selectedDates.value.delete(clickedDateStr);
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = halfDayType.value
|
const type = halfDayType.value
|
||||||
? (halfDayType.value === "AM" ? "700101" : "700102")
|
? (halfDayType.value === "AM" ? "700101" : "700102")
|
||||||
: "700103";
|
: "700103";
|
||||||
|
|
||||||
selectedDates.value.set(clickedDateStr, type);
|
selectedDates.value.set(clickedDateStr, type);
|
||||||
halfDayType.value = null;
|
halfDayType.value = null;
|
||||||
updateCalendarEvents();
|
updateCalendarEvents();
|
||||||
|
|
||||||
if (halfDayButtonsRef.value) {
|
if (halfDayButtonsRef.value) {
|
||||||
halfDayButtonsRef.value.resetHalfDay();
|
halfDayButtonsRef.value.resetHalfDay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 오늘 이후의 날짜만 클릭 가능하도록 설정
|
|
||||||
function markClickableDates() {
|
function markClickableDates() {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const todayStr = new Date().toISOString().split("T")[0]; // 오늘 날짜 YYYY-MM-DD
|
const todayStr = new Date().toISOString().split("T")[0]; // 오늘 날짜 (YYYY-MM-DD)
|
||||||
const todayObj = new Date(todayStr);
|
const todayObj = new Date(todayStr);
|
||||||
|
const oneWeekAgoObj = new Date(todayObj);
|
||||||
|
oneWeekAgoObj.setDate(todayObj.getDate() - 8); // 오늘 기준 7일 전 날짜
|
||||||
|
|
||||||
document.querySelectorAll(".fc-daygrid-day").forEach((cell) => {
|
document.querySelectorAll(".fc-daygrid-day").forEach((cell) => {
|
||||||
const dateStr = cell.getAttribute("data-date");
|
const dateStr = cell.getAttribute("data-date");
|
||||||
if (!dateStr) return; // 날짜가 없으면 스킵
|
if (!dateStr) return; // 날짜가 없으면 스킵
|
||||||
|
|
||||||
const dateObj = new Date(dateStr);
|
const dateObj = new Date(dateStr);
|
||||||
// 주말 (토요일, 일요일)
|
|
||||||
if (dateObj.getDay() === 0 || dateObj.getDay() === 6 || holidayDates.value.has(dateStr)) {
|
// 주말(토요일, 일요일) 또는 공휴일 또는 오늘 -7일 날짜 → 클릭 불가능
|
||||||
|
if (
|
||||||
|
dateObj.getDay() === 0 || // 일요일
|
||||||
|
dateObj.getDay() === 6 || // 토요일
|
||||||
|
holidayDates.value.has(dateStr) || // 공휴일
|
||||||
|
dateObj.getTime() === oneWeekAgoObj.getTime() // 오늘 -7일 날짜
|
||||||
|
) {
|
||||||
cell.classList.remove("clickable");
|
cell.classList.remove("clickable");
|
||||||
cell.classList.add("fc-day-sat-sun");
|
cell.classList.add("fc-day-sat-sun");
|
||||||
|
cell.removeEventListener("click", handleDateClick); // 클릭 이벤트 제거
|
||||||
}
|
}
|
||||||
// 과거 날짜 (오늘 이전)
|
// 오늘 -6일부터 미래 날짜까지 클릭 가능
|
||||||
else if (dateObj < todayObj) {
|
|
||||||
cell.classList.remove("clickable");
|
|
||||||
cell.classList.add("past"); // 과거 날짜 비활성화
|
|
||||||
}
|
|
||||||
// 오늘 & 미래 날짜 (클릭 가능)
|
|
||||||
else {
|
else {
|
||||||
cell.classList.add("clickable");
|
cell.classList.add("clickable");
|
||||||
cell.classList.remove("past", "fc-day-sat-sun");
|
cell.classList.remove("past", "fc-day-sat-sun");
|
||||||
|
cell.addEventListener("click", handleDateClick); // 클릭 이벤트 추가
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -439,11 +456,12 @@ function updateCalendarEvents() {
|
|||||||
.filter(([date, type]) => type !== "delete")
|
.filter(([date, type]) => type !== "delete")
|
||||||
.map(([date, type]) => ({
|
.map(([date, type]) => ({
|
||||||
start: date,
|
start: date,
|
||||||
backgroundColor: "rgb(113 212 243 / 76%)",
|
backgroundColor: "rgb(113 212 243 / 76%)", // 클릭하면 하늘색
|
||||||
textColor: "#fff",
|
textColor: "#fff",
|
||||||
display: "background",
|
display: "background",
|
||||||
classNames: [getVacationTypeClass(type), "selected-event"]
|
classNames: [getVacationTypeClass(type), "selected-event"] // 선택된 날짜 클래스 추가
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filteredFetchedEvents = fetchedEvents.value.filter(event => {
|
const filteredFetchedEvents = fetchedEvents.value.filter(event => {
|
||||||
if (event.saved && selectedDates.value.get(event.start) === "delete") {
|
if (event.saved && selectedDates.value.get(event.start) === "delete") {
|
||||||
if (event.memberSeq === userStore.user.id) {
|
if (event.memberSeq === userStore.user.id) {
|
||||||
@ -452,7 +470,37 @@ function updateCalendarEvents() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
calendarEvents.value = [...filteredFetchedEvents, ...selectedEvents];
|
calendarEvents.value = [...filteredFetchedEvents, ...selectedEvents];
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const todayStr = new Date().toISOString().split("T")[0];
|
||||||
|
const todayElement = document.querySelector(`.fc-daygrid-day[data-date="${todayStr}"]`);
|
||||||
|
|
||||||
|
if (todayElement) {
|
||||||
|
// 오늘 날짜가 선택된 경우 배경 제거
|
||||||
|
if (selectedDates.value.has(todayStr)) {
|
||||||
|
todayElement.classList.remove("fc-day-today"); // 기본 스타일 제거
|
||||||
|
todayElement.classList.add("selected-event"); // 선택된 날짜 스타일 적용
|
||||||
|
|
||||||
|
// 🔹 오전 반차일 경우 'half-day-am' 클래스 추가
|
||||||
|
if (selectedDates.value.get(todayStr) === "700101") {
|
||||||
|
todayElement.classList.add("half-day-am");
|
||||||
|
todayElement.classList.remove("half-day-pm");
|
||||||
|
}
|
||||||
|
// 🔹 오후 반차일 경우 'half-day-pm' 클래스 추가
|
||||||
|
else if (selectedDates.value.get(todayStr) === "700102") {
|
||||||
|
todayElement.classList.add("half-day-pm");
|
||||||
|
todayElement.classList.remove("half-day-am");
|
||||||
|
} else {
|
||||||
|
todayElement.classList.remove("half-day-am", "half-day-pm");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
todayElement.classList.add("fc-day-today"); // 기본 스타일 복원
|
||||||
|
todayElement.classList.remove("selected-event", "half-day-am", "half-day-pm"); // 선택된 상태 해제
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 휴가 종류에 따른 클래스명
|
// 휴가 종류에 따른 클래스명
|
||||||
const getVacationTypeClass = (type) => {
|
const getVacationTypeClass = (type) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user