경로 수정 및 테스트트

This commit is contained in:
dyhj625 2025-01-14 12:23:13 +09:00
parent 559212e9e1
commit e87957c88a
11 changed files with 471 additions and 128 deletions

View File

@ -36,6 +36,7 @@ import BoardComentArea from './BoardComentArea.vue';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import Pagination from '../pagination/Pagination.vue'; import Pagination from '../pagination/Pagination.vue';
import PlusButton from '../button/PlusBtn.vue'; import PlusButton from '../button/PlusBtn.vue';
import { defineEmits } from 'vue';
const comment = ref(false); const comment = ref(false);
@ -43,6 +44,9 @@ const toggleComment = () => {
comment.value = !comment.value comment.value = !comment.value
}; };
// emits
const emit = defineEmits(['submitComment']);
</script> </script>
<style scoped> <style scoped>

View File

@ -51,6 +51,10 @@ defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
attachments: {
type: Array, //
default: () => [], //
},
}); });
</script> </script>
@ -59,4 +63,4 @@ defineProps({
.min-250 { .min-250 {
min-height: 250px !important; min-height: 250px !important;
} }
</style> </style>

View File

@ -24,72 +24,96 @@
</div> </div>
<div class="ms-auto btn-area"> <div class="ms-auto btn-area">
<template v-if="showDetail"> <template v-if="showDetail">
<EditButton /> <EditButton @click="handleEdit" />
<DeleteButton /> <DeleteButton @click="handleDelete" />
</template> </template>
<template v-else> <template v-else>
<template v-if="author"> <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> <i class='bx bx-edit-alt'></i>
</button> </button>
<button class="btn author btn-label-primary btn-icon"> <button class="btn author btn-label-primary btn-icon" @click="handleDelete">
<i class='bx bx-trash' ></i> <i class='bx bx-trash'></i>
</button> </button>
</template> </template>
<BoardRecommendBtn :likeClicked="true" :dislikeClicked="false"/> <BoardRecommendBtn :likeClicked="true" :dislikeClicked="false" />
</template> </template>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router';
import axios from 'axios';
import DeleteButton from '../button/DeleteBtn.vue'; import DeleteButton from '../button/DeleteBtn.vue';
import EditButton from '../button/EditBtn.vue'; import EditButton from '../button/EditBtn.vue';
import BoardRecommendBtn from '../button/BoardRecommendBtn.vue'; import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
// Vue Router
const router = useRouter();
// Props
defineProps({ defineProps({
profileName : { profileName: {
type: String, type: String,
default: '익명', default: '익명',
}, },
unknown : { unknown: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showDetail : { showDetail: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
author : { author: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
}); });
const boardId = 80; //test
//
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> </script>
<style scoped> <style scoped>
.profile-detail span ~ span { .profile-detail span ~ span {
margin-left: 5px; margin-left: 5px;
} }
.ms-auto button + button { .ms-auto button + button {
margin-left: 5px; margin-left: 5px;
}
.btn.author {
height: 30px;
}
@media screen and (max-width: 450px) {
.btn-area {
margin-top: 10px;
width: 100%;
} }
.btn.author { .btn.author {
height: 30px; height: 30px;
} }
}
@media screen and (max-width:450px) {
.btn-area {
margin-top: 10px;
width: 100%;
}
.btn.author {
height: 30px;
}
}
</style> </style>

View File

@ -1,23 +1,43 @@
<template> <template>
<div class="" role="button"> <div class="container mt-4">
<div class="card"> <div v-if="posts.length === 0" class="text-center">
<div class="d-sm-flex"> 게시물이 없습니다.
<div v-if="img"> </div>
<img class="card-img card-img-left" :src="img" alt="" style="width: 200px; height: 200px; object-fit: cover" /> <div v-for="post in posts" :key="post.id" class="card mb-3 shadow-sm">
<div class="row g-0">
<!-- 이미지 섹션 -->
<div v-if="post.img" class="col-md-2">
<img
:src="post.img"
alt="이미지"
class="img-fluid rounded-start"
style="object-fit: cover; height: 100%; width: 100%;"
/>
</div> </div>
<div class="col"> <!-- 게시물 내용 섹션 -->
<div class="col-md-10">
<div class="card-body"> <div class="card-body">
<h5 class="card-title align-items-center"> <!-- 태그 -->
<slot name="badgeType"></slot> <h6 class="badge rounded-pill bg-primary text-white mb-2">
{{ title }} {{ post.category }}
</h5> </h6>
<p class="card-text str_wrap pt-5"> <!-- 제목 -->
{{ content }} <h5 class="card-title">{{ post.title }}</h5>
</p> <!-- 본문 -->
<p class="card-text"> <p class="card-text str_wrap">{{ post.content }}</p>
<small class="text-muted">{{ date }}</small> <!-- 날짜 -->
<slot name="optInfo"></slot> <div class="d-flex justify-content-between">
</p> <small class="text-muted">{{ formatDate(post.date) }}</small>
<!-- 좋아요와 댓글 -->
<div>
<span class="text-muted me-3">
<i class="bx bx-like"></i> {{ post.likes || 0 }}
</span>
<span class="text-muted">
<i class="bx bx-comment"></i> {{ post.comments || 0 }}
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -25,41 +45,75 @@
</div> </div>
</template> </template>
<script setup> <script>
// const data = defineProps(['item']); import axios from "axios";
const prop = defineProps({
title: {
type: String,
default: '제목',
required: true,
},
content: {
type: String,
default: '내용',
required: true,
},
date: {
type: String,
default: 'date',
required: true,
},
img: {
type: String,
required: false,
},
});
const colSetting = () => { export default {
img ? 'col-9' : ''; data() {
}; return {
posts: [], //
};
},
methods: {
async fetchPosts() {
try {
const response = await axios.get("/board/general");
if (response.data && Array.isArray(response.data.data)) {
console.log("API Response:",response.data);
this.posts = 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);
}
},
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")}`;
},
},
mounted() {
this.fetchPosts();
},
};
</script> </script>
<style> <style>
.str_wrap { /* 카드 스타일 */
overflow: hidden; .card {
text-overflow: ellipsis; border: 1px solid #e6e6e6;
display: -webkit-box; border-radius: 8px;
-webkit-line-clamp: 3; transition: transform 0.2s ease-in-out;
-webkit-box-orient: vertical; }
}
.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> </style>

View File

@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import BoardWrite from '@v/board/BoardWrite.vue';
// 초기 렌더링 속도를 위해 지연 로딩 사용 // 초기 렌더링 속도를 위해 지연 로딩 사용
const routes = [ const routes = [
@ -14,6 +13,7 @@ const routes = [
children: [ children: [
{ {
path: '', path: '',
name: 'BoardList',
component: () => import('@v/board/BoardList.vue') component: () => import('@v/board/BoardList.vue')
}, },
{ {
@ -21,8 +21,14 @@ const routes = [
component: () => import('@v/board/BoardWrite.vue') component: () => import('@v/board/BoardWrite.vue')
}, },
{ {
path: 'get/:id', path: ':id',
name: 'BoardDetail',
component: () => import('@v/board/BoardView.vue') component: () => import('@v/board/BoardView.vue')
},
{
path: 'edit/:id',
name: 'BoardEdit',
component: () => import('@v/board/BoardEdit.vue')
} }
] ]
}, },

View File

@ -4,12 +4,12 @@ import axios from 'axios';
const events = ref([]); const events = ref([]);
const fetchEvents = async () => { const fetchEvents = async () => {
const response = await axios.get('/api/calendar/events'); const response = await axios.get('/calendar/events');
events.value = response.data; events.value = response.data;
}; };
const addEvent = async (event) => { const addEvent = async (event) => {
await axios.post('/api/calendar/event', event); await axios.post('/calendar/event', event);
fetchEvents(); fetchEvents();
}; };

View File

@ -0,0 +1,154 @@
<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 FormFile from '@c/input/FormFile.vue';
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from '@api';
//
const title = ref('');
const content = ref('');
const attachFiles = ref([]);
//
const titleAlert = ref(false);
const contentAlert = ref(false);
const attachFilesAlert = ref(false);
//
const router = useRouter();
const route = useRoute();
const currentBoardId = ref(Number(route.params.id));
//
const fetchBoardDetails = async () => {
try {
const response = await axios.get(`/board/${currentBoardId.value}`);
const data = response.data.data.boardDetail;
//
title.value = data.title || '';
content.value = data.content || '';
console.log('게시물 데이터 로드 완료:', { title: title.value, content: content.value });
} catch (error) {
console.error('게시물 가져오기 오류:', error.response || error.message);
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
}
};
//
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);
//
if (attachFiles.value && attachFiles.value.length > 0) {
for (const file of attachFiles.value) {
const formData = new FormData();
formData.append('file', file);
await axios.post(`/board/${currentBoardId.value}/attachments`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
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;
}
</style>

View File

@ -1,30 +1,30 @@
<template> <template>
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<search-bar @update:data="search" /> <search-bar @update:data="search" />
<div class="row g-3"> <div class="row g-3">
<div class="mt-8"> <div class="mt-8">
<router-link to="/board/write"> <router-link to="/board/write">
<WriteButton /> <WriteButton />
</router-link> </router-link>
</div> </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 v-for="(item, index) in filteredList" :key="item.id">
<board-card
:title="item.title"
:content="item.content"
:img="item.img"
:date="item.date"
@click="goDetail(item.id)"
>
<template #badgeType> <template #badgeType>
<span v-if="item.type == 1" class="badge rounded-pill bg-label-danger">공지</span> <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 === 2" class="badge rounded-pill bg-label-primary">자유</span>
<span v-else-if="item.type == 3" class="badge rounded-pill bg-label-success">익명</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> </template>
</board-card> </board-card>
</template> </template>
<div class="mt-8"> <div class="mt-8">
<pagination /> <pagination />
</div> </div>
@ -33,25 +33,36 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import BoardCard from '@c/list/BoardCard.vue'; import BoardCard from '@c/list/BoardCard.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';
import dummy from '@a/boardDummy.json'; import dummy from '@a/boardDummy.json';
import WriteButton from '@c/button/WriteBtn.vue'; import WriteButton from '@c/button/WriteBtn.vue';
const list = ref(dummy); //
const searchText = ref(''); const list = ref(dummy);
const searchText = ref('');
/** 상세로 이동 */ //
const goDetail = idx => { const goDetail = (id) => {
router.push(`/board/get/${idx}`); router.push({ name: 'BoardDetail', params: { id } });
}; };
const search = e => { //
console.log('검색:', e); const search = (e) => {
}; searchText.value = e.trim();
};
//
const filteredList = computed(() =>
list.value.filter((item) =>
item.title.toLowerCase().includes(searchText.value.toLowerCase())
)
);
</script> </script>
<style></style> <style>
/* 필요에 따라 스타일 추가 */
</style>

View File

@ -3,15 +3,27 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<!-- 프로필 헤더 -->
<div class="card-header"> <div class="card-header">
<BoardProfile profileName="만드레야2"/> <BoardProfile :boardId="currentBoardId.value" :profileName="profileName" />
</div> </div>
<!-- 게시글 내용 -->
<div class="card-body"> <div class="card-body">
<BoardContent boardTitle="제목1" boardContent="내용1" :dropdownItems="dropdownItems" /> <h5 class="mb-4">{{ boardTitle }}</h5>
<BoardComentArea /> <!-- 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>
<!-- 댓글 입력 -->
<div class="card-footer"> <div class="card-footer">
<BoardComment /> <BoardComment @submitComment="addComment" />
</div> </div>
</div> </div>
</div> </div>
@ -22,15 +34,89 @@
<script setup> <script setup>
import BoardComentArea from '@c/board/BoardComentArea.vue'; import BoardComentArea from '@c/board/BoardComentArea.vue';
import BoardComment from '@c/board/BoardComment.vue'; import BoardComment from '@c/board/BoardComment.vue';
import BoardContent from '@c/board/BoardContent.vue';
import BoardProfile from '@c/board/BoardProfile.vue'; import BoardProfile from '@c/board/BoardProfile.vue';
import { ref } from 'vue'; import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import axios from '@api';
const dropdownItems = ref([ //
{ label: '내용1'}, const profileName = ref('익명 사용자');
{ label: '내용2'}, const boardTitle = ref('제목 없음');
{ label: '내용3'}, const boardContent = ref('내용 없음');
]); const comments = ref([]);
const attachments = ref([]);
// ID
const route = useRoute();
const currentBoardId = ref(Number(route.params.id));
console.log(currentBoardId.value)
//
const addComment = async (newComment) => {
try {
const requestData = {
content: newComment,
};
const response = await axios.post(`/board/${currentBoardId.value}/comment`, requestData);
comments.value.push(response.data);
alert('댓글이 추가되었습니다.');
} catch (error) {
console.error('댓글 추가 오류:', error);
alert('댓글 추가 중 오류가 발생했습니다.');
}
};
//
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> </script>
<style>
/* 카드 스타일 */
.card {
margin-bottom: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 게시글 내용 스타일 */
.board-content {
white-space: pre-wrap; /* 줄바꿈 및 공백 유지 */
font-size: 16px;
line-height: 1.6;
}
/* 첨부파일 목록 스타일 */
.attachments {
padding-left: 20px;
list-style: disc;
margin-top: 1rem;
}
/* 댓글 섹션 스타일 */
.card-footer {
border-top: 1px solid #e6e6e6;
background-color: #f9f9f9;
}
</style>

View File

@ -55,7 +55,7 @@ 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, watch } from 'vue';
import router from '@/router'; import router from '@/router';
import axios from 'axios'; import axios from '@api';
const categoryList = ['자유', '익명', '공지사항']; const categoryList = ['자유', '익명', '공지사항'];
// input !! // input !!
@ -111,7 +111,7 @@ const write = async () => {
}; };
// API // API
const { data: boardResponse } = await axios.post('/api/board', boardData); const { data: boardResponse } = await axios.post('/board', boardData);
const boardId = boardResponse.data.boardId; const boardId = boardResponse.data.boardId;
// //
@ -135,7 +135,7 @@ const write = async () => {
formData.append("CMNFLESIZ", file.size); // formData.append("CMNFLESIZ", file.size); //
formData.append("CMNFLEREG", fileInfo.registrantId); // ID formData.append("CMNFLEREG", fileInfo.registrantId); // ID
const response = await axios.post(`/api/board/${boardId}/attachments`, formData, { const response = await axios.post(`/board/${boardId}/attachments`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },

View File

@ -8,7 +8,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:10325', // Spring Boot 서버 주소 target: 'http://localhost:10325/api', // Spring Boot 서버 주소
changeOrigin: true, changeOrigin: true,
}, },
}, },