Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
This commit is contained in:
commit
f2c231288c
@ -22,7 +22,7 @@
|
|||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-size: 0px !important;
|
font-size: 0px !important;
|
||||||
}
|
}
|
||||||
/* 연차 그래프 (풀풀) */
|
/* 연차 그래프 (풀) */
|
||||||
.fc-daygrid-event.full-day {
|
.fc-daygrid-event.full-day {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 8px !important;
|
height: 8px !important;
|
||||||
@ -44,7 +44,7 @@
|
|||||||
.fc-daygrid-day-number {
|
.fc-daygrid-day-number {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
/* 데이트피커 뾰족없게게 */
|
/* 데이트피커 뾰족없게 */
|
||||||
.flatpickr-calendar:before,
|
.flatpickr-calendar:before,
|
||||||
.flatpickr-calendar:after {
|
.flatpickr-calendar:after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@ -65,12 +65,10 @@ background-color: rgba(0, 0, 0, 0.05); /* 연한 배경 효과 */
|
|||||||
/* 주말 (토요일, 일요일) 및 공휴일 */
|
/* 주말 (토요일, 일요일) 및 공휴일 */
|
||||||
.fc-day-sat-sun {
|
.fc-day-sat-sun {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
opacity: 0.6; /* 흐려 보이게 */
|
|
||||||
}
|
}
|
||||||
/* 과거 날짜 (오늘 이전) */
|
/* 과거 날짜 (오늘 이전) */
|
||||||
.fc-daygrid-day.past {
|
.fc-daygrid-day.past {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
opacity: 0.6; /* 흐려 보이게 */
|
|
||||||
}
|
}
|
||||||
/* 기본 이벤트 스타일 */
|
/* 기본 이벤트 스타일 */
|
||||||
.fc-daygrid-event {
|
.fc-daygrid-event {
|
||||||
@ -213,7 +211,7 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 20px;
|
margin-left: 12%;
|
||||||
}
|
}
|
||||||
.profile-img {
|
.profile-img {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
@ -252,20 +250,19 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
}
|
}
|
||||||
/* 선택된 (눌린) 버튼 */
|
/* 선택된 (눌린) 버튼 */
|
||||||
.vac-btn.active {
|
.vac-btn.active {
|
||||||
border: 3px solid #ff0000; /* 붉은색 테두리 적용 */
|
box-shadow: 0px 4px 15px rgba(224, 224, 224, 0.3);
|
||||||
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
|
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
.vac-btn-warning{
|
.vac-btn-warning{
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #ffab00;
|
background-color: #ffc144;
|
||||||
border-color: #ffab00;
|
border-color: #ffe605;
|
||||||
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
|
box-shadow: 0 0.125rem 0.25rem 0 rgba(255, 171, 0, 0.4);
|
||||||
}
|
}
|
||||||
/* AM 버튼 (선택된 상태) */
|
/* AM 버튼 (선택된 상태) */
|
||||||
.vac-btn-warning.active {
|
.vac-btn-warning.active {
|
||||||
background-color: #ffca2c !important;
|
background-color: #ff7300 !important;
|
||||||
color: black;
|
color: #fff;;
|
||||||
}
|
}
|
||||||
.vac-btn-info {
|
.vac-btn-info {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -279,7 +276,7 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
/* 버튼 기본 (비활성화일 때 기본 녹색) */
|
/* 버튼 기본 (비활성화일 때 기본 녹색) */
|
||||||
.btn-success {
|
.vac-btn-success {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -294,7 +291,7 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
/* 버튼 활성화 */
|
/* 버튼 활성화 */
|
||||||
.btn-success.active {
|
.vac-btn-success.active {
|
||||||
background-color: #ff0000 !important;
|
background-color: #ff0000 !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
border: 3px solid #eb9f9f !important;
|
border: 3px solid #eb9f9f !important;
|
||||||
@ -302,7 +299,8 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
/* 버튼 비활성화 */
|
/* 버튼 비활성화 */
|
||||||
.btn-success.disabled {
|
.vac-btn-success.disabled {
|
||||||
|
border: 3px solid #e6e4e4; /* 붉은색 테두리 적용 */
|
||||||
background-color: #bbb8b8 !important;
|
background-color: #bbb8b8 !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
@ -353,7 +351,7 @@ opacity: 0.6; /* 흐려 보이게 */
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.btn-success {
|
.vac-btn-success {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|||||||
BIN
public/img/icons/loading.png
Normal file
BIN
public/img/icons/loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="layout">
|
<component :is="layout">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
<LoadingSpinner :isLoading="loadingStore.isLoading" />
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
@ -12,7 +13,10 @@ import { useRoute } from 'vue-router';
|
|||||||
import NormalLayout from './layouts/NormalLayout.vue';
|
import NormalLayout from './layouts/NormalLayout.vue';
|
||||||
import NoLayout from './layouts/NoLayout.vue';
|
import NoLayout from './layouts/NoLayout.vue';
|
||||||
import ToastModal from '@c/modal/ToastModal.vue';
|
import ToastModal from '@c/modal/ToastModal.vue';
|
||||||
|
import { useLoadingStore } from "@s/loadingStore";
|
||||||
|
import LoadingSpinner from "@v/LoadingPage.vue";
|
||||||
|
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const layout = computed(() => {
|
const layout = computed(() => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useToastStore } from '@s/toastStore';
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
import { useLoadingStore } from "@s/loadingStore";
|
||||||
|
|
||||||
const $api = axios.create({
|
const $api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
@ -14,6 +15,9 @@ const $api = axios.create({
|
|||||||
*/
|
*/
|
||||||
$api.interceptors.request.use(
|
$api.interceptors.request.use(
|
||||||
function (config) {
|
function (config) {
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
|
loadingStore.startLoading();
|
||||||
|
|
||||||
let contentType = 'application/json';
|
let contentType = 'application/json';
|
||||||
|
|
||||||
if (config.isFormData) contentType = 'multipart/form-data';
|
if (config.isFormData) contentType = 'multipart/form-data';
|
||||||
@ -24,6 +28,8 @@ $api.interceptors.request.use(
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
|
loadingStore.stopLoading();
|
||||||
// 요청 오류가 있는 작업 수행
|
// 요청 오류가 있는 작업 수행
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
@ -32,17 +38,22 @@ $api.interceptors.request.use(
|
|||||||
// 응답 인터셉터 추가하기
|
// 응답 인터셉터 추가하기
|
||||||
$api.interceptors.response.use(
|
$api.interceptors.response.use(
|
||||||
function (response) {
|
function (response) {
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
|
loadingStore.stopLoading();
|
||||||
// 2xx 범위의 응답 처리
|
// 2xx 범위의 응답 처리
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
|
loadingStore.stopLoading();
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
// 오류 응답 처리
|
// 오류 응답 처리
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
if (!error.config.headers.isLogin) {
|
if (!error.config.headers.isLogin) {
|
||||||
toastStore.onToast('인증이 필요합니다.', 'e');
|
// toastStore.onToast('인증이 필요합니다.', 'e');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- 저장 버튼 -->
|
<!-- 저장 버튼 -->
|
||||||
<div class="save-button-container">
|
<div class="save-button-container">
|
||||||
<button class="btn-success" @click="addVacationRequests"
|
<button class="vac-btn-success" @click="addVacationRequests"
|
||||||
:class="{ active: !isDisabled, disabled: isDisabled }">
|
:class="{ active: !isDisabled, disabled: isDisabled }">
|
||||||
✔
|
✔
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -14,101 +14,98 @@
|
|||||||
:placeholder="title"
|
:placeholder="title"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:min="min"
|
:min="min"
|
||||||
|
autocomplete="off"
|
||||||
@focusout="$emit('focusout', modelValue)"
|
@focusout="$emit('focusout', modelValue)"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
|
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}을 확인해주세요.</div>
|
||||||
{{ title }}을 확인해주세요.
|
|
||||||
</div>
|
|
||||||
<!-- 카테고리 중복 -->
|
<!-- 카테고리 중복 -->
|
||||||
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">
|
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>
|
||||||
카테고리 중복입니다.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '라벨',
|
default: '라벨',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'nameplz',
|
default: 'nameplz',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isEssential: {
|
isEssential: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'text',
|
default: 'text',
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
maxlength: {
|
maxlength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 30,
|
default: 30,
|
||||||
},
|
},
|
||||||
isAlert: {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isCateAlert : {
|
isCateAlert: {
|
||||||
type :Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isLabel : {
|
isLabel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
min: {
|
min: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
required: false,
|
required: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits 정의
|
// Emits 정의
|
||||||
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
|
const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
|
||||||
|
|
||||||
|
// 로컬 상태로 사용하기 위한 `inputValue`
|
||||||
|
const inputValue = ref(props.modelValue);
|
||||||
|
|
||||||
// 로컬 상태로 사용하기 위한 `inputValue`
|
// 부모로 데이터 업데이트
|
||||||
const inputValue = ref(props.modelValue);
|
watch(inputValue, newValue => {
|
||||||
|
emits('update:modelValue', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
// 부모로 데이터 업데이트
|
// 초기값 동기화
|
||||||
watch(inputValue, (newValue) => {
|
watch(
|
||||||
emits('update:modelValue', newValue);
|
() => props.modelValue,
|
||||||
});
|
newValue => {
|
||||||
|
if (inputValue.value !== newValue) {
|
||||||
|
inputValue.value = newValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 초기값 동기화
|
const handleInput = event => {
|
||||||
watch(() => props.modelValue, (newValue) => {
|
const newValue = event.target.value.slice(0, props.maxlength);
|
||||||
if (inputValue.value !== newValue) {
|
|
||||||
inputValue.value = newValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInput = (event) => {
|
|
||||||
const newValue = event.target.value.slice(0, props.maxlength);
|
|
||||||
|
|
||||||
if (newValue.trim() !== '') {
|
|
||||||
emits('update:alert', false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (newValue.trim() !== '') {
|
||||||
|
emits('update:alert', false);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -86,15 +86,15 @@ const profileSize = computed(() => {
|
|||||||
const totalUsers = userList.value.length;
|
const totalUsers = userList.value.length;
|
||||||
|
|
||||||
if (windowWidth.value >= 1650) {
|
if (windowWidth.value >= 1650) {
|
||||||
if (totalUsers <= 10) return "68px";
|
if (totalUsers <= 10) return "60px";
|
||||||
if (totalUsers <= 15) return "55px";
|
if (totalUsers <= 15) return "53px";
|
||||||
return "45px";
|
return "45px";
|
||||||
} else if (windowWidth.value >= 1300) {
|
} else if (windowWidth.value >= 1400) {
|
||||||
if (totalUsers <= 10) return "45px";
|
if (totalUsers <= 10) return "48px";
|
||||||
if (totalUsers <= 15) return "40px";
|
if (totalUsers <= 15) return "30px";
|
||||||
return "30px";
|
return "20px";
|
||||||
} else if (windowWidth.value >= 1024) {
|
} else if (windowWidth.value >= 1024) {
|
||||||
if (totalUsers <= 10) return "40px";
|
if (totalUsers <= 10) return "35px";
|
||||||
if (totalUsers <= 15) return "30px";
|
if (totalUsers <= 15) return "30px";
|
||||||
return "20px";
|
return "20px";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -63,8 +63,8 @@
|
|||||||
margin-left: 260px;
|
margin-left: 260px;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
min-width: auto !important;
|
min-width: auto !important;
|
||||||
right: 24px !important;
|
right: 26px !important;
|
||||||
left: 24px !important;
|
left: 26px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 탑바 범위조정(1200px 이하) */
|
/* 탑바 범위조정(1200px 이하) */
|
||||||
@ -75,8 +75,8 @@
|
|||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
min-width: auto !important;
|
min-width: auto !important;
|
||||||
right: 24px !important;
|
right: 26px !important;
|
||||||
left: 24px !important;
|
left: 26px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,8 +88,8 @@
|
|||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
min-width: auto !important;
|
min-width: auto !important;
|
||||||
right: 24px !important;
|
right: 26px !important;
|
||||||
left: 24px !important;
|
left: 26px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
22
src/stores/loadingStore.js
Normal file
22
src/stores/loadingStore.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
export const useLoadingStore = defineStore("loading", () => {
|
||||||
|
const loadingCount = ref(0); // 요청 개수를 추적
|
||||||
|
|
||||||
|
const startLoading = () => {
|
||||||
|
loadingCount.value++;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopLoading = () => {
|
||||||
|
if (loadingCount.value > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingCount.value--;
|
||||||
|
}, 200); // 약간의 지연을 추가하여 응답이 동시에 도착해도 안정적으로 감소
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = computed(() => loadingCount.value > 0); // 하나라도 요청이 있으면 로딩 활성화
|
||||||
|
|
||||||
|
return { isLoading, startLoading, stopLoading };
|
||||||
|
});
|
||||||
@ -1,30 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="loading-container">
|
<div v-if="isLoading" class="loading-overlay">
|
||||||
<div class="spinner">
|
<div class="loading-container">
|
||||||
🐰
|
<div class="spinner">
|
||||||
|
<img src="/img/icons/loading.png" class="loading-img" />
|
||||||
|
</div>
|
||||||
|
<p class="loading-text">LOADING...</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-text">잠시만 기다려 주세요...</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isLoading: Boolean, // 부모 컴포넌트에서 전달받는 로딩 상태
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 회색 배경으로 클릭 방지 */
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.4); /* 회색 반투명 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999; /* 가장 위에 위치 */
|
||||||
|
pointer-events: auto; /* 모든 클릭 방지 */
|
||||||
|
}
|
||||||
|
|
||||||
/* 로딩 컨테이너 */
|
/* 로딩 컨테이너 */
|
||||||
.loading-container {
|
.loading-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100vh;
|
padding: 20px;
|
||||||
background-color: #f9f9f9;
|
background: none;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-img {
|
||||||
|
width: 80px; /* 원하는 크기로 조정 */
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 빙글빙글 돌아가는 스피너 */
|
/* 빙글빙글 돌아가는 스피너 */
|
||||||
.spinner {
|
.spinner {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
animation: spin 1.2s linear infinite;
|
animation: spin 2.2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 로딩 텍스트 */
|
/* 로딩 텍스트 */
|
||||||
@ -32,8 +60,7 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #555;
|
color: #ffffff;
|
||||||
font-family: "Comic Sans MS", "Arial", sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 회전 애니메이션 */
|
/* 회전 애니메이션 */
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<LoadingSpinner v-if="isLoading" />
|
<div class="row">
|
||||||
<div v-else class="row">
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<!-- 프로필 헤더 -->
|
<!-- 프로필 헤더 -->
|
||||||
@ -26,9 +25,13 @@
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
autocomplete="off"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
placeholder="비밀번호 입력"
|
placeholder="비밀번호 입력"
|
||||||
@input="password = password.replace(/\s/g, '')"
|
@input="
|
||||||
|
password = password.replace(/\s/g, '');
|
||||||
|
inputCheck();
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-primary" @click="submitPassword">확인</button>
|
<button class="btn btn-primary" @click="submitPassword">확인</button>
|
||||||
</div>
|
</div>
|
||||||
@ -131,9 +134,8 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
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 { useToastStore } from '@s/toastStore';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
import LoadingSpinner from "@v/LoadingPage.vue";
|
|
||||||
const isLoading = ref(true);
|
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('');
|
const profileName = ref('');
|
||||||
const boardTitle = ref('제목 없음');
|
const boardTitle = ref('제목 없음');
|
||||||
@ -151,6 +153,7 @@
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
|
const toastStore = useToastStore();
|
||||||
const currentBoardId = ref(Number(route.params.id));
|
const currentBoardId = ref(Number(route.params.id));
|
||||||
const unknown = computed(() => profileName.value === '익명');
|
const unknown = computed(() => profileName.value === '익명');
|
||||||
const currentUserId = computed(() => userStore.user.id); // 현재 로그인한 사용자 id
|
const currentUserId = computed(() => userStore.user.id); // 현재 로그인한 사용자 id
|
||||||
@ -224,10 +227,12 @@
|
|||||||
navigateLastPage: 1,
|
navigateLastPage: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inputCheck = () => {
|
||||||
|
passwordAlert.value = '';
|
||||||
|
};
|
||||||
// 게시물 상세 데이터 불러오기
|
// 게시물 상세 데이터 불러오기
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
|
||||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
@ -245,8 +250,6 @@
|
|||||||
attachments.value = data.attachments || [];
|
attachments.value = data.attachments || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -568,6 +571,7 @@
|
|||||||
const submitPassword = async () => {
|
const submitPassword = async () => {
|
||||||
if (!password.value.trim()) {
|
if (!password.value.trim()) {
|
||||||
passwordAlert.value = '비밀번호를 입력해주세요.';
|
passwordAlert.value = '비밀번호를 입력해주세요.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +580,7 @@
|
|||||||
LOCBRDPWD: password.value,
|
LOCBRDPWD: password.value,
|
||||||
LOCBRDSEQ: currentBoardId.value,
|
LOCBRDSEQ: currentBoardId.value,
|
||||||
});
|
});
|
||||||
|
console.log('response: ', response);
|
||||||
if (response.data.code === 200 && response.data.data === true) {
|
if (response.data.code === 200 && response.data.data === true) {
|
||||||
password.value = '';
|
password.value = '';
|
||||||
isPassword.value = false;
|
isPassword.value = false;
|
||||||
@ -591,18 +595,7 @@
|
|||||||
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.reponse && error.reponse.status === 401) passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
if (error.response && error.response.status === 401) passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
||||||
// if (error.response) {
|
|
||||||
// if (error.response.status === 401) {
|
|
||||||
// passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
|
||||||
// } else {
|
|
||||||
// passwordAlert.value = error.response.data?.message || '서버 오류가 발생했습니다.';
|
|
||||||
// }
|
|
||||||
// } else if (error.request) {
|
|
||||||
// passwordAlert.value = '네트워크 오류가 발생했습니다. 다시 시도해주세요.';
|
|
||||||
// } else {
|
|
||||||
// passwordAlert.value = '요청 중 알 수 없는 오류가 발생했습니다.';
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -664,7 +657,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.code === 200) {
|
if (response.data.code === 200) {
|
||||||
alert('게시물이 삭제되었습니다.');
|
toastStore.onToast('게시물이 삭제되었습니다.');
|
||||||
router.push({ name: 'BoardList' });
|
router.push({ name: 'BoardList' });
|
||||||
} else {
|
} else {
|
||||||
alert('삭제 실패: ' + response.data.message);
|
alert('삭제 실패: ' + response.data.message);
|
||||||
|
|||||||
@ -23,11 +23,7 @@
|
|||||||
<div class="mb-4 d-flex align-items-center">
|
<div class="mb-4 d-flex align-items-center">
|
||||||
<label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label>
|
<label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label>
|
||||||
<div class="d-flex flex-wrap align-items-center mt-3 ms-1">
|
<div class="d-flex flex-wrap align-items-center mt-3 ms-1">
|
||||||
<div
|
<div v-for="(category, index) in categoryList" :key="index" class="form-check me-3">
|
||||||
v-for="(category, index) in categoryList"
|
|
||||||
:key="index"
|
|
||||||
class="form-check me-3"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -41,9 +37,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
|
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">카테고리를 선택해주세요.</div>
|
||||||
카테고리를 선택해주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
||||||
@ -52,6 +46,7 @@
|
|||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
name="pw"
|
name="pw"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="passwordAlert"
|
:is-alert="passwordAlert"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
@ -74,7 +69,11 @@
|
|||||||
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
||||||
|
|
||||||
<ul class="list-group mt-2" v-if="attachFiles.length">
|
<ul class="list-group mt-2" v-if="attachFiles.length">
|
||||||
<li v-for="(file, index) in attachFiles" :key="index" class="list-group-item d-flex justify-content-between align-items-center">
|
<li
|
||||||
|
v-for="(file, index) in attachFiles"
|
||||||
|
:key="index"
|
||||||
|
class="list-group-item d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
{{ file.name }}
|
{{ file.name }}
|
||||||
<button class="close-btn" @click="removeFile(index)">✖</button>
|
<button class="close-btn" @click="removeFile(index)">✖</button>
|
||||||
</li>
|
</li>
|
||||||
@ -82,15 +81,11 @@
|
|||||||
|
|
||||||
<!-- 내용 입력 (에디터) -->
|
<!-- 내용 입력 (에디터) -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="col-md-2 col-form-label">
|
<label class="col-md-2 col-form-label"> 내용 <span class="text-danger">*</span> </label>
|
||||||
내용 <span class="text-danger">*</span>
|
|
||||||
</label>
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<QEditor @update:data="content = $event" />
|
<QEditor @update:data="content = $event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
|
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div>
|
||||||
내용을 입력해주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 d-flex justify-content-end">
|
<div class="mb-4 d-flex justify-content-end">
|
||||||
@ -104,162 +99,162 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
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';
|
import { useToastStore } from '@s/toastStore';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const categoryList = ref([]);
|
const categoryList = ref([]);
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const categoryValue = ref(null);
|
const categoryValue = ref(null);
|
||||||
const content = ref({ ops: [] });
|
const content = ref({ ops: [] });
|
||||||
const isFileValid = ref(true);
|
const isFileValid = ref(true);
|
||||||
|
|
||||||
const titleAlert = ref(false);
|
const titleAlert = ref(false);
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const contentAlert = ref(false);
|
const contentAlert = ref(false);
|
||||||
const categoryAlert = ref(false);
|
const categoryAlert = ref(false);
|
||||||
const attachFilesAlert = ref(false);
|
const attachFilesAlert = ref(false);
|
||||||
|
|
||||||
const attachFiles = ref([]);
|
const attachFiles = ref([]);
|
||||||
const maxFiles = 5;
|
const maxFiles = 5;
|
||||||
const maxSize = 10 * 1024 * 1024;
|
const maxSize = 10 * 1024 * 1024;
|
||||||
const fileError = ref('');
|
const fileError = ref('');
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
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;
|
||||||
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
||||||
if (freeCategory) {
|
if (freeCategory) {
|
||||||
categoryValue.value = freeCategory.CMNCODVAL;
|
categoryValue.value = freeCategory.CMNCODVAL;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('카테고리 불러오기 오류:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error('카테고리 불러오기 오류:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileCount = computed(() => attachFiles.value.length);
|
const fileCount = computed(() => attachFiles.value.length);
|
||||||
|
|
||||||
const handleFileUpload = (files) => {
|
const handleFileUpload = files => {
|
||||||
const validFiles = files.filter(file => file.size <= maxSize);
|
const validFiles = files.filter(file => file.size <= maxSize);
|
||||||
if (files.some(file => file.size > maxSize)) {
|
if (files.some(file => file.size > maxSize)) {
|
||||||
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
||||||
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fileError.value = '';
|
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFile = (index) => {
|
|
||||||
attachFiles.value.splice(index, 1);
|
|
||||||
if (attachFiles.value.length <= maxFiles) {
|
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
}
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(attachFiles, () => {
|
const removeFile = index => {
|
||||||
isFileValid.value = attachFiles.value.length <= maxFiles;
|
attachFiles.value.splice(index, 1);
|
||||||
});
|
if (attachFiles.value.length <= maxFiles) {
|
||||||
|
fileError.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateTitle = () => {
|
watch(attachFiles, () => {
|
||||||
titleAlert.value = title.value.trim().length === 0;
|
isFileValid.value = attachFiles.value.length <= maxFiles;
|
||||||
};
|
});
|
||||||
|
|
||||||
const validatePassword = () => {
|
const validateTitle = () => {
|
||||||
if (categoryValue.value === 300102) {
|
titleAlert.value = title.value.trim().length === 0;
|
||||||
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
};
|
||||||
passwordAlert.value = password.value.length === 0;
|
|
||||||
} else {
|
|
||||||
passwordAlert.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateContent = () => {
|
const validatePassword = () => {
|
||||||
if (!content.value?.ops?.length) {
|
if (categoryValue.value === 300102) {
|
||||||
contentAlert.value = true;
|
password.value = password.value.replace(/\s/g, ''); // 공백 제거
|
||||||
return;
|
passwordAlert.value = password.value.length === 0;
|
||||||
}
|
} else {
|
||||||
|
passwordAlert.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 이미지 포함 여부 확인
|
const validateContent = () => {
|
||||||
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
if (!content.value?.ops?.length) {
|
||||||
// 텍스트 포함 여부 확인
|
contentAlert.value = true;
|
||||||
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
return;
|
||||||
|
|
||||||
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
|
||||||
contentAlert.value = !(hasText || hasImage);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 글쓰기 */
|
|
||||||
const write = async () => {
|
|
||||||
validateTitle();
|
|
||||||
validatePassword();
|
|
||||||
validateContent();
|
|
||||||
categoryAlert.value = categoryValue.value == null;
|
|
||||||
|
|
||||||
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const boardData = {
|
|
||||||
LOCBRDTTL: title.value,
|
|
||||||
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
|
||||||
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
|
||||||
LOCBRDTYP: categoryValue.value
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: boardResponse } = await axios.post('board', boardData);
|
|
||||||
const boardId = boardResponse.data;
|
|
||||||
// 첨부파일 업로드 (비동기 병렬 처리)
|
|
||||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
|
||||||
await Promise.all(attachFiles.value.map(async (file) => {
|
|
||||||
console.log(file);
|
|
||||||
const formData = new FormData();
|
|
||||||
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
|
||||||
|
|
||||||
formData.append('CMNBRDSEQ', boardId);
|
|
||||||
formData.append('CMNFLEORG', fileNameWithoutExt);
|
|
||||||
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
|
||||||
formData.append('CMNFLESIZ', file.size);
|
|
||||||
formData.append('file', file); // 📌 실제 파일 추가
|
|
||||||
|
|
||||||
await axios.post(`board/${boardId}/attachments`, formData,
|
|
||||||
{ isFormData : true }
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
// 이미지 포함 여부 확인
|
||||||
goList();
|
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
||||||
} catch (error) {
|
// 텍스트 포함 여부 확인
|
||||||
console.error(error);
|
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
||||||
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 목록으로 이동 */
|
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
||||||
const goList = () => {
|
contentAlert.value = !(hasText || hasImage);
|
||||||
router.push('/board');
|
};
|
||||||
};
|
|
||||||
|
|
||||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
/** 글쓰기 */
|
||||||
watch(content, () => {
|
const write = async () => {
|
||||||
validateContent();
|
validateTitle();
|
||||||
});
|
validatePassword();
|
||||||
|
validateContent();
|
||||||
|
categoryAlert.value = categoryValue.value == null;
|
||||||
|
|
||||||
|
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const boardData = {
|
||||||
|
LOCBRDTTL: title.value,
|
||||||
|
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
||||||
|
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
||||||
|
LOCBRDTYP: categoryValue.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: boardResponse } = await axios.post('board', boardData);
|
||||||
|
const boardId = boardResponse.data;
|
||||||
|
// 첨부파일 업로드 (비동기 병렬 처리)
|
||||||
|
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
attachFiles.value.map(async file => {
|
||||||
|
console.log(file);
|
||||||
|
const formData = new FormData();
|
||||||
|
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
formData.append('CMNBRDSEQ', boardId);
|
||||||
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
|
formData.append('CMNFLESIZ', file.size);
|
||||||
|
formData.append('file', file); // 📌 실제 파일 추가
|
||||||
|
|
||||||
|
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
||||||
|
goList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 목록으로 이동 */
|
||||||
|
const goList = () => {
|
||||||
|
router.push('/board');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||||
|
watch(content, () => {
|
||||||
|
validateContent();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -148,7 +148,7 @@ function handleMonthChange(viewInfo) {
|
|||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||||
loadCalendarData(year, month);
|
loadCalendarData(year, month);
|
||||||
}
|
}
|
||||||
// 캘린더 클릭
|
// 캘린더 클릭
|
||||||
function handleDateClick(info) {
|
function handleDateClick(info) {
|
||||||
const clickedDateStr = info.dateStr;
|
const clickedDateStr = info.dateStr;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<LoadingSpinner v-if="isLoading" />
|
<div class="row g-3">
|
||||||
<div v-else class="row g-3">
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<!-- 투표 작성 -->
|
<!-- 투표 작성 -->
|
||||||
<WriteBtn @click="voteWrite" />
|
<WriteBtn @click="voteWrite" />
|
||||||
@ -52,8 +51,6 @@ 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 { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import LoadingSpinner from "@v/LoadingPage.vue";
|
|
||||||
const isLoading = ref(true);
|
|
||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const category = ref('0');
|
const category = ref('0');
|
||||||
@ -81,7 +78,6 @@ const changeCheck = () =>{
|
|||||||
}
|
}
|
||||||
//투표목록
|
//투표목록
|
||||||
const getvoteList = async () => {
|
const getvoteList = async () => {
|
||||||
isLoading.value = true;
|
|
||||||
const response = await $api.get('vote/getVoteList',{
|
const response = await $api.get('vote/getVoteList',{
|
||||||
params:
|
params:
|
||||||
{
|
{
|
||||||
@ -93,7 +89,6 @@ const getvoteList = async () => {
|
|||||||
if (response.data.status === "OK") {
|
if (response.data.status === "OK") {
|
||||||
PageData.value = response.data.data;
|
PageData.value = response.data.data;
|
||||||
voteListCardData.value = response.data.data.list;
|
voteListCardData.value = response.data.data.list;
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const selectHandler = () =>{
|
const selectHandler = () =>{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user