Merge branch 'main' into project-list

This commit is contained in:
yoon 2025-02-18 10:32:49 +09:00
commit deddbf7856
20 changed files with 1632 additions and 1663 deletions

28
localhost-key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnTJneuEjlCk/g
mzUQyeI9xWdr5TiYz+/HVdIR91PlswECLXiW+j420ARQ+Dpx8JY1sts9kMki/jnD
8cNFMwSOnJADwyW+bZZYAdGd/OpyyNQWjm903pKntBKDYULebEnviMDQz7+M6J9Y
cVqWSRTMR6VizSYIQ0vIUqQZaISp/TKLvTssjLjwz6DIYtZ2GQ06lAZYmvX6UhSl
32xJQ6GWf4Jnemn/2bKYFGk88d4ORfrhpD1JV3lGQGk0HU/pI8R017pKxq9lq/c4
d8hqN0CdkQUv0lFw6DQgm3etOVOajlY4eDj+Q3mDCERT8meH1PXfuTsHBtTMHxoA
iRtu4YoZAgMBAAECggEBAMti1DrAGXktpCeA0xy8KTDgEJ0TprzYu6Owl1QtA50L
1msvyMYZrfNM3z7Dx8DBKZR2fcqZMgSPQARI5shGoE825HwqcVoNyxIAJ26hIxdj
+PsMrH076gGFmnHkaIRk/G6g9cunomwpcVS3+EwGXz9yEL/cXQEPC+hOovWkrmM6
Ec1oAsqs9DjrK+HzOOcaBuv0Rz9pI7Gob5LQAp8tqOCI4CvP6sdfooSsucCoqB6V
xQAakXbsidamWcLtYkTjY2zYVhHvVMk1H5krfgrsoGIaym/QTnk+YQYFd5jt4FiJ
ziLZXiZJOeJXJVdlAcJF9aUlO48OAKJeBoq3NkcbxbECgYEA+a2pJ6BN8xKvWyZJ
QnpWL5irVKUJF7l1cFvaNJJu4XMS3CfJqdA9X0b7Zuu/8zOdj+5eNpK6Mpz65+mx
+/ToYNYoMewFXlfDpcIpT4FdBJAKsKMua2UlTzOI7DxSrcAGD0nItK3ZovpzmNJo
H90maU0gib0CSsFVvsAsnVVSawcCgYEA7SfP8tc23txMbLzz+9DjlmeqT2v1XI/P
QZEwCO6AIjbbJw3iFYjIgkd86gVGRwIdYEfNoiIk8KT4dMsW4jiwalFa/fA+HW69
pqMf1PFnxNPZim592EANVjzzkN4jm63QzIAFiGcN2K99ltjUWrfrzLkkiFR6ENHF
dgIpWTyAed8CgYBH/CCr8xTmQvnZzsUKbJkSqfKjud1QQMEyPtk/lQRw7at/W91R
n19CbAWpm8jAxp3j1HbHRzB1zTqtyHvvR6ID4Vq/Yev+UlFvJfahHIwD97+NQ87r
WcMS/am5an7v51AX8k7ygLkhuxG++tLYdPtRGtKJw7u4b9tX3rI+Pk4/2wKBgQCn
CIZ3ZMuZ1hHh+Ifj0bGqSqNywvgS1JtGdAsgD1OiRX6/mBCn2CpZUB6T+VkRRFUK
bihQTLo14Au6vxwEA6eFin2LI72sH0ZmarhN1CWhRREQZlguipaaKd3nJ/5udNL+
ZiD/fI4NEzVinJ+csbPcAn7PoqhC1my8fDNBTdKzgwKBgQCvH0MEpkZefqN82CNn
CuJeQYb48mkFgihICeTsfIeG7XsGqfCOlzbJqxCbTX+Na7FUdtmtJUznK+rVGOPh
p+pAw8RbZSIvgzCO1vv0wSHsXxXsieOgwJPZeQqsBWhRs77Ggf9jhIzxcQJuIor3
l7Nxg0eoiqP/rYFyOh83nebPQg==
-----END PRIVATE KEY-----

BIN
localhost.p12 Normal file

Binary file not shown.

26
localhost.pem Normal file
View File

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEUjCCArqgAwIBAgIQA9mbF03CznoBZ2TyJTPO8jANBgkqhkiG9w0BAQsFADCB
jTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTEwLwYDVQQLDChERVNL
VE9QLVNHRkwzNExcaG9zdHBhcmtAREVTS1RPUC1TR0ZMMzRMMTgwNgYDVQQDDC9t
a2NlcnQgREVTS1RPUC1TR0ZMMzRMXGhvc3RwYXJrQERFU0tUT1AtU0dGTDM0TDAe
Fw0yNTAyMTQwMzU2MTJaFw0yNzA1MTQwMzU2MTJaMFwxJzAlBgNVBAoTHm1rY2Vy
dCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTExMC8GA1UECwwoREVTS1RPUC1TR0ZM
MzRMXGhvc3RwYXJrQERFU0tUT1AtU0dGTDM0TDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOdMmd64SOUKT+CbNRDJ4j3FZ2vlOJjP78dV0hH3U+WzAQIt
eJb6PjbQBFD4OnHwljWy2z2QySL+OcPxw0UzBI6ckAPDJb5tllgB0Z386nLI1BaO
b3Tekqe0EoNhQt5sSe+IwNDPv4zon1hxWpZJFMxHpWLNJghDS8hSpBlohKn9Mou9
OyyMuPDPoMhi1nYZDTqUBlia9fpSFKXfbElDoZZ/gmd6af/ZspgUaTzx3g5F+uGk
PUlXeUZAaTQdT+kjxHTXukrGr2Wr9zh3yGo3QJ2RBS/SUXDoNCCbd605U5qOVjh4
OP5DeYMIRFPyZ4fU9d+5OwcG1MwfGgCJG27hihkCAwEAAaNeMFwwDgYDVR0PAQH/
BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFN7IkO4WB6E9
uTxB+KENPr8pN9V4MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsF
AAOCAYEAa9kWqz5NvJo1+9lzTM+QwjeRL7wsfeygdwIx3PRn/0bbyglUO+MhR6oK
cbjzpKj+2C5sWuuNSIOGcU95Dnh6ekQtxjSY3j7gbwOiYmwYj4LVRF9KIeGQgW72
kHA+tnuEsAhe33mloJhGjrZ/cqkxPz31foVpOpeP0l85NTzXGfyDjePivlgfbCUT
8juBEIGD1Go3PTrLoNC0P/1lJAgc1+lGEY2veGQNMqy6TXIhLLHMuXdSEDqQJxjB
N6fNzfZh163jgI4UCpmowoLp6oO5iOlM3cxzsfwGpubf7W9nUOyAO5B4VzsTvqYe
MLfiUKZXlwUb9eyhIhk0UhgCM4IelcRMUH5nLDn6a2Pyu3bs4TpJ1zTmRZt7PjsX
0HllN2/xkp2XRdSLutGTrya5zqo4nLaDa67sTt5WhDp+JRgA3rb5Sgcw78pYEfFq
5IGuKZsuSMy6qZFbTAJVINPKwkH6eBAQcr3PyyCMKdJDFkeVYeuqef5N2u/GpGKO
DQ0E7Vhc
-----END CERTIFICATE-----

2263
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,7 @@
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"upload-images-converter": "^2.0.2", "upload-images-converter": "^2.0.2",
"vite-plugin-mkcert": "^1.17.6",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-flatpickr-component": "^11.0.5", "vue-flatpickr-component": "^11.0.5",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"

View File

@ -5,30 +5,13 @@
display: block !important; display: block !important;
} }
.notice-row td { /* 게시판리스트 */
.bg-label-gray td {
color: #DC3545 !important; color: #DC3545 !important;
} }
.notice-row {
background-color: #f8f9fa;
}
.general-row {
background-color: white;
color: black;
}
.clickable-row {
cursor: pointer;
}
.clickable-row:hover {
background-color: #f1f1f1;
}
.new-badge {
font-size: 0.65rem;
padding: 0.2em 0.4em;
vertical-align: middle;
}
/* 휴가*/ /* 휴가 */
.half-day-buttons { .half-day-buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;

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: 'http://localhost:10325/api/', baseURL: 'https://192.168.0.251:10325/api/',
timeout: 300000, timeout: 300000,
withCredentials: true, withCredentials: true,
}); });
@ -37,7 +37,7 @@ $api.interceptors.response.use(
}, },
function (error) { function (error) {
const toastStore = useToastStore(); const toastStore = useToastStore();
const currentPage = error.config.headers['X-Page-Route']; //const currentPage = error.config.headers['X-Page-Route'];
// 오류 응답 처리 // 오류 응답 처리
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {

View File

@ -0,0 +1,12 @@
<template>
<button type="button" class="btn btn-info" @click="$emit('click')">
<i class="bx bx-left-arrow-alt"></i>
</button>
</template>
<script>
export default {
name: "BackButton",
emits: ["click"],
};
</script>

View File

@ -0,0 +1,23 @@
<template>
<button
type="button"
class="btn btn-primary ms-1"
@click="$emit('click')"
:disabled="!isEnabled"
>
<i class="bx bx-check"></i>
</button>
</template>
<script>
export default {
name: "SaveButton",
props: {
isEnabled: {
type: Boolean,
default: true, //
},
},
emits: ["click"],
};
</script>

View File

@ -40,26 +40,26 @@ const props = defineProps({
required: true, required: true,
}, },
value: { value: {
type: String, type: [String, Number],
default: '0', default: '0',
require: false, require: false,
}, },
isAlert : { isAlert: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false, required: false,
}, },
isLabel : { isLabel: {
type: Boolean, type: Boolean,
default: true, default: true,
required: false, required: false,
}, },
isRow : { isRow: {
type: Boolean, type: Boolean,
default: true, default: true,
required: false, required: false,
}, },
isCommon : { isCommon: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false, required: false,
@ -69,11 +69,19 @@ const props = defineProps({
const emit = defineEmits(['update:data']); const emit = defineEmits(['update:data']);
const selectData = ref(props.value); const selectData = ref(props.value);
// data // props.value watch
watch(() => props.value, (newValue) => {
selectData.value = newValue;
}, { immediate: true });
// data
watch(() => props.data, (newData) => { watch(() => props.data, (newData) => {
if (props.isCommon && newData.length > 0) { if (props.isCommon && newData.length > 0) {
selectData.value = newData[0].value; // value prop '0'()
emit('update:data', selectData.value); if (props.value === '0') {
selectData.value = newData[0].value;
emit('update:data', selectData.value);
}
} }
}, { immediate: true }); }, { immediate: true });
@ -81,4 +89,4 @@ watch(() => props.data, (newData) => {
watch(selectData, (newValue) => { watch(selectData, (newValue) => {
emit('update:data', newValue); emit('update:data', newValue);
}); });
</script> </script>

View File

@ -0,0 +1,214 @@
<template>
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
<div class="modal-content">
<h5 class="modal-title">To. {{ targetUser.MEMBERNAM }} 🎁</h5>
<button class="close-btn" @click="closeModal"></button>
<div class="modal-body">
<p>해당 직원에게 부여할 연차 개수를 선택하세요. (남은 개수: {{ availableQuota }})</p>
<div class="vacation-control">
<button @click="decreaseCount" :disabled="grantCount <= 0" class="count-btn">-</button>
<span class="grant-count">{{ grantCount }}</span>
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
</div>
<button class="gift-btn" @click="saveVacationGrant" :disabled="grantCount === 0">
<i class="bx bx-gift"></i> <!-- 선물상자 아이콘 -->
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, watch, onMounted } from "vue";
import axios from "@api";
const props = defineProps({
isOpen: Boolean, //
targetUser: Object, //
});
const emit = defineEmits(["close", "updateVacation"]);
const grantCount = ref(0);
const maxQuota = 2; // 1
const sentCount = ref(0); //
const availableQuota = ref(2); //
//
const fetchSentVacationCount = async () => {
try {
const payload = { receiverId: props.targetUser.MEMBERSEQ };
const response = await axios.get(`vacation/sent`,{ params: payload });
console.log(response.data.data[0].count)
sentCount.value = response.data.data[0].count || 0; //
availableQuota.value = Math.max(maxQuota - sentCount.value, 0); // (0 )
console.log(`✅ 보낸 개수: ${sentCount.value}, 남은 개수: ${availableQuota.value}`);
} catch (error) {
console.error("🚨 연차 전송 기록 조회 실패:", error);
availableQuota.value = maxQuota;
}
};
//
const increaseCount = () => {
if (grantCount.value < availableQuota.value) {
grantCount.value++;
}
};
//
const decreaseCount = () => {
if (grantCount.value > 0) {
grantCount.value--;
}
};
// (saveVacations API )
const saveVacationGrant = async () => {
try {
const payload = [
{
date: new Date().toISOString().split("T")[0], //
type: "700103", //
senderId: props.targetUser.senderId, // ID
receiverId: props.targetUser.MEMBERSEQ, // ID
count: grantCount.value, //
},
];
const response = await axios.post("vacation/save", payload);
if (response.data && response.data.status === "OK") {
alert("✅ 연차가 부여되었습니다.");
await fetchSentVacationCount(); //
emit("updateVacation"); //
closeModal();
} else {
alert("🚨 연차 추가 중 오류가 발생했습니다.");
}
} catch (error) {
console.error("🚨 연차 추가 실패:", error);
alert("연차 추가에 실패했습니다.");
}
};
//
const closeModal = () => {
emit("close");
};
//
watch(
() => props.isOpen,
async (newVal) => {
if (newVal && props.targetUser && props.targetUser.MEMBERSEQ) {
console.log("🟢 모달이 열렸습니다. 데이터를 로드합니다.");
grantCount.value = 0; //
await fetchSentVacationCount(); //
}
}
);
// targetUser fetchSentVacationCount
watch(
() => props.targetUser,
async (newUser) => {
if (newUser && newUser.MEMBERSEQ) {
console.log(`🔄 새로운 대상(${newUser.name})이 선택되었습니다.`);
await fetchSentVacationCount();
}
},
{ deep: true }
);
//
onMounted(async () => {
if (props.isOpen && props.targetUser && props.targetUser.MEMBERSEQ) {
await fetchSentVacationCount();
}
});
</script>
<style scoped>
/* 모달 본문 */
.modal-content {
background: white;
padding: 20px;
border-radius: 12px;
width: 400px;
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>

View File

@ -0,0 +1,165 @@
<template>
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
<div class="modal-content 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">
<li
v-for="(vacation, index) in mergedVacations"
:key="index"
class="vacation-item"
>
<span v-if="vacation.type === 'used'" class="vacation-index">
{{ totalUsedVacationCount - usedVacations.findIndex(v => v.date === vacation.date) }})
</span>
<span :class="vacation.type === 'used' ? 'minus-symbol' : 'plus-symbol'">
{{ vacation.type === 'used' ? '-' : '+' }}
</span>
<span :style="{ color: userColors[vacation.senderId || vacation.receiverId] || '#000' }"
class="vacation-date">{{ formatDate(vacation.date) }}</span>
</li>
</ol>
</div>
<!-- 연차 데이터 없음 -->
<p v-if="mergedVacations.length === 0" class="no-data">
🚫 사용한 연차가 없습니다.
</p>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed } from "vue";
const props = defineProps({
isOpen: Boolean,
myVacations: {
type: Array,
default: () => [],
},
receivedVacations: {
type: Array,
default: () => [],
},
userColors: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["close"]);
//
const totalUsedVacationCount = computed(() => props.myVacations.length);
// +
const usedVacations = computed(() => props.myVacations.map(v => ({ ...v, type: "used" })));
const receivedVacations = computed(() => props.receivedVacations.map(v => ({ ...v, type: "received" })));
const mergedVacations = computed(() => {
return [...usedVacations.value, ...receivedVacations.value].sort(
(a, b) => new Date(b.date) - new Date(a.date)
);
});
// (YYYY-MM-DD)
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toISOString().split("T")[0]; // YYYY-MM-DD
};
const closeModal = () => {
emit("close");
};
</script>
<style scoped>
/* 모달 스타일 */
.modal-dialog {
display: flex;
justify-content: center;
align-items: center;
}
/* 스크롤 가능한 모달 */
.modal-content {
max-height: 60vh;
overflow-y: auto;
padding: 20px;
width: 75%;
background: white;
border-radius: 8px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}
/* 닫기 버튼 */
.close-btn {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
font-weight: bold;
}
/* 리스트 기본 스타일 */
.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;
}
</style>

View File

@ -1,67 +1,77 @@
<template> <template>
<div class="card-body d-flex justify-content-center"> <div class="card-body d-flex justify-content-center">
<ul class="list-unstyled d-flex align-items-center gap-7 mb-0 mt-2"> <ul class="list-unstyled d-flex align-items-center gap-3 mb-0 mt-2">
<li <li
v-for="(user, index) in sortedUserList" v-for="(user, index) in sortedUserList"
:key="index" :key="index"
:class="{ disabled: user.disabled }" :class="{ disabled: user.disabled }"
@click="toggleDisable(index)" @click="$emit('profileClick', user)"
data-bs-placement="top" data-bs-placement="top"
:aria-label="user.MEMBERSEQ" :aria-label="user.MEMBERSEQ"
> >
<img <img
class="rounded-circle user-avatar" class="rounded-circle user-avatar "
:src="getUserProfileImage(user.MEMBERPRF)" :src="getUserProfileImage(user.MEMBERPRF)"
alt="user" alt="user"
:style="getDynamicStyle(user)" :style="getDynamicStyle(user)"
@error="setDefaultImage" @error="setDefaultImage"
@load="showImage" @load="showImage"
/> />
</li> <span class="remaining-vacation">
</ul> {{ remainingVacationData[user.MEMBERSEQ] || 0 }}
</span>
</li>
</ul>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref, computed, nextTick } from "vue"; import { onMounted, ref, computed, nextTick } from "vue";
import { useUserInfoStore } from "@/stores/useUserInfoStore"; // import { useUserStore } from "@s/useUserStore";
import { useUserStore as useUserListStore } from "@s/userList"; // import { useUserStore as useUserListStore } from "@s/userList";
import $api from "@api"; import $api from "@api";
defineEmits(["profileClick"]);
defineProps({
remainingVacationData: Object,
});
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
const userListStore = useUserListStore(); const userListStore = useUserListStore();
const userList = ref([]); const userList = ref([]);
const userListContainer = ref(null);
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ""); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, "");
const defaultProfile = "/img/icons/icon.png"; const defaultProfile = "/img/icons/icon.png";
const employeeId = ref(null);
const employeeId = ref(null); // ID const userColors = ref({});
onMounted(async () => { onMounted(async () => {
await userStore.userInfo(); // await userStore.userInfo();
await userListStore.fetchUserList(); // if (userStore.user) {
employeeId.value = userStore.user.id;
} else {
console.error("❌ 로그인한 사용자 정보를 불러오지 못했습니다.");
}
userList.value = userListStore.userList; await userListStore.fetchUserList();
userList.value = userListStore.userList;
// ID //
if (userStore.user) { userList.value.forEach(user => {
employeeId.value = userStore.user.id; userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
}
nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach((tooltip) => {
new bootstrap.Tooltip(tooltip);
});
});
}); });
// nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(tooltip => {
new bootstrap.Tooltip(tooltip);
});
});
});
const sortedUserList = computed(() => { const sortedUserList = computed(() => {
if (!employeeId.value) return userList.value; // if (!employeeId.value) return userList.value; //
//
const myProfile = userList.value.find(user => user.MEMBERSEQ === employeeId.value); const myProfile = userList.value.find(user => user.MEMBERSEQ === employeeId.value);
const otherUsers = userList.value.filter(user => user.MEMBERSEQ !== employeeId.value); const otherUsers = userList.value.filter(user => user.MEMBERSEQ !== employeeId.value);
@ -87,7 +97,7 @@
const totalUsers = userList.value.length; const totalUsers = userList.value.length;
if (totalUsers <= 7) return "120px"; // 7 if (totalUsers <= 7) return "120px"; // 7
if (totalUsers <= 10) return "110px"; // ~10 if (totalUsers <= 10) return "100px"; // ~10
if (totalUsers <= 20) return "80px"; // ~20 if (totalUsers <= 20) return "80px"; // ~20
return "60px"; // 20 return "60px"; // 20
}); });
@ -97,11 +107,20 @@
return { return {
width: profileSize.value, width: profileSize.value,
height: profileSize.value, height: profileSize.value,
borderWidth: "3px", borderWidth: "4px",
borderColor: user.usercolor || "#ccc", borderColor: user.usercolor || "#ccc",
borderStyle: "solid",
}; };
}; };
</script> </script>
<style scoped> <style scoped>
/* 남은 연차 개수 스타일 */
.remaining-vacation {
display: block;
text-align: center;
font-size: 14px;
color: #333;
margin-top: 5px;
}
</style> </style>

View File

@ -1,69 +1,146 @@
<template> <template>
<li class="mt-5 card p-5"> <li class="mt-5 card p-5">
<div class="d-flex align-items-center"> <DictWrite
<div class="w-100 d-flex align-items-center"> v-if="isWriteVisible"
<span class="btn btn-primary pe-none">{{ item.category }}</span> @close="isWriteVisible = false"
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong> :dataList="cateList"
</div> @addCategory="addCategory"
<EditBtn /> :formValue="item.WRDDICCAT"
</div> :titleValue="item.WRDDICTTL"
<p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p> :contentValue="$common.contentToHtml(item.WRDDICCON)"
<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"> <div v-else>
<img <div class="d-flex align-items-center">
class="rounded-circle user-avatar" <div class="w-100 d-flex align-items-center">
:src="getProfileImage(item.author.profileImage)" <span class="btn btn-primary pe-none">{{ item.category }}</span>
alt="최초 작성자" <strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
:style="{ borderColor: item.author.color}"
/>
</div> </div>
<div> </div>
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p> <p class="mt-5" 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">
<img
class="rounded-circle user-avatar"
:src="getProfileImage(item.author.profileImage)"
alt="최초 작성자"
:style="{ borderColor: item.author.color}"/>
</div>
<div>
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p>
</div>
</div>
</div>
<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">
<img
class="rounded-circle user-avatar"
:src="getProfileImage(item.lastEditor.profileImage)"
alt="최근 작성자"
:style="{ borderColor: item.lastEditor.color}"/>
</div>
<div>
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<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="edit-btn">
<div class="avatar avatar-sm me-2"> <EditBtn @click="toggleWriteVisible" />
<img
class="rounded-circle user-avatar"
:src="getProfileImage(item.lastEditor.profileImage)"
alt="최근 작성자"
:style="{ borderColor: item.lastEditor.color}"
/>
</div>
<div>
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p>
</div>
</div>
</div> </div>
</li> </li>
</template> </template>
<script setup> <script setup>
import axios from "@api";
import { useToastStore } from '@s/toastStore';
import { ref, toRefs } from 'vue';
import EditBtn from '@/components/button/EditBtn.vue'; import EditBtn from '@/components/button/EditBtn.vue';
import $api from '@api'; import $api from '@api';
import DictWrite from './DictWrite.vue';
const toastStore = useToastStore();
// Props // Props
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
required: true, required: true
}, },
cateList: {
type: Array,
required: false,
}
}); });
//
const localCateList = ref([...props.cateList]);
//
const selectedCategory = ref('');
// cateList emit
const emit = defineEmits(['update:cateList']);
//
const isWriteVisible = ref(false);
// toggle
const toggleWriteVisible = () => {
isWriteVisible.value = !isWriteVisible.value;
};
//
const addCategory = (data) => {
try {
const lastCategory = localCateList.value.length > 0
? localCateList.value[localCateList.value.length - 1]
: null;
const newValue = lastCategory ? lastCategory.value + 1 : 600101;
axios.post('worddict/insertCategory', {
CMNCODNAM: data
}).then(res => {
if(res.data.data === 1){
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
const newCategory = { label: data, value: newValue };
localCateList.value = [newCategory, ...localCateList.value];
selectedCategory.value = newCategory.value;
//
emit('update:cateList', localCateList.value);
}
}).catch(err => {
console.error('카테고리 추가 중 오류:', err);
});
} catch (err) {
console.error('카테고리 추가 함수 오류:', err);
}
}
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
//
//
const formatDate = (dateString) => new Date(dateString).toLocaleString(); const formatDate = (dateString) => new Date(dateString).toLocaleString();
//
//
const getProfileImage = (imagePath) => const getProfileImage = (imagePath) =>
imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg'; imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg';
</script> </script>
<style scoped> <style scoped>
/* 동그란 테두리 설정 */ /* 동그란 테두리 설정 */
.user-avatar { .user-avatar {
border: 3px solid; /* 테두리 */ border: 3px solid; /* 테두리 */
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */ padding: 0.1px; /* 테두리와 이미지 사이의 간격 */
} }
</style>
.edit-btn {
position: absolute;
right: 0.7rem;
top: 1.2rem;
}
</style>

View File

@ -5,10 +5,11 @@
<FormSelect <FormSelect
name="cate" name="cate"
title="카테고리 선택" title="카테고리 선택"
:data="formattedDataList" :data="dataList"
:is-common="true" :is-common="true"
@update:data="selectCategory = $event" @update:data="selectCategory = $event"
@change="onChange" @change="onChange"
:value="formValue"
/> />
</div> </div>
<div class="col-2 btn-margin"> <div class="col-2 btn-margin">
@ -34,11 +35,13 @@
name="word" name="word"
:is-essential="true" :is-essential="true"
:is-alert="wordTitleAlert" :is-alert="wordTitleAlert"
:modelValue="titleValue"
@update:modelValue="wordTitle = $event" @update:modelValue="wordTitle = $event"
/> />
</div> </div>
<div> <div>
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" /> <QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" />
{{ contentValue }}
<div class="text-end mt-5"> <div class="text-end mt-5">
<button class="btn btn-primary" @click="saveWord"> <button class="btn btn-primary" @click="saveWord">
<i class="bx bx-check"></i> <i class="bx bx-check"></i>
@ -75,17 +78,17 @@ const props = defineProps({
dataList: { dataList: {
type: Array, type: Array,
default: () => [] default: () => []
},
formValue : {
type:[String, Number]
},
titleValue : {
type:String,
},contentValue : {
type:String
} }
}); });
//
const formattedDataList = computed(() =>
props.dataList.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}))
);
// //
const showInput = ref(false); const showInput = ref(false);

View File

@ -55,14 +55,14 @@
<template v-if="pagination.currentPage === 1 && !showNotices"> <template v-if="pagination.currentPage === 1 && !showNotices">
<tr v-for="(notice, index) in noticeList" <tr v-for="(notice, index) in noticeList"
:key="'notice-' + index" :key="'notice-' + index"
class="notice-row clickable-row" class="bg-label-gray"
@click="goDetail(notice.id)"> @click="goDetail(notice.id)">
<td>공지</td> <td>공지</td>
<td> <td>
📌 {{ notice.title }} 📌 {{ notice.title }}
<i v-if="notice.img" class="bi bi-image me-1"></i> <i v-if="notice.img" class="bi bi-image me-1"></i>
<i v-if="notice.hasAttachment" class="bi bi-paperclip"></i> <i v-if="notice.hasAttachment" class="bi bi-paperclip"></i>
<span v-if="isNewPost(notice.date)" class="badge bg-danger text-white ms-2 new-badge">N</span> <span v-if="isNewPost(notice.date)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
</td> </td>
<td>{{ notice.author }}</td> <td>{{ notice.author }}</td>
<td>{{ notice.date }}</td> <td>{{ notice.date }}</td>
@ -72,14 +72,14 @@
<!-- 일반 게시물 --> <!-- 일반 게시물 -->
<tr v-for="(post, index) in generalList" <tr v-for="(post, index) in generalList"
:key="'post-' + index" :key="'post-' + index"
class="general-row clickable-row" class="invert-bg-white"
@click="goDetail(post.realId)"> @click="goDetail(post.realId)">
<td>{{ post.id }}</td> <td>{{ post.id }}</td>
<td> <td>
{{ post.title }} {{ post.title }}
<i v-if="post.img" class="bi bi-image me-1"></i> <i v-if="post.img" class="bi bi-image me-1"></i>
<i v-if="post.hasAttachment" class="bi bi-paperclip"></i> <i v-if="post.hasAttachment" class="bi bi-paperclip"></i>
<span v-if="isNewPost(post.date)" class="badge bg-danger text-white ms-2 new-badge">N</span> <span v-if="isNewPost(post.date)" class="badge bg-danger text-white ms-2 fs-tiny">N</span>
</td> </td>
<td>{{ post.author }}</td> <td>{{ post.author }}</td>
<td>{{ post.date }}</td> <td>{{ post.date }}</td>

View File

@ -73,17 +73,8 @@
</div> </div>
<div class="mb-4 d-flex justify-content-end"> <div class="mb-4 d-flex justify-content-end">
<button type="button" class="btn btn-info" @click="goList"> <BackButton @click="goList" />
<i class="bx bx-left-arrow-alt"></i> <SaveButton @click="write" :isEnabled="isFileValid" />
</button>
<button
type="button"
class="btn btn-primary ms-1"
@click="write"
:disabled="!isFileValid"
>
<i class="bx bx-check"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -98,6 +89,8 @@ import FormFile from '@c/input/FormFile.vue';
import { getCurrentInstance, ref, onMounted } from 'vue'; import { getCurrentInstance, ref, onMounted } from 'vue';
import router from '@/router'; import router from '@/router';
import axios from '@api'; import axios from '@api';
import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue'
const categoryList = ref([]); const categoryList = ref([]);
const title = ref(''); const title = ref('');

View File

@ -5,8 +5,28 @@
<div class="row g-0"> <div class="row g-0">
<div class="col app-calendar-content"> <div class="col app-calendar-content">
<div class="card shadow-none border-0"> <div class="card shadow-none border-0">
<ProfileList /> <ProfileList
@profileClick="handleProfileClick"
:remainingVacationData="remainingVacationData"
/>
<div class="card-body"> <div class="card-body">
<VacationModal
v-if="isModalOpen"
:isOpen="isModalOpen"
:myVacations="myVacations"
:receivedVacations="receivedVacations"
:userColors="userColors"
@click="handleProfileClick(user)"
@close="isModalOpen = false"
/>
<VacationGrantModal
v-if="isGrantModalOpen"
:isOpen="isGrantModalOpen"
:targetUser="selectedUser"
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
@close="isGrantModalOpen = false"
@updateVacation="fetchRemainingVacation"
/>
<full-calendar <full-calendar
ref="fullCalendarRef" ref="fullCalendarRef"
:options="calendarOptions" :options="calendarOptions"
@ -31,22 +51,78 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import { reactive, ref, onMounted, nextTick, watchEffect } from "vue"; import { reactive, ref, onMounted, nextTick } from "vue";
import axios from "@api"; import axios from "@api";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue"; import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@/components/vacation/ProfileList.vue"; import ProfileList from "@c/vacation/ProfileList.vue";
import { useUserStore } from "@s/userList"; import { useUserStore as useUserListStore } from "@s/userList";
import VacationModal from "@c/modal/VacationModal.vue"
import { useUserStore } from "@s/useUserStore";
import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
const userStore = useUserStore(); const userStore = useUserStore();
const userListStore = useUserListStore();
const userList = ref([]); const userList = ref([]);
const userColors = ref({}); const userColors = ref({});
const myVacations = ref([]); //
const receivedVacations = ref([]); //
const isModalOpen = ref(false);
const remainingVacationData = ref({});
const isGrantModalOpen = ref(false);
const selectedUser = ref(null);
onMounted(async () => {
await userStore.userInfo();
await fetchRemainingVacation();
});
const fetchRemainingVacation = async () => {
try {
const response = await axios.get("vacation/remaining");
if (response.status === 200) {
remainingVacationData.value = response.data.data.reduce((acc, vacation) => {
acc[vacation.employeeId] = vacation.remainingQuota;
return acc;
}, {});
}
} catch (error) {
console.error("🚨 남은 연차 데이터를 불러오지 못했습니다:", error);
}
};
//
const handleProfileClick = async (user) => {
try {
if (user.MEMBERSEQ === userStore.user.id) {
//
const response = await axios.get(`vacation/history`);
if (response.status === 200 && response.data) {
myVacations.value = response.data.data.usedVacations || [];
receivedVacations.value = response.data.data.receivedVacations || [];
isModalOpen.value = true; //
isGrantModalOpen.value = false;
} else {
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
}
} else {
//
selectedUser.value = user;
isGrantModalOpen.value = true; //
isModalOpen.value = false;
}
} catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error);
}
};
const fetchUserList = async () => { const fetchUserList = async () => {
try { try {
await userStore.fetchUserList(); await userListStore.fetchUserList();
userList.value = userStore.userList; userList.value = userListStore.userList;
if (!userList.value.length) { if (!userList.value.length) {
console.warn("📌 사용자 목록이 비어 있음!"); console.warn("📌 사용자 목록이 비어 있음!");
@ -68,6 +144,7 @@ const calendarEvents = ref([]); // 최종적으로 FullCalendar에 표시할 이
const fetchedEvents = ref([]); // API (, ) const fetchedEvents = ref([]); // API (, )
const selectedDates = ref(new Map()); // const selectedDates = ref(new Map()); //
const halfDayType = ref(null); const halfDayType = ref(null);
const vacationCodeMap = ref({}); //
// (YYYY-MM-DD ) ( ) // (YYYY-MM-DD ) ( )
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
@ -88,6 +165,28 @@ datesSet: handleMonthChange,
events: calendarEvents, events: calendarEvents,
}); });
const fetchVacationCodes = async () => {
try {
const response = await axios.get("vacation/codes");
if (response.status === 200 && response.data) {
//
vacationCodeMap.value = response.data.data.reduce((acc, item) => {
acc[item.code] = item.name; // code key, name value
return acc;
}, {});
} else {
console.warn("❌ 공통 코드 데이터를 불러오지 못했습니다.");
}
} catch (error) {
console.error("🚨 공통 코드 API 호출 실패:", error);
}
};
// 🔹 typeCode code
const getVacationType = (typeCode) => {
return vacationCodeMap.value[typeCode] || "기타";
};
/** /**
* API 이벤트(fetchedEvents) 사용자가 선택한 날짜(selectedDates) 병합하여 * API 이벤트(fetchedEvents) 사용자가 선택한 날짜(selectedDates) 병합하여
* calendarEvents를 업데이트하는 함수 * calendarEvents를 업데이트하는 함수
@ -96,29 +195,26 @@ events: calendarEvents,
*/ */
function updateCalendarEvents() { function updateCalendarEvents() {
const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => { const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => {
let className = "";
let title = "";
if (type === "D") {
className = "selected-am"; // :
title = "오전반차 (선택)";
} else if (type === "N") {
className = "selected-pm"; // :
title = "오후반차 (선택)";
} else {
className = "selected-full"; //
title = "연차 (선택)";
}
return { return {
title, title: getVacationType(type),
start: date, start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)", backgroundColor: "rgba(0, 128, 0, 0.3)",
display: "background", display: "background",
classNames: [className], classNames: [getVacationTypeClass(type)],
}; };
}); });
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents]; calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
} }
/**
* 반차 유형에 따라 클래스명 지정 (색상 변경 없이 영역만 조정)
*/
const getVacationTypeClass = (type) => {
if (type === "700101") return "half-day-am"; //
if (type === "700102") return "half-day-pm"; //
return "full-day"; //
};
/** /**
* 날짜 클릭 이벤트 * 날짜 클릭 이벤트
* - 주말(, ) 공휴일은 클릭되지 않음 * - 주말(, ) 공휴일은 클릭되지 않음
@ -139,9 +235,9 @@ if (holidayDates.value.has(clickedDateStr)) {
if (!selectedDates.value.has(clickedDateStr)) { if (!selectedDates.value.has(clickedDateStr)) {
const type = halfDayType.value const type = halfDayType.value
? halfDayType.value === "AM" ? halfDayType.value === "AM"
? "D" ? "700101"
: "N" : "700102"
: "F"; : "700103";
selectedDates.value.set(clickedDateStr, type); selectedDates.value.set(clickedDateStr, type);
} else { } else {
selectedDates.value.delete(clickedDateStr); selectedDates.value.delete(clickedDateStr);
@ -170,22 +266,11 @@ try {
let dateStr = vac.LOCVACUDT.split("T")[0]; let dateStr = vac.LOCVACUDT.split("T")[0];
let className = "fc-daygrid-event"; let className = "fc-daygrid-event";
let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF"; let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
let title = "연차";
if (vac.LOCVACTYP === "D") {
title = "오전반차";
className += " half-day-am";
} else if (vac.LOCVACTYP === "N") {
title = "오후반차";
className += " half-day-pm";
} else if (vac.LOCVACTYP === "F") {
title = "연차";
className += " full-day";
}
return { return {
title, title: getVacationType(vac.LOCVACTYP),
start: dateStr, start: dateStr,
backgroundColor, backgroundColor,
classNames: [className], classNames: [getVacationTypeClass(vac.LOCVACTYP)],
}; };
}) })
.filter((event) => event !== null); .filter((event) => event !== null);
@ -216,6 +301,7 @@ try {
const response = await axios.post("vacation", vacationRequests); const response = await axios.post("vacation", vacationRequests);
if (response.data && response.data.status === "OK") { if (response.data && response.data.status === "OK") {
alert("휴가가 저장되었습니다."); alert("휴가가 저장되었습니다.");
await fetchRemainingVacation();
// //
const currentDate = fullCalendarRef.value.getApi().getDate(); const currentDate = fullCalendarRef.value.getApi().getDate();
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
@ -281,6 +367,7 @@ fullCalendarRef.value.getApi().refetchEvents();
// //
onMounted(async () => { onMounted(async () => {
await fetchUserList(); // await fetchUserList(); //
await fetchVacationCodes();
const today = new Date(); const today = new Date();
const year = today.getFullYear(); const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0"); const month = String(today.getMonth() + 1).padStart(2, "0");

View File

@ -45,9 +45,10 @@
<!-- 단어 목록 --> <!-- 단어 목록 -->
<ul v-if="total > 0" class="px-0 list-unstyled"> <ul v-if="total > 0" class="px-0 list-unstyled">
<DictCard <DictCard
v-for="item in wordList" v-for="item in wordList"
:key="item.WRDDICSEQ" :key="item.WRDDICSEQ"
:item="item" :item="item"
:cateList="cateList"
/> />
</ul> </ul>
@ -61,7 +62,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watchEffect, computed, onMounted, getCurrentInstance } from 'vue'; import { ref, watchEffect, computed, onMounted, getCurrentInstance, toRaw } from 'vue';
import axios from '@api'; import axios from '@api';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import WriteButton from '@c/button/WriteBtn.vue'; import WriteButton from '@c/button/WriteBtn.vue';
@ -69,7 +70,7 @@
import DictCard from '@/components/wordDict/DictCard.vue'; import DictCard from '@/components/wordDict/DictCard.vue';
import DictWrite from '@/components/wordDict/DictWrite.vue'; import DictWrite from '@/components/wordDict/DictWrite.vue';
import DictAlphabetFilter from '@/components/wordDict/DictAlphabetFilter.vue'; import DictAlphabetFilter from '@/components/wordDict/DictAlphabetFilter.vue';
import commonApi from '@/common/commonApi' import commonApi from '@/common/commonApi';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
const { appContext } = getCurrentInstance(); const { appContext } = getCurrentInstance();
@ -88,8 +89,13 @@
const total = ref(0); const total = ref(0);
// //
const { cateList } = commonApi(); const { cateList } = commonApi({
loadCateList: true
});
const selectedCategory = ref(''); const selectedCategory = ref('');
const selectCategory = ref('');
// //
const selectedAlphabet = ref(''); const selectedAlphabet = ref('');
@ -155,16 +161,16 @@
// //
const addCategory = (data) =>{ const addCategory = (data) =>{
const lastCategory = cateList.value[cateList.value.length - 1]; const lastCategory = cateList.value[cateList.value.length - 1];
const newValue = lastCategory ? parseInt(lastCategory.CMNCODVAL) + 1 : 600101; const newValue = lastCategory ? lastCategory.value + 1 : 600101;
axios.post('worddict/insertCategory',{ axios.post('worddict/insertCategory',{
CMNCODNAM: data CMNCODNAM: data
}).then(res => { }).then(res => {
if(res.data.data == '1'){ if(res.data.data == '1'){
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's'); toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
const newCategory = { CMNCODNAM: data, CMNCODVAL: newValue.toString() }; const newCategory = { label: data, value: newValue };
cateList.value.unshift(newCategory); cateList.value = [newCategory, ...cateList.value];
selectCategory.value = newCategory.CMNCODVAL; selectedCategory.value = newCategory.value;
} }
}) })
} }

View File

@ -1,13 +1,20 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools';
import mkcert from 'vite-plugin-mkcert';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), vueDevTools(),
// 자신의 로컬 서버에 연결하려면 이부분 주석처리
mkcert({
// SSL 키 등록
keyFile: '/localhost-key.pem',
certFile: '/localhost.pem',
}),
], ],
resolve: { resolve: {
alias: { alias: {
@ -18,7 +25,7 @@ export default defineConfig({
'@l': fileURLToPath(new URL('./src/layout/', import.meta.url)), '@l': fileURLToPath(new URL('./src/layout/', import.meta.url)),
'@s': fileURLToPath(new URL('./src/stores/', import.meta.url)), '@s': fileURLToPath(new URL('./src/stores/', import.meta.url)),
'@p': fileURLToPath(new URL('./src/common/plugin/', 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)) '@api': fileURLToPath(new URL('./src/common/axios-interceptor.js', import.meta.url)),
}, },
}, },
}) });