diff --git a/.env.dev b/.env.dev index f608ba3..5702020 100644 --- a/.env.dev +++ b/.env.dev @@ -1,6 +1,6 @@ -VITE_DOMAIN = http://localhost:5173/ +VITE_DOMAIN = https://192.168.0.251:5173/ # VITE_LOGIN_URL = http://localhost:10325/ms/ # VITE_FILE_URL = http://localhost:10325/ms/ -# VITE_API_URL = http://localhost:10325/api/ -VITE_API_URL = http://localhost:10325/test/ +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 \ No newline at end of file diff --git a/.env.mine b/.env.mine new file mode 100644 index 0000000..6f5a98c --- /dev/null +++ b/.env.mine @@ -0,0 +1,6 @@ +VITE_DOMAIN = http://localhost:5173/ +# VITE_LOGIN_URL = http://localhost:10325/ms/ +# VITE_FILE_URL = http://localhost:10325/ms/ +VITE_API_URL = http://localhost:10325/api/ +VITE_TEST_URL = http://localhost:10325/test/ +VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492 \ No newline at end of file diff --git a/package.json b/package.json index 2b5c030..9640429 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,51 @@ { - "name": "front", - "version": "0.0.1", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 0.0.0.0 --mode dev", - "build": "vite build --mode prod", - "preview": "vite preview", - "lint": "eslint . --fix", - "format": "prettier --write src/" - }, - "dependencies": { - "@fullcalendar/core": "^6.1.15", - "@fullcalendar/daygrid": "^6.1.15", - "@fullcalendar/interaction": "^6.1.15", - "@fullcalendar/vue3": "^6.1.15", - "@popperjs/core": "^2.11.8", - "@tinymce/tinymce-vue": "^5.1.1", - "@vueup/vue-quill": "^1.2.0", - "axios": "^1.7.9", - "bootstrap": "^5.3.3", - "bootstrap-icons": "^1.11.3", - "dayjs": "^1.11.13", - "dompurify": "^3.2.3", - "flatpickr": "^4.6.13", - "front": "file:", - "heic2any": "^0.0.4", - "pinia": "^2.2.6", - "pinia-plugin-persist": "^1.0.0", - "quill": "^2.0.3", - "upload-images-converter": "^2.0.2", - "vite-plugin-mkcert": "^1.17.6", - "vue": "^3.5.13", - "vue-flatpickr-component": "^11.0.5", - "vue-router": "^4.4.5", - "vue3-kakao-maps": "^2.3.10" - }, - "devDependencies": { - "@eslint/js": "^9.14.0", - "@vitejs/plugin-vue": "^5.2.1", - "@vue/eslint-config-prettier": "^10.1.0", - "eslint": "^9.14.0", - "eslint-plugin-vue": "^9.30.0", - "prettier": "^3.3.3", - "vite": "^5.4.10", - "vite-plugin-inspect": "^0.8.9", - "vite-plugin-vue-devtools": "^7.6.5" - } + "name": "front", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --mode dev", + "mine": "vite --host 0.0.0.0 --mode mine", + "build": "vite build --mode prod", + "preview": "vite preview", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/vue3": "^6.1.15", + "@popperjs/core": "^2.11.8", + "@tinymce/tinymce-vue": "^5.1.1", + "@vueup/vue-quill": "^1.2.0", + "axios": "^1.7.9", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "dayjs": "^1.11.13", + "dompurify": "^3.2.3", + "flatpickr": "^4.6.13", + "front": "file:", + "heic2any": "^0.0.4", + "pinia": "^2.2.6", + "pinia-plugin-persist": "^1.0.0", + "quill": "^2.0.3", + "upload-images-converter": "^2.0.2", + "vite-plugin-mkcert": "^1.17.6", + "vue": "^3.5.13", + "vue-flatpickr-component": "^11.0.5", + "vue-router": "^4.4.5", + "vue3-kakao-maps": "^2.3.10" + }, + "devDependencies": { + "@eslint/js": "^9.14.0", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-prettier": "^10.1.0", + "eslint": "^9.14.0", + "eslint-plugin-vue": "^9.30.0", + "prettier": "^3.3.3", + "vite": "^5.4.10", + "vite-plugin-inspect": "^0.8.9", + "vite-plugin-vue-devtools": "^7.6.5" + } } diff --git a/public/css/custom.css b/public/css/custom.css index 17633c9..4f25379 100644 --- a/public/css/custom.css +++ b/public/css/custom.css @@ -7,42 +7,184 @@ /* 휴가 */ -.half-day-buttons { - display: flex; - justify-content: center; - gap: 10px; - margin-top: 20px; -} -.half-day-buttons .btn.active { - border: 2px solid black; -} .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; } +/* 오후반차 그래프프 */ .fc-daygrid-event.half-day-pm { width: calc(50% - 4px) !important; margin-left: auto !important } +/* 공휴일,일요일 색상 */ .fc-day-sun .fc-daygrid-day-number, .fc-col-header-cell:first-child .fc-col-header-cell-cushion { color: #ff4500 !important; } +/* 토요일 색상 */ .fc-day-sat .fc-daygrid-day-number, .fc-col-header-cell:last-child .fc-col-header-cell-cushion { color: #6076e0 !important; } +/* 캘린더 날짜 왼쪽 상단 위치하게 */ .fc-daygrid-day-number { margin-right: auto; } +/* 데이트피커 뾰족없게게 */ +.flatpickr-calendar:before, +.flatpickr-calendar:after { + display: none !important; +} + /* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */ + .fc-toolbar-title { + cursor: pointer; +} +/* 클릭 가능한 날짜 (오늘 + 미래) */ +.fc-daygrid-day.clickable { +cursor: pointer; +transition: background-color 0.2s ease-in-out; +} +/* 마우스를 올렸을 때 효과 */ +.fc-daygrid-day.clickable:hover { +background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */ +} +/* 주말 (토요일, 일요일) 및 공휴일 */ +.fc-day-sat-sun { +cursor: not-allowed !important; +opacity: 0.6; /* 흐려 보이게 */ +} +/* 과거 날짜 (오늘 이전) */ +.fc-daygrid-day.past { +cursor: not-allowed !important; +opacity: 0.6; /* 흐려 보이게 */ +} +/* 기본 이벤트 스타일 */ +.fc-daygrid-event { + border: none !important; + border-radius: 4px; +} +/* 오전 반차 (왼쪽 절반) */ +.selected-event.half-day-am { + width: 50% !important; + left: 0 !important; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +/* 오후 반차 (오른쪽 절반) */ +.selected-event.half-day-pm { + width: 50% !important; + margin-left: auto !important; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + + +/* 본인 모달 */ + +/* 닫기 버튼 */ +.close-btn { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + font-size: 18px; + cursor: pointer; +} +/* 리스트 아이템 */ +.vacation-item { + display: flex; + align-items: center; + font-size: 16px; + font-weight: bold; + margin-bottom: 8px; + padding: 5px 10px; + border-radius: 5px; + background: #f9f9f9; +} +/* 모달 본문 스크롤 */ +.modal-body { + max-height: 130px; + overflow-y: auto; +} + +/* 선물하기 모달 */ + +/* 연차 개수 버튼 */ +.count-btn { + font-size: 18px; + padding: 2px 10px; + border: none; + background: #2C3E50; + color: white; + border-radius: 5px; + cursor: pointer; +} +.count-btn:hover { + background: #1d2c44; +} +.count-btn:disabled { + background: #cccccc; + cursor: not-allowed; +} + +/* 버튼 컨테이너 (우측 정렬) */ +.custom-button-container { + display: flex; + justify-content: flex-end; + align-items: center; +} +/* 버튼 기본 스타일 */ +.custom-button { + background: none; /* 배경색 없음 */ + border: none; /* 테두리 없음 */ + padding: 10px; /* 크기 조정 */ + cursor: pointer; /* 클릭 가능하도록 변경 */ +} + +/* 아이콘 색상 변경 (기본) */ +.custom-button i { + color: #282538; /* 기본 아이콘 색상 */ + font-size: 25px; /* 아이콘 크기 */ +} + +/* 버튼 호버 효과 */ +.custom-button:hover i { + color: #ff0800; /* 호버 시 아이콘 색상 변경 */ +} + + + + +/* 모달 배경 투명하게 */ +.modal-dialog { + background: none !important; /* 배경 제거 */ + box-shadow: none !important; /* 음영 제거 */ + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +/* 모달 내용 스타일 */ +.modal-content { + background: #fff; /* 기존 흰색 배경 유지 */ + border-radius: 8px; + box-shadow: none !important; /* 내부 음영 제거 */ + padding: 20px; + max-width: 500px; + width: 100%; +} .grayscaleImg { filter: grayscale(100%); diff --git a/src/common/axios-interceptor.js b/src/common/axios-interceptor.js index 3dd170d..46902ec 100644 --- a/src/common/axios-interceptor.js +++ b/src/common/axios-interceptor.js @@ -3,7 +3,7 @@ import { useRoute } from 'vue-router'; import { useToastStore } from '@s/toastStore'; const $api = axios.create({ - baseURL: 'https://192.168.0.251:10325/api/', + baseURL: import.meta.env.VITE_API_URL, timeout: 300000, withCredentials: true, }); diff --git a/src/components/button/HalfDayButtons.vue b/src/components/button/HalfDayButtons.vue index 83f4c35..9bb125f 100644 --- a/src/components/button/HalfDayButtons.vue +++ b/src/components/button/HalfDayButtons.vue @@ -1,43 +1,133 @@ + - +/* 마우스를 올렸을 때 */ +.btn:hover { + filter: brightness(90%); + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); + transform: scale(1.05); +} + +/* 버튼이 눌렸을 때 */ +.btn:active { + transform: scale(0.9); + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2); +} + +/* 선택된 (눌린) 버튼 */ +.btn.active { + 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; /* 진한 노란색 */ + color: black; +} + +/* PM 버튼 (선택된 상태) */ +.btn-info.active { + background-color: #0b5ed7 !important; /* 진한 파란색 */ + color: white; +} + +/* ✔ 버튼 */ +.btn-success { + font-size: 24px; + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease-in-out; +} + +/* ✔ 버튼 마우스 오버 */ +.btn-success:hover { + background-color: #198754; + box-shadow: 0px 4px 10px rgba(25, 135, 84, 0.4); + transform: scale(1.1); +} + +/* ✔ 버튼 클릭 */ +.btn-success:active { + transform: scale(0.95); + box-shadow: 0px 2px 5px rgba(25, 135, 84, 0.2); +} + diff --git a/src/components/modal/VacationGrantModal.vue b/src/components/modal/VacationGrantModal.vue index 333810e..d3baa12 100644 --- a/src/components/modal/VacationGrantModal.vue +++ b/src/components/modal/VacationGrantModal.vue @@ -1,21 +1,22 @@ diff --git a/src/views/board/BoardWrite.vue b/src/views/board/BoardWrite.vue index b5f0a45..b337d6c 100644 --- a/src/views/board/BoardWrite.vue +++ b/src/views/board/BoardWrite.vue @@ -91,7 +91,9 @@ import router from '@/router'; import axios from '@api'; import SaveButton from '@c/button/SaveBtn.vue'; import BackButton from '@c/button/BackBtn.vue' +import { useToastStore } from '@s/toastStore'; +const toastStore = useToastStore(); const categoryList = ref([]); const title = ref(''); const password = ref(''); @@ -113,6 +115,11 @@ const fetchCategories = async () => { try { const response = await axios.get('board/categories'); categoryList.value = response.data.data; + // "자유" 카테고리 찾기 (CMNCODNAM이 '자유'인 것 선택) + const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유'); + if (freeCategory) { + categoryValue.value = freeCategory.CMNCODVAL; // 기본 선택값 설정 + } } catch (error) { console.error('카테고리 불러오기 오류:', error); } @@ -167,11 +174,11 @@ const write = async () => { } } - alert('게시물이 작성되었습니다.'); + toastStore.onToast('게시물이 작성되었습니다.', 's'); goList(); } catch (error) { console.error(error); - alert('게시물 작성 중 오류가 발생했습니다.'); + toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e'); } }; diff --git a/src/views/vacation/VacationManagement.vue b/src/views/vacation/VacationManagement.vue index 004c2a0..6c26514 100644 --- a/src/views/vacation/VacationManagement.vue +++ b/src/views/vacation/VacationManagement.vue @@ -9,6 +9,7 @@ { + return ( + selectedDates.value.size > 0 || + myVacations.value.some(vac => selectedDates.value.has(vac.date.split("T")[0])) + ); +}); + + +/** ✅ selectedDates가 변경될 때 버튼 상태 즉시 업데이트 */ +watch( + () => Array.from(selectedDates.value.keys()), // keys()를 Array로 변환해서 감시 + (newKeys) => { + }, + { deep: true } +); + +function handleDateClick(info) { + const clickedDateStr = info.dateStr; + const clickedDate = info.date; + const todayStr = new Date().toISOString().split("T")[0]; + + if ( + clickedDate.getDay() === 0 || + clickedDate.getDay() === 6 || + holidayDates.value.has(clickedDateStr) || + clickedDateStr < todayStr + ) { + return; + } + const isMyVacation = myVacations.value.some(vac => { + const vacDate = vac.date ? String(vac.date).substring(0, 10) : ""; + return vacDate === clickedDateStr && !vac.receiverId; + }); + + if (isMyVacation) { + if (selectedDates.value.get(clickedDateStr) === "delete") { + selectedDates.value.delete(clickedDateStr); + } else { + selectedDates.value.set(clickedDateStr, "delete"); + } + 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(); + } + } + const calendarOptions = reactive({ plugins: [dayGridPlugin, interactionPlugin], initialView: "dayGridMonth", @@ -150,17 +216,28 @@ } }); - // FullCalendar 헤더 제목(.fc-toolbar-title) 클릭 시 데이트피커 열기 + // FullCalendar 헤더 제목(.fc-toolbar-title) 클릭 시 데이트피커 열기 nextTick(() => { - const titleEl = document.querySelector('.fc-toolbar-title'); - if (titleEl) { + const titleEl = document.querySelector('.fc-toolbar-title'); + if (titleEl) { titleEl.style.cursor = 'pointer'; titleEl.addEventListener('click', () => { - fpInstance.open(); + // 화면 중앙 정렬을 위한 스타일 조정 + const dpEl = calendarDatepicker.value; + dpEl.style.display = 'block'; + dpEl.style.position = 'fixed'; + dpEl.style.top = '22%'; + dpEl.style.left = '66%'; + dpEl.style.transform = 'translate(-50%, -50%)'; + dpEl.style.zIndex = '9999'; + dpEl.style.border = 'none'; + dpEl.style.outline = 'none'; + dpEl.style.backgroundColor = 'transparent'; + fpInstance.open(); }); - } + } }); - }); + }) // 연차 내역 API (초기 호출용) async function fetchVacationHistory(year) { @@ -272,7 +349,6 @@ const selectedEvents = Array.from(selectedDates.value) .filter(([date, type]) => type !== "delete") .map(([date, type]) => ({ - title: getVacationType(type), start: date, backgroundColor: "rgb(113 212 243 / 76%)", textColor: "#fff", @@ -298,47 +374,6 @@ return "full-day"; }; - function handleDateClick(info) { - const clickedDateStr = info.dateStr; - const clickedDate = info.date; - const todayStr = new Date().toISOString().split("T")[0]; - - if ( - clickedDate.getDay() === 0 || - clickedDate.getDay() === 6 || - holidayDates.value.has(clickedDateStr) || - clickedDateStr < todayStr - ) { - return; - } - const isMyVacation = myVacations.value.some(vac => { - const vacDate = vac.date ? String(vac.date).substring(0, 10) : ""; - return vacDate === clickedDateStr && !vac.receiverId; - }); - - if (isMyVacation) { - if (selectedDates.value.get(clickedDateStr) === "delete") { - selectedDates.value.delete(clickedDateStr); - } else { - selectedDates.value.set(clickedDateStr, "delete"); - } - 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(); - } - function toggleHalfDay(type) { halfDayType.value = halfDayType.value === type ? null : type; } @@ -381,6 +416,7 @@ } async function saveVacationChanges() { + if (!hasChanges.value) return; const selectedDatesArray = Array.from(selectedDates.value); const vacationsToAdd = selectedDatesArray .filter(([date, type]) => type !== "delete") @@ -405,7 +441,7 @@ delete: vacationsToDelete }); if (response.data && response.data.status === "OK") { - alert("✅ 휴가 변경 사항이 저장되었습니다."); + toastStore.onToast('휴가 변경 사항이 저장되었습니다.', 's'); await fetchRemainingVacation(); if (isModalOpen.value) { await fetchVacationHistory(lastRemainingYear.value); @@ -415,11 +451,11 @@ selectedDates.value.clear(); updateCalendarEvents(); } else { - alert("❌ 휴가 저장 중 오류가 발생했습니다."); + toastStore.onToast('휴가 저장 중 오류가 발생했습니다.', 'e'); } } catch (error) { console.error("🚨 휴가 변경 저장 실패:", error); - alert("❌ 휴가 저장 요청에 실패했습니다."); + toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e'); } } @@ -447,6 +483,46 @@ await nextTick(); fullCalendarRef.value.getApi().refetchEvents(); } + /** ✅ 오늘 이후의 날짜만 클릭 가능하도록 설정 */ +function markClickableDates() { + nextTick(() => { + const todayStr = new Date().toISOString().split("T")[0]; // 오늘 날짜 YYYY-MM-DD + const todayObj = new Date(todayStr); + + 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)) { + cell.classList.remove("clickable"); + cell.classList.add("fc-day-sat-sun"); + } + // 과거 날짜 (오늘 이전) + else if (dateObj < todayObj) { + cell.classList.remove("clickable"); + cell.classList.add("past"); // 과거 날짜 비활성화 + } + // 오늘 & 미래 날짜 (클릭 가능) + else { + cell.classList.add("clickable"); + cell.classList.remove("past", "fc-day-sat-sun"); + } + }); + }); +} + +/** ✅ onMounted 및 달력 변경 시 실행 */ +onMounted(() => { + markClickableDates(); +}); + +watch([holidayDates, lastRemainingYear, lastRemainingMonth], () => { + markClickableDates(); +}); + onMounted(async () => { await fetchUserList(); @@ -460,10 +536,5 @@ diff --git a/vite.config.js b/vite.config.js index 6d58ffd..9ff7390 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,27 +5,33 @@ import vueDevTools from 'vite-plugin-vue-devtools'; import mkcert from 'vite-plugin-mkcert'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - vueDevTools(), - // 자신의 로컬 서버에 연결하려면 이부분 주석처리 - mkcert({ - // SSL 키 등록 - keyFile: '/localhost-key.pem', - certFile: '/localhost.pem', - }), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - '@a': fileURLToPath(new URL('./src/assets/', import.meta.url)), - '@c': fileURLToPath(new URL('./src/components/', import.meta.url)), - '@v': fileURLToPath(new URL('./src/views/', import.meta.url)), - '@l': fileURLToPath(new URL('./src/layout/', import.meta.url)), - '@s': fileURLToPath(new URL('./src/stores/', import.meta.url)), - '@p': fileURLToPath(new URL('./src/common/plugin/', import.meta.url)), - '@api': fileURLToPath(new URL('./src/common/axios-interceptor.js', import.meta.url)), +export default defineConfig(({ mode }) => { + const plugins = [vue(), vueDevTools()]; + + // dev: https, mine: http + if (mode === 'dev') { + plugins.push( + mkcert({ + // SSL 키 등록 + keyFile: '/localhost-key.pem', + certFile: '/localhost.pem', + }), + ); + } + + return { + plugins, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@a': fileURLToPath(new URL('./src/assets/', import.meta.url)), + '@c': fileURLToPath(new URL('./src/components/', import.meta.url)), + '@v': fileURLToPath(new URL('./src/views/', import.meta.url)), + '@l': fileURLToPath(new URL('./src/layout/', import.meta.url)), + '@s': fileURLToPath(new URL('./src/stores/', import.meta.url)), + '@p': fileURLToPath(new URL('./src/common/plugin/', import.meta.url)), + '@api': fileURLToPath(new URL('./src/common/axios-interceptor.js', import.meta.url)), + }, }, - }, + }; });