Merge branch 'main' into project-list

This commit is contained in:
yoon 2025-02-27 13:30:42 +09:00
commit f11de2f2ab
30 changed files with 817 additions and 537 deletions

View File

@ -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_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/ # VITE_FILE_URL = http://localhost:10325/ms/
# VITE_API_URL = http://localhost:10325/api/ VITE_API_URL = https://192.168.0.251:10325/api/
VITE_API_URL = http://localhost:10325/test/ VITE_TEST_URL = https://192.168.0.251:10325/test/
VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492 VITE_KAKAO_MAP_KEY=6f092e8f45ee81186bb6d8408f66a492

6
.env.mine Normal file
View 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

View File

@ -1,50 +1,51 @@
{ {
"name": "front", "name": "front",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0 --mode dev", "dev": "vite --host 0.0.0.0 --mode dev",
"build": "vite build --mode prod", "mine": "vite --host 0.0.0.0 --mode mine",
"preview": "vite preview", "build": "vite build --mode prod",
"lint": "eslint . --fix", "preview": "vite preview",
"format": "prettier --write src/" "lint": "eslint . --fix",
}, "format": "prettier --write src/"
"dependencies": { },
"@fullcalendar/core": "^6.1.15", "dependencies": {
"@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/core": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15", "@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/vue3": "^6.1.15", "@fullcalendar/interaction": "^6.1.15",
"@popperjs/core": "^2.11.8", "@fullcalendar/vue3": "^6.1.15",
"@tinymce/tinymce-vue": "^5.1.1", "@popperjs/core": "^2.11.8",
"@vueup/vue-quill": "^1.2.0", "@tinymce/tinymce-vue": "^5.1.1",
"axios": "^1.7.9", "@vueup/vue-quill": "^1.2.0",
"bootstrap": "^5.3.3", "axios": "^1.7.9",
"bootstrap-icons": "^1.11.3", "bootstrap": "^5.3.3",
"dayjs": "^1.11.13", "bootstrap-icons": "^1.11.3",
"dompurify": "^3.2.3", "dayjs": "^1.11.13",
"flatpickr": "^4.6.13", "dompurify": "^3.2.3",
"front": "file:", "flatpickr": "^4.6.13",
"heic2any": "^0.0.4", "front": "file:",
"pinia": "^2.2.6", "heic2any": "^0.0.4",
"pinia-plugin-persist": "^1.0.0", "pinia": "^2.2.6",
"quill": "^2.0.3", "pinia-plugin-persist": "^1.0.0",
"upload-images-converter": "^2.0.2", "quill": "^2.0.3",
"vite-plugin-mkcert": "^1.17.6", "upload-images-converter": "^2.0.2",
"vue": "^3.5.13", "vite-plugin-mkcert": "^1.17.6",
"vue-flatpickr-component": "^11.0.5", "vue": "^3.5.13",
"vue-router": "^4.4.5", "vue-flatpickr-component": "^11.0.5",
"vue3-kakao-maps": "^2.3.10" "vue-router": "^4.4.5",
}, "vue3-kakao-maps": "^2.3.10"
"devDependencies": { },
"@eslint/js": "^9.14.0", "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@eslint/js": "^9.14.0",
"@vue/eslint-config-prettier": "^10.1.0", "@vitejs/plugin-vue": "^5.2.1",
"eslint": "^9.14.0", "@vue/eslint-config-prettier": "^10.1.0",
"eslint-plugin-vue": "^9.30.0", "eslint": "^9.14.0",
"prettier": "^3.3.3", "eslint-plugin-vue": "^9.30.0",
"vite": "^5.4.10", "prettier": "^3.3.3",
"vite-plugin-inspect": "^0.8.9", "vite": "^5.4.10",
"vite-plugin-vue-devtools": "^7.6.5" "vite-plugin-inspect": "^0.8.9",
} "vite-plugin-vue-devtools": "^7.6.5"
}
} }

View File

@ -2,42 +2,184 @@
/* 휴가 */ /* 휴가 */
.half-day-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.half-day-buttons .btn.active {
border: 2px solid black;
}
.fc-daygrid-day-events { .fc-daygrid-day-events {
max-height: 100px !important; max-height: 100px !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
/* 이벤트 선 없게 */
.fc-event { .fc-event {
border: none; border: none;
} }
/* 오전전반차 그래프 */
.fc-daygrid-event.half-day-am { .fc-daygrid-event.half-day-am {
width: calc(50% - 4px) !important; width: calc(50% - 4px) !important;
} }
/* 오후반차 그래프프 */
.fc-daygrid-event.half-day-pm { .fc-daygrid-event.half-day-pm {
width: calc(50% - 4px) !important; width: calc(50% - 4px) !important;
margin-left: auto !important margin-left: auto !important
} }
/* 공휴일,일요일 색상 */
.fc-day-sun .fc-daygrid-day-number, .fc-day-sun .fc-daygrid-day-number,
.fc-col-header-cell:first-child .fc-col-header-cell-cushion { .fc-col-header-cell:first-child .fc-col-header-cell-cushion {
color: #ff4500 !important; color: #ff4500 !important;
} }
/* 토요일 색상 */
.fc-day-sat .fc-daygrid-day-number, .fc-day-sat .fc-daygrid-day-number,
.fc-col-header-cell:last-child .fc-col-header-cell-cushion { .fc-col-header-cell:last-child .fc-col-header-cell-cushion {
color: #6076e0 !important; color: #6076e0 !important;
} }
/* 캘린더 날짜 왼쪽 상단 위치하게 */
.fc-daygrid-day-number { .fc-daygrid-day-number {
margin-right: auto; margin-right: auto;
} }
/* 데이트피커 뾰족없게게 */
.flatpickr-calendar:before,
.flatpickr-calendar:after {
display: none !important;
}
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
/* 클릭 가능한 날짜 (오늘 + 미래) */
.fc-daygrid-day.clickable {
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
/* 마우스를 올렸을 때 효과 */
.fc-daygrid-day.clickable:hover {
background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */
}
/* 주말 (토요일, 일요일) 및 공휴일 */
.fc-day-sat-sun {
cursor: not-allowed !important;
opacity: 0.6; /* 흐려 보이게 */
}
/* 과거 날짜 (오늘 이전) */
.fc-daygrid-day.past {
cursor: not-allowed !important;
opacity: 0.6; /* 흐려 보이게 */
}
/* 기본 이벤트 스타일 */
.fc-daygrid-event {
border: none !important;
border-radius: 4px;
}
/* 오전 반차 (왼쪽 절반) */
.selected-event.half-day-am {
width: 50% !important;
left: 0 !important;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
/* 오후 반차 (오른쪽 절반) */
.selected-event.half-day-pm {
width: 50% !important;
margin-left: auto !important;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
/* 본인 모달 */
/* 닫기 버튼 */
.close-btn {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
}
/* 리스트 아이템 */
.vacation-item {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
padding: 5px 10px;
border-radius: 5px;
background: #f9f9f9;
}
/* 모달 본문 스크롤 */
.modal-body {
max-height: 130px;
overflow-y: auto;
}
/* 선물하기 모달 */
/* 연차 개수 버튼 */
.count-btn {
font-size: 18px;
padding: 2px 10px;
border: none;
background: #2C3E50;
color: white;
border-radius: 5px;
cursor: pointer;
}
.count-btn:hover {
background: #1d2c44;
}
.count-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
/* 버튼 컨테이너 (우측 정렬) */
.custom-button-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
/* 버튼 기본 스타일 */
.custom-button {
background: none; /* 배경색 없음 */
border: none; /* 테두리 없음 */
padding: 10px; /* 크기 조정 */
cursor: pointer; /* 클릭 가능하도록 변경 */
}
/* 아이콘 색상 변경 (기본) */
.custom-button i {
color: #282538; /* 기본 아이콘 색상 */
font-size: 25px; /* 아이콘 크기 */
}
/* 버튼 호버 효과 */
.custom-button:hover i {
color: #ff0800; /* 호버 시 아이콘 색상 변경 */
}
/* 모달 배경 투명하게 */
.modal-dialog {
background: none !important; /* 배경 제거 */
box-shadow: none !important; /* 음영 제거 */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 모달 내용 스타일 */
.modal-content {
background: #fff; /* 기존 흰색 배경 유지 */
border-radius: 8px;
box-shadow: none !important; /* 내부 음영 제거 */
padding: 20px;
max-width: 500px;
width: 100%;
}
.grayscaleImg { .grayscaleImg {
filter: grayscale(100%); filter: grayscale(100%);

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import { useRoute } from 'vue-router';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
const $api = axios.create({ const $api = axios.create({
baseURL: 'https://192.168.0.251:10325/api/', baseURL: import.meta.env.VITE_API_URL,
timeout: 300000, timeout: 300000,
withCredentials: true, withCredentials: true,
}); });

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<BoardProfile <BoardProfile
:unknown="unknown" :unknown="comment.author === '익명'"
:isCommentAuthor="isCommentAuthor" :isCommentAuthor="isCommentAuthor"
:boardId="comment.boardId" :boardId="comment.boardId"
:profileName="comment.author" :profileName="comment.author"
@ -11,7 +11,7 @@
:isLike="!isLike" :isLike="!isLike"
:isCommentPassword="comment.isCommentPassword" :isCommentPassword="comment.isCommentPassword"
:isCommentProfile="true" :isCommentProfile="true"
@editClick="$emit('editClick', comment)" @editClick="aaaa"
@deleteClick="$emit('deleteClick', comment)" @deleteClick="$emit('deleteClick', comment)"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
@ -29,8 +29,8 @@
</div> </div>
<span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span> <span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span>
</div> </div>
<p>authorId:{{ comment.authorId }}</p> <!-- <p>authorId:{{ comment.authorId }}</p>
<p>코멘트 비교: {{comment.isCommentAuthor}}</p> <p>코멘트 비교: {{comment.isCommentAuthor}}</p> -->
<div class="mt-6"> <div class="mt-6">
@ -47,20 +47,29 @@
</div> </div>
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/> <PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
<BoardCommentArea v-if="isComment" @submitComment="submitComment"/> <BoardCommentArea v-if="isComment" :unknown="unknown" @submitComment="submitComment"/>
<!-- 대댓글 --> <!-- 대댓글 -->
<ul v-if="comment.children && comment.children.length" class="list-unstyled"> <ul v-if="comment.children && comment.children.length" class="list-unstyled">
<li <li
v-for="child in comment.children" v-for="child in comment.children"
:key="child.commentId" :key="child.commentId"
class="mt-8 pt-6 ps-10 border-top" class="mt-8 pt-6 ps-10 border-top"
> >
<!-- <p>대댓글 데이터(JSON): {{ JSON.stringify(child, null, 2) }}</p> -->
<!-- <p>comment child: {{ comment.children }}</p> -->
<!-- :unknown="child.author === '익명'" -->
<BoardComment <BoardComment
:comment="child" :comment="child"
:unknown="unknown" :unknown="child.author === '익명'"
:isPlusButton="false" :isPlusButton="false"
:isLike="true" :isLike="true"
:isCommentProfile="true"
:isCommentAuthor="child.isCommentAuthor"
@editClick="$emit('editClick', $event)"
@deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment" @submitComment="submitComment"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
@ -155,4 +164,8 @@ const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value); emit('submitEdit', props.comment, localEditedContent.value);
}; };
const aaaa = () => {
emit('editClick', props.comment);
}
</script> </script>

View File

@ -17,6 +17,7 @@
rows="3" rows="3"
v-model="comment" v-model="comment"
></textarea> ></textarea>
<span v-if="commentAlert" class="invalid-feedback d-block text-start ms-2">{{ commentAlert }}</span>
</div> </div>
</div> </div>
@ -42,6 +43,7 @@
id="basic-default-password" id="basic-default-password"
class="form-control flex-grow-1" class="form-control flex-grow-1"
v-model="password" v-model="password"
placeholder="비밀번호 입력"
/> />
<span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span> <span v-if="passwordAlert" class="invalid-feedback d-block text-start ms-2">{{ passwordAlert }}</span>
</div> </div>
@ -74,28 +76,35 @@ const props = defineProps({
passwordAlert: { passwordAlert: {
type: String, type: String,
default: false default: false
},
commentAlert: {
type: String,
default: false
} }
}); });
const comment = ref(''); const comment = ref('');
const password = ref(''); const password = ref('');
const isCheck = ref(false); const isCheck = ref(props.unknown);
const emit = defineEmits(['submitComment']);
const emit = defineEmits(['submitComment']);
const LOCBRDTYP = isCheck.value ? '300102' : null;
function handleCommentSubmit() { function handleCommentSubmit() {
if (props.unknown && isCheck.value && !password.value) {
alert('익명 댓글을 작성하려면 비밀번호를 입력해야 합니다.');
return;
}
const LOCBRDTYP = isCheck.value ? '300102' : null;
emit('submitComment', { emit('submitComment', {
comment: comment.value, comment: comment.value,
password: isCheck.value ? password.value : '', password: isCheck.value ? password.value : '',
LOCBRDTYP, isCheck: isCheck.value,
LOCBRDTYP, // '300102'
isCheck: isCheck.value isCheck: isCheck.value
}); });
comment.value = '';
password.value = '';
isCheck.value = false; //
} }
watch(() => props.passwordAlert, () => {
if (!props.passwordAlert) {
comment.value = '';
password.value = '';
}
});
</script> </script>

View File

@ -1,43 +1,133 @@
<template> <template>
<div class="half-day-buttons"> <div class="menu gap-4 justify-content-center mt-5">
<!-- 오전 반차 버튼 -->
<button <button
class="btn btn-info" class="btn btn-warning"
:class="{ active: halfDayType === 'AM' }" :class="{ active: halfDayType === 'AM' }"
@click="toggleHalfDay('AM')" @click="toggleHalfDay('AM')"
> >
<i class="bi bi-sun"></i> <i class="bi bi-sun"></i>
</button> </button>
<!-- 오후 반차 버튼 -->
<button <button
class="btn btn-warning" class="btn btn-info"
:class="{ active: halfDayType === 'PM' }" :class="{ active: halfDayType === 'PM' }"
@click="toggleHalfDay('PM')" @click="toggleHalfDay('PM')"
> >
<i class="bi bi-moon"></i> <i class="bi bi-moon"></i>
</button> </button>
<!-- 저장 버튼 -->
<div class="save-button-container"> <div class="save-button-container">
<button class="btn btn-success" @click="addVacationRequests"> <button class="btn btn-success" @click="addVacationRequests" :disabled="isDisabled">
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup> <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 halfDayType = ref(null);
const toggleHalfDay = (type) => { const toggleHalfDay = (type) => {
halfDayType.value = halfDayType.value === type ? null : type; halfDayType.value = type;
emit("toggleHalfDay", halfDayType.value); emit("toggleHalfDay", halfDayType.value);
// 1
setTimeout(() => {
halfDayType.value = null;
}, 1000);
};
//
const resetHalfDay = () => {
halfDayType.value = null;
emit("resetHalfDay");
}; };
const addVacationRequests = () => { const addVacationRequests = () => {
emit("addVacationRequests"); emit("addVacationRequests");
}; };
defineExpose({ resetHalfDay });
</script> </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>

View File

@ -78,6 +78,7 @@ const props = defineProps({
min: { min: {
type: String, type: String,
default: '', default: '',
required: false,
} }
}); });

View File

@ -1,21 +1,22 @@
<template> <template>
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal"> <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> <h5 class="modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
<button class="close-btn" @click="closeModal"></button> <button class="close-btn" @click="closeModal"></button>
<div class="modal-body"> <div class="modal-body">
<p>선물할 연차 개수를 선택하세요.</p> <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> <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> <button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
</div> </div>
<div class="custom-button-container">
<button class="gift-btn" @click="saveVacationGrant" :disabled="grantCount === 0"> <button class="custom-button" @click="saveVacationGrant" :disabled="grantCount === 0">
<i class="bx bx-gift"></i> <i class="bx bx-gift"></i>
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -24,7 +25,9 @@
<script setup> <script setup>
import { ref, defineProps, defineEmits, watch, onMounted } from "vue"; import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
import axios from "@api"; import axios from "@api";
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore();
const props = defineProps({ const props = defineProps({
isOpen: Boolean, isOpen: Boolean,
targetUser: Object, targetUser: Object,
@ -72,21 +75,18 @@
count: grantCount.value, count: grantCount.value,
}, },
]; ];
console.log(props.targetUser)
console.log(payload)
const response = await axios.post("vacation", payload); const response = await axios.post("vacation", payload);
console.log(response)
if (response.data && response.data.status === "OK") { if (response.data && response.data.status === "OK") {
alert("✅ 연차가 부여되었습니다."); toastStore.onToast('연차가 선물되었습니다.', 's');
await fetchSentVacationCount(); await fetchSentVacationCount();
emit("updateVacation"); emit("updateVacation");
closeModal(); closeModal();
} else { } else {
alert("🚨 연차 추가 중 오류가 발생했습니다."); toastStore.onToast(' 연차 선물 중 오류가 발생했습니다.', 'e');
} }
} catch (error) { } catch (error) {
console.error("🚨 연차 추가 실패:", error); console.error("🚨 연차 추가 실패:", error);
alert("연차 추가에 실패했습니다."); toastStore.onToast(' 연차 선물 실패!!.', 'e');
} }
}; };
@ -98,7 +98,6 @@
() => props.isOpen, () => props.isOpen,
async (newVal) => { async (newVal) => {
if (newVal && props.targetUser && props.targetUser.MEMBERSEQ) { if (newVal && props.targetUser && props.targetUser.MEMBERSEQ) {
console.log("🟢 모달이 열렸습니다. 데이터를 로드합니다.");
await fetchSentVacationCount(); await fetchSentVacationCount();
} }
} }
@ -124,81 +123,4 @@
<style scoped> <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;
}
/* 연차 개수 조정 버튼 */
.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;
}
</style> </style>

View File

@ -1,29 +1,28 @@
<template> <template>
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal"> <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> <h5 class="modal-title">📅 연차 사용 내역</h5>
<button class="close-btn" @click="closeModal"></button> <button class="close-btn" @click="closeModal"></button>
<!-- 연차 목록 --> <!-- 연차 목록 -->
<div class="modal-body" v-if="mergedVacations.length > 0"> <div class="modal-body" v-if="mergedVacations.length > 0">
<ol class="vacation-list"> <ol class="list-group-numbered px-0 mt-4">
<li <li
v-for="(vac, index) in mergedVacations" v-for="(vac, index) in mergedVacations"
:key="vac._expandIndex" :key="vac._expandIndex"
class="vacation-item" class="vacation-item"
> >
<!-- Used 항목만 인덱스 표시 --> <!-- 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] }}) {{ usedVacationIndexMap[vac._expandIndex] }})
</span> </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' ? '-' : '+' }} {{ vac.category === 'used' ? '-' : '+' }}
</span> </span>
<span <span
:style="{ color: userColors[vac.senderId || vac.receiverId] || '#000' }" :style="{ color: userColors[vac.senderId || vac.receiverId] || '#000' }"
class="vacation-date"
> >
{{ formatDate(vac.date) }} {{ formatDate(vac.date) }}
</span> </span>
@ -32,7 +31,7 @@
</div> </div>
<!-- 연차 데이터 없음 --> <!-- 연차 데이터 없음 -->
<p v-else class="no-data"> <p v-else class="text-sm-center mt-10 text-gray">
🚫 사용한 연차가 없습니다. 🚫 사용한 연차가 없습니다.
</p> </p>
</div> </div>
@ -139,86 +138,4 @@ const closeModal = () => {
</script> </script>
<style scoped> <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;
}
/* 리스트 기본 스타일 */
.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;
}
</style> </style>

View File

@ -1,13 +1,15 @@
<template> <template>
<div class="card mb-6"> <div class="card mb-6">
<div class="card-body" v-if="!data.localVote.LOCVOTDEL" > <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 <img
class="rounded-circle user-avatar border border-3 w-px-40" class="rounded-circle user-avatar border border-3 w-px-40"
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`" :src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
:style="`border-color: ${data.localVote.usercolor} !important;`" :style="`border-color: ${data.localVote.usercolor} !important;`"
alt="user" alt="user"
/> />
<div class="w-100"> <div class="w-100">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="user-info"> <div class="user-info">
@ -24,7 +26,6 @@
class="bx btn btn-danger" class="bx btn btn-danger"
@click="endBtn(data.localVote.LOCVOTSEQ)" @click="endBtn(data.localVote.LOCVOTSEQ)"
>종료</button> >종료</button>
<EditBtn v-if="!data.localVote.LOCVOTDDT" @click="updateVote(data.localVote.LOCVOTSEQ)"/>
<DeleteBtn @click="voteDelete(data.localVote.LOCVOTSEQ)" /> <DeleteBtn @click="voteDelete(data.localVote.LOCVOTSEQ)" />
</div> </div>
</div> </div>
@ -32,10 +33,8 @@
</div> </div>
</div> </div>
</h5> </h5>
<h5>{{ data.localVote.LOCVOTTTL }}</h5> <h5 class="mb-1">{{ data.localVote.LOCVOTTTL }}</h5>
<div class="mb-1">{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</div> <small >{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</small>
<!-- 투표완료시-->
<button v-if="data.yesVotetotal > 0 && !data.localVote.LOCVOTDDT" class="btn btn-primary btn-sm" >재투표</button>
<!-- 투표안했을시--> <!-- 투표안했을시-->
<div v-if="data.localVote.LOCVOTDDT && data.voteResult.length == 0"> <div v-if="data.localVote.LOCVOTDDT && data.voteResult.length == 0">
<small class="text-primary text-uppercase">투표 결과없음 (😂아무도 투표하지 않았습니다)</small> <small class="text-primary text-uppercase">투표 결과없음 (😂아무도 투표하지 않았습니다)</small>
@ -48,13 +47,14 @@
:data="data.voteDetails" :data="data.voteDetails"
:voteInfo="data.localVote" :voteInfo="data.localVote"
:total="data.voteDetails.length "/> :total="data.voteDetails.length "/>
<!-- 투표완/미완 인원 -->
<vote-user-list v-if="!data.localVote.LOCVOTDDT"
:data="data.voteMembers"/>
<!-- 투표 결과 --> <!-- 투표 결과 -->
<div v-if="data.localVote.LOCVOTDDT" class="mt-3"> <div v-if="data.localVote.LOCVOTDDT" class="mt-3">
<vote-result-list :data="data.voteResult" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES"/> <vote-result-list :data="data.voteResult" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES"/>
</div> </div>
<!-- 투표완/미완 인원 -->
<vote-user-list
:data="data.voteMembers"/>
</div> </div>
</div> </div>
<div v-else class="card-body"> <div v-else class="card-body">

View File

@ -12,7 +12,7 @@
/> />
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1" class="d-flex align-items-center"> <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 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 <form-input
class="flex-grow-1 me-2" class="flex-grow-1 me-2"
:title="'항목 ' + (index + data.length + 1)" :title="'항목 ' + (index + data.length + 1)"
@ -37,12 +37,13 @@
</div> </div>
</div> </div>
<button class="btn btn-primary btn-sm" @click="selectVote">투표하기</button> <save-btn class="btn-sm mt-2" @click="selectVote"/>
</template> </template>
<script setup> <script setup>
import $api from '@api'; import $api from '@api';
import PlusBtn from '@c/button/PlusBtn.vue'; import PlusBtn from '@c/button/PlusBtn.vue';
import SaveBtn from '@c/button/SaveBtn.vue'
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import voteCardCheckList from '@c/voteboard/voteCardCheckList.vue'; import voteCardCheckList from '@c/voteboard/voteCardCheckList.vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@ -78,7 +79,8 @@ const props = defineProps({
const emit = defineEmits(['addContents','checkedNames']); const emit = defineEmits(['addContents','checkedNames']);
// //
const addContentSave = (voteId) =>{ 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: "" }]; itemList.value = [{ content: "", url: "" }];
} }

View File

@ -10,6 +10,7 @@
class="rounded-circle user-avatar border border-3" class="rounded-circle user-avatar border border-3"
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`" :src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
:style="`border-color: ${data.usercolor} !important;`" :style="`border-color: ${data.usercolor} !important;`"
@error="$event.target.src = '/img/icons/icon.png'"
alt="user" alt="user"
/> />
</li> </li>

View File

@ -10,6 +10,7 @@
class="rounded-circle user-avatar border border-3" class="rounded-circle user-avatar border border-3"
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`" :src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
:style="`border-color: ${data.usercolor} !important;`" :style="`border-color: ${data.usercolor} !important;`"
@error="$event.target.src = '/img/icons/icon.png'"
alt="user" alt="user"
/> />
</li> </li>

View File

@ -1,32 +1,33 @@
<template> <template>
<div class="position-relative me-2"> <div class="d-flex align-items-center">
<i class="bx bx-link-alt" @click="togglePopover"></i> <!-- 링크 아이콘 -->
<!-- 링크 팝업 --> <i class="bx bx-link-alt me-2" @click="togglePopover"></i>
<!-- 링크 입력창 (옆으로 나오게) -->
<div <div
v-if="isPopoverVisible" v-if="isPopoverVisible"
class="popover bs-popover-auto fade show d-flex align-items-center" class="popover-container d-flex align-items-center"
role="tooltip"
:style="popoverStyle"
> >
<div class="popover-arrow"></div>
<input <input
v-model="link" v-model="link"
placeholder="URL을 입력해주세요" placeholder="URL을 입력해주세요"
class="form-control me-2 flex-grow-1" class="form-control me-2"
style="min-width: 200px;" style="min-width: 200px;"
/> />
<button type="button" class="btn btn-sm btn-primary ms-2" @click="saveLink"> <save-btn class="btn-sm" @click="saveLink"/>
등록
</button>
</div> </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> <a :href="formattedLink" target="_blank" rel="noopener noreferrer">{{ link }}</a>
</div> </span>
</div> </div>
</template> </template>
<script setup> <script setup>
import SaveBtn from '@c/button/SaveBtn.vue'
import { ref, computed } from "vue"; import { ref, computed } from "vue";
const props = defineProps({ const props = defineProps({
@ -36,45 +37,27 @@ const emit = defineEmits(["update:modelValue"]);
const isPopoverVisible = ref(false); const isPopoverVisible = ref(false);
const link = ref(props.modelValue || ""); const link = ref(props.modelValue || "");
const popoverStyle = ref({}); const isLinkSaved = ref(false); // Track if the link has been saved
const formattedLink = computed(() => { 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 togglePopover = () => {
const buttonRect = event.target.getBoundingClientRect(); isPopoverVisible.value = !isPopoverVisible.value;
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 saveLink = () => { const saveLink = () => {
emit("update:modelValue", link.value); emit("update:modelValue", link.value);
isPopoverVisible.value = false; isLinkSaved.value = true; // Set the link as saved
isPopoverVisible.value = false;
}; };
</script> </script>
<style scoped> <style scoped>
.popover { .popover-container {
max-width: 300px; display: flex;
border-radius: 6px; align-items: center;
padding: 5px; gap: 8px; /* 아이콘과 입력창 간격 조정 */
}
.popover-arrow {
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%);
} }
</style> </style>

View File

@ -14,10 +14,10 @@
<button v-if="isRandom" class="btn btn-primary" type="button" disabled=""> <button v-if="isRandom" class="btn btn-primary" type="button" disabled="">
<span class="spinner-grow me-1" role="status" aria-hidden="true"></span> <span class="spinner-grow me-1" role="status" aria-hidden="true"></span>
랜덤뽑기중.. random..
</button> </button>
<div class="d-grid w-100 mt-6"> <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> </div>
</template> </template>

View File

@ -1,8 +1,8 @@
<template> <template>
<li class="mt-5 card p-5"> <li class="mt-5 card p-5">
<DictWrite <DictWrite
v-if="isWriteVisible" v-if="writeStore.isItemActive(item.WRDDICSEQ)"
@close="isWriteVisible = false" @close="writeStore.closeAll();"
:dataList="cateList" :dataList="cateList"
@addWord="editWord" @addWord="editWord"
:NumValue="item.WRDDICSEQ" :NumValue="item.WRDDICSEQ"
@ -35,7 +35,9 @@
class="rounded-circle user-avatar" class="rounded-circle user-avatar"
:src="getProfileImage(item.author.profileImage)" :src="getProfileImage(item.author.profileImage)"
alt="최초 작성자" alt="최초 작성자"
:style="{ borderColor: item.author.color}"/> :style="{ borderColor: item.author.color}"
@error="setDefaultImage"
/>
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formattedDate(item.author.createdAt) }}</p> <p class="mb-0 small fw-medium">{{ formattedDate(item.author.createdAt) }}</p>
@ -52,7 +54,9 @@
class="rounded-circle user-avatar" class="rounded-circle user-avatar"
:src="getProfileImage(item.lastEditor.profileImage)" :src="getProfileImage(item.lastEditor.profileImage)"
alt="최근 작성자" alt="최근 작성자"
:style="{ borderColor: item.lastEditor.color}"/> :style="{ borderColor: item.lastEditor.color}"
@error="setDefaultImage"
/>
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formattedDate(item.lastEditor.updatedAt) }}</p> <p class="mb-0 small fw-medium">{{ formattedDate(item.lastEditor.updatedAt) }}</p>
@ -62,7 +66,7 @@
</div> </div>
<div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'"> <div class="edit-btn" v-if="userStore.user.role !== 'ROLE_ADMIN'">
<EditBtn @click="toggleWriteVisible" /> <EditBtn @click="writeStore.toggleItem(item.WRDDICSEQ)" />
</div> </div>
</li> </li>
</template> </template>
@ -75,8 +79,10 @@ import EditBtn from '@/components/button/EditBtn.vue';
import $api from '@api'; import $api from '@api';
import DictWrite from './DictWrite.vue'; import DictWrite from './DictWrite.vue';
import { formattedDate } from "@/common/formattedDate"; import { formattedDate } from "@/common/formattedDate";
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
import { useWriteVisibleStore } from '@s/writeVisible';
const writeStore = useWriteVisibleStore();
// //
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
@ -107,13 +113,13 @@ const props = defineProps({
const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']); const emit = defineEmits(['update:cateList','refreshWordList', 'updateChecked']);
// //
const isWriteVisible = ref(false); // const isWriteVisible = ref(false);
// toggle // toggle
const toggleWriteVisible = () => { // const toggleWriteVisible = () => {
isWriteVisible.value = !isWriteVisible.value; // isWriteVisible.value = !isWriteVisible.value;
}; // };
// //
// const addCategory = (data) => { // const addCategory = (data) => {
@ -176,7 +182,8 @@ const editWord = (data) => {
.then((res) => { .then((res) => {
if (res.data.data === 1) { if (res.data.data === 1) {
toastStore.onToast('✅ 용어가 수정되었습니다.', 's'); toastStore.onToast('✅ 용어가 수정되었습니다.', 's');
isWriteVisible.value = false; // isWriteVisible.value = false;
writeStore.closeAll();
emit('refreshWordList'); emit('refreshWordList');
} else { } else {
console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data); console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data);
@ -191,9 +198,6 @@ const editWord = (data) => {
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
//
// const formatDate = (dateString) => new Date(dateString).toLocaleString();
// //
const defaultProfile = "/img/icons/icon.png"; const defaultProfile = "/img/icons/icon.png";
@ -201,6 +205,10 @@ const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile; return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
}; };
const setDefaultImage = (event) => {
event.target.src = defaultProfile;
};
// //
const toggleCheck = (event) => { const toggleCheck = (event) => {

View File

@ -65,6 +65,7 @@ import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue'; import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue'; import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue'; import PlusBtn from '../button/PlusBtn.vue';
// import { clearConfig } from 'dompurify';
// import { useUserInfoStore } from '@s/useUserInfoStore'; // import { useUserInfoStore } from '@s/useUserInfoStore';
// //
@ -153,20 +154,32 @@ const onChange = (newValue) => {
// //
const saveWord = () => { const saveWord = () => {
//validation //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; wordTitleAlert.value = true;
return; return;
} else {
wordTitleAlert.value = false;
}
//
let inserts = [];
if(inserts.length === 0 && content.value?.ops?.length > 0){
inserts = content.value.ops.map(op => op.insert.trim());
} }
// //
if(content.value == ''){ if(content.value == '' || inserts.join('') === ''){
wordContentAlert.value = true; wordContentAlert.value = true;
return; return;
} }
const wordData = { const wordData = {
id: props.NumValue || null, id: props.NumValue || null,
@ -181,12 +194,26 @@ const saveWord = () => {
// focusout // focusout
const handleCategoryFocusout = (value) => { 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); // 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; addCategoryAlert.value = true;
// focus // focus

View File

@ -8,9 +8,8 @@
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse"> <div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row align-items-center ms-auto"> <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="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="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> <i class="bx bx-bell bx-md bx-log-out cursor-pointer p-1" @click="handleLogout"></i>
@ -152,7 +151,13 @@
<!-- User --> <!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown"> <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"> <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> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
@ -227,40 +232,37 @@
</nav> </nav>
</template> </template>
<script setup> <script setup>
import { useAuthStore } from '@s/useAuthStore'; import { useAuthStore } from '@s/useAuthStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useThemeStore } from '@s/darkmode'; import { useThemeStore } from '@s/darkmode';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import $api from '@api'; import $api from '@api';
const user = ref(null); const user = ref(null);
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); //const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const baseUrl = import.meta.env.BASE_URL;
const authStore = useAuthStore(); const authStore = useAuthStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const router = useRouter(); const router = useRouter();
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore(); const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
onMounted(async () => { onMounted(async () => {
if (isDarkMode) { if (isDarkMode) {
switchToDarkMode(); switchToDarkMode();
} else { } else {
switchToLightMode(); switchToLightMode();
} }
await userStore.userInfo();
user.value = userStore.user;
});
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
await userStore.userInfo();
user.value = userStore.user;
});
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
</script> </script>
<style></style> <style></style>

View File

@ -1,15 +0,0 @@
import { defineStore } from "pinia";
export const useVoteStore = defineStore("vote", {
state: () => ({
selectedVote: {}
}),
actions: {
setVoteData(data) {
this.selectedVote = data;
},
clearVoteData() {
this.selectedVote = {};
}
}
});

View 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
};
});

View File

@ -140,7 +140,6 @@ import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import axios from '@api'; import axios from '@api';
import { formattedDate } from '@/common/formattedDate.js';
// //
const profileName = ref(''); const profileName = ref('');
@ -376,63 +375,77 @@ const fetchComments = async (page = 1) => {
}; };
// //
const handleCommentSubmit = async (data) => { const handleCommentSubmit = async (data, isCheck) => {
if (!data) {
console.error("handleCommentSubmit: data가 undefined입니다.");
return;
}
const { comment, password } = data; const { comment, password } = data;
const LOCBRDTYP = data.LOCBRDTYP || null; // undefined const LOCBRDTYP = data.LOCBRDTYP || null;
if (!comment || comment.trim() === "") {
commentAlert.value = '댓글을 입력해주세요.';
return;
} else {
commentAlert.value = '';
}
if (unknown.value && isCheck && (!password || password.trim() === "")) {
passwordAlert.value = "비밀번호를 입력해야 합니다.";
return;
}
try { try {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
LOCBRDSEQ: currentBoardId.value, LOCBRDSEQ: currentBoardId.value,
LOCCMTRPY: comment, LOCCMTRPY: comment,
LOCCMTPWD: password, LOCCMTPWD: isCheck ? password : '',
LOCCMTPNT: 1, LOCCMTPNT: 1,
LOCBRDTYP // LOCBRDTYP
}); });
if (response.status === 200) { if (response.status === 200) {
console.log('댓글 작성 성공:', response.data.message); console.log('댓글 작성 성공:', response.data.message);
passwordAlert.value = '';
commentAlert.value = '';
await fetchComments(); await fetchComments();
} else { } else {
console.log('댓글 작성 실패:', response.data.message); console.error('댓글 작성 실패:', response.data.message);
} }
} catch (error) { } catch (error) {
console.log('댓글 작성 중 오류 발생:', error); console.error('댓글 작성 중 오류 발생:', error);
} }
}; };
// ( `BoardCommentList` ) // ( `BoardCommentList` )
const handleCommentReply = async (reply) => { const handleCommentReply = async (reply) => {
try { try {
// ( LOCBRDTYP 300102 ) const response = await axios.post(`board/${currentBoardId.value}/comment`, {
const requestBody = {
LOCBRDSEQ: currentBoardId.value, LOCBRDSEQ: currentBoardId.value,
LOCCMTRPY: reply.comment, LOCCMTRPY: reply.comment,
LOCCMTPWD: reply.password || null, LOCCMTPWD: reply.password || null,
LOCCMTPNT: reply.parentId, LOCCMTPNT: reply.parentId,
LOCBRDTYP: reply.isCheck ? "300102" : null LOCBRDTYP: reply.isCheck ? "300102" : null
}; });
console.log(requestBody)
const response = await axios.post(`board/${currentBoardId.value}/comment`, requestBody);
if (response.status === 200) { if (response.status === 200) {
if (response.data.code === 200) { if (response.data.code === 200) { //
console.log('대댓글 작성 성공:', response.data); console.log('대댓글 작성 성공:', response.data);
await fetchComments(); await fetchComments(); //
} else { } else {
console.log('대댓글 작성 실패 - 서버 응답:', response.data); console.log('대댓글 작성 실패 - 서버 응답:', response.data);
alert('대댓글 작성에 실패했습니다.'); alert('대댓글 작성에 실패했습니다.');
} }
} }
} catch (error) { } catch (error) {
console.error('🚨 대댓글 작성 중 오류 발생:', error); console.error('대댓글 작성 중 오류 발생:', error);
if (error.response) { if (error.response) {
console.error('📌 서버 응답 에러:', error.response.data); console.error('서버 응답 에러:', error.response.data);
} }
alert('대댓글 작성 중 오류가 발생했습니다.'); alert('대댓글 작성 중 오류가 발생했습니다.');
} }
}; }
// //
const editClick = (unknown) => { const editClick = (unknown) => {
@ -484,6 +497,7 @@ const editComment = (comment) => {
// //
if (unknown.value) { if (unknown.value) {
console.log('익명 코멘트인가?')
toggleCommentPassword(comment, "edit"); toggleCommentPassword(comment, "edit");
} }
} }
@ -736,6 +750,14 @@ const handleCommentDeleted = (deletedCommentId) => {
console.error("❌ 삭제할 댓글을 찾을 수 없음:", deletedCommentId); console.error("❌ 삭제할 댓글을 찾을 수 없음:", deletedCommentId);
}; };
//
const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음";
const dateObj = new Date(dateString);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;
};
const formattedBoardDate = computed(() => formattedDate(date.value)); const formattedBoardDate = computed(() => formattedDate(date.value));
// //

View File

@ -91,7 +91,9 @@ import router from '@/router';
import axios from '@api'; import axios from '@api';
import SaveButton from '@c/button/SaveBtn.vue'; import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue' import BackButton from '@c/button/BackBtn.vue'
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore();
const categoryList = ref([]); const categoryList = ref([]);
const title = ref(''); const title = ref('');
const password = ref(''); const password = ref('');
@ -113,6 +115,11 @@ const fetchCategories = async () => {
try { try {
const response = await axios.get('board/categories'); const response = await axios.get('board/categories');
categoryList.value = response.data.data; categoryList.value = response.data.data;
// "" (CMNCODNAM '' )
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
if (freeCategory) {
categoryValue.value = freeCategory.CMNCODVAL; //
}
} catch (error) { } catch (error) {
console.error('카테고리 불러오기 오류:', error); console.error('카테고리 불러오기 오류:', error);
} }
@ -167,11 +174,11 @@ const write = async () => {
} }
} }
alert('게시물이 작성되었습니다.'); toastStore.onToast('게시물이 작성되었습니다.', 's');
goList(); goList();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
alert('게시물 작성 중 오류가 발생했습니다.'); toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
} }
}; };
</script> </script>

View File

@ -9,6 +9,7 @@
<HalfDayButtons <HalfDayButtons
@toggleHalfDay="toggleHalfDay" @toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges" @addVacationRequests="saveVacationChanges"
:isDisabled="!hasChanges"
/> />
</div> </div>
<ProfileList <ProfileList
@ -78,7 +79,9 @@
import { useUserStore } from "@s/userList"; import { useUserStore } from "@s/userList";
import { useUserInfoStore } from "@s/useUserInfoStore"; import { useUserInfoStore } from "@s/useUserInfoStore";
import { fetchHolidays } from "@c/calendar/holiday.js"; import { fetchHolidays } from "@c/calendar/holiday.js";
import { useToastStore } from '@s/toastStore';
const toastStore = useToastStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const userListStore = useUserStore(); const userListStore = useUserStore();
const userList = ref([]); const userList = ref([]);
@ -101,11 +104,74 @@
const vacationCodeMap = ref({}); const vacationCodeMap = ref({});
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
const fetchedEvents = ref([]); const fetchedEvents = ref([]);
const halfDayButtonsRef = ref(null);
// ref // ref
const calendarDatepicker = ref(null); const calendarDatepicker = ref(null);
let fpInstance = 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({ const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin], plugins: [dayGridPlugin, interactionPlugin],
initialView: "dayGridMonth", initialView: "dayGridMonth",
@ -150,17 +216,28 @@
} }
}); });
// FullCalendar (.fc-toolbar-title) // FullCalendar (.fc-toolbar-title)
nextTick(() => { nextTick(() => {
const titleEl = document.querySelector('.fc-toolbar-title'); const titleEl = document.querySelector('.fc-toolbar-title');
if (titleEl) { if (titleEl) {
titleEl.style.cursor = 'pointer'; titleEl.style.cursor = 'pointer';
titleEl.addEventListener('click', () => { 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 ( ) // API ( )
async function fetchVacationHistory(year) { async function fetchVacationHistory(year) {
@ -272,7 +349,6 @@
const selectedEvents = Array.from(selectedDates.value) const selectedEvents = Array.from(selectedDates.value)
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
.map(([date, type]) => ({ .map(([date, type]) => ({
title: getVacationType(type),
start: date, start: date,
backgroundColor: "rgb(113 212 243 / 76%)", backgroundColor: "rgb(113 212 243 / 76%)",
textColor: "#fff", textColor: "#fff",
@ -298,47 +374,6 @@
return "full-day"; 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) { function toggleHalfDay(type) {
halfDayType.value = halfDayType.value === type ? null : type; halfDayType.value = halfDayType.value === type ? null : type;
} }
@ -381,6 +416,7 @@
} }
async function saveVacationChanges() { async function saveVacationChanges() {
if (!hasChanges.value) return;
const selectedDatesArray = Array.from(selectedDates.value); const selectedDatesArray = Array.from(selectedDates.value);
const vacationsToAdd = selectedDatesArray const vacationsToAdd = selectedDatesArray
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
@ -405,7 +441,7 @@
delete: vacationsToDelete delete: vacationsToDelete
}); });
if (response.data && response.data.status === "OK") { if (response.data && response.data.status === "OK") {
alert("✅ 휴가 변경 사항이 저장되었습니다."); toastStore.onToast('휴가 변경 사항이 저장되었습니다.', 's');
await fetchRemainingVacation(); await fetchRemainingVacation();
if (isModalOpen.value) { if (isModalOpen.value) {
await fetchVacationHistory(lastRemainingYear.value); await fetchVacationHistory(lastRemainingYear.value);
@ -415,11 +451,11 @@
selectedDates.value.clear(); selectedDates.value.clear();
updateCalendarEvents(); updateCalendarEvents();
} else { } else {
alert("❌ 휴가 저장 중 오류가 발생했습니다."); toastStore.onToast('휴가 저장 중 오류가 발생했습니다.', 'e');
} }
} catch (error) { } catch (error) {
console.error("🚨 휴가 변경 저장 실패:", error); console.error("🚨 휴가 변경 저장 실패:", error);
alert("❌ 휴가 저장 요청에 실패했습니다."); toastStore.onToast('휴가 저장 요청에 실패했습니다.', 'e');
} }
} }
@ -447,6 +483,46 @@
await nextTick(); await nextTick();
fullCalendarRef.value.getApi().refetchEvents(); 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 () => { onMounted(async () => {
await fetchUserList(); await fetchUserList();
@ -460,10 +536,5 @@
</script> </script>
<style> <style>
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
/* 데이트피커 인풋은 Flatpickr에서 동적으로 스타일 적용됨 */
</style> </style>

View File

@ -50,7 +50,6 @@ import $api from '@api';
import Quill from 'quill'; import Quill from 'quill';
import WriteBtn from '@c/button/WriteBtn.vue'; import WriteBtn from '@c/button/WriteBtn.vue';
import voteList from '@c/voteboard/voteCardList.vue'; import voteList from '@c/voteboard/voteCardList.vue';
import { useVoteStore } from '@s/voteDetail';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const toastStore = useToastStore(); const toastStore = useToastStore();
@ -69,11 +68,8 @@ const router = useRouter();
onMounted(async () => { onMounted(async () => {
getvoteList(); getvoteList();
}); });
const voteStore = useVoteStore();
// //
const voteWrite = () => { const voteWrite = () => {
voteStore.setVoteData(selectedVote.value);
console.log("Pinia 상태 업데이트됨:", voteStore.selectedVote);
router.push('/voteboard/write'); router.push('/voteboard/write');
}; };

View File

@ -17,7 +17,8 @@
name="title" name="title"
:is-essential="true" :is-essential="true"
:is-alert="titleAlert" :is-alert="titleAlert"
:v-model="title" v-model="title"
@keyup="ValidHandler('title')"
/> />
<form-input <form-input
title="종료날짜" title="종료날짜"
@ -27,20 +28,25 @@
:is-alert="endDateAlert" :is-alert="endDateAlert"
v-model="endDate" v-model="endDate"
:min="today" :min="today"
@change="ValidHandlerendDate"
/> />
<!-- 항목 입력 반복 --> <!-- 항목 입력 반복 -->
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-center mb-2 position-relative"> <div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-start">
<form-input <div class="flex-grow-1 me-2 ">
class="flex-grow-1 me-2" <form-input
:title="'항목 ' + (index + 1)" :title="'항목 ' + (index + 1)"
:name="'content' + index" :name="'content' + index"
:is-essential="index < 2" :is-essential="index < 2"
:is-alert="contentAlerts[index]" :is-alert="contentAlerts[index]"
v-model="item.content" v-model="item.content"
/> @keyup="ValidHandler('content' + (index + 1))"
<link-input v-model="item.url" /> />
<delete-btn @click="removeItem(index)" :disabled="index < 2" class="ms-2" /> <link-input v-model="item.url" class="mb-1"/>
</div> </div>
<!-- delete-btn을 오른쪽으로 정렬 -->
<delete-btn @click="removeItem(index)" :disabled="index < 2" />
</div>
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" /> <plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
<div> <div>
<label class="list-group-item"> <label class="list-group-item">
@ -92,7 +98,6 @@ import LinkInput from "@/components/voteboard/voteLinkInput.vue";
import { voteCommon } from '@s/voteCommon'; import { voteCommon } from '@s/voteCommon';
import { useUserStore } from '@s/userList'; import { useUserStore } from '@s/userList';
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useVoteStore } from "@s/voteDetail";
const userStore = useUserStore(); const userStore = useUserStore();
const today = new Date().toISOString().substring(0, 10); const today = new Date().toISOString().substring(0, 10);
@ -115,12 +120,6 @@ const userSet = ({ userList, userTotal }) => {
userListTotal.value = userTotal; userListTotal.value = userTotal;
}; };
const voteStore = useVoteStore();
onMounted(()=>{
console.log('상세데이터',voteStore.selectedVote);
})
const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers }) => { const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers }) => {
activeUserList.value = activeUsers; activeUserList.value = activeUsers;
@ -133,7 +132,7 @@ const saveValid = () => {
if (disabledUsers.value.length === 0) { if (disabledUsers.value.length === 0) {
activeUserList.value = [...userStore.userList]; activeUserList.value = [...userStore.userList];
} }
if (title.value === '') { if (title.value.trim() === '') {
titleAlert.value = true; titleAlert.value = true;
valid = false; valid = false;
} else { } else {
@ -145,13 +144,13 @@ const saveValid = () => {
} else { } else {
endDateAlert.value = false; endDateAlert.value = false;
} }
if (itemList.value[0].content === '') { if (itemList.value[0].content.trim() === '') {
contentAlerts.value[0] = true; contentAlerts.value[0] = true;
valid = false; valid = false;
} else { } else {
contentAlerts.value[0] = false; contentAlerts.value[0] = false;
} }
if (itemList.value[1].content === '') { if (itemList.value[1].content.trim() === '') {
contentAlerts.value[1] = true; contentAlerts.value[1] = true;
valid = false; valid = false;
} else { } else {
@ -168,6 +167,7 @@ const saveValid = () => {
} }
}; };
const saveVote = () => { const saveVote = () => {
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
const unwrappedUserList = toRaw(activeUserList.value); const unwrappedUserList = toRaw(activeUserList.value);
const listId = unwrappedUserList.map(item => ({ const listId = unwrappedUserList.map(item => ({
id: item.MEMBERSEQ, id: item.MEMBERSEQ,
@ -175,9 +175,9 @@ const saveVote = () => {
$api.post('vote/insertWord',{ $api.post('vote/insertWord',{
addvoteIs :addvoteitem.value === false ? '0' :'1' addvoteIs :addvoteitem.value === false ? '0' :'1'
,votemMltiIs: addvotemulti.value === false ? '0' : '1' ,votemMltiIs: addvotemulti.value === false ? '0' : '1'
,title :title.value ,title :title.value.trim()
,endDate :endDate.value ,endDate :endDate.value
,itemList :itemList.value ,itemList :filteredItemList
,activeUserList :listId ,activeUserList :listId
}).then((res)=>{ }).then((res)=>{
if(res.data.status == 'OK'){ 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 = () => { const goList = () => {
router.push('/voteboard'); router.push('/voteboard');
}; };

View File

@ -14,7 +14,7 @@
<!-- 단어 갯수, 작성하기 --> <!-- 단어 갯수, 작성하기 -->
<div class="mt-4"> <div class="mt-4">
단어 : {{ total }} 단어 : {{ total }}
<WriteButton @click="toggleWriteForm" /> <WriteButton @click="writeStore.toggleItem(999999)" />
</div> </div>
<!-- --> <!-- -->
@ -28,8 +28,8 @@
</div> </div>
<!-- 작성 --> <!-- 작성 -->
<div v-if="isWriteVisible" class="mt-5"> <div v-if="writeStore.isItemActive(999999)" class="mt-5">
<DictWrite @close="isWriteVisible = false" :dataList="cateList" @addWord="addWord"/> <DictWrite @close="writeStore.closeAll()" :dataList="cateList" @addWord="addWord"/>
</div> </div>
</div> </div>
@ -78,9 +78,13 @@
import commonApi from '@/common/commonApi'; import commonApi from '@/common/commonApi';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
import { useWriteVisibleStore } from '@s/writeVisible';
//
const writeStore = useWriteVisibleStore();
// //
const userStore = useUserInfoStore(); // const userStore = useUserInfoStore();
const { appContext } = getCurrentInstance(); const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common; const $common = appContext.config.globalProperties.$common;
@ -118,7 +122,7 @@
const searchText = ref(''); const searchText = ref('');
// //
const isWriteVisible = ref(false); // const isWriteVisible = ref(false);
// //
onMounted(() => { onMounted(() => {
@ -168,9 +172,9 @@
} }
// toggle // toggle
const toggleWriteForm = () => { // const toggleWriteForm = () => {
isWriteVisible.value = !isWriteVisible.value; // isWriteVisible.value = !isWriteVisible.value;
}; // };
// //
// const addCategory = (data) =>{ // const addCategory = (data) =>{
@ -197,15 +201,14 @@
const addWord = (wordData, data) => { const addWord = (wordData, data) => {
let category = null; let category = null;
// //
const existingCategory = cateList.value.find(item => item.label === data); const existingCategory = cateList.value.find(item => item.label === data.trim());
if (existingCategory) {
console.log('카테고리 중복');
if (existingCategory) {
// //
category = existingCategory.label == '' ? wordData.category : existingCategory.value; category = existingCategory.label == '' ? wordData.category : existingCategory.value;
} else { } else {
// //
console.log('카테고리 없음'); // console.log(' ');
const lastCategory = cateList.value[cateList.value.length - 1]; const lastCategory = cateList.value[cateList.value.length - 1];
category = lastCategory ? lastCategory.value + 1 : 600101; category = lastCategory ? lastCategory.value + 1 : 600101;
} }
@ -224,7 +227,8 @@
axios.post('worddict/insertWord', payload).then(res => { axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') { if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's'); toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false; // isWriteVisible.value = false;
writeStore.closeAll();
getwordList(); getwordList();
const newCategory = { label: data, value: category }; // data const newCategory = { label: data, value: category }; // data
cateList.value = [newCategory, ...cateList.value]; cateList.value = [newCategory, ...cateList.value];
@ -234,7 +238,8 @@
axios.post('worddict/insertWord', payload).then(res => { axios.post('worddict/insertWord', payload).then(res => {
if (res.data.status === 'OK') { if (res.data.status === 'OK') {
toastStore.onToast('용어가 등록 되었습니다.', 's'); toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false; // isWriteVisible.value = false;
writeStore.closeAll();
getwordList(); getwordList();
} }
}); });
@ -268,7 +273,8 @@
.then(res => { .then(res => {
if (res.data.status == 'OK') { if (res.data.status == 'OK') {
toastStore.onToast('용어 삭제가 완료되었습니다.', 's'); toastStore.onToast('용어 삭제가 완료되었습니다.', 's');
isWriteVisible.value = false; // isWriteVisible.value = false;
writeStore.closeAll();
getwordList(); getwordList();
// //
@ -277,7 +283,7 @@
} }
}) })
.catch(error => { .catch(error => {
console.error('삭제 요청 중 오류 발생:', error); // console.error(' :', error);
toastStore.onToast('오류가 발생했습니다. 다시 시도해주세요.', 'e'); toastStore.onToast('오류가 발생했습니다. 다시 시도해주세요.', 'e');
}); });

View File

@ -5,27 +5,33 @@ import vueDevTools from 'vite-plugin-vue-devtools';
import mkcert from 'vite-plugin-mkcert'; import mkcert from 'vite-plugin-mkcert';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [ const plugins = [vue(), vueDevTools()];
vue(),
vueDevTools(), // dev: https, mine: http
// 자신의 로컬 서버에 연결하려면 이부분 주석처리 if (mode === 'dev') {
mkcert({ plugins.push(
// SSL 키 등록 mkcert({
keyFile: '/localhost-key.pem', // SSL 키 등록
certFile: '/localhost.pem', 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)), return {
'@c': fileURLToPath(new URL('./src/components/', import.meta.url)), plugins,
'@v': fileURLToPath(new URL('./src/views/', import.meta.url)), resolve: {
'@l': fileURLToPath(new URL('./src/layout/', import.meta.url)), alias: {
'@s': fileURLToPath(new URL('./src/stores/', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'@p': fileURLToPath(new URL('./src/common/plugin/', import.meta.url)), '@a': fileURLToPath(new URL('./src/assets/', import.meta.url)),
'@api': fileURLToPath(new URL('./src/common/axios-interceptor.js', 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)),
},
}, },
}, };
}); });