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>
|
||||
<html
|
||||
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"
|
||||
data-theme="theme-default"
|
||||
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",
|
||||
"@tinymce/tinymce-vue": "^5.1.1",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
height: 8px !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 0px !important;
|
||||
margin-left: -0.5% !important;
|
||||
}
|
||||
/* 오후 반차 그래프 (오른쪽 절반) */
|
||||
.fc-daygrid-event.half-day-pm {
|
||||
@ -24,6 +25,7 @@
|
||||
margin-left: auto !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 0px !important;
|
||||
margin-right: -0.5% !important;
|
||||
}
|
||||
/* 연차 그래프 (풀) */
|
||||
.fc-daygrid-event.full-day {
|
||||
@ -69,7 +71,7 @@ background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */
|
||||
.fc-day-sat-sun {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
/* 과거 날짜 (오늘 이전) */
|
||||
/* 과거 날짜 (오늘 -7일일) */
|
||||
.fc-daygrid-day.past {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
@ -422,4 +424,51 @@ cursor: not-allowed !important;
|
||||
.end-project {
|
||||
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
|
||||
* @returns
|
||||
*/
|
||||
getProfileImage(profileImg) {
|
||||
let profileImgUrl = '/img/icons/icon.png'; // 기본 프로필 이미지 경로
|
||||
getProfileImage(profileImg, isAnonymous = false) {
|
||||
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}`;
|
||||
|
||||
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 {
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
<div class="mt-6">
|
||||
<template v-if="comment.isEditTextarea">
|
||||
<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">
|
||||
<SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn>
|
||||
</div>
|
||||
@ -60,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<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 BoardCommentArea from './BoardCommentArea.vue';
|
||||
import PlusButton from '../button/PlusBtn.vue';
|
||||
@ -109,6 +110,7 @@
|
||||
password: {
|
||||
type: String,
|
||||
},
|
||||
editCommentAlert: String,
|
||||
});
|
||||
|
||||
// emits 정의
|
||||
@ -121,6 +123,7 @@
|
||||
'submitEdit',
|
||||
'cancelEdit',
|
||||
'update:password',
|
||||
'inputDetector',
|
||||
]);
|
||||
|
||||
const filterInput = event => {
|
||||
@ -165,6 +168,14 @@
|
||||
},
|
||||
);
|
||||
|
||||
// text 변화 감지하여 부모에게 전달
|
||||
watch(
|
||||
() => localEditedContent.value,
|
||||
newVal => {
|
||||
emit('inputDetector');
|
||||
},
|
||||
);
|
||||
|
||||
// watch(() => props.comment.isDeleted, () => {
|
||||
// console.log("BoardComment - isDeleted 상태 변경됨:", newVal);
|
||||
|
||||
|
||||
@ -12,50 +12,62 @@
|
||||
<!-- 텍스트박스 -->
|
||||
<div class="w-100">
|
||||
<textarea
|
||||
class="form-control"
|
||||
class="form-control mb-2"
|
||||
placeholder="댓글 달기"
|
||||
rows="3"
|
||||
:maxlength="maxLength"
|
||||
v-model="comment"
|
||||
@input="alertTextHandler"
|
||||
></textarea>
|
||||
<span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span>
|
||||
<span v-else class="invalid-feedback d-block text-start ms-2">{{ textAlert }}</span>
|
||||
<span v-if="commentAlert" class="invalid-feedback d-inline text-start ms-2 mb-2">{{ commentAlert }}</span>
|
||||
<span v-else class="invalid-feedback d-inline text-start ms-2">{{ textAlert }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 옵션 및 버튼 섹션 -->
|
||||
<div class="d-flex justify-content-between flex-wrap mt-4">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
||||
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
|
||||
<input class="form-check-input" type="checkbox" :id="`checkboxAnnonymous${commnetId}`" v-model="isCheck" />
|
||||
<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>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<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 d-flex align-items-center">
|
||||
<input
|
||||
type="password"
|
||||
id="basic-default-password"
|
||||
class="form-control flex-grow-1"
|
||||
autocomplete="new-password"
|
||||
v-model="password"
|
||||
placeholder="비밀번호 입력"
|
||||
@input="passwordAlertTextHandler"
|
||||
class="form-check-input me-2"
|
||||
type="checkbox"
|
||||
:id="`checkboxAnnonymous${commnetId}`"
|
||||
v-model="isCheck"
|
||||
@change="pwd2AlertHandler"
|
||||
/>
|
||||
<label class="form-check-label" :for="`checkboxAnnonymous${commnetId}`">익명</label>
|
||||
</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 class="ms-auto mt-3 mt-md-0">
|
||||
<SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
|
||||
</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>
|
||||
@ -136,6 +148,11 @@
|
||||
resetCommentForm();
|
||||
};
|
||||
|
||||
// 비밀번호 경고 초기화
|
||||
const pwd2AlertHandler = () => {
|
||||
if (isCheck.value === false) passwordAlert2.value = '';
|
||||
};
|
||||
|
||||
// 입력 필드 리셋 함수 추가
|
||||
const resetCommentForm = () => {
|
||||
comment.value = '';
|
||||
|
||||
@ -11,14 +11,16 @@
|
||||
:passwordCommentAlert="passwordCommentAlert || ''"
|
||||
:currentPasswordCommentId="currentPasswordCommentId"
|
||||
:password="password"
|
||||
:editCommentAlert="editCommentAlert[comment.commentId]"
|
||||
@editClick="handleEditClick"
|
||||
@deleteClick="handleDeleteClick"
|
||||
@submitPassword="submitPassword"
|
||||
@submitComment="submitComment"
|
||||
@submitEdit="handleSubmitEdit"
|
||||
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
|
||||
@cancelEdit="handleCancelEdit"
|
||||
@updateReaction="reactionData => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
|
||||
@update:password="updatePassword"
|
||||
@inputDetector="$emit('inputDetector')"
|
||||
>
|
||||
<!-- 대댓글 -->
|
||||
<template #reply>
|
||||
@ -35,14 +37,16 @@
|
||||
:currentPasswordCommentId="currentPasswordCommentId"
|
||||
:passwordCommentAlert="passwordCommentAlert"
|
||||
:password="password"
|
||||
:editCommentAlert="editCommentAlert[child.commentId]"
|
||||
@editClick="handleReplyEditClick"
|
||||
@deleteClick="$emit('deleteClick', child)"
|
||||
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
|
||||
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent, child.commentId)"
|
||||
@cancelEdit="$emit('cancelEdit', child)"
|
||||
@submitComment="submitComment"
|
||||
@updateReaction="handleUpdateReaction"
|
||||
@submitPassword="$emit('submitPassword', child, password)"
|
||||
@update:password="$emit('update:password', $event)"
|
||||
@inputDetector="$emit('inputDetector')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
@ -95,6 +99,7 @@
|
||||
index: {
|
||||
type: Number,
|
||||
},
|
||||
editCommentAlert: Object,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@ -106,6 +111,7 @@
|
||||
'clearPassword',
|
||||
'submitEdit',
|
||||
'update:password',
|
||||
'inputDetector',
|
||||
]);
|
||||
|
||||
const submitComment = replyData => {
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<div class="d-flex align-items-center">
|
||||
<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 class="me-2">
|
||||
@ -120,6 +126,14 @@
|
||||
|
||||
// 프로필 이미지 경로 설정
|
||||
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>
|
||||
|
||||
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="종료일"
|
||||
type="date"
|
||||
name="endDay"
|
||||
:min="todays"
|
||||
:min="selectedProject.PROJCTSTR"
|
||||
:modelValue="selectedProject.PROJCTEND"
|
||||
@update:modelValue="selectedProject.PROJCTEND = $event"
|
||||
/>
|
||||
@ -161,7 +161,7 @@
|
||||
</template>
|
||||
|
||||
<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 CenterModal from '@c/modal/CenterModal.vue';
|
||||
import $api from '@api';
|
||||
@ -177,10 +177,12 @@ import ArrInput from '@c/input/ArrInput.vue';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import commonApi from '@/common/commonApi';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
|
||||
// 스토어
|
||||
const toastStore = useToastStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
|
||||
// Props 정의
|
||||
const props = defineProps({
|
||||
@ -254,11 +256,6 @@ const isProjectCreator = computed(() => {
|
||||
return user.value?.id === props.projctCreatorId;
|
||||
});
|
||||
|
||||
// dayjs 인스턴스 가져오기
|
||||
const dayjs = inject('dayjs');
|
||||
|
||||
// 오늘 날짜를 YYYY-MM-DD 형식으로 변환
|
||||
const todays = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
||||
const isProjectExpired = computed(() => {
|
||||
@ -330,6 +327,19 @@ const closeModal = () => {
|
||||
|
||||
// 수정 모달 열기
|
||||
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;
|
||||
originalColor.value = props.projctCol;
|
||||
};
|
||||
@ -364,6 +374,18 @@ const hasChanges = computed(() => {
|
||||
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 = () => {
|
||||
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
|
||||
@ -393,7 +415,7 @@ const handleUpdate = () => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
||||
closeEditModal();
|
||||
// 상위 컴포넌트에 업데이트 알림
|
||||
|
||||
emit('update');
|
||||
}
|
||||
});
|
||||
@ -448,7 +470,8 @@ const handleDelete = () => {
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||
location.reload()
|
||||
projectStore.getProjectList();
|
||||
projectStore.getMemberProjects();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
:type="'date'"
|
||||
name="endDay"
|
||||
:modelValue="endDay"
|
||||
:min = "today"
|
||||
:min = "startDay"
|
||||
@update:modelValue="endDay = $event"
|
||||
/>
|
||||
|
||||
@ -207,19 +207,14 @@
|
||||
// 등록 :: 주소 업데이트 핸들러
|
||||
const handleAddressUpdate = (data) => {
|
||||
addressData.value = data;
|
||||
};
|
||||
} ;
|
||||
|
||||
// 종료일이 시작일보다 이전 날짜라면 종료일을 시작일로 맞추기
|
||||
watch([startDay, endDay], () => {
|
||||
if (startDay.value && endDay.value) {
|
||||
const start = new Date(startDay.value);
|
||||
const end = new Date(endDay.value);
|
||||
|
||||
if (end < start) {
|
||||
endDay.value = startDay.value;
|
||||
}
|
||||
// 시작일이 종료일보다 크면 종료일 리셋
|
||||
watch(startDay, (newStartDate) => {
|
||||
if (new Date(newStartDate) > new Date(endDay.value)) {
|
||||
endDay.value = '';
|
||||
}
|
||||
}, { flush: 'post' });
|
||||
});
|
||||
|
||||
// 프로젝트 등록
|
||||
const handleCreate = async () => {
|
||||
@ -247,6 +242,8 @@
|
||||
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||
closeCreateModal();
|
||||
getProjectList();
|
||||
projectStore.getMemberProjects();
|
||||
formReset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
for="profilePic"
|
||||
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
||||
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>
|
||||
|
||||
@ -53,14 +53,14 @@
|
||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||
|
||||
<FormSelect
|
||||
title="비밀번호 힌트"
|
||||
name="pwhint"
|
||||
:is-essential="true"
|
||||
:is-row="false"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="pwhintList"
|
||||
@update:data="pwhint = $event"
|
||||
title="비밀번호 힌트"
|
||||
name="pwhint"
|
||||
:is-essential="true"
|
||||
:is-row="false"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="pwhintList"
|
||||
@update:data="pwhint = $event"
|
||||
/>
|
||||
|
||||
<UserFormInput
|
||||
@ -143,11 +143,13 @@
|
||||
name="phone"
|
||||
:isEssential="true"
|
||||
:is-alert="phoneAlert"
|
||||
@update:data="phone = $event.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1')"
|
||||
@update:data="phone = $event"
|
||||
@update:alert="phoneAlert = $event"
|
||||
@blur="checkPhoneDuplicate"
|
||||
:maxlength="11"
|
||||
:value="phone"
|
||||
@keypress="onlyNumber"
|
||||
|
||||
/>
|
||||
<span v-if="phoneError" class="invalid-feedback d-block">{{ phoneError }}</span>
|
||||
|
||||
@ -160,9 +162,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
import commonApi from '@/common/commonApi'
|
||||
import commonApi from '@/common/commonApi';
|
||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
||||
import FormSelect from '@c/input/FormSelect.vue';
|
||||
import ArrInput from '@c/input/ArrInput.vue';
|
||||
@ -185,13 +187,13 @@
|
||||
const birth = ref('');
|
||||
const address = ref('');
|
||||
const detailAddress = ref('');
|
||||
const postcode = ref(''); // 우편번호
|
||||
const postcode = ref(''); // 우편번호
|
||||
const phone = ref('');
|
||||
const phoneError = ref('');
|
||||
const color = ref(''); // 선택된 color
|
||||
const colorError = ref('');
|
||||
const mbti = ref(''); // 선택된 MBTI
|
||||
const pwhint = ref(''); // 선택된 pwhint
|
||||
const mbti = ref(''); // 선택된 MBTI
|
||||
const pwhint = ref(''); // 선택된 pwhint
|
||||
|
||||
const profilAlert = ref(false);
|
||||
const idAlert = ref(false);
|
||||
@ -209,7 +211,6 @@
|
||||
|
||||
const toastStore = useToastStore();
|
||||
|
||||
|
||||
// 프로필 체크
|
||||
const profileValid = (size, type) => {
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
@ -238,7 +239,7 @@
|
||||
// 사이즈, 파일 타입 안 맞으면 기본 이미지
|
||||
if (!profileValid(file.size, file.type)) {
|
||||
e.target.value = '';
|
||||
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
|
||||
profileLabel.style.backgroundImage = 'url("img/avatars/default-Profile.jpg")';
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -275,16 +276,17 @@
|
||||
|
||||
// 컬러, mbti, 비밀번호 힌트 목록 불러오기
|
||||
const { colorList, mbtiList, pwhintList } = commonApi({
|
||||
loadColor: true, colorType: 'YON',
|
||||
loadColor: true,
|
||||
colorType: 'YON',
|
||||
loadMbti: true,
|
||||
loadPwhint: true,
|
||||
});
|
||||
|
||||
// 주소 업데이트 핸들러
|
||||
const handleAddressUpdate = (addressData) => {
|
||||
const handleAddressUpdate = addressData => {
|
||||
address.value = addressData.address;
|
||||
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;
|
||||
colorError.value = '';
|
||||
colorErrorAlert.value = false;
|
||||
@ -319,10 +321,16 @@
|
||||
await checkColorDuplicate();
|
||||
}
|
||||
|
||||
const onlyNumber = (event) => {
|
||||
// 숫자가 아니면 입력 차단
|
||||
if (!/^[0-9]$/.test(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 회원가입
|
||||
const handleSubmit = async () => {
|
||||
|
||||
await checkColorDuplicate();
|
||||
|
||||
idAlert.value = id.value.trim() === '';
|
||||
@ -334,6 +342,12 @@
|
||||
addressAlert.value = address.value.trim() === '';
|
||||
phoneAlert.value = phone.value.trim() === '';
|
||||
|
||||
if (!/^\d+$/.test(phone.value)) {
|
||||
phoneAlert.value = true;
|
||||
} else {
|
||||
phoneAlert.value = false;
|
||||
}
|
||||
|
||||
// 프로필 이미지 체크
|
||||
if (!profile.value) {
|
||||
profilerr.value = '프로필 이미지를 선택해주세요.';
|
||||
@ -343,9 +357,21 @@
|
||||
profilAlert.value = false;
|
||||
}
|
||||
|
||||
if (profilAlert.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) {
|
||||
if (
|
||||
profilAlert.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;
|
||||
}
|
||||
|
||||
@ -364,7 +390,7 @@
|
||||
formData.append('memberMbt', mbti.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) {
|
||||
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
|
||||
@ -372,4 +398,3 @@
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, nextTick, computed } from 'vue';
|
||||
import { useUserStore } from '@s/userList';
|
||||
import { useProjectStore } from '@s/useProjectStore';
|
||||
import $api from '@api';
|
||||
|
||||
const emit = defineEmits(['user-list-update']);
|
||||
@ -106,7 +107,7 @@ const isUserDisabled = (user) => {
|
||||
// 클릭 시 활성화/비활성화 및 DB 업데이트
|
||||
// showOnlyActive가 true일 때는 toggleDisable 함수가 실행되지 않음
|
||||
const toggleDisable = async (index) => {
|
||||
if (props.showOnlyActive) return; // showOnlyActive가 true이면 함수 실행 중지
|
||||
if (props.showOnlyActive) return;
|
||||
|
||||
const user = displayedUserList.value[index];
|
||||
if (user) {
|
||||
@ -125,6 +126,11 @@ const toggleDisable = async (index) => {
|
||||
if (originalIndex !== -1) {
|
||||
userList.value[originalIndex].PROJCTYON = newParticipationStatus ? '0' : '1';
|
||||
}
|
||||
|
||||
// 변경 후 프로젝트 목록 새로고침
|
||||
const projectStore = useProjectStore();
|
||||
await projectStore.getProjectList('', '', 'true');
|
||||
await projectStore.getMemberProjects();
|
||||
}
|
||||
} else {
|
||||
// 원래 userList에서 해당 사용자를 찾아 업데이트
|
||||
|
||||
@ -87,27 +87,6 @@ profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}
|
||||
const setDefaultImage = (event) => (event.target.src = defaultProfile);
|
||||
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) => ({
|
||||
borderWidth: "3px",
|
||||
borderColor: user.usercolor || "#ccc",
|
||||
|
||||
@ -23,6 +23,8 @@
|
||||
|
||||
<!-- Drag Target Area To SlideIn Menu On Small Screens -->
|
||||
<div class="drag-target"></div>
|
||||
|
||||
<ScrollTopButton />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@ -32,6 +34,7 @@
|
||||
import TheChat from './TheChat.vue';
|
||||
import { nextTick } from 'vue';
|
||||
import { wait } from '@/common/utils';
|
||||
import ScrollTopButton from '@c/button/ScrollTopButton.vue';
|
||||
|
||||
window.isDarkStyle = window.Helpers.isDarkStyle();
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ const sendMessage = () => {
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,17 +1,7 @@
|
||||
<template>
|
||||
<footer class="content-footer footer bg-footer-theme">
|
||||
<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>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@ -74,6 +74,12 @@
|
||||
<div class="text-truncate">Commuters</div>
|
||||
</RouterLink>
|
||||
</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' : ''">
|
||||
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||
@ -86,7 +92,13 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -8,6 +8,17 @@
|
||||
|
||||
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
||||
<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="switchToDarkMode"><i class="bx bxs-moon"></i></button> -->
|
||||
|
||||
@ -234,36 +245,95 @@
|
||||
<script setup>
|
||||
import { useAuthStore } from '@s/useAuthStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useThemeStore } from '@s/darkmode';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
const user = ref(null);
|
||||
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const baseUrl = import.meta.env.VITE_SERVER;
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
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 () => {
|
||||
if (isDarkMode) {
|
||||
switchToDarkMode();
|
||||
} else {
|
||||
switchToLightMode();
|
||||
}
|
||||
// if (isDarkMode) {
|
||||
// switchToDarkMode();
|
||||
// } else {
|
||||
// switchToLightMode();
|
||||
// }
|
||||
|
||||
await userStore.userInfo();
|
||||
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 () => {
|
||||
await authStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@s/useAuthStore';
|
||||
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
|
||||
// 초기 렌더링 속도를 위해 지연 로딩 사용
|
||||
const routes = [
|
||||
@ -85,8 +85,9 @@ const routes = [
|
||||
component: () => import('@v/commuters/TheCommuters.vue'),
|
||||
},
|
||||
{
|
||||
path: '/sample',
|
||||
component: () => import('@c/calendar/SampleCalendar.vue'),
|
||||
path: '/authorization',
|
||||
component: () => import('@v/admin/TheAuthorization.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "/:anything(.*)",
|
||||
@ -102,38 +103,27 @@ const router = createRouter({
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
await authStore.checkAuthStatus(); // 로그인 상태 확인
|
||||
const allowedUserId = 26; // 특정 ID (변경필요!!)
|
||||
const userStore = useUserInfoStore();
|
||||
const userId = userStore.user?.id ?? null;
|
||||
|
||||
// 로그인이 필요한 페이지인데 로그인되지 않은 경우 → 로그인 페이지로 이동
|
||||
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||
// 로그인이 필요한 페이지인데 로그인되지 않은 경우 → 로그인 페이지로 이동
|
||||
next({ name: 'Login' });
|
||||
} else if (to.meta.requiresGuest && authStore.isAuthenticated) {
|
||||
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
||||
next({ name: 'Home' });
|
||||
} else {
|
||||
next();
|
||||
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@ -6,22 +6,96 @@
|
||||
설명 : 프로젝트 목록
|
||||
*/
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
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', {
|
||||
params: {
|
||||
searchKeyword: searchText || '',
|
||||
category: selectedYear || '',
|
||||
excludeEnded: excludeEnded
|
||||
},
|
||||
});
|
||||
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;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -109,6 +109,7 @@
|
||||
:passwordCommentAlert="passwordCommentAlert"
|
||||
:currentPasswordCommentId="currentPasswordCommentId"
|
||||
:password="password"
|
||||
:editCommentAlert="editCommentAlert"
|
||||
@editClick="editComment"
|
||||
@deleteClick="deleteComment"
|
||||
@updateReaction="handleCommentReaction"
|
||||
@ -118,6 +119,7 @@
|
||||
@cancelEdit="handleCancelEdit"
|
||||
@submitEdit="handleSubmitEdit"
|
||||
@update:password="updatePassword"
|
||||
@inputDetector="inputDetector"
|
||||
/>
|
||||
<Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
|
||||
</div>
|
||||
@ -133,11 +135,13 @@
|
||||
import BoardCommentList from '@c/board/BoardCommentList.vue';
|
||||
import BoardRecommendBtn from '@c/button/BoardRecommendBtn.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 { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import axios from '@api';
|
||||
|
||||
const $common = inject('common');
|
||||
// 게시물 데이터 상태
|
||||
const profileName = ref('');
|
||||
const boardTitle = ref('제목 없음');
|
||||
@ -161,6 +165,7 @@
|
||||
const unknown = computed(() => profileName.value === '익명');
|
||||
const currentUserId = computed(() => userStore?.user?.id); // 현재 로그인한 사용자 id
|
||||
const authorId = ref(''); // 작성자 id
|
||||
const editCommentAlert = ref({}); //댓글, 대댓글 오류 메세지 객체
|
||||
|
||||
const isAuthor = computed(() => currentUserId.value === authorId.value);
|
||||
const commentsWithAuthStatus = computed(() => {
|
||||
@ -234,6 +239,7 @@
|
||||
const inputCheck = () => {
|
||||
passwordAlert.value = '';
|
||||
};
|
||||
|
||||
// 게시물 상세 데이터 불러오기
|
||||
const fetchBoardDetails = async () => {
|
||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
@ -437,6 +443,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 댓글, 대댓글 오류 메세지 초기화
|
||||
const inputDetector = () => {
|
||||
editCommentAlert.value = {};
|
||||
};
|
||||
|
||||
// 게시글 수정 버튼 클릭
|
||||
const editClick = unknown => {
|
||||
const isUnknown = unknown?.unknown ?? false;
|
||||
@ -706,10 +717,10 @@
|
||||
|
||||
if (targetComment) {
|
||||
// console.log('타겟',targetComment)
|
||||
// ✅ 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
||||
// 댓글 내용만 "삭제된 댓글입니다."로 변경하고, 구조는 유지
|
||||
targetComment.content = '댓글이 삭제되었습니다.';
|
||||
targetComment.author = '알 수 없음'; // 익명 처리
|
||||
targetComment.isDeleted = true; // ✅ 삭제 상태를 추가
|
||||
targetComment.isDeleted = true; // 삭제 상태를 추가
|
||||
}
|
||||
} else {
|
||||
toastStore.onToast('댓글 삭제에 실패했습니다.', 'e');
|
||||
@ -721,6 +732,7 @@
|
||||
|
||||
// 댓글 수정 확인
|
||||
const handleSubmitEdit = async (comment, editedContent) => {
|
||||
if (!checkValidation(comment, editedContent)) return;
|
||||
togglePassword();
|
||||
try {
|
||||
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 targetComment = findCommentById(comment.commentId, comments.value);
|
||||
@ -799,8 +820,23 @@
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.board-content img {
|
||||
.board-content {
|
||||
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>
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CommuteCalendar from '@c/commuters/CommuteCalendar.vue';
|
||||
import CommuteCalendar from '@c/commuters/CommuterCalendar.vue';
|
||||
</script>
|
||||
|
||||
@ -152,23 +152,30 @@ function handleMonthChange(viewInfo) {
|
||||
loadCalendarData(year, month);
|
||||
}
|
||||
// 캘린더 클릭
|
||||
// 캘린더 클릭
|
||||
function handleDateClick(info) {
|
||||
const clickedDateStr = info.dateStr;
|
||||
const clickedDate = info.date;
|
||||
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 (
|
||||
clickedDate.getDay() === 0 ||
|
||||
clickedDate.getDay() === 6 ||
|
||||
holidayDates.value.has(clickedDateStr) ||
|
||||
clickedDateStr < todayStr
|
||||
clickedDate.getDay() === 0 || // 일요일
|
||||
clickedDate.getDay() === 6 || // 토요일
|
||||
holidayDates.value.has(clickedDateStr) || // 공휴일
|
||||
clickedDateStr <= oneWeekAgoObj.toISOString().split("T")[0] // 오늘 -7일 날짜 포함 과거 날짜 클릭 방지
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMyVacation = myVacations.value.some(vac => {
|
||||
const vacDate = vac.date ? vac.date.substring(0, 10) : "";
|
||||
return vacDate === clickedDateStr && !vac.receiverId;
|
||||
});
|
||||
|
||||
if (isMyVacation) {
|
||||
if (selectedDates.value.get(clickedDateStr) === "delete") {
|
||||
selectedDates.value.delete(clickedDateStr);
|
||||
@ -178,45 +185,55 @@ function handleDateClick(info) {
|
||||
updateCalendarEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDates.value.has(clickedDateStr)) {
|
||||
selectedDates.value.delete(clickedDateStr);
|
||||
updateCalendarEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
const type = halfDayType.value
|
||||
? (halfDayType.value === "AM" ? "700101" : "700102")
|
||||
: "700103";
|
||||
|
||||
selectedDates.value.set(clickedDateStr, type);
|
||||
halfDayType.value = null;
|
||||
updateCalendarEvents();
|
||||
|
||||
if (halfDayButtonsRef.value) {
|
||||
halfDayButtonsRef.value.resetHalfDay();
|
||||
}
|
||||
}
|
||||
// 오늘 이후의 날짜만 클릭 가능하도록 설정
|
||||
|
||||
function markClickableDates() {
|
||||
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 oneWeekAgoObj = new Date(todayObj);
|
||||
oneWeekAgoObj.setDate(todayObj.getDate() - 8); // 오늘 기준 7일 전 날짜
|
||||
|
||||
document.querySelectorAll(".fc-daygrid-day").forEach((cell) => {
|
||||
const dateStr = cell.getAttribute("data-date");
|
||||
if (!dateStr) return; // 날짜가 없으면 스킵
|
||||
|
||||
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.add("fc-day-sat-sun");
|
||||
cell.removeEventListener("click", handleDateClick); // 클릭 이벤트 제거
|
||||
}
|
||||
// 과거 날짜 (오늘 이전)
|
||||
else if (dateObj < todayObj) {
|
||||
cell.classList.remove("clickable");
|
||||
cell.classList.add("past"); // 과거 날짜 비활성화
|
||||
}
|
||||
// 오늘 & 미래 날짜 (클릭 가능)
|
||||
// 오늘 -6일부터 미래 날짜까지 클릭 가능
|
||||
else {
|
||||
cell.classList.add("clickable");
|
||||
cell.classList.remove("past", "fc-day-sat-sun");
|
||||
cell.addEventListener("click", handleDateClick); // 클릭 이벤트 추가
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -439,11 +456,12 @@ function updateCalendarEvents() {
|
||||
.filter(([date, type]) => type !== "delete")
|
||||
.map(([date, type]) => ({
|
||||
start: date,
|
||||
backgroundColor: "rgb(113 212 243 / 76%)",
|
||||
backgroundColor: "rgb(113 212 243 / 76%)", // 클릭하면 하늘색
|
||||
textColor: "#fff",
|
||||
display: "background",
|
||||
classNames: [getVacationTypeClass(type), "selected-event"]
|
||||
classNames: [getVacationTypeClass(type), "selected-event"] // 선택된 날짜 클래스 추가
|
||||
}));
|
||||
|
||||
const filteredFetchedEvents = fetchedEvents.value.filter(event => {
|
||||
if (event.saved && selectedDates.value.get(event.start) === "delete") {
|
||||
if (event.memberSeq === userStore.user.id) {
|
||||
@ -452,7 +470,37 @@ function updateCalendarEvents() {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user