Merge remote-tracking branch 'origin/main' into board-comment-2

This commit is contained in:
kimdaae328 2025-02-28 13:19:27 +09:00
commit 08dabf4191
11 changed files with 171 additions and 90 deletions

View File

@ -1,6 +1,6 @@
VITE_DOMAIN = https://192.168.0.251:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/
VITE_SERVER = httpS://192.168.0.251:10325/
VITE_API_URL = https://192.168.0.251:10325/api/
VITE_TEST_URL = https://192.168.0.251:10325/test/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

View File

@ -1,6 +1,6 @@
VITE_DOMAIN = http://localhost:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/
VITE_SERVER = http://localhost:10325/
VITE_API_URL = http://localhost:10325/api/
VITE_TEST_URL = http://localhost:10325/test/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

View File

@ -3,22 +3,33 @@
/* 휴가 */
.fc-daygrid-day-events {
max-height: 100px !important;
overflow-y: auto !important;
}
/* 이벤트 선 없게 */
.fc-event {
border: none;
}
/* 오전전반차 그래프 */
/* 오전 반차 그래프 (왼쪽 절반) */
.fc-daygrid-event.half-day-am {
width: calc(50% - 4px) !important;
width: 50% !important;
height: 8px !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 오후반차 그래프프 */
/* 오후 반차 그래프 (오른쪽 절반) */
.fc-daygrid-event.half-day-pm {
width: calc(50% - 4px) !important;
margin-left: auto !important
width: 50% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 연차 그래프 (풀풀) */
.fc-daygrid-event.full-day {
width: 100% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 공휴일,일요일 색상 */
.fc-day-sun .fc-daygrid-day-number,
@ -39,8 +50,8 @@
.flatpickr-calendar:after {
display: none !important;
}
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
/* 클릭 가능한 날짜 (오늘 + 미래) */
@ -84,7 +95,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 본인 모달 */
/* 닫기 버튼 */
.close-btn {
position: absolute;
@ -109,7 +119,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 선물하기 모달 */
/* 연차 개수 버튼 */
.count-btn {
font-size: 18px;
@ -127,7 +136,6 @@ opacity: 0.6; /* 흐려 보이게 */
background: #cccccc;
cursor: not-allowed;
}
/* 버튼 컨테이너 (우측 정렬) */
.custom-button-container {
display: flex;
@ -141,18 +149,17 @@ opacity: 0.6; /* 흐려 보이게 */
padding: 10px; /* 크기 조정 */
cursor: pointer; /* 클릭 가능하도록 변경 */
}
/* 아이콘 색상 변경 (기본) */
.custom-button i {
color: #282538; /* 기본 아이콘 색상 */
font-size: 25px; /* 아이콘 크기 */
}
/* 버튼 호버 효과 */
.custom-button:hover i {
color: #ff0800; /* 호버 시 아이콘 색상 변경 */
}
.grayscaleImg {
filter: grayscale(100%);
}

View File

@ -7,12 +7,6 @@
//-----------------
let menu, animate;
<<<<<<< HEAD
var menu, animate;
(function () {
// Initialize menu
//-----------------
=======
let layoutMenuEl = document.querySelectorAll('#layout-menu');
layoutMenuEl.forEach(function (element) {
menu = new Menu(element, {
@ -23,7 +17,6 @@ var menu, animate;
window.Helpers.scrollToActive((animate = false));
window.Helpers.mainMenu = menu;
});
>>>>>>> board-comment
// Initialize menu togglers and bind click on each
let menuToggler = document.querySelectorAll('.layout-menu-toggle');

View File

@ -28,10 +28,11 @@
</template>
<script setup>
import { defineEmits, ref, defineProps } from "vue";
import { defineEmits, ref, defineProps, watch } from "vue";
const props = defineProps({
isDisabled: Boolean
isDisabled: Boolean,
selectedDate: String // props
});
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
@ -39,15 +40,16 @@ const halfDayType = ref(null);
const toggleHalfDay = (type) => {
halfDayType.value = type;
emit("toggleHalfDay", halfDayType.value);
// 1
setTimeout(() => {
halfDayType.value = null;
}, 1000);
};
// `selectedDate`
watch(() => props.selectedDate, (newDate) => {
if (newDate) {
resetHalfDay();
}
});
//
const resetHalfDay = () => {
halfDayType.value = null;
@ -89,20 +91,20 @@ defineExpose({ resetHalfDay });
/* 선택된 (눌린) 버튼 */
.btn.active {
border: 3px solid #fff; /* 흰색 테두리 강조 */
border: 3px solid #fff;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
/* AM 버튼 (선택된 상태) */
.btn-warning.active {
background-color: #ffca2c !important; /* 진한 노란색 */
background-color: #ffca2c !important;
color: black;
}
/* PM 버튼 (선택된 상태) */
.btn-info.active {
background-color: #0b5ed7 !important; /* 진한 파란색 */
background-color: #0b5ed7 !important;
color: white;
}

View File

@ -1,11 +1,12 @@
<template>
<div class="mb-4 row">
<label :for="name" class="col-md-2 col-form-label">{{ title }}</label>
<label :for="inputId" class="col-md-2 col-form-label">{{ title }}</label>
<div class="col-md-10">
<input
class="form-control"
type="file"
:id="name"
:id="inputId"
ref="fileInput"
@change="changeHandler"
multiple
/>
@ -21,7 +22,7 @@ import { ref ,computed} from 'vue';
import { fileMsg } from '@/common/msgEnum';
// Props
const prop = defineProps({
const props = defineProps({
title: {
type: String,
default: '라벨',
@ -29,7 +30,7 @@ const prop = defineProps({
},
name: {
type: String,
default: 'nameplz',
default: 'fileInput',
required: true,
},
isAlert: {
@ -38,12 +39,13 @@ const prop = defineProps({
required: false,
},
});
const inputId = computed(() => props.name || 'defaultFileInput');
const emits = defineEmits(['update:data', 'update:isValid']);
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILE_COUNT = 5; //
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; //
const ALLOWED_FILE_TYPES = []; //
const showError = ref(false);
const fileMsgKey = ref(''); //
@ -51,9 +53,12 @@ const fileMsgKey = ref(''); // 에러 메시지 키
const changeHandler = (event) => {
const files = Array.from(event.target.files);
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
const invalidFiles = files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type));
//
// ALLOWED_FILE_TYPES
const invalidFiles = ALLOWED_FILE_TYPES.length > 0
? files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type))
: [];
if (totalSize > MAX_TOTAL_SIZE) {
showError.value = true;
fileMsgKey.value = 'FileMaxSizeMsg';

View File

@ -310,6 +310,18 @@ const closeEditModal = () => {
isEditModalOpen.value = false;
};
//
const hasChanges = computed(() => {
return selectedProject.value.PROJCTNAM !== props.title ||
selectedProject.value.PROJCTSTR !== props.strdate ||
selectedProject.value.PROJCTEND !== props.enddate ||
selectedProject.value.PROJCTZIP !== props.addressZip ||
selectedProject.value.PROJCTARR !== props.address ||
selectedProject.value.PROJCTDTL !== props.addressdtail ||
selectedProject.value.PROJCTDES !== props.description ||
selectedProject.value.PROJCTCOL !== props.projctCol;
});
//
const handleUpdate = () => {
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
@ -318,6 +330,11 @@ const handleUpdate = () => {
return;
}
if (!hasChanges.value) {
toastStore.onToast('변경된 내용이 없습니다.', 'e');
return;
}
$api.patch('project/update', {
projctSeq: selectedProject.value.PROJCTSEQ,
projctNam: selectedProject.value.PROJCTNAM,
@ -336,7 +353,6 @@ const handleUpdate = () => {
closeEditModal();
//
emit('update');
window.location.reload()
}
});
};

View File

@ -1,7 +1,7 @@
<template>
<ul class="list-unstyled users-list d-flex align-items-center gap-1">
<li
v-for="(user, index) in userList"
v-for="(user, index) in sortedUserList"
:key="index"
class="avatar pull-up"
:class="{ 'opacity-100': isUserDisabled(user) }"
@ -25,7 +25,7 @@
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue';
import { onMounted, ref, nextTick, computed } from 'vue';
import { useUserStore } from '@s/userList';
import $api from '@api';
@ -41,6 +41,19 @@ const props = defineProps({
}
});
// computed
const sortedUserList = computed(() => {
return [...userList.value].sort((a, b) => {
const aDisabled = isUserDisabled(a);
const bDisabled = isUserDisabled(b);
//
if (!aDisabled && bDisabled) return -1;
if (aDisabled && !bDisabled) return 1;
return 0;
});
});
//
const fetchProjectParticipation = async () => {
if (props.projctSeq) {
@ -79,27 +92,33 @@ const isUserDisabled = (user) => {
// / DB
const toggleDisable = async (index) => {
const user = userList.value[index];
const user = sortedUserList.value[index];
if (user) {
const newParticipationStatus = props.projctSeq
? user.PROJCTYON === '1'
: !user.disabled;
if (props.projctSeq) {
const response = await $api.patch('project/updateYon', {
memberSeq: user.MEMBERSEQ,
projctSeq: props.projctSeq,
projctYon: newParticipationStatus ? '0' : '1'
});
if (response.status === 200) {
user.PROJCTYON = newParticipationStatus ? '0' : '1';
// userList
const originalIndex = userList.value.findIndex(u => u.MEMBERSEQ === user.MEMBERSEQ);
if (originalIndex !== -1) {
userList.value[originalIndex].PROJCTYON = newParticipationStatus ? '0' : '1';
}
}
} else {
user.disabled = newParticipationStatus;
// userList
const originalIndex = userList.value.findIndex(u => u.MEMBERSEQ === user.MEMBERSEQ);
if (originalIndex !== -1) {
userList.value[originalIndex].disabled = newParticipationStatus;
emitUserListUpdate();
}
}
}
};
// emit

View File

@ -32,17 +32,17 @@
window.isDarkStyle = window.Helpers.isDarkStyle();
const loadScript = src => {
const loadScript = src => {
const script = document.createElement('script');
script.src = src;
script.type = 'text/javascript';
script.async = true;
document.body.appendChild(script);
};
nextTick(async () => {
};
nextTick(async () => {
await wait(200);
loadScript('/vendor/js/menu.js');
// loadScript('/js/main.js');
});
loadScript('/js/main.js');
});
</script>
<style></style>

View File

@ -241,7 +241,7 @@
const user = ref(null);
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const baseUrl = import.meta.env.BASE_URL;
const baseUrl = import.meta.env.VITE_SERVER;
const authStore = useAuthStore();
const userStore = useUserInfoStore();

View File

@ -7,9 +7,11 @@
<div class="sidebar-content">
<div class="sidebar-actions text-center my-3">
<HalfDayButtons
ref="halfDayButtonsRef"
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
:isDisabled="!hasChanges"
:selectedDate="selectedDate"
/>
</div>
<ProfileList
@ -58,28 +60,31 @@
</div>
</template>
<script setup>
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
import axios from "@api";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect
import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css";
<script setup>
import { reactive, ref, onMounted, nextTick, computed, watch, onBeforeUnmount } from "vue";
import axios from "@api";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect
import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css";
import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@c/vacation/ProfileList.vue";
import VacationModal from "@c/modal/VacationModal.vue";
import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
import { useUserStore } from "@s/userList";
import { useUserInfoStore } from "@s/useUserInfoStore";
import { fetchHolidays } from "@c/calendar/holiday.js";
import { useToastStore } from '@s/toastStore';
import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@c/vacation/ProfileList.vue";
import VacationModal from "@c/modal/VacationModal.vue";
import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
import { useUserStore } from "@s/userList";
import { useUserInfoStore } from "@s/useUserInfoStore";
import { fetchHolidays } from "@c/calendar/holiday.js";
import { useToastStore } from '@s/toastStore';
import { useRouter } from "vue-router";
const router = useRouter();
const toastStore = useToastStore();
const userStore = useUserInfoStore();
@ -90,7 +95,7 @@
const receivedVacations = ref([]);
const isModalOpen = ref(false);
const remainingVacationData = ref({});
const selectedDate = ref(null);
const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false);
@ -106,6 +111,39 @@
const fetchedEvents = ref([]);
const halfDayButtonsRef = ref(null);
//
router.beforeEach((to, from, next) => {
if (hasChanges.value) {
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
if (!answer) {
return next(false); //
}
}
next();
});
onBeforeUnmount(() => {
window.removeEventListener("beforeunload", preventUnsavedChanges);
});
function preventUnsavedChanges(event) {
if (hasChanges.value) {
event.preventDefault();
event.returnValue = ""; //
}
}
// `selectedDates`
watch(
() => Array.from(selectedDates.value.keys()), //
(newKeys) => {
if (halfDayButtonsRef.value) {
halfDayButtonsRef.value.resetHalfDay();
}
},
{ deep: true }
);
// ref
const calendarDatepicker = ref(null);
let fpInstance = null;
@ -192,6 +230,7 @@ function handleDateClick(info) {
await fetchRemainingVacation();
const currentYear = new Date().getFullYear();
await fetchVacationHistory(currentYear);
window.addEventListener("beforeunload", preventUnsavedChanges);
// Flatpickr ( )
fpInstance = flatpickr(calendarDatepicker.value, {