Merge branch 'main' into login
This commit is contained in:
commit
93693c4479
@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import router from "@/router/index";
|
||||
|
||||
const $api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
baseURL: 'http://localhost:10325/api/',
|
||||
timeout: 300000,
|
||||
withCredentials : true
|
||||
})
|
||||
|
||||
@ -11,9 +11,9 @@
|
||||
</div>
|
||||
<!-- 텍스트박스 -->
|
||||
<div class="w-100">
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="주제에 대한 생각을 자유롭게 댓글로 표현해 주세요. 여러분의 다양한 의견을 기다립니다."
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="주제에 대한 생각을 자유롭게 댓글로 표현해 주세요. 여러분의 다양한 의견을 기다립니다."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
@ -22,36 +22,38 @@
|
||||
<!-- 옵션 및 버튼 섹션 -->
|
||||
<div class="d-flex justify-content-between flex-wrap mt-4">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
<!-- 익명 체크박스 -->
|
||||
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
||||
<div class="form-check form-check-inline mb-0 me-4">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="inlineCheckbox1"
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="inlineCheckbox1"
|
||||
v-model="isCheck"
|
||||
/>
|
||||
<label class="form-check-label" for="inlineCheckbox1">익명</label>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 비밀번호 입력 필드 (익명이 선택된 경우에만 표시) -->
|
||||
<div v-if="isCheck" class="d-flex align-items-center flex-grow-1">
|
||||
<label class="form-label mb-0 me-3" for="basic-default-password">비밀번호</label>
|
||||
<input
|
||||
type="password"
|
||||
type="password"
|
||||
id="basic-default-password"
|
||||
class="form-control flex-grow-1"
|
||||
placeholder=""
|
||||
class="form-control flex-grow-1"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 답변 쓰기 버튼 -->
|
||||
<div class="ms-auto mt-3 mt-md-0">
|
||||
<button class="btn btn-primary">답변 쓰기</button>
|
||||
<button class="btn btn-primary">
|
||||
<i class="icon-base bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@ -3,13 +3,12 @@
|
||||
<li>
|
||||
<BoardProfile profileName=곤데리 :showDetail="false" :author="true" />
|
||||
<div class="mt-2">저도 궁금합니다.</div>
|
||||
<button type="button" class="btn btn-text-primary" @click="toggleComment">답변달기</button>
|
||||
<PlusButton @click="toggleComment"/>
|
||||
<BoardComentArea v-if="comment" />
|
||||
<ul class="list-unstyled twoDepth">
|
||||
<li>
|
||||
<BoardProfile profileName=곤데리2 :showDetail="false" />
|
||||
<div class="mt-2">저도 궁금합니다.</div>
|
||||
<button type="button" class="btn btn-text-primary" @click="toggleComment">답변달기</button>
|
||||
<BoardComentArea v-if="comment" />
|
||||
</li>
|
||||
</ul>
|
||||
@ -17,13 +16,13 @@
|
||||
<li>
|
||||
<BoardProfile profileName=곤데리 :showDetail="false" />
|
||||
<div class="mt-2">저도 궁금합니다.</div>
|
||||
<button type="button" class="btn btn-text-primary" @click="toggleComment">답변달기</button>
|
||||
<PlusButton @click="toggleComment"/>
|
||||
<BoardComentArea v-if="comment" />
|
||||
</li>
|
||||
<li>
|
||||
<BoardProfile :showDetail="false" :unknown="false" />
|
||||
<BoardProfile profileName=곤데리 :showDetail="false" />
|
||||
<div class="mt-2">저도 궁금합니다.</div>
|
||||
<button type="button" class="btn btn-text-primary" @click="toggleComment">답변달기</button>
|
||||
<PlusButton @click="toggleComment"/>
|
||||
<BoardComentArea v-if="comment" />
|
||||
</li>
|
||||
</ul>
|
||||
@ -36,6 +35,8 @@ import BoardProfile from './BoardProfile.vue';
|
||||
import BoardComentArea from './BoardComentArea.vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import Pagination from '../pagination/Pagination.vue';
|
||||
import PlusButton from '../button/PlusBtn.vue';
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const comment = ref(false);
|
||||
|
||||
@ -43,6 +44,9 @@ const toggleComment = () => {
|
||||
comment.value = !comment.value
|
||||
};
|
||||
|
||||
// emits 정의
|
||||
const emit = defineEmits(['submitComment']);
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -63,4 +67,4 @@ const toggleComment = () => {
|
||||
.btn-text-primary:focus {
|
||||
background-color: transparent
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BoardRecommendBtn from './BoardRecommendBtn.vue';
|
||||
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
|
||||
|
||||
defineProps({
|
||||
boardTitle : {
|
||||
@ -51,6 +51,10 @@ defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
attachments: {
|
||||
type: Array, // 첨부파일 배열 타입
|
||||
default: () => [], // 기본값 설정
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
@ -59,4 +63,4 @@ defineProps({
|
||||
.min-250 {
|
||||
min-height: 250px !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -24,74 +24,98 @@
|
||||
</div>
|
||||
<div class="ms-auto btn-area">
|
||||
<template v-if="showDetail">
|
||||
<button class="btn btn-label-primary btn-icon">
|
||||
<i class='bx bx-edit-alt'></i>
|
||||
</button>
|
||||
<button class="btn btn-label-primary btn-icon">
|
||||
<i class='bx bx-trash' ></i>
|
||||
</button>
|
||||
<EditButton @click="handleEdit" />
|
||||
<DeleteButton @click="handleDelete" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="author">
|
||||
<button class="btn author btn-label-primary btn-icon">
|
||||
<button class="btn author btn-label-primary btn-icon" @click="handleEdit">
|
||||
<i class='bx bx-edit-alt'></i>
|
||||
</button>
|
||||
<button class="btn author btn-label-primary btn-icon">
|
||||
<i class='bx bx-trash' ></i>
|
||||
<button class="btn author btn-label-primary btn-icon" @click="handleDelete">
|
||||
<i class='bx bx-trash'></i>
|
||||
</button>
|
||||
</template>
|
||||
<BoardRecommendBtn :likeClicked="true" :dislikeClicked="false"/>
|
||||
<BoardRecommendBtn :likeClicked="true" :dislikeClicked="false" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BoardRecommendBtn from './BoardRecommendBtn.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from '@api';
|
||||
import DeleteButton from '../button/DeleteBtn.vue';
|
||||
import EditButton from '../button/EditBtn.vue';
|
||||
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// Vue Router 인스턴스
|
||||
const router = useRouter();
|
||||
|
||||
// Props 정의
|
||||
defineProps({
|
||||
profileName : {
|
||||
profileName: {
|
||||
type: String,
|
||||
default: '익명',
|
||||
},
|
||||
unknown : {
|
||||
unknown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDetail : {
|
||||
showDetail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
author : {
|
||||
author: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const boardId = 100; //수정필요!!
|
||||
// 수정 버튼 핸들러
|
||||
const handleEdit = () => {
|
||||
router.push({ name: 'BoardEdit', params: { id: boardId } });
|
||||
};
|
||||
|
||||
// 삭제 버튼 핸들러
|
||||
const handleDelete = async () => {
|
||||
if (confirm('정말 이 게시물을 삭제하시겠습니까?')) {
|
||||
try {
|
||||
await axios.delete(`board/${boardId}`);
|
||||
alert('게시물이 성공적으로 삭제되었습니다.');
|
||||
router.push({ name: 'BoardList' });
|
||||
} catch (error) {
|
||||
console.error('게시물 삭제 중 오류 발생:', error);
|
||||
alert('게시물 삭제에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-detail span ~ span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.profile-detail span ~ span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ms-auto button + button {
|
||||
margin-left: 5px;
|
||||
.ms-auto button + button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn.author {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
.btn-area {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn.author {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width:450px) {
|
||||
.btn-area {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn.author {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
|
||||
13
src/components/button/DeleteBtn.vue
Normal file
13
src/components/button/DeleteBtn.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<button class="btn btn-label-primary btn-icon">
|
||||
<i class='bx bx-trash' ></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeleteButton',
|
||||
methods: {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
13
src/components/button/EditBtn.vue
Normal file
13
src/components/button/EditBtn.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<button class="btn btn-label-primary btn-icon">
|
||||
<i class="bx bx-edit-alt"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditButton',
|
||||
methods: {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
13
src/components/button/PlusBtn.vue
Normal file
13
src/components/button/PlusBtn.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<button class="btn btn-label-primary btn-icon">
|
||||
<i class="icon-base bx bx-plus"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PlusButton',
|
||||
methods: {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
13
src/components/button/WriteBtn.vue
Normal file
13
src/components/button/WriteBtn.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<button class="btn btn-label-primary btn-icon float-end">
|
||||
<i class="bx bx-edit"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WriteButton',
|
||||
methods: {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -67,7 +67,7 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import CenterModal from '@c/modal/CenterModal.vue';
|
||||
import { inject, onMounted, reactive, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import axios from '@api';
|
||||
import { isEmpty } from '@/common/utils';
|
||||
import FormInput from '../input/FormInput.vue';
|
||||
import FlatPickr from 'vue-flatpickr-component';
|
||||
|
||||
@ -5,22 +5,26 @@
|
||||
<span :class="isEssential ? 'text-red' : 'none'">*</span>
|
||||
</label>
|
||||
<div class="col-md-10">
|
||||
<input :id="name"
|
||||
<input
|
||||
:id="name"
|
||||
class="form-control"
|
||||
:type="type"
|
||||
@input="updateInput"
|
||||
:value="value"
|
||||
v-model="inputValue"
|
||||
:maxLength="maxlength"
|
||||
:placeholder="title" />
|
||||
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">{{ title }}을 확인해주세요.</div>
|
||||
:placeholder="title"
|
||||
/>
|
||||
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">
|
||||
{{ title }}을 확인해주세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const prop = defineProps({
|
||||
// Props 정의
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '라벨',
|
||||
@ -34,39 +38,42 @@ const prop = defineProps({
|
||||
isEssential: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
required: false,
|
||||
},
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
require: false,
|
||||
},
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
required: false,
|
||||
},
|
||||
isAlert : {
|
||||
isAlert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:data'])
|
||||
// Emits 정의
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
||||
const updateInput = function (event) {
|
||||
//Type Number 일때 maxlength 적용 안됨 방지
|
||||
if (event.target.value.length > prop.maxlength) {
|
||||
event.target.value = event.target.value.slice(0, prop.maxlength);
|
||||
// 로컬 상태로 사용하기 위한 `inputValue`
|
||||
const inputValue = ref(props.modelValue);
|
||||
|
||||
// 부모로 데이터 업데이트
|
||||
watch(inputValue, (newValue) => {
|
||||
emits('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// 초기값 동기화
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (inputValue.value !== newValue) {
|
||||
inputValue.value = newValue;
|
||||
}
|
||||
emits('update:data', event.target.value);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -1,23 +1,38 @@
|
||||
<template>
|
||||
<div class="" role="button">
|
||||
<div class="card">
|
||||
<div class="d-sm-flex">
|
||||
<div v-if="img">
|
||||
<img class="card-img card-img-left" :src="img" alt="" style="width: 200px; height: 200px; object-fit: cover" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title align-items-center">
|
||||
<slot name="badgeType"></slot>
|
||||
{{ title }}
|
||||
</h5>
|
||||
<p class="card-text str_wrap pt-5">
|
||||
{{ content }}
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">{{ date }}</small>
|
||||
<slot name="optInfo"></slot>
|
||||
</p>
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="row g-0">
|
||||
<!-- 이미지 섹션 -->
|
||||
<div v-if="img" class="col-md-2">
|
||||
<img
|
||||
:src="img"
|
||||
alt="이미지"
|
||||
class="img-fluid rounded-start"
|
||||
style="object-fit: cover; height: 100%; width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<!-- 게시물 내용 섹션 -->
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<!-- 태그 -->
|
||||
<h6 class="badge rounded-pill bg-primary text-white mb-2">
|
||||
{{ category }}
|
||||
</h6>
|
||||
<!-- 제목 -->
|
||||
<h5 class="card-title">{{ title }}</h5>
|
||||
<!-- 본문 -->
|
||||
<p class="card-text str_wrap">{{ content }}</p>
|
||||
<!-- 날짜 -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">{{ formatDate(date) }}</small>
|
||||
<!-- 좋아요와 댓글 -->
|
||||
<div>
|
||||
<span class="text-muted me-3">
|
||||
<i class="bx bx-like"></i> {{ likes || 0 }}
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
<i class="bx bx-comment"></i> {{ comments || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,41 +40,79 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// const data = defineProps(['item']);
|
||||
const prop = defineProps({
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
img: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '제목',
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '내용',
|
||||
required: true,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: 'date',
|
||||
required: true,
|
||||
},
|
||||
img: {
|
||||
type: String,
|
||||
required: false,
|
||||
likes: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const colSetting = () => {
|
||||
img ? 'col-9' : '';
|
||||
};
|
||||
comments: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
|
||||
date.getDate()
|
||||
).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
|
||||
date.getMinutes()
|
||||
).padStart(2, "0")}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.str_wrap {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
/* 카드 스타일 */
|
||||
.card {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 텍스트 줄임 표시 */
|
||||
.str_wrap {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 이미지 스타일 */
|
||||
.img-fluid {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
/* 태그 배지 스타일 */
|
||||
.badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
37
src/components/list/BoardCardList.vue
Normal file
37
src/components/list/BoardCardList.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="container mt-4">
|
||||
<div v-if="posts.length === 0" class="text-center">
|
||||
게시물이 없습니다.
|
||||
</div>
|
||||
<div v-for="post in posts" :key="post.id">
|
||||
<BoardCard
|
||||
:img="post.img"
|
||||
:category="post.category"
|
||||
:title="post.title"
|
||||
:content="post.content"
|
||||
:date="post.date"
|
||||
:likes="post.likes"
|
||||
:comments="post.comments"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BoardCard from './BoardCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BoardCard,
|
||||
},
|
||||
props: {
|
||||
posts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@ -43,6 +43,12 @@
|
||||
<div class="text-truncate">Board</div>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="menu-item" :class="$route.path.includes('/vacation') ? 'active' : ''">
|
||||
<RouterLink class="menu-link" to="/vacation">
|
||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||
<div class="text-truncate">vacation</div>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
||||
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import BoardWrite from '@v/board/BoardWrite.vue';
|
||||
|
||||
// 초기 렌더링 속도를 위해 지연 로딩 사용
|
||||
const routes = [
|
||||
@ -14,6 +13,7 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'BoardList',
|
||||
component: () => import('@v/board/BoardList.vue')
|
||||
},
|
||||
{
|
||||
@ -21,15 +21,26 @@ const routes = [
|
||||
component: () => import('@v/board/BoardWrite.vue')
|
||||
},
|
||||
{
|
||||
path: 'get/:id',
|
||||
path: ':id',
|
||||
name: 'BoardDetail',
|
||||
component: () => import('@v/board/BoardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'edit/:id',
|
||||
name: 'BoardEdit',
|
||||
component: () => import('@v/board/BoardEdit.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
// 레이아웃 필요없을 때 예시
|
||||
// {
|
||||
// path: '/login',
|
||||
// component: () => import('@v/user/TheLogin.vue'),
|
||||
// meta: { layout: 'NoLayout' },
|
||||
// },
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@v/user/TheLogin.vue'),
|
||||
meta: { layout: 'NoLayout' },
|
||||
path: '/vacation',
|
||||
component: () => import('@v/vacation/VacationManagement.vue'),
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
|
||||
20
src/stores/calendarStore.js
Normal file
20
src/stores/calendarStore.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { ref } from 'vue';
|
||||
import axios from '@api';
|
||||
|
||||
const events = ref([]);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const response = await axios.get('/calendar/events');
|
||||
events.value = response.data;
|
||||
};
|
||||
|
||||
const addEvent = async (event) => {
|
||||
await axios.post('/calendar/event', event);
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
export default {
|
||||
events,
|
||||
fetchEvents,
|
||||
addEvent,
|
||||
};
|
||||
55
src/views/AddEventModal.vue
Normal file
55
src/views/AddEventModal.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>휴가 추가</h3>
|
||||
<input type="text" v-model="title" placeholder="제목" />
|
||||
<input type="date" v-model="date" />
|
||||
<button @click="addEvent">추가</button>
|
||||
<button @click="$emit('close')">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import calendarStore from '@s/calendarStore';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
date: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addEvent() {
|
||||
if (this.title && this.date) {
|
||||
calendarStore.addEvent({ title: this.title, start: this.date });
|
||||
this.$emit('close');
|
||||
} else {
|
||||
alert('모든 필드를 입력해주세요.');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
146
src/views/board/BoardEdit.vue
Normal file
146
src/views/board/BoardEdit.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="pb-4 rounded-top">
|
||||
<div class="container py-12 px-xl-10 px-4" style="padding-bottom: 0px !important">
|
||||
<h3 class="text-center mb-2 mt-4">글 수정</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-12">
|
||||
<div class="card-body">
|
||||
<!-- 제목 입력 -->
|
||||
<FormInput
|
||||
title="제목"
|
||||
name="title"
|
||||
:is-essential="true"
|
||||
:is-alert="titleAlert"
|
||||
v-model="title"
|
||||
/>
|
||||
|
||||
<!-- 내용 입력 -->
|
||||
<div class="mb-4">
|
||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||
내용
|
||||
<span class="text-red">*</span>
|
||||
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">
|
||||
내용을 확인해주세요.
|
||||
</div>
|
||||
</label>
|
||||
<div class="col-md-12">
|
||||
<QEditor v-model="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="mb-4 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-info right" @click="goList">
|
||||
<i class="bx bx-left-arrow-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-1" @click="updateBoard">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import QEditor from '@c/editor/QEditor.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from '@api';
|
||||
|
||||
// 상태 변수
|
||||
const title = ref('');
|
||||
const content = ref('');
|
||||
|
||||
// 경고 상태
|
||||
const titleAlert = ref(false);
|
||||
const contentAlert = ref(false);
|
||||
|
||||
// 라우터
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const currentBoardId = ref(route.params.id); // 라우트에서 ID 가져오기
|
||||
console.log(currentBoardId.value)
|
||||
// 게시물 데이터 로드
|
||||
const fetchBoardDetails = async () => {
|
||||
try {
|
||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const data = response.data.data.boardDetail;
|
||||
|
||||
if (!data) {
|
||||
console.error('API에서 게시물 데이터를 반환하지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터 설정
|
||||
title.value = data.title || '제목 없음';
|
||||
content.value = data.content || '내용 없음';
|
||||
|
||||
} catch (error) {
|
||||
console.error('게시물 가져오기 오류:', error.response || error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 목록 페이지로 이동
|
||||
const goList = () => {
|
||||
router.push('/board');
|
||||
};
|
||||
|
||||
// 게시물 수정
|
||||
const updateBoard = async () => {
|
||||
// 유효성 검사
|
||||
if (!title.value) {
|
||||
titleAlert.value = true;
|
||||
return;
|
||||
}
|
||||
titleAlert.value = false;
|
||||
|
||||
if (!content.value) {
|
||||
contentAlert.value = true;
|
||||
return;
|
||||
}
|
||||
contentAlert.value = false;
|
||||
|
||||
try {
|
||||
// 수정 데이터 전송
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value,
|
||||
LOCBRDCON: content.value,
|
||||
};
|
||||
|
||||
await axios.put(`board/${currentBoardId.value}`, boardData);
|
||||
|
||||
alert('게시물이 수정되었습니다.');
|
||||
goList();
|
||||
} catch (error) {
|
||||
console.error('게시물 수정 중 오류 발생:', error);
|
||||
alert('게시물 수정에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(() => {
|
||||
if (currentBoardId.value) {
|
||||
fetchBoardDetails();
|
||||
} else {
|
||||
console.error('잘못된 게시물 ID:', currentBoardId.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text-red {
|
||||
color: red;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.display-block {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@ -1,32 +1,15 @@
|
||||
<template>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<search-bar @update:data="search" />
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="mt-8">
|
||||
<router-link to="/board/write">
|
||||
<button class="btn btn-label-primary btn-icon float-end">
|
||||
<i class="bx bx-edit-alt"></i>
|
||||
</button>
|
||||
<WriteButton />
|
||||
</router-link>
|
||||
</div>
|
||||
<template v-for="(item, index) in list" :key="item.id">
|
||||
<board-card :title="item.title" :content="item.content" :img="item.img" :date="item.date" @click="goDetail(item.id)">
|
||||
<template #badgeType>
|
||||
<span v-if="item.type == 1" class="badge rounded-pill bg-label-danger">공지</span>
|
||||
<span v-else-if="item.type == 2" class="badge rounded-pill bg-label-primary">자유</span>
|
||||
<span v-else-if="item.type == 3" class="badge rounded-pill bg-label-success">익명</span>
|
||||
</template>
|
||||
<template #optInfo>
|
||||
<small v-show="item.viewCount" style="padding-left: 10px" class="text-muted"
|
||||
><i class="fa-regular fa-eye"></i> {{ item.viewCount }}
|
||||
</small>
|
||||
<small v-show="item.cmtCount" style="padding-left: 10px" class="text-muted"
|
||||
><i class="fa-regular fa-comment-dots"></i> {{ item.cmtCount }}
|
||||
</small>
|
||||
</template>
|
||||
</board-card>
|
||||
</template>
|
||||
|
||||
<board-card :posts="filteredList" @click="goDetail" />
|
||||
|
||||
<div class="mt-8">
|
||||
<pagination />
|
||||
</div>
|
||||
@ -35,24 +18,59 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import BoardCard from '@/components/list/BoardCard.vue';
|
||||
import Pagination from '@/components/pagination/Pagination.vue';
|
||||
import SearchBar from '@/components/search/SearchBar.vue';
|
||||
import router from '@/router';
|
||||
import dummy from '@a/boardDummy.json';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import BoardCard from '@/components/list/BoardCardList.vue';
|
||||
import Pagination from '@c/pagination/Pagination.vue';
|
||||
import SearchBar from '@c/search/SearchBar.vue';
|
||||
import router from '@/router';
|
||||
import WriteButton from '@c/button/WriteBtn.vue';
|
||||
import axios from '@api';
|
||||
|
||||
const list = ref(dummy);
|
||||
const searchText = ref('');
|
||||
// 데이터 초기화
|
||||
const list = ref([]);
|
||||
const searchText = ref('');
|
||||
|
||||
/** 상세로 이동 */
|
||||
const goDetail = idx => {
|
||||
router.push(`/board/get/${idx}`);
|
||||
};
|
||||
// 상세 페이지 이동
|
||||
const goDetail = (id) => {
|
||||
router.push({ name: 'BoardDetail', params: { id } });
|
||||
};
|
||||
|
||||
// 검색 처리
|
||||
const search = (e) => {
|
||||
searchText.value = e.trim();
|
||||
};
|
||||
|
||||
// 검색 결과 필터링
|
||||
const filteredList = computed(() =>
|
||||
list.value.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
// 게시물 데이터 로드
|
||||
const fetchPosts = async () => {
|
||||
try {
|
||||
const response = await axios.get("board/general");
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
list.value = response.data.data.map((post) => ({
|
||||
...post,
|
||||
img: post.img || null,
|
||||
likes: post.likes || 0,
|
||||
comments: post.comments || 0,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch posts:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 데이터 로드
|
||||
onMounted(() => {
|
||||
fetchPosts();
|
||||
});
|
||||
|
||||
const search = e => {
|
||||
console.log('검색:', e);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style>
|
||||
/* 필요에 따라 스타일 추가 */
|
||||
</style>
|
||||
|
||||
@ -3,15 +3,32 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<!-- 프로필 헤더 -->
|
||||
<div class="card-header">
|
||||
<BoardProfile profileName="만드레야2"/>
|
||||
<BoardProfile :boardId="currentBoardId.value" :profileName="profileName" />
|
||||
</div>
|
||||
<!-- 게시글 내용 -->
|
||||
<div class="card-body">
|
||||
<BoardContent boardTitle="제목1" boardContent="내용1" :dropdownItems="dropdownItems" />
|
||||
<BoardComentArea />
|
||||
<h5 class="mb-4">{{ boardTitle }}</h5>
|
||||
<!-- HTML 콘텐츠 렌더링 -->
|
||||
<div class="board-content" v-html="boardContent"></div>
|
||||
<!-- 첨부파일 목록 -->
|
||||
<ul v-if="attachments.length" class="attachments mt-4">
|
||||
<li v-for="(attachment, index) in attachments" :key="index">
|
||||
<a :href="attachment.url" target="_blank">{{ attachment.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 댓글 영역 -->
|
||||
<BoardComentArea :comments="comments" />
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<BoardComment />
|
||||
<!-- 수정 버튼 -->
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="goToEditPage"
|
||||
>
|
||||
글 수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -20,17 +37,52 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BoardComentArea from '@/components/board/BoardComentArea.vue';
|
||||
import BoardComment from '@/components/board/BoardComment.vue';
|
||||
import BoardContent from '@/components/board/BoardContent.vue';
|
||||
import BoardProfile from '@/components/board/BoardProfile.vue';
|
||||
import { ref } from 'vue';
|
||||
import BoardComentArea from '@c/board/BoardComentArea.vue';
|
||||
import BoardProfile from '@c/board/BoardProfile.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from '@api';
|
||||
|
||||
const dropdownItems = ref([
|
||||
{ label: '내용1'},
|
||||
{ label: '내용2'},
|
||||
{ label: '내용3'},
|
||||
]);
|
||||
// 게시물 데이터 상태
|
||||
const profileName = ref('익명 사용자');
|
||||
const boardTitle = ref('제목 없음');
|
||||
const boardContent = ref('내용 없음');
|
||||
const comments = ref([]);
|
||||
const attachments = ref([]);
|
||||
|
||||
// 라우트에서 ID 가져오기
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const currentBoardId = ref(Number(route.params.id));
|
||||
console.log(currentBoardId.value)
|
||||
|
||||
// 글 수정 페이지로 이동
|
||||
const goToEditPage = () => {
|
||||
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
|
||||
};
|
||||
|
||||
// 게시물 상세 데이터 불러오기
|
||||
const fetchBoardDetails = async () => {
|
||||
try {
|
||||
const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const data = response.data.data;
|
||||
|
||||
// API 응답 데이터 반영
|
||||
const boardDetail = data.boardDetail || {};
|
||||
profileName.value = boardDetail.author || '익명 사용자';
|
||||
boardTitle.value = boardDetail.title || '제목 없음';
|
||||
boardContent.value = boardDetail.content || '내용 없음';
|
||||
|
||||
attachments.value = data.attachments || [];
|
||||
comments.value = data.comments || [];
|
||||
} catch (error) {
|
||||
console.error('게시물 가져오기 오류:', error);
|
||||
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(() => {
|
||||
fetchBoardDetails();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -55,9 +55,10 @@ import FormSelect from '@c/input/FormSelect.vue';
|
||||
import FormFile from '@c/input/FormFile.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import router from '@/router';
|
||||
import axios from '@api';
|
||||
|
||||
const categoryList = ['자유', '익명', '공지사항'];
|
||||
// input 경고문 만들어야함
|
||||
// input 경고문 만들어야함!!
|
||||
const title = ref('');
|
||||
const password = ref('');
|
||||
const category = ref(0);
|
||||
@ -76,9 +77,81 @@ const goList = () => {
|
||||
router.push('/board');
|
||||
};
|
||||
|
||||
const write = () => {
|
||||
console.log('작성');
|
||||
const write = async () => {
|
||||
// 입력값 유효성 검사
|
||||
if (!title.value) {
|
||||
titleAlert.value = true;
|
||||
return;
|
||||
} else {
|
||||
titleAlert.value = false;
|
||||
}
|
||||
|
||||
if (category.value === 1 && !password.value) {
|
||||
passwordAlert.value = true;
|
||||
return;
|
||||
} else {
|
||||
passwordAlert.value = false;
|
||||
}
|
||||
|
||||
if (!content.value) {
|
||||
contentAlert.value = true;
|
||||
return;
|
||||
} else {
|
||||
contentAlert.value = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 게시물 작성 데이터 준비
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value,
|
||||
LOCBRDCON: content.value,
|
||||
LOCBRDPWD: category.value === 1 ? password.value : null,
|
||||
LOCBRDTYP: category.value === 1 ? 'S' : 'F', //공지사항 추가해야함!!
|
||||
// MEMBERSEQ: 로그인이용자 id(세션)
|
||||
};
|
||||
|
||||
// 게시물 작성 API 호출
|
||||
const { data: boardResponse } = await axios.post('board', boardData);
|
||||
const boardId = boardResponse.data.boardId;
|
||||
|
||||
// 첨부파일 처리
|
||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||
for (const file of attachFiles.value) {
|
||||
const realName = file.name.substring(0, file.name.lastIndexOf('.'));
|
||||
const fileInfo = {
|
||||
path: "/uploads", // 파일 경로 (수정 필요)
|
||||
originalName: realName, // 확장자를 제외한 파일명
|
||||
extension: file.name.split('.').pop(), // 파일 확장자
|
||||
registrantId: 1, // 등록자 ID (수정 필요)
|
||||
};
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file); // 첨부 파일
|
||||
formData.append("CMNFLEPAT", fileInfo.path); // 파일 경로
|
||||
formData.append("CMNFLENAM", fileInfo.originalName); // 파일 명(확장자제외)
|
||||
formData.append("CMNFLEORG", fileInfo.originalName); // 원본 파일명(확장자제외)
|
||||
formData.append("CMNFLEEXT", fileInfo.extension); // 파일 확장자
|
||||
formData.append("CMNFLESIZ", file.size); // 파일 크기
|
||||
formData.append("CMNFLEREG", fileInfo.registrantId); // 등록자 ID
|
||||
|
||||
const response = await axios.post(`board/${boardId}/attachments`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert("게시물이 작성되었습니다.");
|
||||
goList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("게시물 작성 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
56
src/views/vacation/ProfileList.vue
Normal file
56
src/views/vacation/ProfileList.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="profile-list">
|
||||
<div v-for="profile in profiles" :key="profile.id" class="profile">
|
||||
<img :src="profile.avatar" alt="프로필 사진" class="avatar" />
|
||||
<div class="info">
|
||||
<p class="name">{{ profile.name }}</p>
|
||||
<p class="vacation-count">남은 휴가: {{ profile.remainingVacations }}일</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
profiles: [
|
||||
{ id: 1, name: '김철수', avatar: '/avatars/user1.png', remainingVacations: 15 },
|
||||
{ id: 2, name: '박영희', avatar: '/avatars/user2.png', remainingVacations: 11 },
|
||||
{ id: 3, name: '이민호', avatar: '/avatars/user3.png', remainingVacations: 10 },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.vacation-count {
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
181
src/views/vacation/VacationManagement.vue
Normal file
181
src/views/vacation/VacationManagement.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="vacation-management">
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="save-button-container">
|
||||
<button class="btn btn-success" @click="addVacationRequest">✔</button>
|
||||
</div>
|
||||
<div class="card app-calendar-wrapper">
|
||||
<div class="row g-0">
|
||||
<div class="col app-calendar-content">
|
||||
<div class="card shadow-none border-0">
|
||||
<div class="card-body pb-0">
|
||||
<full-calendar
|
||||
ref="fullCalendarRef"
|
||||
:events="calendarEvents"
|
||||
:options="calendarOptions"
|
||||
defaultView="dayGridMonth"
|
||||
class="flatpickr-calendar-only"
|
||||
>
|
||||
</full-calendar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="half-day-buttons">
|
||||
<button class="btn btn-info" :class="{ active: halfDayType === 'AM' }" @click="toggleHalfDay('AM')">☀️ 오전반차</button>
|
||||
<button class="btn btn-warning" :class="{ active: halfDayType === 'PM' }" @click="toggleHalfDay('PM')">🌙 오후반차</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import 'flatpickr/dist/flatpickr.min.css';
|
||||
import '@/assets/css/app-calendar.css';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const fullCalendarRef = ref(null);
|
||||
const calendarEvents = ref([]);
|
||||
const selectedDates = ref([]);
|
||||
const halfDayType = ref(null); // 오전/오후 반차 선택
|
||||
|
||||
const calendarOptions = reactive({
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'today',
|
||||
center: 'title',
|
||||
right: 'prev,next',
|
||||
},
|
||||
locale: 'ko',
|
||||
selectable: true,
|
||||
dateClick: handleDateClick,
|
||||
});
|
||||
|
||||
function handleDateClick(info) {
|
||||
const date = info.dateStr;
|
||||
const dayElement = info.dayEl;
|
||||
|
||||
if (!selectedDates.value.includes(date)) {
|
||||
selectedDates.value.push(date);
|
||||
if (halfDayType.value === 'AM') {
|
||||
dayElement.style.backgroundImage = 'linear-gradient(to bottom, #ade3ff 50%, transparent 50%)';
|
||||
} else if (halfDayType.value === 'PM') {
|
||||
dayElement.style.backgroundImage = 'linear-gradient(to top, #ade3ff 50%, transparent 50%)';
|
||||
} else {
|
||||
dayElement.style.backgroundColor = '#ade3ff';
|
||||
}
|
||||
} else {
|
||||
selectedDates.value = selectedDates.value.filter((d) => d !== date);
|
||||
dayElement.style.backgroundColor = '';
|
||||
dayElement.style.backgroundImage = '';
|
||||
}
|
||||
|
||||
halfDayType.value = null; // 날짜 클릭 후 반차 선택 초기화
|
||||
}
|
||||
|
||||
function toggleHalfDay(type) {
|
||||
halfDayType.value = halfDayType.value === type ? null : type;
|
||||
}
|
||||
|
||||
function addVacationRequest() {
|
||||
if (selectedDates.value.length === 0) {
|
||||
alert('Please select at least one date.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newEvents = selectedDates.value.map((date) => ({
|
||||
title: halfDayType.value ? `${halfDayType.value} Half Day Vacation` : 'Vacation',
|
||||
start: date,
|
||||
allDay: true,
|
||||
}));
|
||||
|
||||
calendarEvents.value = [...calendarEvents.value, ...newEvents];
|
||||
alert(`Vacation added for dates: ${selectedDates.value.join(', ')} as ${halfDayType.value || 'Full Day'}`);
|
||||
selectedDates.value = [];
|
||||
halfDayType.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vacation-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.save-button-container {
|
||||
position: fixed;
|
||||
top: 900px; /* 탑바 아래로 간격 조정 */
|
||||
right: 400px;
|
||||
z-index: 1050; /* 탑바보다 높은 값 */
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.save-button-container {
|
||||
top: 70px; /* 모바일에서 탑바 아래 간격 조정 */
|
||||
right: 5px;
|
||||
left: 5px;
|
||||
width: calc(100% - 10px); /* 모바일 화면에 맞게 크기 조정 */
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.half-day-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.fc-day-sun .fc-col-header-cell-cushion,
|
||||
.fc-day-sun a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.fc-day-sat .fc-col-header-cell-cushion,
|
||||
.fc-day-sat a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.flatpickr-calendar-only input.flatpickr-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pt-2.px-3 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
position: relative !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user