localhost-front/src/views/board/BoardList.vue

388 lines
15 KiB
Vue

<template>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card">
<div class="card-header d-flex flex-column">
<!-- 검색창 -->
<div class="mb-3 w-100">
<search-bar @update:data="search" @keyup.enter="searchOnEnter" :initKeyword="searchText" class="flex-grow-1" />
</div>
<div class="d-flex align-items-center" style="gap: 15px">
<!-- 리스트 갯수 선택 -->
<select class="form-select w-auto" v-model="selectedSize" @change="handleSizeChange($event)" style="margin-left: 0">
<option value="10">10개씩</option>
<option value="20">20개씩</option>
<option value="30">30개씩</option>
<option value="50">50개씩</option>
<option value="100">100개씩</option>
</select>
<!-- 셀렉트 박스 -->
<select class="form-select w-auto" v-model="selectedOrder" @change="handleSortChange($event)">
<option value="date">날짜</option>
<option value="views">조회수</option>
</select>
<!-- 공지 접기 기능 -->
<div class="form-check mb-0">
<input
class="form-check-input mt-1"
type="checkbox"
v-model="showNotices"
id="hideNotices"
@change="handlePageFilter()"
/>
<label class="form-check-label" for="hideNotices">공지 숨기기</label>
</div>
<!-- 새 글쓰기 버튼 -->
<router-link to="/board/write" class="ms-auto">
<WriteButton />
</router-link>
</div>
</div>
<div class="card-datatable m">
<div class="table-responsive">
<table class="datatables-users table border-top dataTable dtr-column">
<thead>
<tr>
<th style="width: 11%" class="text-center fw-bold">번호</th>
<th style="width: 45%" class="text-center fw-bold">제목</th>
<th style="width: 10%" class="text-center fw-bold">작성자</th>
<th style="width: 15%" class="text-center fw-bold">작성일</th>
<th style="width: 9%" class="text-center fw-bold">조회수</th>
</tr>
</thead>
<tbody>
<!-- 공지사항 -->
<template v-if="pagination.currentPage === 1 && !showNotices">
<tr
v-for="(notice, index) in noticeList"
:key="'notice-' + index"
class="bg-label-gray fw-bold"
@click="goDetail(notice.id)"
>
<td class="text-center">공지</td>
<td class="cursor-pointer">
<div class="d-flex align-items-center">
<span class="me-1">📌</span>
<span>{{ truncateTitle(notice.title) }}</span>
<span v-if="notice.commentCount" class="text-danger fw-bold mx-1">
[ {{ notice.commentCount }} ]
</span>
<i v-if="notice.img" class="bi bi-image mx-1"></i>
<i
v-if="Array.isArray(notice.hasAttachment) && notice.hasAttachment.length > 0"
class="bi bi-paperclip"
></i>
<span v-if="isNewPost(notice.rawDate)" class="box-new badge text-white ms-2 fs-tiny"> N </span>
</div>
</td>
<td class="text-center">{{ notice.author }}</td>
<td class="text-center">{{ notice.date }}</td>
<td class="text-center">{{ notice.views }}</td>
</tr>
</template>
<!-- 일반 게시물 -->
<tr
v-for="(post, index) in generalList"
:key="'post-' + index"
class="invert-bg-white"
@click="goDetail(post.realId)"
>
<td class="text-center">{{ post.id }}</td>
<td class="cursor-pointer">
<div class="d-flex align-items-center">
{{ truncateTitle(post.title) }}
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
<i v-if="post.img" class="bi bi-image mx-1"></i>
<i
v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0"
class="bi bi-paperclip"
></i>
<span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span>
</div>
</td>
<td class="text-center">{{ post.nickname ? post.nickname : post.author }}</td>
<td class="text-center">{{ post.date }}</td>
<td class="text-center">{{ post.views }}</td>
</tr>
</tbody>
</table>
<!-- 게시물이 없을 때 -->
<div v-if="generalList.length === 0">
<p class="text-center pt-10 mt-2 mb-0 text-muted">게시물이 없습니다.</p>
</div>
</div>
</div>
<div class="card-footer">
<!-- 페이지네이션 -->
<div class="row g-3">
<div class="mt-8">
<Pagination v-if="pagination.pages" v-bind="pagination" @update:currentPage="handlePageChange" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import Pagination from '@c/pagination/Pagination.vue';
import SearchBar from '@c/search/SearchBar.vue';
//import { route, router } from '@/router';
import { useRoute, useRouter } from 'vue-router';
import WriteButton from '@c/button/WriteBtn.vue';
import axios from '@api';
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import 'bootstrap-icons/font/bootstrap-icons.css';
dayjs.extend(isToday);
dayjs.extend(isYesterday);
// 데이터 초기화
const generalList = ref([]);
const noticeList = ref([]);
const searchText = ref('');
const selectedOrder = ref('date');
const selectedSize = ref(10);
const showNotices = ref(false);
const router = useRouter();
const route = useRoute();
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 => {
saveFilterToStorage(id);
router.push({ name: 'BoardDetail', params: { id: id } });
};
const truncateTitle = title => {
return title.length > 28 ? title.slice(0, 28) + '...' : title;
};
// 로컬 스토리지 필터 저장
const saveFilterToStorage = seq => {
const query = {
page: 1,
size: selectedSize.value,
orderBy: selectedOrder.value,
searchText: searchText.value,
showNotice: showNotices.value,
};
// 목록으로 바로 보낼때 필터 유지값
//localStorage.setItem(`boardList_${seq}`, JSON.stringify(query));
};
// 스토리지 초기화
const clearFliterStorage = () => {
Object.keys(localStorage).forEach(key => {
if (key.startsWith('boardList_')) {
localStorage.removeItem(key);
}
});
};
// 날짜 포맷 변환 함수 (오늘이면 HH:mm, 아니면 YYYY-MM-DD)
const formatDate = dateString => {
const date = dayjs(dateString);
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
};
// 새로 올라온 게시물 여부 판단 (오늘 또는 어제 작성된 경우)
const isNewPost = dateString => {
const date = dayjs(dateString);
return date.isToday() || date.isYesterday();
};
// 검색 처리
const search = e => {
searchText.value = e.trim();
handlePageFilter();
};
// 정렬 변경 핸들러
const handleSortChange = e => {
selectedOrder.value = e.target.value;
handlePageFilter();
};
// 리스트 개수 변경 핸들러
const handleSizeChange = e => {
selectedSize.value = e.target.value;
handlePageFilter();
};
// 일반 게시물 데이터 로드
const fetchGeneralPosts = async (page = 1, keyword = '', orderBy = 'date', size = 10) => {
try {
const { data } = await axios.get('board/general', {
params: {
page,
size: size,
orderBy: orderBy,
searchKeyword: keyword,
},
});
if (data?.data) {
const totalPosts = data.data.total;
generalList.value = data.data.list.map((post, index) => ({
realId: post.id,
id: totalPosts - (page - 1) * selectedSize.value - index,
title: post.title,
author: post.author || '익명',
rawDate: post.date,
date: formatDate(post.date), // 날짜 변환 적용
views: post.cnt || 0,
hasAttachment: post.hasAttachment,
nickname: post.nickname || null,
img: post.firstImageUrl || null,
commentCount: post.commentCount,
}));
pagination.value = {
...pagination.value,
currentPage: data.data.pageNum,
pages: data.data.pages,
prePage: data.data.prePage,
nextPage: data.data.nextPage,
isFirstPage: data.data.isFirstPage,
isLastPage: data.data.isLastPage,
hasPreviousPage: data.data.hasPreviousPage,
hasNextPage: data.data.hasNextPage,
navigatePages: data.data.navigatePages,
navigatepageNums: data.data.navigatepageNums,
navigateFirstPage: data.data.navigateFirstPage,
navigateLastPage: data.data.navigateLastPage,
};
}
} catch (error) {}
};
// 공지사항 데이터 로드
const fetchNoticePosts = async () => {
try {
const { data } = await axios.get('board/notices', {
params: { searchKeyword: searchText.value },
});
if (data?.data) {
noticeList.value = data.data.map(post => ({
id: post.id,
title: post.title,
author: post.author || '관리자',
date: formatDate(post.date),
rawDate: post.date,
views: post.cnt || 0,
hasAttachment: post.hasAttachment,
img: post.firstImageUrl || null,
commentCount: post.commentCount,
}));
}
} catch (error) {}
};
// Enter 키를 눌렀을 때
const searchOnEnter = event => {
const searchTextValue = event.target.value.trim();
if (!searchTextValue || searchTextValue[0] === ' ') {
return; // 검색어가 비어있거나 첫 글자가 공백이면 실행 안 함
}
searchText.value = searchTextValue;
handlePageFilter();
};
// 페이지 변경
const handlePageChange = page => {
if (page !== pagination.value.currentPage) {
handlePageFilter(page);
}
};
// 페이지 이동 (검색 필터 유지)
const handlePageFilter = (page = 1) => {
router.push({
path: '/board',
query: {
page: page,
size: selectedSize.value,
orderBy: selectedOrder.value,
searchText: searchText.value,
showNotice: showNotices.value,
},
});
};
// 페이징, 검색 필터 감지
watch(
() => route.query,
newQuery => {
pagination.currentPage = newQuery.page || 1;
const keyword = newQuery?.searchText;
const orderBy = newQuery?.orderBy;
const size = newQuery?.size;
fetchGeneralPosts(pagination.currentPage, keyword, orderBy, size);
},
);
// 데이터 로드
onMounted(() => {
// 쿼리 파라미터에서 페이지 정보 추출
const page = route.query?.page;
const keyword = route.query?.searchText;
const orderBy = route.query?.orderBy;
const size = route.query?.size;
const showNotice = route.query?.showNotice;
// 필터 항목 세팅
if (keyword) searchText.value = keyword;
if (orderBy) selectedOrder.value = orderBy;
if (size) selectedSize.value = size;
if (showNotice) showNotices.value = showNotice == 'false' ? false : true;
clearFliterStorage();
fetchNoticePosts();
fetchGeneralPosts(page, keyword, orderBy, size);
});
</script>
<style scoped>
@media (max-width: 768px) {
.w-md-100 {
width: 100% !important;
}
}
.comment-count {
font-size: 0.9rem;
font-weight: bold;
border-radius: 4px;
padding: 2px 6px;
position: relative;
top: -1px;
}
</style>