Merge branch 'main' into login

This commit is contained in:
yoon 2025-02-03 15:12:24 +09:00
commit 66061435e1
9 changed files with 295 additions and 183 deletions

View File

@ -1,20 +1,20 @@
<template>
<div>
<BoardProfile :profileName="comment.author" :showDetail="false" :author="true" />
<div class="my-6">
<p>{{ comment.content }}</p>
<BoardProfile :profileName="comment.author" :showDetail="false" :author="true" :isChild="isChild" />
<div class="mt-6">
<p class="m-0">{{ comment.content }}</p>
</div>
<PlusButton v-if="isPlusButton" @click="toggleComment"/>
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
<BoardComentArea v-if="isComment" @submit="submitComment"/>
<!-- 대댓글 -->
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
<li
v-for="child in comment.children"
:key="child.id"
class="pt-6 ps-10"
class="pt-8 ps-10"
>
<BoardComment :comment="child" :isPlusButton="false" @submitComment="addChildComment" />
<BoardComment :comment="child" :isPlusButton="false" :isChild="true" @submitComment="addChildComment" />
</li>
</ul>
<!-- <ul class="list-unstyled twoDepth">
@ -46,7 +46,11 @@ const props = defineProps({
isPlusButton: {
type: Boolean,
default: true,
}
},
isChild: {
type: Boolean,
default: false,
},
});
// emits

View File

@ -3,18 +3,15 @@
<li
v-for="comment in comments"
:key="comment.id"
class="mt-6"
class="mt-8"
>
<BoardComment :comment="comment" @submitComment="addComment" />
</li>
</ul>
<Pagination/>
</template>
<script setup>
import { ref } from 'vue';
import Pagination from '../pagination/Pagination.vue';
import BoardComment from './BoardComment.vue'
const comments = ref([
@ -48,4 +45,4 @@ const comments = ref([
children: [],
},
]);
</script>
</script>

View File

@ -3,7 +3,7 @@
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
<!-- 제목 섹션 -->
<div class="me-1">
<h5 class="mb-0">{{ boardTitle }}</h5>
<h5 class="mb-0">{{ boardTitle }}adada</h5>
</div>
<!-- 첨부파일 섹션 -->
<div v-if="dropdownItems.length > 0" class="btn-group">

View File

@ -10,13 +10,13 @@
<span>2024.12.10 10:46</span>
<template v-if="showDetail">
<span>
<i class="fa-regular fa-eye"></i> 1
<i class="fa-regular fa-eye"></i> {{ views }}
</span>
<span>
<i class="fa-regular fa-thumbs-up"></i> 1
<i class="fa-regular fa-thumbs-up"></i> {{ likes }}
</span>
<span>
<i class="fa-regular fa-thumbs-down"></i> 1
<i class="fa-regular fa-thumbs-down"></i> {{ dislikes }}
</span>
</template>
</div>
@ -38,7 +38,7 @@
<i class='bx bx-trash'></i>
</button> -->
</template>
<BoardRecommendBtn :likeClicked="true" :dislikeClicked="false" :isRecommend="false" />
<BoardRecommendBtn v-if="!isChild" :isRecommend="false" />
</template>
</div>
</div>
@ -50,13 +50,12 @@ 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({
const props = defineProps({
profileName: {
type: String,
default: '익명',
@ -73,6 +72,22 @@ defineProps({
type: Boolean,
default: false,
},
views: {
type: Number,
default: 0,
},
likes: {
type: Number,
default: null,
},
dislikes: {
type: Number,
default: null,
},
isChild: {
type: Boolean,
default: false,
}
});
const boardId = 100; //!!

View File

@ -1,14 +1,16 @@
<template v-if="isRecommend">
<button class="btn btn-label-primary btn-icon" :class="likeClicked ? 'clicked' : '', bigBtn ? 'big' : '' ">
<i class="fa-regular fa-thumbs-up"></i> <span class="num">1</span>
<button class="btn btn-label-primary btn-icon" :class="{'clicked': likeClicked, 'big': bigBtn}" @click="handleLike">
<i class="fa-regular fa-thumbs-up"></i> <span class="num">{{ likeCount }}</span>
</button>
<button class="btn btn-label-danger btn-icon" :class="dislikeClicked ? 'clicked' : '', bigBtn ? 'big' : '' ">
<i class="fa-regular fa-thumbs-down"></i> <span class="num">1</span>
<button class="btn btn-label-danger btn-icon" :class="{'clicked': dislikeClicked, 'big': bigBtn}" @click="handleDislike">
<i class="fa-regular fa-thumbs-down"></i> <span class="num">{{ dislikeCount }}</span>
</button>
</template>
<script setup>
defineProps({
import { ref, watch } from 'vue';
const props = defineProps({
likeClicked : {
type : Boolean,
default : false,
@ -24,14 +26,71 @@ defineProps({
isRecommend: {
type:Boolean,
default:true,
}
},
boardId: {
type: Number,
required: true,
},
commentId: {
type: Number,
default: null,
},
likeCount: {
type: Number,
default: 0,
},
dislikeCount: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['updateReaction']);
const likeClicked = ref(props.likeClicked);
const dislikeClicked = ref(props.dislikeClicked);
const likeCount = ref(props.likeCount);
const dislikeCount = ref(props.dislikeCount);
// likeCount dislikeCount
watch(() => props.likeCount, (newVal) => {
likeCount.value = newVal;
});
watch(() => props.dislikeCount, (newVal) => {
dislikeCount.value = newVal;
});
const handleLike = () => {
likeClicked.value = !likeClicked.value;
likeCount.value += likeClicked.value ? 1 : -1;
emit('updateReaction', { type: 'like', boardId: props.boardId, commentId: props.commentId });
if(likeClicked.value === true){
if(dislikeClicked.value === true) {
dislikeClicked.value = false;
dislikeCount.value += -1
}
}
};
const handleDislike = () => {
dislikeClicked.value = !dislikeClicked.value;
dislikeCount.value += dislikeClicked.value ? 1 : -1;
emit('updateReaction', { type: 'dislike', boardId: props.boardId, commentId: props.commentId });
if(dislikeClicked.value === true){
if(likeClicked.value === true) {
likeClicked.value = false;
likeCount.value += -1
}
}
};
</script>
<style scoped>
.btn + .btn {
margin-left: 5px;
}
}
.num {
margin-left: 5px;
@ -73,4 +132,4 @@ defineProps({
font-size: 12px;
}
}
</style>
</style>

View File

@ -1,142 +1,124 @@
<template>
<div class="card mb-3 shadow-sm">
<div class="row g-0">
<!-- 이미지 섹션 -->
<div v-if="img" class="col-sm-2">
<img
:src="img"
alt="이미지"
class="img-fluid rounded-start"
style="object-fit: cover; height: 100%; width: 100%;"
/>
</div>
<!-- 게시물 내용 섹션 -->
<div :class="contentColClass">
<div class="card-body">
<!-- 태그 -->
<h6 class="badge rounded-pill bg-primary text-white mb-2">
{{ category }}
</h6>
<!-- 제목 -->
<h5 class="card-title">
{{ title }}
<span class="text-muted me-3" v-if="attachment">
<i class="fa-solid fa-paperclip"></i>
</span>
</h5>
<!-- 본문 -->
<div class="card-text str_wrap my-5">{{ content }}</div>
<!-- 날짜 -->
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start">
<small class="text-muted">{{ formattedDate }}</small>
<!-- 조회수, 좋아요, 댓글 -->
<div class="d-flex mt-2 mt-sm-0">
<span class="text-muted me-3">
<i class="fa-regular fa-eye"></i> {{ views || 0 }}
</span>
<span class="text-muted me-3" v-if="likes != null">
<i class="bx bx-like"></i> {{ likes }}
</span>
<span class="text-muted" v-if="comments !== null">
<i class="bx bx-comment"></i> {{ comments }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3 shadow-sm fixed-card" style="width: 18rem; height: 17rem">
<!-- 이미지가 있을 경우 card-img-top으로 표시 -->
<img
v-if="img"
:src="img"
class="card-img-top"
alt="이미지"
style="object-fit: cover; height: 100px;"
/>
<div class="card-body">
<!-- 카테고리 태그 (존재할 경우) -->
<h6 v-if="category" class="badge rounded-pill bg-primary text-white mb-2">
{{ category }}
</h6>
<!-- 제목과 첨부파일 아이콘 -->
<h5 class="card-title">
{{ title }}
<span class="text-muted" v-if="attachment">
<i class="fa-solid fa-paperclip"></i>
</span>
</h5>
<!-- 본문 -->
<p class="card-text limit-two-lines">{{ content }}</p>
<!-- 날짜 조회수, 좋아요, 댓글 -->
<p class="card-text">
<small class="text-muted">{{ formattedDate }}</small>
<div class="d-flex mt-2 mt-sm-0">
<span class="text-muted me-3">
<i class="fa-regular fa-eye"></i> {{ views || 0 }}
</span>
<span class="text-muted me-3" v-if="comments !== null">
<i class="bx bx-comment"></i> {{ comments }}
</span>
<span class="text-muted me-3" v-if="likes != null">
<i class="bx bx-like"></i> {{ likes }}
</span>
<span class="text-muted me-3" v-if="dislikes != null">
<i class="bx bx-dislike"></i> {{ dislikes }}
</span>
</div>
</p>
</div>
</div>
</template>
</template>
<script setup>
import { computed } from 'vue';
import { defineProps } from 'vue';
<script setup>
import { computed } from 'vue';
import { defineProps } from 'vue';
// Props
const props = defineProps({
const props = defineProps({
img: {
type: String,
default: null,
type: String,
default: null,
},
category: {
type: String,
required: false,
type: String,
required: false,
},
title: {
type: String,
required: true,
type: String,
required: true,
},
content: {
type: String,
required: true,
type: String,
required: true,
},
date: {
type: String,
required: true,
type: String,
required: true,
},
views: {
type: Number,
default: 0,
type: Number,
default: 0,
},
likes: {
type: Number,
default: null,
type: Number,
default: null,
},
dislikes: {
type: Number,
default: null,
},
comments: {
type: Number,
default: null,
type: Number,
default: null,
},
attachment: {
type: Boolean,
default: false,
type: Boolean,
default: false,
}
});
});
// computed
const contentColClass = computed(() => {
return props.img ? 'col-sm-10 col-12' : 'col-sm-12';
});
const formattedDate = computed(() => {
const dateObj = new Date(props.date);
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${String(dateObj.getDate()).padStart(2, '0')} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;
});
</script>
// 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>
<style>
/* 카드 스타일 */
.card {
<style scoped>
.card {
border: 1px solid #e6e6e6;
border-radius: 8px;
transition: transform 0.2s ease-in-out;
}
}
.card:hover {
.card:hover {
transform: scale(1.02);
}
}
/* 텍스트 줄임 표시 */
.str_wrap {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* card-img-top의 모서리 둥글게 처리 */
.card-img-top {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
/* 이미지 스타일 */
.img-fluid {
border-radius: 8px 0 0 8px;
/* 내용 텍스트를 두 줄로 제한 */
.limit-two-lines {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 태그 배지 스타일 */
.badge {
font-size: 0.8rem;
padding: 5px 10px;
}
</style>
</style>

View File

@ -1,21 +1,25 @@
<template>
<div class="mt-4">
<div class="container my-4">
<div class="row">
<div v-if="posts.length === 0" class="text-center">
<p class="text-muted mt-4">게시물이 없습니다.</p>
</div>
<div v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
<div class="col-md-3 mb-4 d-flex justify-content-center" v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
<BoardCard
:img="post.img || null"
:category="post.category || ''"
:img="post.img || null"
:category="post.category || ''"
:title="post.title"
:content="post.content"
:date="post.date"
:views="post.views || 0"
:views="post.views || 0"
:likes="post.likes"
:dislikes="post.dislikes"
v-bind="getBoardCardProps(post)"
:attachment="post.attachment || false"
@click="() => goDetail(post.id)"
:attachment="post.attachment || false"
@click="() => goDetail(post.id)"
/>
</div>
</div>
</div>
</template>

View File

@ -1,36 +1,19 @@
<template>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="container flex-grow-1 container-p-y">
<div class="row mb-4">
<!-- 검색창 -->
<div class="col">
<div class="container col-11 px-10 ">
<search-bar @update:data="search" />
</div>
</div>
<div class="row">
<!-- 글쓰기 -->
<div class="mb-4">
<div class="container col-1 px-12 py-2">
<router-link to="/board/write">
<WriteButton />
</router-link>
</div>
</div>
<!-- 공지사항 리스트 -->
<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="row g-3">
<!-- 셀렉트 박스 -->
<div class="col-12 col-md-auto">
<select class="form-select" v-model="selectedOrder" @change="handleSortChange">
@ -39,6 +22,16 @@
</select>
</div>
</div>
<!-- 공지사항 리스트 -->
<div v-if="pagination.currentPage === 1" class="mb-8">
<div class="row">
<BoardCardList :posts="noticeList" @click="goDetail" />
</div>
</div>
<!-- 일반 리스트 -->
<div>
<div class="row">
<BoardCardList :posts="generalList" @click="goDetail" />
</div>
@ -133,7 +126,8 @@ const fetchGeneralPosts = async (page = 1) => {
id: post.id || index,
img: post.firstImageUrl || null,
views: post.cnt || 0,
likes: post.likeCount != null ? post.likeCount : null,
likes: post.likeCount != null ? post.dislikeCount : null,
dislikes: post.dislikeCount != null ? post.likeCount : null,
comments: post.commentCount != null ? post.commentCount : null,
attachment: post.hasAttachment || false,
content: post.plainContent || '',

View File

@ -5,28 +5,62 @@
<div class="card">
<!-- 프로필 헤더 -->
<div class="card-header">
<BoardProfile :boardId="currentBoardId" :profileName="profileName" />
<hr/>
<BoardProfile
:boardId="currentBoardId"
:profileName="profileName"
:views="views"
:likes="likes"
:dislikes="dislikes"
class="pb-6 border-bottom"
/>
</div>
<!-- 게시글 내용 -->
<div class="card-body">
<h5 class="mb-4">{{ boardTitle }}</h5>
<!-- HTML 콘텐츠 렌더링 -->
<div class="board-content text-body" style="line-height: 1.6;" v-html="$common.contentToHtml(boardContent)">
<div class="d-flex justify-content-between align-items-center flex-wrap mb-6 gap-2">
<!-- 제목 섹션 -->
<div class="me-1">
<h5 class="mb-4">{{ boardTitle }}</h5>
</div>
<!-- 첨부파일 섹션 -->
<div v-if="attachments" class="btn-group">
<button type="button" class="btn btn-label-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-download me-2"></i>
첨부파일
<!-- (<span class="attachment-num">{{ dropdownItems.length }}</span>) -->
</button>
<!-- <ul class="dropdown-menu">
<li v-for="(item, index) in dropdownItems" :key="index">
<a class="dropdown-item" href="javascript:void(0);">
{{ item.label }}
</a>
</li>
</ul> -->
</div>
</div>
<!-- HTML 콘텐츠 렌더링 -->
<div class="board-content text-body" style="line-height: 1.6;" v-html="$common.contentToHtml(boardContent)"></div>
<!-- 좋아요 버튼 -->
<div class="row justify-content-center my-10">
<BoardRecommendBtn :bigBtn="true"/>
<BoardRecommendBtn
:bigBtn="true"
:boardId="currentBoardId"
:commentId="null"
:likeCount="currentLikeCount"
:dislikeCount="currentDislikeCount"
@updateReaction="handleUpdateReaction"
/>
</div>
<!-- 첨부파일 목록 -->
<ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
<!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
<li v-for="(attachment, index) in attachments" :key="index" class="mb-2">
<a :href="attachment.url" target="_blank" class="text-decoration-none">{{ attachment.name }}</a>
</li>
</ul>
</ul> -->
<!-- 댓글 영역 -->
<BoardComentArea :comments="comments" />
@ -38,6 +72,8 @@
</div>
<div class="card-footer">
<BoardCommentList/>
<Pagination/>
</div>
</div>
</div>
@ -47,21 +83,24 @@
<script setup>
import BoardComentArea from '@c/board/BoardComentArea.vue';
import BoardCommentList from '@/components/board/BoardCommentList.vue';
import BoardProfile from '@c/board/BoardProfile.vue';
import BoardCommentList from '@/components/board/BoardCommentList.vue';
import BoardRecommendBtn from '@/components/button/BoardRecommendBtn.vue';
import Pagination from '@/components/pagination/Pagination.vue';
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from '@api';
import Quill from 'quill';
//
const profileName = ref('익명 사용자');
const boardTitle = ref('제목 없음');
const boardContent = ref('');
const convertedContent = ref('내용 없음');
const comments = ref([]);
const attachments = ref([]);
const views = ref(0);
const likes = ref(0);
const dislikes = ref(0);
const attachment = ref(false);
// ID
const route = useRoute();
@ -81,19 +120,37 @@ const fetchBoardDetails = async () => {
// API
const boardDetail = data.boardDetail || {};
profileName.value = boardDetail.author || '익명 사용자';
boardTitle.value = boardDetail.title || '제목 없음';
boardContent.value = boardDetail.content || '';
// console.log('boardDetail:', boardDetail);
profileName.value = data.author || '익명 사용자';
boardTitle.value = data.title || '제목 없음';
boardContent.value = data.content || '';
views.value = data.cnt || 0;
likes.value = data.likeCount || 0;
dislikes.value = data.dislikeCount || 0;
attachment.value = data.hasAttachment || null;
attachments.value = data.attachments || [];
comments.value = data.comments || [];
} catch (error) {
console.error('게시물 가져오기 오류:', error);
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
}
};
const currentLikeCount = ref(10);
const currentDislikeCount = ref(2);
// ,
const handleUpdateReaction = async ({ type, boardId, commentId }) => {
try {
const cmtId = commentId !== null ? commentId : 0;
const response = await axios.post(`/board/${boardId}/${cmtId}/reaction`, { type });
} catch (error) {
alert('반응을 업데이트하는 중 오류 발생');
}
};
//
onMounted(() => {
fetchBoardDetails();