Merge branch 'main' into khj
This commit is contained in:
commit
3681ed9031
@ -4,14 +4,14 @@
|
|||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
<ToastModal />
|
<ToastModal />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
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';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="mb-2 row">
|
<div class="mb-2 row">
|
||||||
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span :class="isEssential ? 'text-red' : 'none'">*</span>
|
<span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -11,21 +11,29 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 게시물 내용 섹션 -->
|
<!-- 게시물 내용 섹션 -->
|
||||||
<div class="col-md-10">
|
<div :class="contentColClass">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- 태그 -->
|
<!-- 태그 -->
|
||||||
<h6 class="badge rounded-pill bg-primary text-white mb-2">
|
<h6 class="badge rounded-pill bg-primary text-white mb-2">
|
||||||
{{ category }}
|
{{ category }}
|
||||||
</h6>
|
</h6>
|
||||||
<!-- 제목 -->
|
<!-- 제목 -->
|
||||||
<h5 class="card-title">{{ title }}</h5>
|
<h5 class="card-title">
|
||||||
|
{{ title }}
|
||||||
|
<span class="text-muted me-3" v-if="attachment">
|
||||||
|
<i class="fa-solid fa-paperclip"></i>
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
<!-- 본문 -->
|
<!-- 본문 -->
|
||||||
<p class="card-text str_wrap">{{ content }}</p>
|
<p class="card-text str_wrap">{{ content }}</p>
|
||||||
<!-- 날짜 -->
|
<!-- 날짜 -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<small class="text-muted">{{ formatDate(date) }}</small>
|
<small class="text-muted">{{ formattedDate }}</small>
|
||||||
<!-- 좋아요와 댓글 -->
|
<!-- 조회수, 좋아요, 댓글 -->
|
||||||
<div>
|
<div>
|
||||||
|
<span class="text-muted me-3">
|
||||||
|
<i class="fa-regular fa-eye"></i> {{ views || 0 }}
|
||||||
|
</span>
|
||||||
<span class="text-muted me-3">
|
<span class="text-muted me-3">
|
||||||
<i class="bx bx-like"></i> {{ likes || 0 }}
|
<i class="bx bx-like"></i> {{ likes || 0 }}
|
||||||
</span>
|
</span>
|
||||||
@ -40,49 +48,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { computed } from 'vue';
|
||||||
props: {
|
import { defineProps } from 'vue';
|
||||||
img: {
|
|
||||||
type: String,
|
// Props 정의
|
||||||
default: null,
|
const props = defineProps({
|
||||||
},
|
img: {
|
||||||
category: {
|
type: String,
|
||||||
type: String,
|
default: null,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
likes: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
comments: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
category: {
|
||||||
formatDate(dateString) {
|
type: String,
|
||||||
const date = new Date(dateString);
|
required: false,
|
||||||
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")}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
likes: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// computed 속성
|
||||||
|
const contentColClass = computed(() => {
|
||||||
|
return props.img ? 'col-md-10 col-12' : 'col-md-12';
|
||||||
|
});
|
||||||
|
|
||||||
|
// formattedDate을 computed로 정의
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
const date = new Date(props.date);
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -10,8 +10,10 @@
|
|||||||
:title="post.title"
|
:title="post.title"
|
||||||
:content="post.content"
|
:content="post.content"
|
||||||
:date="post.date"
|
:date="post.date"
|
||||||
|
:views="post.views"
|
||||||
:likes="post.likes"
|
:likes="post.likes"
|
||||||
:comments="post.comments"
|
:comments="post.comments"
|
||||||
|
:attachment="post.attachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,35 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination pagination-rounded justify-content-center">
|
<ul class="pagination pagination-rounded justify-content-center">
|
||||||
<!-- <li class="page-item first">
|
<!-- 첫 페이지 이동 -->
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevrons-left bx-sm"></i></a>
|
<li
|
||||||
</li> -->
|
class="page-item first"
|
||||||
<!-- <li class="page-item prev">
|
@click="emitPageChange(1)"
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevron-left bx-sm"></i></a>
|
:class="{ disabled: isFirstPage }"
|
||||||
</li> -->
|
>
|
||||||
<li class="page-item active">
|
<a class="page-link" href="javascript:void(0);">
|
||||||
<a class="page-link" href="javascript:void(0);">1</a>
|
<i class="tf-icon bx bx-chevrons-left bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">2</a>
|
<!-- 이전 페이지 이동 -->
|
||||||
|
<li
|
||||||
|
class="page-item prev"
|
||||||
|
@click="emitPageChange(navigateFirstPage-1)"
|
||||||
|
:class="{ disabled: !hasPreviousPage }"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevron-left bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">3</a>
|
<!-- 페이지 번호들 -->
|
||||||
|
<li
|
||||||
|
v-for="page in navigatepageNums"
|
||||||
|
:key="page"
|
||||||
|
:class="['page-item', { active: page === currentPage }]"
|
||||||
|
@click="emitPageChange(page)"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">{{ page }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">4</a>
|
<!-- 다음 페이지 이동 -->
|
||||||
|
<li
|
||||||
|
class="page-item next"
|
||||||
|
@click="emitPageChange(navigateLastPage+1)"
|
||||||
|
:class="{ disabled: !hasNextPage }"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevron-right bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">5</a>
|
<!-- 마지막 페이지 이동 -->
|
||||||
</li>
|
<li
|
||||||
<li class="page-item next">
|
class="page-item last"
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevron-right bx-sm"></i></a>
|
@click="emitPageChange(pages)"
|
||||||
</li>
|
:class="{ disabled: isLastPage }"
|
||||||
<li class="page-item last">
|
>
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevrons-right bx-sm"></i></a>
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevrons-right bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
// Props 정의
|
||||||
|
const props = defineProps({
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
prePage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nextPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isFirstPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isLastPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasPreviousPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasNextPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigatePages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigatepageNums: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigateFirstPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigateLastPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 이벤트 정의
|
||||||
|
const emit = defineEmits(['update:currentPage']);
|
||||||
|
|
||||||
|
// 페이지 변경 메서드
|
||||||
|
const emitPageChange = (page) => {
|
||||||
|
if (page !== props.currentPage && page >= 1 && page <= props.pages) {
|
||||||
|
emit('update:currentPage', page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@ -1,24 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<!-- 검색 -->
|
<div class="row mb-4">
|
||||||
<search-bar @update:data="search" />
|
<!-- 검색창 -->
|
||||||
|
<div class="col">
|
||||||
|
<search-bar @update:data="search" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 리스트 -->
|
<div class="row">
|
||||||
<div class="row g-3">
|
<!-- 새 글쓰기 -->
|
||||||
<div class="mt-8">
|
<div class="mb-4">
|
||||||
<router-link to="/board/write">
|
<router-link to="/board/write">
|
||||||
<WriteButton />
|
<WriteButton />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<board-card :posts="paginatedList" @click="goDetail" />
|
<!-- 공지사항 리스트 -->
|
||||||
|
<div v-if="pagination.currentPage === 1" class="mb-8">
|
||||||
|
<div class="row g-3">
|
||||||
|
<h3>공지사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<BoardCardList :posts="noticeList" @click="goDetail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 페이지네이션 -->
|
<!-- 일반 리스트 -->
|
||||||
|
<div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<h3 class="col">일반게시판</h3>
|
||||||
|
|
||||||
|
<!-- 셀렉트 박스 -->
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<select class="form-select" v-model="selectedOrder" @change="handleSortChange">
|
||||||
|
<option value="date">최신날짜</option>
|
||||||
|
<option value="views">조회수</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<BoardCardList :posts="generalList" @click="goDetail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 페이지네이션 -->
|
||||||
|
<div class="row g-3">
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<pagination
|
<Pagination
|
||||||
:current-page="currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:total-pages="totalPages"
|
:pages="pagination.pages"
|
||||||
@update:page="changePage"
|
:prePage="pagination.prePage"
|
||||||
|
:nextPage="pagination.nextPage"
|
||||||
|
:isFirstPage="pagination.isFirstPage"
|
||||||
|
:isLastPage="pagination.isLastPage"
|
||||||
|
:hasPreviousPage="pagination.hasPreviousPage"
|
||||||
|
:hasNextPage="pagination.hasNextPage"
|
||||||
|
:navigatePages="pagination.navigatePages"
|
||||||
|
:navigatepageNums="pagination.navigatepageNums"
|
||||||
|
:navigateFirstPage="pagination.navigateFirstPage"
|
||||||
|
:navigateLastPage="pagination.navigateLastPage"
|
||||||
|
@update:currentPage="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +69,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import BoardCard from '@/components/list/BoardCardList.vue';
|
import BoardCardList from '@/components/list/BoardCardList.vue';
|
||||||
import Pagination from '@c/pagination/Pagination.vue';
|
import Pagination from '@c/pagination/Pagination.vue';
|
||||||
import SearchBar from '@c/search/SearchBar.vue';
|
import SearchBar from '@c/search/SearchBar.vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
@ -35,75 +77,118 @@ import WriteButton from '@c/button/WriteBtn.vue';
|
|||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
// 데이터 초기화
|
// 데이터 초기화
|
||||||
const list = ref([]);
|
const generalList = ref([]);
|
||||||
|
const noticeList = ref([]);
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
|
const selectedOrder = ref('date');
|
||||||
|
const sortDirection = ref('desc');
|
||||||
|
const pagination = ref({
|
||||||
|
currentPage: 1,
|
||||||
|
pages: 1,
|
||||||
|
prePage: 0,
|
||||||
|
nextPage: 1,
|
||||||
|
isFirstPage: true,
|
||||||
|
isLastPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
hasNextPage: false,
|
||||||
|
navigatePages: 10,
|
||||||
|
navigatepageNums: [1],
|
||||||
|
navigateFirstPage: 1,
|
||||||
|
navigateLastPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
// 상세 페이지 이동
|
// 상세 페이지 이동
|
||||||
const goDetail = (id) => {
|
const goDetail = (id) => {
|
||||||
console.log('Navigating to ID:', id)
|
|
||||||
router.push({ name: 'BoardDetail', params: { id } });
|
router.push({ name: 'BoardDetail', params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 검색 처리
|
// 검색 처리
|
||||||
const search = (e) => {
|
const search = (e) => {
|
||||||
searchText.value = e.trim();
|
searchText.value = e.trim();
|
||||||
|
fetchGeneralPosts(1);
|
||||||
|
fetchNoticePosts(searchText.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 검색 결과 필터링
|
// 정렬 변경 핸들러
|
||||||
const filteredList = computed(() =>
|
const handleSortChange = () => {
|
||||||
list.value.filter((item) =>
|
fetchGeneralPosts(1);
|
||||||
item.title.toLowerCase().includes(searchText.value.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 페이지네이션 상태
|
|
||||||
const currentPage = ref(1); // 현재 페이지 번호
|
|
||||||
const itemsPerPage = 5; // 한 페이지에 표시할 아이템 수
|
|
||||||
|
|
||||||
// 현재 페이지 데이터 계산
|
|
||||||
const paginatedList = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * itemsPerPage;
|
|
||||||
const end = start + itemsPerPage;
|
|
||||||
return filteredList.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 총 페이지 수 계산
|
|
||||||
const totalPages = computed(() => {
|
|
||||||
return Math.ceil(filteredList.value.length / itemsPerPage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 페이지 변경 함수
|
|
||||||
const changePage = (page) => {
|
|
||||||
if (page >= 1 && page <= totalPages.value) {
|
|
||||||
currentPage.value = page;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 데이터 로드
|
// 게시물 데이터 로드(일반)
|
||||||
const fetchPosts = async () => {
|
const fetchGeneralPosts = async (page = 1) => {
|
||||||
const response = await axios.get("board/general");
|
const response = await axios.get("board/general", {
|
||||||
console.log(response.data.data.list)
|
params: {
|
||||||
|
page: page,
|
||||||
|
orderBy: selectedOrder.value,
|
||||||
|
sortDirection: sortDirection.value,
|
||||||
|
searchKeyword: searchText.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data && response.data.data && Array.isArray(response.data.data.list)) {
|
if (response.data && response.data.data) {
|
||||||
list.value = response.data.data.list.map((post, index) => ({
|
const data = response.data.data;
|
||||||
|
// 게시물 리스트 업데이트
|
||||||
|
generalList.value = data.list.map((post, index) => ({
|
||||||
...post,
|
...post,
|
||||||
id: post.id || index,
|
id: post.id || index,
|
||||||
img: post.img || null,
|
img: post.img || null,
|
||||||
likes: post.likes || 0,
|
views: post.cnt || 0,
|
||||||
comments: post.comments || 0,
|
likes: post.likeCount != null ? post.likeCount : null,
|
||||||
|
comments: post.commentCount != null ? post.commentCount : null,
|
||||||
|
attachment: post.hasAttachment || false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 페이지네이션 정보 업데이트
|
||||||
|
pagination.value = {
|
||||||
|
currentPage: data.pageNum,
|
||||||
|
pages: data.pages,
|
||||||
|
prePage: data.prePage,
|
||||||
|
nextPage: data.nextPage,
|
||||||
|
isFirstPage: data.isFirstPage,
|
||||||
|
isLastPage: data.isLastPage,
|
||||||
|
hasPreviousPage: data.hasPreviousPage,
|
||||||
|
hasNextPage: data.hasNextPage,
|
||||||
|
navigatePages: data.navigatePages,
|
||||||
|
navigatepageNums: data.navigatepageNums,
|
||||||
|
navigateFirstPage: data.navigateFirstPage,
|
||||||
|
navigateLastPage: data.navigateLastPage
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error("데이터 오류:", response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 게시물 데이터 로드(공지사항)
|
||||||
|
const fetchNoticePosts = async () => {
|
||||||
|
const response = await axios.get("board/notices", {
|
||||||
|
params: {
|
||||||
|
searchKeyword: searchText.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.data && Array.isArray(response.data.data)) {
|
||||||
|
noticeList.value = response.data.data.map((post, index) => ({
|
||||||
|
...post,
|
||||||
|
id: post.id || index,
|
||||||
|
img: post.img || null,
|
||||||
|
views: post.cnt || 0,
|
||||||
|
attachment: post.hasAttachment || false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected API response structure:", response.data);
|
console.error("데이터 오류:", response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지 변경
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
if (page !== pagination.value.currentPage) {
|
||||||
|
fetchGeneralPosts(page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 데이터 로드
|
// 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPosts();
|
fetchGeneralPosts();
|
||||||
|
fetchNoticePosts();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 필요에 따라 스타일 추가 */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -5,17 +5,17 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<!-- 프로필 헤더 -->
|
<!-- 프로필 헤더 -->
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<BoardProfile :boardId="currentBoardId.value" :profileName="profileName" />
|
<BoardProfile :boardId="currentBoardId" :profileName="profileName" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 게시글 내용 -->
|
<!-- 게시글 내용 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-4">{{ boardTitle }}</h5>
|
<h5 class="mb-4">{{ boardTitle }}</h5>
|
||||||
<!-- HTML 콘텐츠 렌더링 -->
|
<!-- HTML 콘텐츠 렌더링 -->
|
||||||
<div class="board-content" v-html="boardContent"></div>
|
<div class="board-content text-body" style="line-height: 1.6;" v-html="convertedContent"></div>
|
||||||
<!-- 첨부파일 목록 -->
|
<!-- 첨부파일 목록 -->
|
||||||
<ul v-if="attachments.length" class="attachments mt-4">
|
<ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
|
||||||
<li v-for="(attachment, index) in attachments" :key="index">
|
<li v-for="(attachment, index) in attachments" :key="index" class="mb-2">
|
||||||
<a :href="attachment.url" target="_blank">{{ attachment.name }}</a>
|
<a :href="attachment.url" target="_blank" class="text-decoration-none">{{ attachment.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- 댓글 영역 -->
|
<!-- 댓글 영역 -->
|
||||||
@ -23,10 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 수정 버튼 -->
|
<!-- 수정 버튼 -->
|
||||||
<div class="card-footer d-flex justify-content-end">
|
<div class="card-footer d-flex justify-content-end">
|
||||||
<button
|
<button class="btn btn-primary" @click="goToEditPage">
|
||||||
class="btn btn-primary"
|
|
||||||
@click="goToEditPage"
|
|
||||||
>
|
|
||||||
글 수정
|
글 수정
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -42,11 +39,14 @@ import BoardProfile from '@c/board/BoardProfile.vue';
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
import Quill from 'quill';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('익명 사용자');
|
const profileName = ref('익명 사용자');
|
||||||
const boardTitle = ref('제목 없음');
|
const boardTitle = ref('제목 없음');
|
||||||
const boardContent = ref('내용 없음');
|
const boardContent = ref('');
|
||||||
|
const convertedContent = ref('내용 없음');
|
||||||
const comments = ref([]);
|
const comments = ref([]);
|
||||||
const attachments = ref([]);
|
const attachments = ref([]);
|
||||||
|
|
||||||
@ -54,7 +54,6 @@ const attachments = ref([]);
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentBoardId = ref(Number(route.params.id));
|
const currentBoardId = ref(Number(route.params.id));
|
||||||
console.log(currentBoardId.value)
|
|
||||||
|
|
||||||
// 글 수정 페이지로 이동
|
// 글 수정 페이지로 이동
|
||||||
const goToEditPage = () => {
|
const goToEditPage = () => {
|
||||||
@ -71,7 +70,22 @@ const fetchBoardDetails = async () => {
|
|||||||
const boardDetail = data.boardDetail || {};
|
const boardDetail = data.boardDetail || {};
|
||||||
profileName.value = boardDetail.author || '익명 사용자';
|
profileName.value = boardDetail.author || '익명 사용자';
|
||||||
boardTitle.value = boardDetail.title || '제목 없음';
|
boardTitle.value = boardDetail.title || '제목 없음';
|
||||||
boardContent.value = boardDetail.content || '내용 없음';
|
boardContent.value = boardDetail.content || '';
|
||||||
|
|
||||||
|
// Quill을 사용하여 Delta 데이터를 HTML로 변환
|
||||||
|
if (boardContent.value) {
|
||||||
|
try {
|
||||||
|
const quillContainer = document.createElement('div');
|
||||||
|
const quillInstance = new Quill(quillContainer);
|
||||||
|
quillInstance.setContents(JSON.parse(boardContent.value));
|
||||||
|
convertedContent.value = DOMPurify.sanitize(quillContainer.innerHTML);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Delta 데이터 변환 오류:', parseError);
|
||||||
|
convertedContent.value = '내용을 표시할 수 없습니다.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
convertedContent.value = '내용 없음';
|
||||||
|
}
|
||||||
|
|
||||||
attachments.value = data.attachments || [];
|
attachments.value = data.attachments || [];
|
||||||
comments.value = data.comments || [];
|
comments.value = data.comments || [];
|
||||||
@ -83,7 +97,6 @@ const fetchBoardDetails = async () => {
|
|||||||
|
|
||||||
// 컴포넌트 마운트 시 데이터 로드
|
// 컴포넌트 마운트 시 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('Route Params:', route.params);
|
|
||||||
fetchBoardDetails();
|
fetchBoardDetails();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -9,36 +9,69 @@
|
|||||||
|
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<FormInput title="제목" name="title" :is-essential="true" :is-alert="titleAlert" @update:data="title = $event" />
|
|
||||||
|
|
||||||
<FormSelect title="카테고리" name="cate" :is-essential="true" :data="categoryList" @update:data="category = $event" />
|
|
||||||
|
|
||||||
<FormInput
|
<FormInput
|
||||||
v-show="category == 1"
|
title="제목"
|
||||||
title="비밀번호"
|
name="title"
|
||||||
name="pw"
|
|
||||||
type="password"
|
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="passwordAlert"
|
:is-alert="titleAlert"
|
||||||
@update:data="password = $event"
|
v-model="title"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormFile title="첨부파일" name="files" :is-alert="attachFilesAlert" @update:data="attachFiles = $event" />
|
<!-- 카테고리 선택 -->
|
||||||
|
<div class="mb-4 d-flex align-items-center">
|
||||||
|
<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
|
||||||
|
v-for="(categoryName, index) in categoryList"
|
||||||
|
:key="index"
|
||||||
|
class="form-check me-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
:id="`category-${index}`"
|
||||||
|
:value="index"
|
||||||
|
v-model="category"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="`category-${index}`">
|
||||||
|
{{ categoryName }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 비밀번호 필드 -->
|
||||||
|
<div v-if="category === 1" class="mb-4">
|
||||||
|
<FormInput
|
||||||
|
title="비밀번호"
|
||||||
|
name="pw"
|
||||||
|
type="password"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-alert="passwordAlert"
|
||||||
|
v-model="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormFile
|
||||||
|
title="첨부파일"
|
||||||
|
name="files"
|
||||||
|
:is-alert="attachFilesAlert"
|
||||||
|
@update:data="attachFiles = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||||
내용
|
내용
|
||||||
<span class="text-red">*</span>
|
<span class="text-danger">*</span>
|
||||||
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<!-- <TEditor @update:data="content = $event"/> -->
|
|
||||||
<QEditor @update:data="content = $event" />
|
<QEditor @update:data="content = $event" />
|
||||||
</div>
|
</div>
|
||||||
</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 right" @click="goList"><i class='bx bx-left-arrow-alt'></i></button>
|
<button type="button" class="btn btn-info" @click="goList"><i class='bx bx-left-arrow-alt'></i></button>
|
||||||
<button type="button" class="btn btn-primary ms-1" @click="write"><i class='bx bx-check'></i></button>
|
<button type="button" class="btn btn-primary ms-1" @click="write"><i class='bx bx-check'></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,114 +82,73 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import TEditor from '@c/editor/TEditor.vue';
|
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormSelect from '@c/input/FormSelect.vue';
|
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
import { ref, watch } from 'vue';
|
import { ref } from 'vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
const categoryList = ['자유', '익명', '공지사항'];
|
const categoryList = ['자유', '익명', '공지사항']; // 카테고리 이름
|
||||||
// input 경고문 만들어야함!!
|
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const category = ref(0);
|
const category = ref(0); // 기본값 0
|
||||||
const content = ref('');
|
const content = ref('');
|
||||||
const attachFiles = ref(null);
|
const attachFiles = ref(null);
|
||||||
|
|
||||||
//input 경고창 관리
|
const titleAlert = ref(false);
|
||||||
const titleAlert = ref(true);
|
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const contentAlert = ref(false);
|
const contentAlert = ref(false);
|
||||||
const attachFilesAlert = ref(false);
|
const attachFilesAlert = ref(false);
|
||||||
|
|
||||||
|
|
||||||
const goList = () => {
|
const goList = () => {
|
||||||
// 목록으로 이동 나중엔 페이지 정보 ,검색 정보도 붙여야됨
|
|
||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
const write = async () => {
|
const write = async () => {
|
||||||
// 입력값 유효성 검사
|
titleAlert.value = !title.value;
|
||||||
if (!title.value) {
|
passwordAlert.value = category.value === 1 && !password.value;
|
||||||
titleAlert.value = true;
|
contentAlert.value = !content.value;
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
titleAlert.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.value === 1 && !password.value) {
|
if (titleAlert.value || passwordAlert.value || contentAlert.value) {
|
||||||
passwordAlert.value = true;
|
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
passwordAlert.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content.value) {
|
|
||||||
contentAlert.value = true;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
contentAlert.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 게시물 작성 데이터 준비
|
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value,
|
LOCBRDTTL: title.value,
|
||||||
LOCBRDCON: content.value,
|
LOCBRDCON: content.value,
|
||||||
LOCBRDPWD: category.value === 1 ? password.value : null,
|
LOCBRDPWD: category.value === 1 ? password.value : null,
|
||||||
LOCBRDTYP: category.value === 1 ? 'S' : 'F', //공지사항 추가해야함!!
|
LOCBRDTYP: category.value === 1 ? 'S' : 'F',
|
||||||
// MEMBERSEQ: 로그인이용자 id(세션)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 작성 API 호출
|
|
||||||
const { data: boardResponse } = await axios.post('board', boardData);
|
const { data: boardResponse } = await axios.post('board', boardData);
|
||||||
const boardId = boardResponse.data.boardId;
|
const boardId = boardResponse.data.CMNBRDSEQ;
|
||||||
|
|
||||||
// 첨부파일 처리
|
|
||||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||||
for (const file of attachFiles.value) {
|
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();
|
const formData = new FormData();
|
||||||
formData.append("MEMBERSEQ",registrantId); // 첨부 파일
|
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
||||||
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, {
|
formData.append('CMNBRDSEQ', boardId);
|
||||||
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
|
formData.append('CMNFLESIZ', file.size);
|
||||||
|
formData.append('CMNFLEPAT', 'boardfile');
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
await axios.post(`board/${boardId}/attachments`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("게시물이 작성되었습니다.");
|
alert('게시물이 작성되었습니다.');
|
||||||
goList();
|
goList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert("게시물 작성 중 오류가 발생했습니다.");
|
alert('게시물 작성 중 오류가 발생했습니다.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-red {
|
|
||||||
color: red;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user