Merge branch 'main' into vacation
This commit is contained in:
commit
2166bc66a9
@ -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="/"
|
||||
|
||||
@ -432,4 +432,29 @@ cursor: not-allowed !important;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
/* commuters project list end */
|
||||
/* 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 */
|
||||
@ -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 = '';
|
||||
|
||||
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,7 +1,6 @@
|
||||
<template>
|
||||
<div class="row g-0">
|
||||
<div class="col-6 pe-1">
|
||||
<p class="mb-1">출근시간</p>
|
||||
<button
|
||||
class="btn border-3 w-100 py-0 h-px-50"
|
||||
:class="workTime ? 'p-0 btn-primary pe-none' : 'btn-outline-primary'"
|
||||
@ -13,7 +12,6 @@
|
||||
</div>
|
||||
|
||||
<div class="col-6 ps-1">
|
||||
<p class="mb-1">퇴근시간</p>
|
||||
<button
|
||||
class="btn btn-outline-secondary border-3 w-100 py-0 h-px-50"
|
||||
@click="setLeaveTime"
|
||||
@ -29,7 +27,6 @@
|
||||
import { ref, defineProps, defineEmits, onMounted, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
import { useGeolocation } from '@vueuse/core';
|
||||
import { useToastStore } from '@/stores/toastStore';
|
||||
|
||||
const props = defineProps({
|
||||
userId: {
|
||||
@ -47,7 +44,6 @@ const emit = defineEmits(['workTimeUpdated', 'leaveTimeUpdated']);
|
||||
const workTime = ref(null);
|
||||
const leaveTime = ref(null)
|
||||
const userLocation = ref(null);
|
||||
const toastStore = useToastStore();
|
||||
|
||||
// 위치 정보 가져오기 설정
|
||||
const { coords, isSupported, error } = useGeolocation({
|
||||
@ -74,12 +70,12 @@ const getAddress = (lat, lng) => {
|
||||
// 위치 정보 가져오기 함수
|
||||
const getLocation = async () => {
|
||||
if (!isSupported.value) {
|
||||
toastStore.onToast('브라우저가 위치 정보를 지원하지 않습니다.', 'e');
|
||||
alert('브라우저가 위치 정보를 지원하지 않습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error.value) {
|
||||
toastStore.onToast(`위치 정보를 가져오는데 실패했습니다: ${error.value.message}`, 'e');
|
||||
alert(`위치 정보를 가져오는데 실패했습니다: ${error.value.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -93,7 +89,7 @@ const getLocation = async () => {
|
||||
const address = await getAddress(coords.value.latitude, coords.value.longitude);
|
||||
return address;
|
||||
} catch (error) {
|
||||
toastStore.onToast(error, 'e');
|
||||
alert(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -135,7 +131,6 @@ const setWorkTime = async () => {
|
||||
commutArr: address,
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('출근 완료.', 's');
|
||||
todayCommuterInfo();
|
||||
|
||||
emit('workTimeUpdated', true);
|
||||
@ -150,11 +145,6 @@ const setLeaveTime = () => {
|
||||
commutLve: leaveTime.value || null,
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
if (leaveTime.value) {
|
||||
toastStore.onToast('퇴근 시간 초기화', 'e');
|
||||
} else {
|
||||
toastStore.onToast('퇴근 완료', 's');
|
||||
}
|
||||
todayCommuterInfo();
|
||||
// 부모 컴포넌트에 업데이트 이벤트 발생
|
||||
emit('leaveTimeUpdated', true);
|
||||
|
||||
@ -86,7 +86,6 @@ import '@/assets/css/app-calendar.css';
|
||||
import { fetchHolidays } from '@c/calendar/holiday';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import { useToastStore } from '@/stores/toastStore';
|
||||
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||
import CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
|
||||
import BackBtn from '@c/button/BackBtn.vue';
|
||||
@ -96,7 +95,6 @@ const user = ref({});
|
||||
const project = ref({});
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
const toastStore = useToastStore();
|
||||
|
||||
const dayjs = inject('dayjs');
|
||||
const fullCalendarRef = ref(null);
|
||||
@ -134,19 +132,6 @@ const handleProjectDrop = ({ event, targetProject }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// select값에 드롭한 프로젝트가 없는 경우
|
||||
if (!selectedProject.value) {
|
||||
$api.patch('project/updateYon', {
|
||||
projctSeq: targetProject.PROJCTSEQ,
|
||||
memberSeq: user.value.id,
|
||||
projctYon: '1'
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
projectStore.getProjectList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 선택된 프로젝트 변경
|
||||
checkedInProject.value = targetProject;
|
||||
projectStore.setSelectedProject(targetProject);
|
||||
@ -159,8 +144,6 @@ const handleProjectDrop = ({ event, targetProject }) => {
|
||||
memberSeq: user.value.id,
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('출근 프로젝트가 변경 되었습니다.', 's');
|
||||
|
||||
todaysCommuter();
|
||||
loadCommuters();
|
||||
}
|
||||
@ -317,15 +300,15 @@ const loadCommuters = async () => {
|
||||
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}"]`);
|
||||
|
||||
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 ms-1 position-relative z-5';
|
||||
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'; };
|
||||
|
||||
|
||||
@ -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에서 해당 사용자를 찾아 업데이트
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -9,8 +9,13 @@
|
||||
<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 projectStore.memberProjectList" :key="item.PROJCTSEQ" :value="item.PROJCTSEQ">
|
||||
{{ item.PROJCTNAM }}
|
||||
<!-- 내가 참여하고 있는 프로젝트 그룹 -->
|
||||
<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>
|
||||
|
||||
@ -243,7 +248,7 @@
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useThemeStore } from '@s/darkmode';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_SERVER;
|
||||
@ -256,11 +261,28 @@
|
||||
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.memberProjectList.find(
|
||||
// 전체 프로젝트 리스트에서 선택된 프로젝트 찾기
|
||||
const selected = projectStore.projectList.find(
|
||||
project => project.PROJCTSEQ === selectedProject.value
|
||||
);
|
||||
|
||||
@ -269,6 +291,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 프로젝트 변경 감지
|
||||
watch(() => projectStore.selectedProject, (newProject) => {
|
||||
if (newProject) {
|
||||
selectedProject.value = newProject.PROJCTSEQ;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||
|
||||
@ -282,25 +311,23 @@
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
|
||||
await projectStore.getProjectList('', '', 'true');
|
||||
|
||||
// 사용자가 참여하고 있는 프로젝트 목록
|
||||
await projectStore.getMemberProjects();
|
||||
|
||||
// memberProjectList가 로드된 후 selectedProject 업데이트
|
||||
if (projectStore.selectedProject) {
|
||||
selectedProject.value = projectStore.selectedProject.PROJCTSEQ;
|
||||
// 저장된 선택 프로젝트
|
||||
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]);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// projectStore.selectedProject 변경 감지
|
||||
watch(() => projectStore.selectedProject, (newProject) => {
|
||||
if (newProject) {
|
||||
selectedProject.value = newProject.PROJCTSEQ;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authStore.logout();
|
||||
|
||||
@ -35,7 +35,6 @@ export const useProjectStore = defineStore('project', () => {
|
||||
const res = await $api.get(`project/${userStore.user.id}`);
|
||||
memberProjectList.value = res.data.data;
|
||||
|
||||
// 로그인 직후 자동으로 프로젝트 선택 (watch와 별개로 직접 처리)
|
||||
if (memberProjectList.value.length > 0 && !selectedProject.value) {
|
||||
setSelectedProject(memberProjectList.value[0]);
|
||||
}
|
||||
@ -62,32 +61,34 @@ export const useProjectStore = defineStore('project', () => {
|
||||
|
||||
// 프로젝트 리스트가 변경될 때 자동으로 반응
|
||||
watch(projectList, (newList) => {
|
||||
|
||||
// 선택된 프로젝트가 없고 목록이 있는 경우
|
||||
if (!selectedProject.value && newList.length > 0) {
|
||||
setSelectedProject(newList[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);
|
||||
const exists = newList.some(project => project.PROJCTSEQ === selectedProject.value.PROJCTSEQ);
|
||||
|
||||
if (!exists) {
|
||||
setSelectedProject(newList[0]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 목록이 비어있으면 선택된 프로젝트를 null로 설정
|
||||
setSelectedProject(null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
projectList,
|
||||
selectedProject,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user