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

291 lines
9.5 KiB
Vue

<template>
<div class="container flex-grow-1 container-p-y">
<div class="row mb-4">
<!-- 검색창 -->
<div class="container col-8 px-3">
<search-bar @update:data="search" />
</div>
<!-- 글쓰기 -->
<div class="container col-2 px-12 py-2">
<router-link to="/board/write">
<WriteButton />
</router-link>
</div>
<div class="row g-3">
<!-- 셀렉트 박스 -->
<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 class="container col-1 px-0 py-2">
<label>
<input type="checkbox" v-model="showNotices" /> 공지 숨기기
</label>
</div>
<!-- 리스트 갯수 선택 -->
<div class="container col-1 px-0 py-2">
<select class="form-select" v-model="selectedSize" @change="handleSizeChange">
<option value="10">10개씩</option>
<option value="20">20개씩</option>
<option value="30">30개씩</option>
<option value="50">50개씩</option>
</select>
</div>
</div>
</div>
<br>
<!-- 게시판 -->
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th style="width: 8%;">번호</th>
<th style="width: 50%;">제목</th>
<th style="width: 15%;">작성자</th>
<th style="width: 12%;">작성일</th>
<th style="width: 10%;">조회수</th>
</tr>
</thead>
<tbody>
<!-- 공지사항 -->
<template v-if="pagination.currentPage === 1 && !showNotices">
<tr v-for="(notice, index) in noticeList"
:key="'notice-' + index"
class="notice-row clickable-row"
@click="goDetail(notice.id)">
<td>공지</td>
<td>
📌 {{ notice.title }}
<i v-if="notice.img" class="bi bi-image me-1"></i>
<i v-if="notice.hasAttachment" class="bi bi-paperclip"></i>
<span v-if="isNewPost(notice.date)" class="badge bg-danger text-white ms-2 new-badge">N</span>
</td>
<td>{{ notice.author }}</td>
<td>{{ notice.date }}</td>
<td>{{ notice.views }}</td>
</tr>
</template>
<!-- 일반 게시물 -->
<tr v-for="(post, index) in generalList"
:key="'post-' + index"
class="general-row clickable-row"
@click="goDetail(post.id)">
<td>{{ post.id }}</td>
<td>
{{ post.title }}
<i v-if="post.img" class="bi bi-image me-1"></i>
<i v-if="post.hasAttachment" class="bi bi-paperclip"></i>
<span v-if="isNewPost(post.date)" class="badge bg-danger text-white ms-2 new-badge">N</span>
</td>
<td>{{ post.author }}</td>
<td>{{ post.date }}</td>
<td>{{ post.views }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 페이지네이션 -->
<div class="row g-3">
<div class="mt-8">
<Pagination
v-bind="pagination"
@update:currentPage="handlePageChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from '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';
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 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) => {
router.push({ name: 'BoardDetail', params: { id } });
};
// 날짜 포맷 변환 함수 (오늘이면 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();
fetchGeneralPosts(1);
};
// 정렬 변경 핸들러
const handleSortChange = () => {
fetchGeneralPosts(1);
};
// 리스트 개수 변경 핸들러
const handleSizeChange = () => {
fetchGeneralPosts(1);
};
// 일반 게시물 데이터 로드
const fetchGeneralPosts = async (page = 1) => {
try {
const { data } = await axios.get("board/general", {
params: {
page,
size: selectedSize.value,
orderBy: selectedOrder.value,
searchKeyword: searchText.value
}
});
if (data?.data) {
console.log(data)
const totalPosts = data.data.total; // 전체 게시물 개수 받아오기 (API가 제공해야 함)
generalList.value = data.data.list.map((post, index) => ({
id: totalPosts - ((page - 1) * selectedSize.value) - index,
title: post.title,
author: post.author || '익명',
date: formatDate(post.date), // 날짜 변환 적용
views: post.cnt || 0,
hasAttachment: post.hasAttachment || false,
img: post.firstImageUrl || null
}));
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) {
console.error("데이터 오류:", 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),
views: post.cnt || 0,
hasAttachment: post.hasAttachment || false,
img: post.firstImageUrl || null
}));
}
} catch (error) {
console.error("데이터 오류:", error);
}
};
// 페이지 변경
const handlePageChange = (page) => {
if (page !== pagination.value.currentPage) {
fetchGeneralPosts(page);
}
};
// 데이터 로드
onMounted(() => {
fetchNoticePosts();
fetchGeneralPosts();
});
</script>
<style scoped>
/* 공지사항 스타일 */
.notice-row {
background-color: #f8f9fa;
}
.notice-row td {
color: #DC3545 !important;
}
/* 일반 게시물 스타일 */
.general-row {
background-color: white;
color: black;
}
/* 행 전체 클릭 가능 */
.clickable-row {
cursor: pointer;
}
.clickable-row:hover {
background-color: #f1f1f1;
}
/* 새 글 아이콘 크기 조정 */
.new-badge {
font-size: 0.65rem; /* 원래 크기의 약 절반 */
padding: 0.2em 0.4em; /* 배지 크기 조정 */
vertical-align: middle; /* 텍스트 정렬 */
}
</style>