Merge branch 'main' into board-comment-2
This commit is contained in:
commit
07740d0a96
6
.env.dev
6
.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
|
||||
6
.env.mine
Normal file
6
.env.mine
Normal file
@ -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
|
||||
97
package.json
97
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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,2 @@
|
||||
/* 여기에 dark css 작성 */
|
||||
|
||||
|
||||
.display-block {
|
||||
display: block !important;
|
||||
}
|
||||
@ -1,48 +1,157 @@
|
||||
/* 여기에 light css 작성 */
|
||||
|
||||
|
||||
.display-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
/* 휴가 */
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/* 선물하기 모달 */
|
||||
|
||||
/* 연차 개수 버튼 */
|
||||
.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; /* 호버 시 아이콘 색상 변경 */
|
||||
}
|
||||
|
||||
.grayscaleImg {
|
||||
filter: grayscale(100%);
|
||||
|
||||
2
public/vendor/css/rtl/core.css
vendored
2
public/vendor/css/rtl/core.css
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/css/rtl/theme-default.css
vendored
2
public/vendor/css/rtl/theme-default.css
vendored
File diff suppressed because one or more lines are too long
@ -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,
|
||||
});
|
||||
|
||||
@ -40,39 +40,39 @@ const common = {
|
||||
return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환
|
||||
},
|
||||
|
||||
// /**
|
||||
// * Date 타입 문자열 포멧팅
|
||||
// *
|
||||
// * @param {string} dateStr
|
||||
// * @return
|
||||
// * 1. Date type 인 경우 예시 '25-02-24 12:02'
|
||||
// * 2. Date type 이 아닌 경우 입력값 리턴
|
||||
// *
|
||||
// */
|
||||
// dateFormatter(dateStr) {
|
||||
// const date = new Date(dateStr);
|
||||
// const dateCheck = date.getTime();
|
||||
/**
|
||||
* Date 타입 문자열 포멧팅
|
||||
*
|
||||
* @param {string} dateStr
|
||||
* @return
|
||||
* 1. Date type 인 경우 예시 '25-02-24 12:02'
|
||||
* 2. Date type 이 아닌 경우 입력값 리턴
|
||||
*
|
||||
*/
|
||||
dateFormatter(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
const dateCheck = date.getTime();
|
||||
|
||||
// if (isNaN(dateCheck)) {
|
||||
// return dateStr;
|
||||
// } else {
|
||||
// const { year, month, day, hours, minutes } = this.formatDateTime(date);
|
||||
// return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
// }
|
||||
// },
|
||||
if (isNaN(dateCheck)) {
|
||||
return dateStr;
|
||||
} else {
|
||||
const { year, month, day, hours, minutes } = this.formatDateTime(date);
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
},
|
||||
|
||||
// formatDateTime(date) {
|
||||
// const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
||||
formatDateTime(date) {
|
||||
const zeroFormat = num => (num < 10 ? `0${num}` : num);
|
||||
|
||||
// return {
|
||||
// year: date.getFullYear(),
|
||||
// month: zeroFormat(date.getMonth() + 1),
|
||||
// day: zeroFormat(date.getDate()),
|
||||
// hours: zeroFormat(date.getHours()),
|
||||
// minutes: zeroFormat(date.getMinutes()),
|
||||
// seconds: zeroFormat(date.getSeconds()),
|
||||
// };
|
||||
// },
|
||||
return {
|
||||
year: date.getFullYear(),
|
||||
month: zeroFormat(date.getMonth() + 1),
|
||||
day: zeroFormat(date.getDate()),
|
||||
hours: zeroFormat(date.getHours()),
|
||||
minutes: zeroFormat(date.getMinutes()),
|
||||
seconds: zeroFormat(date.getSeconds()),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,43 +1,133 @@
|
||||
<template>
|
||||
<div class="half-day-buttons">
|
||||
<div class="menu gap-4 justify-content-center mt-5">
|
||||
<!-- 오전 반차 버튼 -->
|
||||
<button
|
||||
class="btn btn-info"
|
||||
class="btn btn-warning"
|
||||
:class="{ active: halfDayType === 'AM' }"
|
||||
@click="toggleHalfDay('AM')"
|
||||
>
|
||||
<i class="bi bi-sun"></i>
|
||||
</button>
|
||||
|
||||
<!-- 오후 반차 버튼 -->
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
class="btn btn-info"
|
||||
:class="{ active: halfDayType === 'PM' }"
|
||||
@click="toggleHalfDay('PM')"
|
||||
>
|
||||
<i class="bi bi-moon"></i>
|
||||
</button>
|
||||
|
||||
<!-- 저장 버튼 -->
|
||||
<div class="save-button-container">
|
||||
<button class="btn btn-success" @click="addVacationRequests">
|
||||
<button class="btn btn-success" @click="addVacationRequests" :disabled="isDisabled">
|
||||
✔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, ref } from "vue";
|
||||
import { defineEmits, ref, defineProps } from "vue";
|
||||
|
||||
const emit = defineEmits(["toggleHalfDay", "addVacationRequests"]);
|
||||
const props = defineProps({
|
||||
isDisabled: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
|
||||
const halfDayType = ref(null);
|
||||
|
||||
const toggleHalfDay = (type) => {
|
||||
halfDayType.value = halfDayType.value === type ? null : type;
|
||||
halfDayType.value = type;
|
||||
|
||||
emit("toggleHalfDay", halfDayType.value);
|
||||
|
||||
// ✅ 버튼 클릭 후 1초 후 자동 비활성화
|
||||
setTimeout(() => {
|
||||
halfDayType.value = null;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 날짜 선택 후 반차 버튼 상태 초기화
|
||||
const resetHalfDay = () => {
|
||||
halfDayType.value = null;
|
||||
emit("resetHalfDay");
|
||||
};
|
||||
|
||||
const addVacationRequests = () => {
|
||||
emit("addVacationRequests");
|
||||
};
|
||||
|
||||
defineExpose({ resetHalfDay });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped>
|
||||
/* 버튼 기본 스타일 */
|
||||
.btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
</style>
|
||||
/* 마우스를 올렸을 때 */
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mb-2 row" >
|
||||
<div class="mb-2 row">
|
||||
<div class="d-flex">
|
||||
<label :for="name" class="col-md-2 col-form-label">
|
||||
{{ title }}
|
||||
@ -28,7 +28,6 @@
|
||||
placeholder="기본주소"
|
||||
readonly
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -87,7 +86,11 @@ const props = defineProps({
|
||||
},
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
default: () => ({
|
||||
postcode: '',
|
||||
address: '',
|
||||
detailAddress: ''
|
||||
}),
|
||||
required: false
|
||||
}
|
||||
});
|
||||
@ -95,13 +98,13 @@ const props = defineProps({
|
||||
// watch 설정 수정
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue) {
|
||||
postcode.value = newValue.PROJCTZIP || '';
|
||||
address.value = newValue.PROJCTARR || '';
|
||||
detailAddress.value = newValue.PROJCTDTL || '';
|
||||
postcode.value = newValue.postcode || '';
|
||||
address.value = newValue.address || '';
|
||||
detailAddress.value = newValue.detailAddress || '';
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const emits = defineEmits(['update:data', 'update:alert']);
|
||||
const emits = defineEmits(['update:data', 'update:alert', 'update:modelValue']);
|
||||
|
||||
// 주소 검색 팝업 열기
|
||||
const openAddressSearch = () => {
|
||||
@ -136,6 +139,7 @@ const emitAddressData = () => {
|
||||
detailAddress: detailAddress.value,
|
||||
};
|
||||
emits('update:data', fullAddress);
|
||||
emits('update:modelValue', fullAddress); // modelValue도 부모로 전달
|
||||
};
|
||||
|
||||
// isAlert를 false로 설정
|
||||
|
||||
@ -14,13 +14,13 @@
|
||||
:placeholder="title"
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
@focusout="$emit('focusout', modelValue)"
|
||||
@focusout="$emit('focusout', modelValue)"
|
||||
/>
|
||||
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
|
||||
{{ title }}을 확인해주세요.
|
||||
</div>
|
||||
<!-- 카테고리 중복 -->
|
||||
<div class="invalid-feedback" :class="isCateAlert ? 'display-block' : ''">
|
||||
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">
|
||||
카테고리 중복입니다.
|
||||
</div>
|
||||
</div>
|
||||
@ -78,11 +78,13 @@ const props = defineProps({
|
||||
min: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
// Emits 정의
|
||||
const emits = defineEmits(['update:modelValue', 'focusout']);
|
||||
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
|
||||
|
||||
|
||||
// 로컬 상태로 사용하기 위한 `inputValue`
|
||||
const inputValue = ref(props.modelValue);
|
||||
@ -90,6 +92,11 @@ const inputValue = ref(props.modelValue);
|
||||
// 부모로 데이터 업데이트
|
||||
watch(inputValue, (newValue) => {
|
||||
emits('update:modelValue', newValue);
|
||||
|
||||
// 값이 입력될 때 `alert`를 false로 설정
|
||||
if (newValue.trim() !== '') {
|
||||
emits('update:alert', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기값 동기화
|
||||
@ -98,10 +105,7 @@ watch(() => props.modelValue, (newValue) => {
|
||||
inputValue.value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.none {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,8 +3,13 @@
|
||||
<div class="row g-0">
|
||||
<div class="card-body">
|
||||
<!-- 제목 -->
|
||||
<h5 class="card-title">
|
||||
<h5 class="card-title d-flex justify-content-between">
|
||||
{{ title }}
|
||||
|
||||
<div>
|
||||
<EditBtn @click.stop="openEditModal" />
|
||||
<DeleteBtn @click.stop="handleDelete" class="ms-1"/>
|
||||
</div>
|
||||
</h5>
|
||||
<!-- 날짜 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
@ -27,29 +32,23 @@
|
||||
<!-- 주소 -->
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||
<div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
|
||||
<i
|
||||
class="bx bxs-map cursor-pointer"
|
||||
ref="mapIconRef"
|
||||
></i>
|
||||
<i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i>
|
||||
<div class="ms-2">주소</div>
|
||||
</div>
|
||||
<div class="ms-12 position-relative">
|
||||
{{ address }} {{ addressdtail }}
|
||||
<!-- 팝오버 -->
|
||||
<div
|
||||
v-if="isPopoverVisible"
|
||||
class="position-absolute w-100 map text-end"
|
||||
@click.stop
|
||||
>
|
||||
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
|
||||
<div v-if="isPopoverVisible" class="position-absolute w-100 map text-end">
|
||||
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
|
||||
<div class="card">
|
||||
<div class="card-body p-1">
|
||||
<KakaoMap
|
||||
v-if="coordinates"
|
||||
:lat="coordinates.lat"
|
||||
:lng="coordinates.lng"
|
||||
:draggable="false"
|
||||
class="w-100 h-px-200"
|
||||
>
|
||||
>
|
||||
<KakaoMapMarker
|
||||
:lat="coordinates.lat"
|
||||
:lng="coordinates.lng"
|
||||
@ -59,39 +58,122 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctCol} !important;`" @click.stop="openModal">log</button>
|
||||
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">log</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로그 모달 -->
|
||||
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||
<template #title> Log </template>
|
||||
<template #body>
|
||||
<div class="ms-4 mt-2 border p-3" v-if="logData">
|
||||
<p class="mb-1">{{ logData.createDate }}</p>
|
||||
<strong>[{{ logData.creator }}] 프로젝트 등록</strong>
|
||||
</div>
|
||||
|
||||
<div class="log-item" v-if="logData?.updateDate">
|
||||
<div class="ms-4 mt-2 border p-3">
|
||||
<p class="mb-1">{{ logData.updateDate }}</p>
|
||||
<strong>[{{ logData.updater }}] 프로젝트 수정</strong>
|
||||
<div v-if="logData.length > 0">
|
||||
<div
|
||||
v-for="(log, index) in logData"
|
||||
:key="index"
|
||||
class="ms-4 mt-2 border p-3"
|
||||
>
|
||||
<p class="mb-1">{{ log.logDate }}</p>
|
||||
<strong>{{ log.logMessage }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" class="btn btn-secondary" @click="closeModal">닫기</button>
|
||||
<BackBtn @click="closeModal" />
|
||||
</template>
|
||||
</CenterModal>
|
||||
|
||||
<!-- 수정 모달 -->
|
||||
<CenterModal :display="isEditModalOpen" @close="closeEditModal">
|
||||
<template #title> 프로젝트 수정 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
:modelValue="selectedProject.PROJCTNAM"
|
||||
@update:modelValue="selectedProject.PROJCTNAM = $event"
|
||||
@update:alert="nameAlert = $event"
|
||||
/>
|
||||
|
||||
<FormSelect
|
||||
title="컬러"
|
||||
name="color"
|
||||
:is-essential="true"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="allColors"
|
||||
:value="selectedProject.PROJCTCOL"
|
||||
@update:data="selectedProject.PROJCTCOL = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="시작일"
|
||||
type="date"
|
||||
name="startDay"
|
||||
:is-essential="true"
|
||||
:modelValue="selectedProject.PROJCTSTR"
|
||||
@update:modelValue="selectedProject.PROJCTSTR = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="종료일"
|
||||
type="date"
|
||||
name="endDay"
|
||||
:modelValue="selectedProject.PROJCTEND"
|
||||
@update:modelValue="selectedProject.PROJCTEND = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="설명"
|
||||
name="description"
|
||||
:modelValue="selectedProject.PROJCTDES"
|
||||
@update:modelValue="selectedProject.PROJCTDES = $event"
|
||||
/>
|
||||
|
||||
<ArrInput
|
||||
title="주소"
|
||||
name="address"
|
||||
:is-essential="true"
|
||||
:is-row="true"
|
||||
:modelValue="{
|
||||
address: selectedProject.PROJCTARR,
|
||||
detailAddress: selectedProject.PROJCTDTL,
|
||||
postcode: selectedProject.PROJCTZIP
|
||||
}"
|
||||
@update:data="updateAddress"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<BackButton @click="closeEditModal" />
|
||||
<SaveButton @click="handleUpdate" />
|
||||
</template>
|
||||
</CenterModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, onMounted, ref } from 'vue';
|
||||
import { defineProps, onMounted, ref, computed, inject } from 'vue';
|
||||
import UserList from '@c/user/UserList.vue';
|
||||
import CenterModal from '@c/modal/CenterModal.vue';
|
||||
import $api from '@api';
|
||||
import { KakaoMap, KakaoMapMarker } from 'vue3-kakao-maps';
|
||||
import BackBtn from '@c/button/BackBtn.vue';
|
||||
import BackButton from '@c/button/BackBtn.vue';
|
||||
import SaveButton from '@c/button/SaveBtn.vue';
|
||||
import EditBtn from '../button/EditBtn.vue';
|
||||
import DeleteBtn from '../button/DeleteBtn.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import FormSelect from '@c/input/FormSelect.vue';
|
||||
import ArrInput from '@c/input/ArrInput.vue';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import commonApi from '@/common/commonApi';
|
||||
|
||||
// 스토어
|
||||
const toastStore = useToastStore();
|
||||
const userStore = useUserInfoStore();
|
||||
|
||||
// Props 정의
|
||||
const props = defineProps({
|
||||
@ -120,39 +202,144 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
addressZip: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projctSeq: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
projctCol: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
projctColor: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
// Emit 정의
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
// 로그 모달 상태
|
||||
const isModalOpen = ref(false);
|
||||
const logData = ref(null);
|
||||
const logData = ref([]);
|
||||
|
||||
// 주소 팝오버 상태
|
||||
const isPopoverVisible = ref(false);
|
||||
const mapIconRef = ref(null);
|
||||
const coordinates = ref(null);
|
||||
|
||||
const fetchLogData = async () => {
|
||||
const response = await $api.get(`project/log/${props.projctSeq}`);
|
||||
logData.value = response.data.data.length > 0 ? response.data.data[0] : {};
|
||||
// 수정 모달 상태
|
||||
const isEditModalOpen = ref(false);
|
||||
const originalColor = ref('');
|
||||
const nameAlert = ref(false);
|
||||
const user = ref(null);
|
||||
|
||||
// 수정할 프로젝트 데이터
|
||||
const selectedProject = ref({
|
||||
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,
|
||||
});
|
||||
|
||||
// 컬러 목록 가져오기
|
||||
const { colorList } = commonApi({
|
||||
loadColor: true,
|
||||
colorType: 'YNP',
|
||||
});
|
||||
|
||||
// 기존 컬러 + 사용 가능 한 컬러
|
||||
const allColors = computed(() => {
|
||||
const existingColor = { value: selectedProject.value.PROJCTCOL, label: selectedProject.value.projctcolor };
|
||||
return [existingColor, ...colorList.value];
|
||||
});
|
||||
|
||||
// 수정 :: 주소
|
||||
const updateAddress = addressData => {
|
||||
selectedProject.value = {
|
||||
...selectedProject.value,
|
||||
PROJCTZIP: addressData.postcode,
|
||||
PROJCTARR: addressData.address,
|
||||
PROJCTDTL: addressData.detailAddress,
|
||||
};
|
||||
};
|
||||
|
||||
// 로그 데이터 가져오기
|
||||
const getLogData = async () => {
|
||||
const res = await $api.get(`project/log/${props.projctSeq}`);
|
||||
|
||||
logData.value = res.data.data;
|
||||
};
|
||||
|
||||
// 로그 모달 열기
|
||||
const openModal = async () => {
|
||||
await fetchLogData();
|
||||
await getLogData();
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
|
||||
// 로그 모달 닫기
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
};
|
||||
|
||||
// 수정 모달 열기
|
||||
const openEditModal = () => {
|
||||
isEditModalOpen.value = true;
|
||||
originalColor.value = props.projctCol;
|
||||
|
||||
// 사용자 정보 가져오기 (필요한 경우)
|
||||
if (!user.value) {
|
||||
userStore.userInfo().then(() => {
|
||||
user.value = userStore.user;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 수정 모달 닫기
|
||||
const closeEditModal = () => {
|
||||
isEditModalOpen.value = false;
|
||||
};
|
||||
|
||||
// 프로젝트 수정
|
||||
const handleUpdate = () => {
|
||||
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
|
||||
|
||||
if (nameAlert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$api.patch('project/update', {
|
||||
projctSeq: selectedProject.value.PROJCTSEQ,
|
||||
projctNam: selectedProject.value.PROJCTNAM,
|
||||
projctCol: selectedProject.value.PROJCTCOL,
|
||||
projctArr: selectedProject.value.PROJCTARR,
|
||||
projctDtl: selectedProject.value.PROJCTDTL,
|
||||
projctZip: selectedProject.value.PROJCTZIP,
|
||||
projctStr: selectedProject.value.PROJCTSTR,
|
||||
projctEnd: selectedProject.value.PROJCTEND || null,
|
||||
projctDes: selectedProject.value.PROJCTDES || null,
|
||||
projctUmb: user.value?.name,
|
||||
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value,
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
||||
closeEditModal();
|
||||
// 상위 컴포넌트에 업데이트 알림
|
||||
emit('update');
|
||||
window.location.reload()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 주소를 좌표로 변환하는 함수
|
||||
const convertAddressToCoordinates = () => {
|
||||
@ -174,11 +361,27 @@ const convertAddressToCoordinates = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 주소 변환
|
||||
onMounted(() => {
|
||||
// 프로젝트 삭제
|
||||
const handleDelete = () => {
|
||||
$api.patch('project/delete', {
|
||||
projctSeq: props.projctSeq,
|
||||
projctCol: props.projctCol,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 실행
|
||||
onMounted(async () => {
|
||||
convertAddressToCoordinates();
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
|
||||
<template>
|
||||
<div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display, 'display-block': display , 'modal-back' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
|
||||
<div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display , 'd-block': display , 'bg-dark bg-opacity-50' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
|
||||
<div @click.stop class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title m-auto fw-bold" id="modalCenterTitle">
|
||||
<slot name="title">Modal Title</slot>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @click="closeModal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot name="body">Modal body</slot>
|
||||
@ -21,26 +20,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const prop = defineProps({
|
||||
display : {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const emit = defineEmits(['close' , 'reset']);
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close' , false);
|
||||
if (prop.create) {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
emit('close', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.modal-back {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content p-5">
|
||||
<h5 class="modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>선물할 연차 개수를 선택하세요.</p>
|
||||
|
||||
<div class="vacation-control">
|
||||
<div class="justify-content-center d-sm-flex gap-sm-3 align-items-md-center mt-8">
|
||||
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
||||
<span class="grant-count">{{ grantCount }}</span>
|
||||
<span class="text-dark fw-bold fs-4">{{ grantCount }}</span>
|
||||
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
|
||||
</div>
|
||||
|
||||
<button class="gift-btn" @click="saveVacationGrant" :disabled="grantCount === 0">
|
||||
<div class="custom-button-container">
|
||||
<button class="custom-button" @click="saveVacationGrant" :disabled="grantCount === 0">
|
||||
<i class="bx bx-gift"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,7 +25,9 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
|
||||
import axios from "@api";
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
targetUser: Object,
|
||||
@ -72,21 +75,18 @@
|
||||
count: grantCount.value,
|
||||
},
|
||||
];
|
||||
console.log(props.targetUser)
|
||||
console.log(payload)
|
||||
const response = await axios.post("vacation", payload);
|
||||
console.log(response)
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("✅ 연차가 부여되었습니다.");
|
||||
toastStore.onToast('연차가 선물되었습니다.', 's');
|
||||
await fetchSentVacationCount();
|
||||
emit("updateVacation");
|
||||
closeModal();
|
||||
} else {
|
||||
alert("🚨 연차 추가 중 오류가 발생했습니다.");
|
||||
toastStore.onToast(' 연차 선물 중 오류가 발생했습니다.', 'e');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 추가 실패:", error);
|
||||
alert("연차 추가에 실패했습니다.");
|
||||
toastStore.onToast(' 연차 선물 실패!!.', 'e');
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,7 +98,6 @@
|
||||
() => props.isOpen,
|
||||
async (newVal) => {
|
||||
if (newVal && props.targetUser && props.targetUser.MEMBERSEQ) {
|
||||
console.log("🟢 모달이 열렸습니다. 데이터를 로드합니다.");
|
||||
await fetchSentVacationCount();
|
||||
}
|
||||
}
|
||||
@ -123,82 +122,24 @@
|
||||
|
||||
|
||||
<style scoped>
|
||||
/* 모달 배경 투명하게 */
|
||||
.modal-dialog {
|
||||
background: none !important; /* 배경 제거 */
|
||||
box-shadow: none !important; /* 음영 제거 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 모달 본문 */
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 닫기 버튼 */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 연차 개수 조정 버튼 */
|
||||
.vacation-control {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.count-btn {
|
||||
font-size: 18px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.count-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 개수 표시 */
|
||||
.grant-count {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 선물 아이콘 버튼 */
|
||||
.gift-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.gift-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.gift-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* 모달 내용 스타일 */
|
||||
.modal-content {
|
||||
background: #fff; /* 기존 흰색 배경 유지 */
|
||||
border-radius: 8px;
|
||||
box-shadow: none !important; /* 내부 음영 제거 */
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,29 +1,28 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
||||
<div class="modal-content modal-scroll">
|
||||
<div class="modal-content p-5 modal-scroll">
|
||||
<h5 class="modal-title">📅 내 연차 사용 내역</h5>
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<!-- 연차 목록 -->
|
||||
<div class="modal-body" v-if="mergedVacations.length > 0">
|
||||
<ol class="vacation-list">
|
||||
<ol class="list-group-numbered px-0 mt-4">
|
||||
<li
|
||||
v-for="(vac, index) in mergedVacations"
|
||||
:key="vac._expandIndex"
|
||||
class="vacation-item"
|
||||
>
|
||||
<!-- Used 항목만 인덱스 표시 -->
|
||||
<span v-if="vac.category === 'used'" class="vacation-index">
|
||||
<span v-if="vac.category === 'used'" class="fw-bold text-dark me-2">
|
||||
{{ usedVacationIndexMap[vac._expandIndex] }})
|
||||
</span>
|
||||
|
||||
<span :class="vac.category === 'used' ? 'minus-symbol' : 'plus-symbol'">
|
||||
<span :class="vac.category === 'used' ? 'fw-bold text-danger me-2' : 'fw-bold text-primary me-2'">
|
||||
{{ vac.category === 'used' ? '-' : '+' }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
:style="{ color: userColors[vac.senderId || vac.receiverId] || '#000' }"
|
||||
class="vacation-date"
|
||||
>
|
||||
{{ formatDate(vac.date) }}
|
||||
</span>
|
||||
@ -32,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 연차 데이터 없음 -->
|
||||
<p v-else class="no-data">
|
||||
<p v-else class="text-sm-center mt-10 text-gray">
|
||||
🚫 사용한 연차가 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -139,86 +138,25 @@ const closeModal = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 모달 본문 */
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 닫기 버튼 */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 모달 배경 투명하게 */
|
||||
.modal-dialog {
|
||||
background: none !important; /* 배경 제거 */
|
||||
box-shadow: none !important; /* 음영 제거 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 리스트 기본 스타일 */
|
||||
.vacation-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* 리스트 아이템 */
|
||||
.vacation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 인덱스 (연차 사용 개수) */
|
||||
.vacation-index {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* "-" 빨간색 */
|
||||
.minus-symbol {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* "+" 파란색 */
|
||||
.plus-symbol {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 날짜 스타일 */
|
||||
.vacation-date {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 연차 데이터 없음 */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 모달 본문 스크롤: 높이가 300px 이상이면 스크롤바 표시 */
|
||||
.modal-body {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
/* 모달 내용 스타일 */
|
||||
.modal-content {
|
||||
background: #fff; /* 기존 흰색 배경 유지 */
|
||||
border-radius: 8px;
|
||||
box-shadow: none !important; /* 내부 음영 제거 */
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SearchBar @update:data="search"/>
|
||||
<SearchBar @update:data="search" />
|
||||
<div class="d-flex align-items-center">
|
||||
<CategoryBtn :lists="yearCategory" @update:data="selectedCategory = $event" />
|
||||
<WriteBtn class="mt-2 ms-auto" @click="openCreateModal" />
|
||||
@ -11,7 +11,7 @@
|
||||
<p class="text-muted mt-4">등록된 프로젝트가 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ" @click="openEditModal(post)" class="cursor-pointer">
|
||||
<div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ">
|
||||
<ProjectCard
|
||||
:title="post.PROJCTNAM"
|
||||
:description="post.PROJCTDES"
|
||||
@ -19,358 +19,239 @@
|
||||
:enddate="post.PROJCTEND"
|
||||
:address="post.PROJCTARR"
|
||||
:addressdtail="post.PROJCTDTL"
|
||||
:addressZip="post.PROJCTZIP"
|
||||
:projctSeq="post.PROJCTSEQ"
|
||||
:projctCol="post.projctcolor"
|
||||
:projctCol="post.PROJCTCOL"
|
||||
:projctColor="post.projctcolor"
|
||||
@update="getProjectList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<CenterModal :display="isCreateModalOpen" @close="closeCreateModal">
|
||||
<template #title> 프로젝트 등록 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
@update:modelValue="name = $event"
|
||||
/>
|
||||
<form @reset.prevent="formReset">
|
||||
<CenterModal :display="isCreateModalOpen" @close="closeCreateModal" :create="true" @reset="formReset">
|
||||
<template #title> 프로젝트 등록 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
:modelValue="name"
|
||||
@update:alert="nameAlert = $event"
|
||||
@update:modelValue="name = $event"
|
||||
/>
|
||||
|
||||
<FormSelect
|
||||
title="컬러"
|
||||
name="color"
|
||||
:is-essential="true"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="colorList"
|
||||
@update:data="color = $event"
|
||||
/>
|
||||
<FormSelect
|
||||
title="컬러"
|
||||
name="color"
|
||||
:is-essential="true"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="colorList"
|
||||
@update:data="color = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="시작 일"
|
||||
type="date"
|
||||
name="startDay"
|
||||
v-model="startDay"
|
||||
:is-essential="true"
|
||||
/>
|
||||
<FormInput
|
||||
title="시작 일"
|
||||
name="startDay"
|
||||
:type="'date'"
|
||||
:is-essential="true"
|
||||
:modelValue="startDay"
|
||||
v-model="startDay"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="종료 일"
|
||||
name="endDay"
|
||||
:type="'date'"
|
||||
@update:modelValue="endDay = $event"
|
||||
/>
|
||||
<FormInput
|
||||
title="종료 일"
|
||||
:type="'date'"
|
||||
name="endDay"
|
||||
:modelValue="endDay"
|
||||
@update:modelValue="endDay = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="설명"
|
||||
name="description"
|
||||
@update:modelValue="description = $event"
|
||||
/>
|
||||
<FormInput
|
||||
title="설명"
|
||||
name="description"
|
||||
:modelValue="description"
|
||||
@update:modelValue="description = $event"
|
||||
/>
|
||||
|
||||
<ArrInput
|
||||
title="주소"
|
||||
name="address"
|
||||
:isEssential="true"
|
||||
:is-row="true"
|
||||
:is-alert="addressAlert"
|
||||
@update:data="handleAddressUpdate"
|
||||
@update:alert="addressAlert = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<BackButton @click="closeCreateModal" />
|
||||
<SaveButton @click="handleCreate" />
|
||||
</template>
|
||||
</CenterModal>
|
||||
|
||||
<!-- 수정 모달 -->
|
||||
<CenterModal :display="isEditModalOpen" @close="closeEditModal">
|
||||
<template #title> 프로젝트 수정 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
:modelValue="selectedProject.PROJCTNAM"
|
||||
@update:modelValue="selectedProject.PROJCTNAM = $event"
|
||||
/>
|
||||
|
||||
<FormSelect
|
||||
title="컬러"
|
||||
name="color"
|
||||
:is-essential="true"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:data="allColors"
|
||||
:value="selectedProject.PROJCTCOL"
|
||||
@update:data="selectedProject.PROJCTCOL = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="시작일"
|
||||
type="date"
|
||||
name="startDay"
|
||||
:is-essential="true"
|
||||
:modelValue="selectedProject.PROJCTSTR"
|
||||
@update:modelValue="selectedProject.PROJCTSTR = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="종료일"
|
||||
type="date"
|
||||
name="endDay"
|
||||
:modelValue="selectedProject.PROJCTEND"
|
||||
@update:modelValue="selectedProject.PROJCTEND = $event"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
title="설명"
|
||||
name="description"
|
||||
:modelValue="selectedProject.PROJCTDES"
|
||||
@update:modelValue="selectedProject.PROJCTDES = $event"
|
||||
/>
|
||||
|
||||
<ArrInput
|
||||
title="주소"
|
||||
name="address"
|
||||
:is-essential="true"
|
||||
:is-row="true"
|
||||
:modelValue="selectedProject"
|
||||
@update:data="updateAddress"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<BackButton @click="closeEditModal" />
|
||||
<SaveButton @click="handleUpdate" />
|
||||
</template>
|
||||
</CenterModal>
|
||||
<ArrInput
|
||||
title="주소"
|
||||
name="address"
|
||||
:isEssential="true"
|
||||
:is-row="true"
|
||||
:is-alert="addressAlert"
|
||||
:modelValue="addressData"
|
||||
@update:data="handleAddressUpdate"
|
||||
@update:alert="addressAlert = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<BackButton type="reset" @click="closeCreateModal" />
|
||||
<SaveButton @click="handleCreate" />
|
||||
</template>
|
||||
</CenterModal>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, ref, watch, onMounted } from 'vue';
|
||||
import SearchBar from '@c/search/SearchBar.vue';
|
||||
import ProjectCard from '@c/list/ProjectCard.vue';
|
||||
import CategoryBtn from '@c/category/CategoryBtn.vue';
|
||||
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||
import CenterModal from '@c/modal/CenterModal.vue';
|
||||
import FormSelect from '@c/input/FormSelect.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import ArrInput from '@c/input/ArrInput.vue';
|
||||
import commonApi from '@/common/commonApi';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import $api from '@api';
|
||||
import SaveButton from '@c/button/SaveBtn.vue';
|
||||
import BackButton from '@c/button/BackBtn.vue'
|
||||
import { computed, ref, watch, onMounted, inject } from 'vue';
|
||||
import SearchBar from '@c/search/SearchBar.vue';
|
||||
import ProjectCard from '@c/list/ProjectCard.vue';
|
||||
import CategoryBtn from '@c/category/CategoryBtn.vue';
|
||||
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||
import CenterModal from '@c/modal/CenterModal.vue';
|
||||
import FormSelect from '@c/input/FormSelect.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import ArrInput from '@c/input/ArrInput.vue';
|
||||
import commonApi from '@/common/commonApi';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import $api from '@api';
|
||||
import SaveButton from '@c/button/SaveBtn.vue';
|
||||
import BackButton from '@c/button/BackBtn.vue';
|
||||
|
||||
const dayjs = inject('dayjs');
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
const toastStore = useToastStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
const toastStore = useToastStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
|
||||
// 상태 관리
|
||||
const user = ref(null);
|
||||
const selectedCategory = ref(null);
|
||||
const searchText = ref('');
|
||||
// 상태 관리
|
||||
const user = ref(null);
|
||||
const selectedCategory = ref(null);
|
||||
const searchText = ref('');
|
||||
|
||||
// 등록 모달 상태
|
||||
const isCreateModalOpen = ref(false);
|
||||
const name = ref('');
|
||||
const color = ref('');
|
||||
const address = ref('');
|
||||
const detailAddress = ref('');
|
||||
const postcode = ref('');
|
||||
const startDay = ref(today);
|
||||
const endDay = ref('');
|
||||
const description = ref('');
|
||||
const nameAlert = ref(false);
|
||||
const addressAlert = ref(false);
|
||||
// dayjs 인스턴스 가져오기
|
||||
const dayjs = inject('dayjs');
|
||||
|
||||
// 수정 모달 상태
|
||||
const isEditModalOpen = ref(false);
|
||||
const originalColor = ref('');
|
||||
const selectedProject = ref({
|
||||
PROJCTSEQ: '',
|
||||
PROJCTNAM: '',
|
||||
PROJCTSTR: '',
|
||||
PROJCTEND: '',
|
||||
PROJCTZIP: '',
|
||||
PROJCTARR: '',
|
||||
PROJCTDTL: '',
|
||||
PROJCTDES: '',
|
||||
PROJCTCOL: '',
|
||||
projctcolor: '',
|
||||
});
|
||||
// 오늘 날짜를 YYYY-MM-DD 형식으로 변환
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// API 호출
|
||||
const { yearCategory, colorList } = commonApi({
|
||||
loadColor: true,
|
||||
colorType: 'YNP',
|
||||
loadYearCategory: true,
|
||||
});
|
||||
// 등록 모달 상태
|
||||
const isCreateModalOpen = ref(false);
|
||||
const name = ref('');
|
||||
const color = ref('');
|
||||
|
||||
// 검색 처리
|
||||
const search = async (searchKeyword) => {
|
||||
searchText.value = searchKeyword.trim();
|
||||
await getProjectList();
|
||||
};
|
||||
const startDay = ref(today);
|
||||
const endDay = ref('');
|
||||
const description = ref('');
|
||||
const nameAlert = ref(false);
|
||||
const addressAlert = ref(false);
|
||||
|
||||
const selectedYear = computed(() => {
|
||||
if (!selectedCategory.value || selectedCategory.value === 900101) {
|
||||
return null;
|
||||
}
|
||||
// 선택된 category 값 label 값으로 변환
|
||||
return yearCategory.value.find(item => item.value === selectedCategory.value)?.label || null;
|
||||
});
|
||||
|
||||
// 프로젝트 목록 조회
|
||||
const getProjectList = async () => {
|
||||
await projectStore.getProjectList(searchText.value, selectedYear.value);
|
||||
};
|
||||
|
||||
// 카테고리 변경 감지
|
||||
watch(selectedCategory, async () => {
|
||||
await getProjectList();
|
||||
});
|
||||
|
||||
// 등록 모달 관리
|
||||
const openCreateModal = () => {
|
||||
isCreateModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
isCreateModalOpen.value = false;
|
||||
resetCreateForm();
|
||||
};
|
||||
|
||||
const resetCreateForm = () => {
|
||||
name.value = '';
|
||||
color.value = '';
|
||||
address.value = '';
|
||||
detailAddress.value = '';
|
||||
postcode.value = '';
|
||||
startDay.value = today;
|
||||
endDay.value = '';
|
||||
description.value = '';
|
||||
nameAlert.value = false;
|
||||
addressAlert.value = false;
|
||||
};
|
||||
|
||||
// 등록 :: 주소 업데이트 핸들러
|
||||
const handleAddressUpdate = addressData => {
|
||||
address.value = addressData.address;
|
||||
detailAddress.value = addressData.detailAddress;
|
||||
postcode.value = addressData.postcode;
|
||||
};
|
||||
|
||||
// 프로젝트 등록
|
||||
const handleCreate = async () => {
|
||||
nameAlert.value = name.value.trim() === '';
|
||||
addressAlert.value = address.value.trim() === '';
|
||||
|
||||
if (nameAlert.value || addressAlert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$api.post('project/insert', {
|
||||
projctNam: name.value,
|
||||
projctCol: color.value,
|
||||
projctStr: startDay.value,
|
||||
projctEnd: endDay.value || null,
|
||||
projctDes: description.value || null,
|
||||
projctArr: address.value,
|
||||
projctDtl: detailAddress.value,
|
||||
projctZip: postcode.value,
|
||||
projctCmb: user.value.name,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||
closeCreateModal();
|
||||
getProjectList();
|
||||
}
|
||||
const addressData = ref({
|
||||
postcode: '',
|
||||
address: '',
|
||||
detailAddress: ''
|
||||
});
|
||||
};
|
||||
|
||||
// 수정 모달 관리
|
||||
const openEditModal = (post) => {
|
||||
isEditModalOpen.value = true;
|
||||
selectedProject.value = { ...post };
|
||||
originalColor.value = post.PROJCTCOL;
|
||||
};
|
||||
// API 호출
|
||||
const { yearCategory, colorList } = commonApi({
|
||||
loadColor: true,
|
||||
colorType: 'YNP',
|
||||
loadYearCategory: true,
|
||||
});
|
||||
|
||||
const closeEditModal = () => {
|
||||
isEditModalOpen.value = false;
|
||||
};
|
||||
|
||||
// 기존 컬러 + 사용 가능 한 컬러
|
||||
const allColors = computed(() => {
|
||||
const existingColor = { value: selectedProject.value.PROJCTCOL, label: selectedProject.value.projctcolor };
|
||||
return [existingColor, ...colorList.value];
|
||||
});
|
||||
|
||||
// 변경된 내용 있는지 확인
|
||||
const hasChanges = computed(() => {
|
||||
const original = projectStore.projectList.find(p => p.PROJCTSEQ === selectedProject.value.PROJCTSEQ);
|
||||
if (!original) return false;
|
||||
|
||||
return (
|
||||
original.PROJCTNAM !== selectedProject.value.PROJCTNAM ||
|
||||
original.PROJCTCOL !== selectedProject.value.PROJCTCOL ||
|
||||
original.PROJCTARR !== selectedProject.value.PROJCTARR ||
|
||||
original.PROJCTDTL !== selectedProject.value.PROJCTDTL ||
|
||||
original.PROJCTZIP !== selectedProject.value.PROJCTZIP ||
|
||||
original.PROJCTSTR !== selectedProject.value.PROJCTSTR ||
|
||||
original.PROJCTEND !== selectedProject.value.PROJCTEND ||
|
||||
original.PROJCTDES !== selectedProject.value.PROJCTDES
|
||||
);
|
||||
});
|
||||
|
||||
// 수정 :: 주소
|
||||
const updateAddress = (addressData) => {
|
||||
selectedProject.value = {
|
||||
...selectedProject.value,
|
||||
PROJCTZIP: addressData.postcode,
|
||||
PROJCTARR: addressData.address,
|
||||
PROJCTDTL: addressData.detailAddress
|
||||
// 검색 처리
|
||||
const search = async searchKeyword => {
|
||||
searchText.value = searchKeyword.trim();
|
||||
await getProjectList();
|
||||
};
|
||||
};
|
||||
|
||||
// 프로젝트 수정
|
||||
const handleUpdate = () => {
|
||||
if (!hasChanges.value) {
|
||||
toastStore.onToast('변경된 내용이 없습니다.', 'e');
|
||||
return;
|
||||
const selectedYear = computed(() => {
|
||||
if (!selectedCategory.value || selectedCategory.value === 900101) {
|
||||
return null;
|
||||
}
|
||||
// 선택된 category 값 label 값으로 변환
|
||||
return yearCategory.value.find(item => item.value === selectedCategory.value)?.label || null;
|
||||
});
|
||||
|
||||
// 프로젝트 목록 조회
|
||||
const getProjectList = async () => {
|
||||
await projectStore.getProjectList(searchText.value, selectedYear.value);
|
||||
};
|
||||
|
||||
// 카테고리 변경 감지
|
||||
watch(selectedCategory, async () => {
|
||||
await getProjectList();
|
||||
});
|
||||
|
||||
// 등록 모달 관리
|
||||
const openCreateModal = () => {
|
||||
isCreateModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
isCreateModalOpen.value = false;
|
||||
};
|
||||
|
||||
const formReset = () => {
|
||||
name.value = '';
|
||||
color.value = '';
|
||||
addressData.value = {
|
||||
postcode: '',
|
||||
address: '',
|
||||
detailAddress: ''
|
||||
};
|
||||
startDay.value = today;
|
||||
endDay.value = '';
|
||||
description.value = '';
|
||||
nameAlert.value = false;
|
||||
addressAlert.value = false;
|
||||
}
|
||||
|
||||
$api.patch('project/update', {
|
||||
projctSeq: selectedProject.value.PROJCTSEQ,
|
||||
projctNam: selectedProject.value.PROJCTNAM,
|
||||
projctCol: selectedProject.value.PROJCTCOL,
|
||||
projctArr: selectedProject.value.PROJCTARR,
|
||||
projctDtl: selectedProject.value.PROJCTDTL,
|
||||
projctZip: selectedProject.value.PROJCTZIP,
|
||||
projctStr: selectedProject.value.PROJCTSTR,
|
||||
projctEnd: selectedProject.value.PROJCTEND || null,
|
||||
projctDes: selectedProject.value.PROJCTDES || null,
|
||||
projctUmb: user.value.name,
|
||||
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
||||
closeEditModal();
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
// 등록 :: 주소 업데이트 핸들러
|
||||
const handleAddressUpdate = (data) => {
|
||||
addressData.value = data;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getProjectList();
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
});
|
||||
// 시작일이나 종료일이 변경될 때마다 종료일을 유효성 검사
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 프로젝트 등록
|
||||
const handleCreate = async () => {
|
||||
|
||||
nameAlert.value = name.value.trim() === '';
|
||||
addressAlert.value = addressData.value.address.trim() === '';
|
||||
|
||||
if (nameAlert.value || addressAlert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$api.post('project/insert', {
|
||||
|
||||
projctNam: name.value,
|
||||
projctCol: color.value,
|
||||
projctStr: startDay.value,
|
||||
projctEnd: endDay.value || null,
|
||||
projctDes: description.value || null,
|
||||
projctArr: addressData.value.address,
|
||||
projctDtl: addressData.value.detailAddress,
|
||||
projctZip: addressData.value.postcode,
|
||||
projctCmb: user.value.name,
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||
closeCreateModal();
|
||||
getProjectList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getProjectList();
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="input-group mb-3 d-flex">
|
||||
<input type="text" class="form-control" placeholder="Search" @change="search" />
|
||||
<input type="text" class="form-control" placeholder="Search" @change="search" @input="preventLeadingSpace" />
|
||||
<button type="button" class="btn btn-primary"><i class="bx bx-search bx-md"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
@ -15,11 +15,17 @@
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:data']);
|
||||
|
||||
const search = function (event) {
|
||||
|
||||
//Type Number 일때 maxlength 적용 안됨 방지
|
||||
if (event.target.value.length > props.maxlength) {
|
||||
event.target.value = event.target.value.slice(0, props.maxlength);
|
||||
}
|
||||
emits('update:data', event.target.value);
|
||||
};
|
||||
|
||||
const preventLeadingSpace = function (event) {
|
||||
event.target.value = event.target.value.trimStart();
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="card mb-6">
|
||||
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" >
|
||||
<h5 class="card-title mb-1"><div class="list-group-item list-group-item-action d-flex align-items-center cursor-pointer">
|
||||
<h5 class="card-title mb-1">
|
||||
<div class="list-unstyled users-list d-flex align-items-center gap-1">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3 w-px-40"
|
||||
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
||||
alt="user"
|
||||
/>
|
||||
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="user-info">
|
||||
@ -24,7 +26,6 @@
|
||||
class="bx btn btn-danger"
|
||||
@click="endBtn(data.localVote.LOCVOTSEQ)"
|
||||
>종료</button>
|
||||
<EditBtn v-if="!data.localVote.LOCVOTDDT" @click="updateVote(data.localVote.LOCVOTSEQ)"/>
|
||||
<DeleteBtn @click="voteDelete(data.localVote.LOCVOTSEQ)" />
|
||||
</div>
|
||||
</div>
|
||||
@ -32,10 +33,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<h5>{{ data.localVote.LOCVOTTTL }}</h5>
|
||||
<div class="mb-1">{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</div>
|
||||
<!-- 투표완료시-->
|
||||
<button v-if="data.yesVotetotal > 0 && !data.localVote.LOCVOTDDT" class="btn btn-primary btn-sm" >재투표</button>
|
||||
<h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}</h5>
|
||||
<small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
|
||||
<!-- 투표안했을시-->
|
||||
<div v-if="data.localVote.LOCVOTDDT && data.voteResult.length == 0">
|
||||
<small class="text-primary text-uppercase">투표 결과없음 (😂아무도 투표하지 않았습니다)</small>
|
||||
@ -48,13 +47,14 @@
|
||||
:data="data.voteDetails"
|
||||
:voteInfo="data.localVote"
|
||||
:total="data.voteDetails.length "/>
|
||||
<!-- 투표완/미완 인원 -->
|
||||
<vote-user-list v-if="!data.localVote.LOCVOTDDT"
|
||||
:data="data.voteMembers"/>
|
||||
|
||||
<!-- 투표 결과 -->
|
||||
<div v-if="data.localVote.LOCVOTDDT" class="mt-3">
|
||||
<vote-result-list :data="data.voteResult" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES"/>
|
||||
</div>
|
||||
<!-- 투표완/미완 인원 -->
|
||||
<vote-user-list
|
||||
:data="data.voteMembers"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="card-body">
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
/>
|
||||
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1" class="d-flex align-items-center">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-center">
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start">
|
||||
<form-input
|
||||
class="flex-grow-1 me-2"
|
||||
:title="'항목 ' + (index + data.length + 1)"
|
||||
@ -37,12 +37,13 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" @click="selectVote">투표하기</button>
|
||||
<save-btn class="btn-sm mt-2" @click="selectVote"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import $api from '@api';
|
||||
import PlusBtn from '@c/button/PlusBtn.vue';
|
||||
import SaveBtn from '@c/button/SaveBtn.vue'
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import voteCardCheckList from '@c/voteboard/voteCardCheckList.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
@ -78,7 +79,8 @@ const props = defineProps({
|
||||
const emit = defineEmits(['addContents','checkedNames']);
|
||||
//항목추가
|
||||
const addContentSave = (voteId) =>{
|
||||
emit('addContents',itemList.value,voteId);
|
||||
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
|
||||
emit('addContents',filteredItemList,voteId);
|
||||
itemList.value = [{ content: "", url: "" }];
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
alt="user"
|
||||
/>
|
||||
</li>
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
alt="user"
|
||||
/>
|
||||
</li>
|
||||
|
||||
@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<div class="position-relative me-2">
|
||||
<i class="bx bx-link-alt" @click="togglePopover"></i>
|
||||
<!-- 링크 팝업 -->
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- 링크 아이콘 -->
|
||||
<i class="bx bx-link-alt me-2" @click="togglePopover"></i>
|
||||
|
||||
<!-- 링크 입력창 (옆으로 나오게) -->
|
||||
<div
|
||||
v-if="isPopoverVisible"
|
||||
class="popover bs-popover-auto fade show d-flex align-items-center"
|
||||
role="tooltip"
|
||||
:style="popoverStyle"
|
||||
class="popover-container d-flex align-items-center"
|
||||
>
|
||||
<div class="popover-arrow"></div>
|
||||
<input
|
||||
v-model="link"
|
||||
placeholder="URL을 입력해주세요"
|
||||
class="form-control me-2 flex-grow-1"
|
||||
class="form-control me-2"
|
||||
style="min-width: 200px;"
|
||||
/>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-2" @click="saveLink">
|
||||
등록
|
||||
</button>
|
||||
<save-btn class="btn-sm" @click="saveLink"/>
|
||||
|
||||
</div>
|
||||
<!-- 등록된 링크 표시 -->
|
||||
<div v-if="link" class="mt-1">
|
||||
|
||||
<!-- 등록된 링크, 입력창이 보이지 않고 등록된 링크만 보일 때 -->
|
||||
<span v-if="isLinkSaved && !isPopoverVisible" class="ms-2">
|
||||
<a :href="formattedLink" target="_blank" rel="noopener noreferrer">{{ link }}</a>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SaveBtn from '@c/button/SaveBtn.vue'
|
||||
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
@ -36,45 +37,27 @@ const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const isPopoverVisible = ref(false);
|
||||
const link = ref(props.modelValue || "");
|
||||
const popoverStyle = ref({});
|
||||
const isLinkSaved = ref(false); // Track if the link has been saved
|
||||
|
||||
const formattedLink = computed(() => {
|
||||
return link.value.startsWith("http") ? link.value : "http://" + link.value;
|
||||
return link.value.startsWith("http") ? link.value : "http://" + link.value;
|
||||
});
|
||||
|
||||
const togglePopover = (event) => {
|
||||
const buttonRect = event.target.getBoundingClientRect();
|
||||
const parentRect = event.target.parentElement.getBoundingClientRect();
|
||||
|
||||
popoverStyle.value = {
|
||||
position: "absolute",
|
||||
top: `${buttonRect.bottom - parentRect.top + 5}px`,
|
||||
left: `${buttonRect.left - parentRect.left}px`,
|
||||
zIndex: "1050",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
isPopoverVisible.value = !isPopoverVisible.value;
|
||||
const togglePopover = () => {
|
||||
isPopoverVisible.value = !isPopoverVisible.value;
|
||||
};
|
||||
|
||||
const saveLink = () => {
|
||||
emit("update:modelValue", link.value);
|
||||
isPopoverVisible.value = false;
|
||||
emit("update:modelValue", link.value);
|
||||
isLinkSaved.value = true; // Set the link as saved
|
||||
isPopoverVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.popover {
|
||||
max-width: 300px;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
.popover-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px; /* 아이콘과 입력창 간격 조정 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -14,10 +14,10 @@
|
||||
|
||||
<button v-if="isRandom" class="btn btn-primary" type="button" disabled="">
|
||||
<span class="spinner-grow me-1" role="status" aria-hidden="true"></span>
|
||||
랜덤뽑기중..
|
||||
random..
|
||||
</button>
|
||||
<div class="d-grid w-100 mt-6">
|
||||
<button v-if="!isRandom && !randomResultNum" @click="randomList" class="btn btn-primary">랜덤 1위 뽑기</button>
|
||||
<button v-if="!isRandom && !randomResultNum" @click="randomList" class="btn btn-primary"><i class='bx bx-sync'></i></button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<li class="mt-5 card p-5">
|
||||
<DictWrite
|
||||
v-if="isWriteVisible"
|
||||
@close="isWriteVisible = false"
|
||||
v-if="writeStore.isItemActive(item.WRDDICSEQ)"
|
||||
@close="writeStore.closeAll();"
|
||||
:dataList="cateList"
|
||||
@addWord="editWord"
|
||||
:NumValue="item.WRDDICSEQ"
|
||||
@ -27,7 +27,7 @@
|
||||
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
|
||||
<p class="mt-5 dict-content-wrap" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
|
||||
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
||||
<div class="d-flex flex-wrap align-items-center mb-50">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
@ -35,10 +35,12 @@
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.author.profileImage)"
|
||||
alt="최초 작성자"
|
||||
:style="{ borderColor: item.author.color}"/>
|
||||
:style="{ borderColor: item.author.color}"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 small fw-medium">{{ formattedDate(item.author.createdAt) }}</p>
|
||||
<p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.author.createdAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,17 +54,19 @@
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||
alt="최근 작성자"
|
||||
:style="{ borderColor: item.lastEditor.color}"/>
|
||||
:style="{ borderColor: item.lastEditor.color}"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 small fw-medium">{{ formattedDate(item.lastEditor.updatedAt) }}</p>
|
||||
<p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.lastEditor.updatedAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
|
||||
<EditBtn @click="toggleWriteVisible" />
|
||||
<EditBtn @click="writeStore.toggleItem(item.WRDDICSEQ)" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@ -74,9 +78,10 @@ import { ref, toRefs, getCurrentInstance, } from 'vue';
|
||||
import EditBtn from '@/components/button/EditBtn.vue';
|
||||
import $api from '@api';
|
||||
import DictWrite from './DictWrite.vue';
|
||||
import { formattedDate } from "@/common/formattedDate";
|
||||
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import { useWriteVisibleStore } from '@s/writeVisible';
|
||||
|
||||
const writeStore = useWriteVisibleStore();
|
||||
|
||||
// 유저 구분
|
||||
const userStore = useUserInfoStore();
|
||||
@ -107,13 +112,13 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']);
|
||||
|
||||
// 글 수정 상태
|
||||
const isWriteVisible = ref(false);
|
||||
// const isWriteVisible = ref(false);
|
||||
|
||||
|
||||
// 글 수정 toggle
|
||||
const toggleWriteVisible = () => {
|
||||
isWriteVisible.value = !isWriteVisible.value;
|
||||
};
|
||||
// const toggleWriteVisible = () => {
|
||||
// isWriteVisible.value = !isWriteVisible.value;
|
||||
// };
|
||||
|
||||
//카테고리 등록 수정
|
||||
// const addCategory = (data) => {
|
||||
@ -176,7 +181,8 @@ const editWord = (data) => {
|
||||
.then((res) => {
|
||||
if (res.data.data === 1) {
|
||||
toastStore.onToast('✅ 용어가 수정되었습니다.', 's');
|
||||
isWriteVisible.value = false;
|
||||
// isWriteVisible.value = false;
|
||||
writeStore.closeAll();
|
||||
emit('refreshWordList');
|
||||
} else {
|
||||
console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data);
|
||||
@ -191,9 +197,6 @@ const editWord = (data) => {
|
||||
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
// 날짜 포맷
|
||||
// const formatDate = (dateString) => new Date(dateString).toLocaleString();
|
||||
|
||||
// 프로필 이미지
|
||||
const defaultProfile = "/img/icons/icon.png";
|
||||
|
||||
@ -201,6 +204,10 @@ const getProfileImage = (profilePath) => {
|
||||
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
||||
};
|
||||
|
||||
const setDefaultImage = (event) => {
|
||||
event.target.src = defaultProfile;
|
||||
};
|
||||
|
||||
|
||||
// 체크 상태 변경 시 부모로 전달
|
||||
const toggleCheck = (event) => {
|
||||
|
||||
@ -65,6 +65,7 @@ import QEditor from '@/components/editor/QEditor.vue';
|
||||
import FormInput from '@/components/input/FormInput.vue';
|
||||
import FormSelect from '@/components/input/FormSelect.vue';
|
||||
import PlusBtn from '../button/PlusBtn.vue';
|
||||
// import { clearConfig } from 'dompurify';
|
||||
// import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
|
||||
// 유저 구분
|
||||
@ -153,20 +154,34 @@ const onChange = (newValue) => {
|
||||
//용어 등록
|
||||
const saveWord = () => {
|
||||
//validation
|
||||
console.log('computedTitle.value', computedTitle.value);
|
||||
|
||||
let computedTitleTrim;
|
||||
|
||||
if(computedTitle.value != undefined){
|
||||
computedTitleTrim = computedTitle.value.trim()
|
||||
}
|
||||
|
||||
// 용어 체크
|
||||
if(computedTitle.value == undefined){
|
||||
if(computedTitleTrim == undefined || computedTitleTrim == ''){
|
||||
wordTitleAlert.value = true;
|
||||
return;
|
||||
} else {
|
||||
wordTitleAlert.value = false;
|
||||
}
|
||||
|
||||
// 내용 확인
|
||||
let inserts = [];
|
||||
if (inserts.length === 0 && content.value?.ops?.length > 0) {
|
||||
inserts = content.value.ops.map(op =>
|
||||
typeof op.insert === 'string' ? op.insert.trim() : op.insert
|
||||
);
|
||||
}
|
||||
|
||||
// 내용 체크
|
||||
if(content.value == ''){
|
||||
if(content.value == '' || inserts.join('') === ''){
|
||||
wordContentAlert.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const wordData = {
|
||||
id: props.NumValue || null,
|
||||
@ -181,12 +196,26 @@ const saveWord = () => {
|
||||
|
||||
// 카테고리 focusout 이벤트 핸들러 추가
|
||||
const handleCategoryFocusout = (value) => {
|
||||
const valueTrim = value.trim();
|
||||
|
||||
const existingCategory = props.dataList.find(item => item.label === value);
|
||||
const existingCategory = props.dataList.find(item => item.label === valueTrim);
|
||||
// console.log('existingCategory', existingCategory);
|
||||
|
||||
if (existingCategory) {
|
||||
// console.log('이미 존재하는 카테고리입니다:', value);
|
||||
// 카테고리 입력시 공백
|
||||
if(valueTrim == ''){
|
||||
//alert('공백 ㄴㄴ');
|
||||
addCategoryAlert.value = true;
|
||||
|
||||
// 공백시 강제 focus
|
||||
setTimeout(() => {
|
||||
const inputElement = categoryInputRef.value?.$el?.querySelector('input');
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
||||
}else if (existingCategory) {
|
||||
addCategoryAlert.value = true;
|
||||
|
||||
// 중복시 강제 focus
|
||||
|
||||
@ -8,9 +8,8 @@
|
||||
|
||||
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<i class="bx bx-bell bx-md bx-log-out cursor-pointer p-1" @click="handleLogout"></i>
|
||||
|
||||
@ -152,7 +151,13 @@
|
||||
<!-- User -->
|
||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
|
||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-px-40 rounded-circle" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||
<img
|
||||
v-if="user"
|
||||
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||
alt="Profile Image"
|
||||
class="w-px-40 h-px-40 rounded-circle"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
/>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
@ -227,40 +232,37 @@
|
||||
</nav>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useAuthStore } from '@s/useAuthStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useThemeStore } from '@s/darkmode';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import $api from '@api';
|
||||
import { useAuthStore } from '@s/useAuthStore';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useThemeStore } from '@s/darkmode';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
const user = ref(null);
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const user = ref(null);
|
||||
//const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const baseUrl = import.meta.env.BASE_URL;
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const router = useRouter();
|
||||
|
||||
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||
|
||||
onMounted(async () => {
|
||||
if (isDarkMode) {
|
||||
switchToDarkMode();
|
||||
} else {
|
||||
switchToLightMode();
|
||||
}
|
||||
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
onMounted(async () => {
|
||||
if (isDarkMode) {
|
||||
switchToDarkMode();
|
||||
} else {
|
||||
switchToLightMode();
|
||||
}
|
||||
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authStore.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
@ -12,19 +12,16 @@ import $api from '@api';
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
const projectList = ref([]);
|
||||
|
||||
const getProjectList = async (searchText, selectedYear) => {
|
||||
try {
|
||||
const res = await $api.get('project/select', {
|
||||
params: {
|
||||
searchKeyword: searchText,
|
||||
category: selectedYear,
|
||||
},
|
||||
});
|
||||
projectList.value = res.data.data.projectList;
|
||||
} catch (error) {
|
||||
console.error('프로젝트 목록 조회 실패:', error);
|
||||
}
|
||||
const getProjectList = async (searchText = '', selectedYear = '') => {
|
||||
const res = await $api.get('project/select', {
|
||||
params: {
|
||||
searchKeyword: searchText || '',
|
||||
category: selectedYear || '',
|
||||
},
|
||||
});
|
||||
projectList.value = res.data.data.projectList;
|
||||
};
|
||||
|
||||
|
||||
return { projectList, getProjectList };
|
||||
});
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useVoteStore = defineStore("vote", {
|
||||
state: () => ({
|
||||
selectedVote: {}
|
||||
}),
|
||||
actions: {
|
||||
setVoteData(data) {
|
||||
this.selectedVote = data;
|
||||
},
|
||||
clearVoteData() {
|
||||
this.selectedVote = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
48
src/stores/writeVisible.js
Normal file
48
src/stores/writeVisible.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
작성자 : 조대원
|
||||
작성일 : 2025-02-27
|
||||
수정자 :
|
||||
수정일 :
|
||||
설명 : 용어집 작성, 수정 공통관리
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useWriteVisibleStore = defineStore('writeVisible', () => {
|
||||
// 현재 열려있는 항목의 ID를 저장 (열린 것이 없으면 null)
|
||||
const activeItemId = ref(null);
|
||||
|
||||
// 특정 항목의 열림/닫힘 상태 확인
|
||||
function isItemActive(itemId) {
|
||||
return activeItemId.value === itemId;
|
||||
}
|
||||
|
||||
// 항목 토글 - 현재 열려있으면 닫고, 닫혀있으면 열기
|
||||
function toggleItem(itemId) {
|
||||
if (activeItemId.value === itemId) {
|
||||
// 현재 열려있는 항목을 다시 클릭하면 닫기
|
||||
activeItemId.value = null;
|
||||
} else {
|
||||
// 다른 항목 클릭시 해당 항목을 열고 이전 항목은 자동으로 닫힘
|
||||
activeItemId.value = itemId;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 항목 강제로 열기 (다른 항목은 닫힘)
|
||||
function setActiveItem(itemId) {
|
||||
activeItemId.value = itemId;
|
||||
}
|
||||
|
||||
// 모든 항목 닫기
|
||||
function closeAll() {
|
||||
activeItemId.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
activeItemId,
|
||||
isItemActive,
|
||||
toggleItem,
|
||||
setActiveItem,
|
||||
closeAll
|
||||
};
|
||||
});
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
<HalfDayButtons
|
||||
@toggleHalfDay="toggleHalfDay"
|
||||
@addVacationRequests="saveVacationChanges"
|
||||
:isDisabled="!hasChanges"
|
||||
/>
|
||||
</div>
|
||||
<ProfileList
|
||||
@ -78,7 +79,9 @@
|
||||
import { useUserStore } from "@s/userList";
|
||||
import { useUserInfoStore } from "@s/useUserInfoStore";
|
||||
import { fetchHolidays } from "@c/calendar/holiday.js";
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
const userStore = useUserInfoStore();
|
||||
const userListStore = useUserStore();
|
||||
const userList = ref([]);
|
||||
@ -101,11 +104,74 @@
|
||||
const vacationCodeMap = ref({});
|
||||
const holidayDates = ref(new Set());
|
||||
const fetchedEvents = ref([]);
|
||||
const halfDayButtonsRef = ref(null);
|
||||
|
||||
// 데이트피커 인풋 ref
|
||||
const calendarDatepicker = ref(null);
|
||||
let fpInstance = null;
|
||||
|
||||
/** ✅ 변경사항 여부 확인 */
|
||||
const hasChanges = computed(() => {
|
||||
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,9 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
|
||||
.fc-toolbar-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 데이트피커 인풋은 Flatpickr에서 동적으로 스타일 적용됨 */
|
||||
/* 모달 본문 스크롤 */
|
||||
.modal-body {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -50,7 +50,6 @@ import $api from '@api';
|
||||
import Quill from 'quill';
|
||||
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||
import voteList from '@c/voteboard/voteCardList.vue';
|
||||
import { useVoteStore } from '@s/voteDetail';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
@ -69,11 +68,8 @@ const router = useRouter();
|
||||
onMounted(async () => {
|
||||
getvoteList();
|
||||
});
|
||||
const voteStore = useVoteStore();
|
||||
//글작성
|
||||
const voteWrite = () => {
|
||||
voteStore.setVoteData(selectedVote.value);
|
||||
console.log("Pinia 상태 업데이트됨:", voteStore.selectedVote);
|
||||
router.push('/voteboard/write');
|
||||
};
|
||||
|
||||
|
||||
@ -17,7 +17,8 @@
|
||||
name="title"
|
||||
:is-essential="true"
|
||||
:is-alert="titleAlert"
|
||||
:v-model="title"
|
||||
v-model="title"
|
||||
@keyup="ValidHandler('title')"
|
||||
/>
|
||||
<form-input
|
||||
title="종료날짜"
|
||||
@ -27,20 +28,25 @@
|
||||
:is-alert="endDateAlert"
|
||||
v-model="endDate"
|
||||
:min="today"
|
||||
@change="ValidHandlerendDate"
|
||||
/>
|
||||
<!-- 항목 입력 반복 -->
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-center mb-2 position-relative">
|
||||
<form-input
|
||||
class="flex-grow-1 me-2"
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start">
|
||||
<div class="flex-grow-1 me-2 ">
|
||||
<form-input
|
||||
:title="'항목 ' + (index + 1)"
|
||||
:name="'content' + index"
|
||||
:is-essential="index < 2"
|
||||
:is-alert="contentAlerts[index]"
|
||||
v-model="item.content"
|
||||
/>
|
||||
<link-input v-model="item.url" />
|
||||
<delete-btn @click="removeItem(index)" :disabled="index < 2" class="ms-2" />
|
||||
@keyup="ValidHandler('content' + (index + 1))"
|
||||
/>
|
||||
<link-input v-model="item.url" class="mb-1"/>
|
||||
</div>
|
||||
<!-- delete-btn을 오른쪽으로 정렬 -->
|
||||
<delete-btn @click="removeItem(index)" :disabled="index < 2" />
|
||||
</div>
|
||||
|
||||
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
|
||||
<div>
|
||||
<label class="list-group-item">
|
||||
@ -92,7 +98,6 @@ import LinkInput from "@/components/voteboard/voteLinkInput.vue";
|
||||
import { voteCommon } from '@s/voteCommon';
|
||||
import { useUserStore } from '@s/userList';
|
||||
import { useRoute } from "vue-router";
|
||||
import { useVoteStore } from "@s/voteDetail";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
@ -115,12 +120,6 @@ const userSet = ({ userList, userTotal }) => {
|
||||
userListTotal.value = userTotal;
|
||||
};
|
||||
|
||||
const voteStore = useVoteStore();
|
||||
|
||||
onMounted(()=>{
|
||||
console.log('상세데이터',voteStore.selectedVote);
|
||||
})
|
||||
|
||||
|
||||
const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers }) => {
|
||||
activeUserList.value = activeUsers;
|
||||
@ -133,7 +132,7 @@ const saveValid = () => {
|
||||
if (disabledUsers.value.length === 0) {
|
||||
activeUserList.value = [...userStore.userList];
|
||||
}
|
||||
if (title.value === '') {
|
||||
if (title.value.trim() === '') {
|
||||
titleAlert.value = true;
|
||||
valid = false;
|
||||
} else {
|
||||
@ -145,13 +144,13 @@ const saveValid = () => {
|
||||
} else {
|
||||
endDateAlert.value = false;
|
||||
}
|
||||
if (itemList.value[0].content === '') {
|
||||
if (itemList.value[0].content.trim() === '') {
|
||||
contentAlerts.value[0] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[0] = false;
|
||||
}
|
||||
if (itemList.value[1].content === '') {
|
||||
if (itemList.value[1].content.trim() === '') {
|
||||
contentAlerts.value[1] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
@ -168,6 +167,7 @@ const saveValid = () => {
|
||||
}
|
||||
};
|
||||
const saveVote = () => {
|
||||
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
|
||||
const unwrappedUserList = toRaw(activeUserList.value);
|
||||
const listId = unwrappedUserList.map(item => ({
|
||||
id: item.MEMBERSEQ,
|
||||
@ -175,9 +175,9 @@ const saveVote = () => {
|
||||
$api.post('vote/insertWord',{
|
||||
addvoteIs :addvoteitem.value === false ? '0' :'1'
|
||||
,votemMltiIs: addvotemulti.value === false ? '0' : '1'
|
||||
,title :title.value
|
||||
,title :title.value.trim()
|
||||
,endDate :endDate.value
|
||||
,itemList :itemList.value
|
||||
,itemList :filteredItemList
|
||||
,activeUserList :listId
|
||||
}).then((res)=>{
|
||||
if(res.data.status == 'OK'){
|
||||
@ -186,6 +186,20 @@ const saveVote = () => {
|
||||
}
|
||||
})
|
||||
};
|
||||
const ValidHandler = (field) => {
|
||||
if(field == 'title'){
|
||||
titleAlert.value = false;
|
||||
}
|
||||
if(field == 'content1'){
|
||||
contentAlerts.value[0] = false;
|
||||
}
|
||||
if(field == 'content2'){
|
||||
contentAlerts.value[1] = false;
|
||||
}
|
||||
}
|
||||
const ValidHandlerendDate = () =>{
|
||||
endDateAlert.value = false;
|
||||
}
|
||||
const goList = () => {
|
||||
router.push('/voteboard');
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<!-- 단어 갯수, 작성하기 -->
|
||||
<div class="mt-4">
|
||||
단어 : {{ total }}
|
||||
<WriteButton @click="toggleWriteForm" />
|
||||
<WriteButton @click="writeStore.toggleItem(999999)" />
|
||||
</div>
|
||||
|
||||
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
||||
@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 작성 -->
|
||||
<div v-if="isWriteVisible" class="mt-5">
|
||||
<DictWrite @close="isWriteVisible = false" :dataList="cateList" @addWord="addWord"/>
|
||||
<div v-if="writeStore.isItemActive(999999)" class="mt-5">
|
||||
<DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,9 +78,13 @@
|
||||
import commonApi from '@/common/commonApi';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import { useWriteVisibleStore } from '@s/writeVisible';
|
||||
|
||||
// 작성창 구분
|
||||
const writeStore = useWriteVisibleStore();
|
||||
|
||||
// 유저 구분
|
||||
const userStore = useUserInfoStore();
|
||||
// const userStore = useUserInfoStore();
|
||||
|
||||
const { appContext } = getCurrentInstance();
|
||||
const $common = appContext.config.globalProperties.$common;
|
||||
@ -118,7 +122,7 @@
|
||||
const searchText = ref('');
|
||||
|
||||
// 작성
|
||||
const isWriteVisible = ref(false);
|
||||
// const isWriteVisible = ref(false);
|
||||
|
||||
// 데이터 로드
|
||||
onMounted(() => {
|
||||
@ -168,9 +172,9 @@
|
||||
}
|
||||
|
||||
// 작성 toggle
|
||||
const toggleWriteForm = () => {
|
||||
isWriteVisible.value = !isWriteVisible.value;
|
||||
};
|
||||
// const toggleWriteForm = () => {
|
||||
// isWriteVisible.value = !isWriteVisible.value;
|
||||
// };
|
||||
|
||||
//카테고리 등록
|
||||
// const addCategory = (data) =>{
|
||||
@ -197,15 +201,14 @@
|
||||
const addWord = (wordData, data) => {
|
||||
let category = null;
|
||||
// 카테고리 체크
|
||||
const existingCategory = cateList.value.find(item => item.label === data);
|
||||
if (existingCategory) {
|
||||
console.log('카테고리 중복');
|
||||
const existingCategory = cateList.value.find(item => item.label === data.trim());
|
||||
|
||||
if (existingCategory) {
|
||||
//카테고리 있을시 그냥 저장
|
||||
category = existingCategory.label == '' ? wordData.category : existingCategory.value;
|
||||
} else {
|
||||
//카테고리 없을시 카테고리 와 용어 둘다 저장
|
||||
console.log('카테고리 없음');
|
||||
// console.log('카테고리 없음');
|
||||
const lastCategory = cateList.value[cateList.value.length - 1];
|
||||
category = lastCategory ? lastCategory.value + 1 : 600101;
|
||||
}
|
||||
@ -224,9 +227,10 @@
|
||||
axios.post('worddict/insertWord', payload).then(res => {
|
||||
if (res.data.status === 'OK') {
|
||||
toastStore.onToast('용어가 등록 되었습니다.', 's');
|
||||
isWriteVisible.value = false;
|
||||
// isWriteVisible.value = false;
|
||||
writeStore.closeAll();
|
||||
getwordList();
|
||||
const newCategory = { label: data, value: category }; // 여기서 data 사용
|
||||
const newCategory = { label: data, value: category };
|
||||
cateList.value = [newCategory, ...cateList.value];
|
||||
}
|
||||
});
|
||||
@ -234,12 +238,13 @@
|
||||
axios.post('worddict/insertWord', payload).then(res => {
|
||||
if (res.data.status === 'OK') {
|
||||
toastStore.onToast('용어가 등록 되었습니다.', 's');
|
||||
isWriteVisible.value = false;
|
||||
// isWriteVisible.value = false;
|
||||
writeStore.closeAll();
|
||||
getwordList();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// 체크 상태 업데이트
|
||||
@ -268,7 +273,8 @@
|
||||
.then(res => {
|
||||
if (res.data.status == 'OK') {
|
||||
toastStore.onToast('용어 삭제가 완료되었습니다.', 's');
|
||||
isWriteVisible.value = false;
|
||||
// isWriteVisible.value = false;
|
||||
writeStore.closeAll();
|
||||
getwordList();
|
||||
|
||||
// 삭제 후 초기화
|
||||
@ -277,7 +283,7 @@
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('삭제 요청 중 오류 발생:', error);
|
||||
// console.error('삭제 요청 중 오류 발생:', error);
|
||||
toastStore.onToast('오류가 발생했습니다. 다시 시도해주세요.', 'e');
|
||||
});
|
||||
|
||||
|
||||
@ -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)),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user