Merge branch 'main' into vacation

This commit is contained in:
dyhj625 2025-03-14 14:32:21 +09:00
commit 2166bc66a9
11 changed files with 169 additions and 85 deletions

View File

@ -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="/"

View File

@ -433,3 +433,28 @@ cursor: not-allowed !important;
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 */

View File

@ -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 = '';

View 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>

View File

@ -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);

View File

@ -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'; };

View File

@ -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

View File

@ -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();

View File

@ -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>

View File

@ -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();

View File

@ -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,