board-list-d머지

This commit is contained in:
dyhj625 2025-02-21 09:50:09 +09:00
parent 52809ba743
commit 04b7db1560
6 changed files with 448 additions and 242 deletions

View File

@ -14,16 +14,29 @@
@deleteClick="deleteClick"
@submitPassword="submitPassword"
@updateReaction="handleUpdateReaction"
@toggleEdit="emit('toggleEdit', comment.commentId, true)"
@toggleEdit="toggleEdit(true)"
/>
<!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group">
<input
type="password"
class="form-control"
v-model="password"
placeholder="비밀번호 입력"
/>
<button class="btn btn-primary" @click="submitPassword">확인</button>
</div>
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
</div>
<div class="mt-6">
<template v-if="isEditTextarea">
<textarea v-model="editedContent" class="form-control"></textarea>
<div class="mt-2 d-flex justify-content-end">
<button class="btn btn-secondary me-2" @click="emit('toggleEdit', comment.commentId, false)">취소</button>
<button class="btn btn-primary" @click="submitEdit">수정 완료</button>
<button class="btn btn-secondary me-2" @click="cancelEdit">취소</button>
<button class="btn btn-primary" @click="submitEdit">수정</button>
</div>
</template>
<template v-else>
@ -51,21 +64,11 @@
/>
</li>
</ul>
<!-- <ul class="list-unstyled twoDepth">
<li>
<BoardProfile profileName=곤데리2 :showDetail="false" />
<div class="mt-2">저도 궁금합니다.</div>
<BoardCommentArea v-if="comment" />
</li>
</ul> -->
<!-- <BoardProfile profileName=곤데리 :showDetail="false" />
<div class="mt-2">저도 궁금합니다.</div>
<PlusButton @click="toggleComment"/>
<BoardCommentArea v-if="comment" /> -->
</div>
</template>
<script setup>
import axios from '@api';
import { defineProps, defineEmits, ref } from 'vue';
import BoardProfile from './BoardProfile.vue';
import BoardCommentArea from './BoardCommentArea.vue';
@ -99,7 +102,7 @@ const props = defineProps({
});
// emits
const emit = defineEmits(['submitComment', 'updateReaction', 'toggleEdit', 'editClick']);
const emit = defineEmits(['submitComment', 'updateReaction', 'toggleEdit',]);
//
const isComment = ref(false);
@ -107,6 +110,22 @@ const toggleComment = () => {
isComment.value = !isComment.value;
};
// &
const password = ref('');
const passwordAlert = ref('');
const isPassword = ref(false);
const isEditTextarea = ref(false);
const lastClickedButton = ref("");
const toggleEdit = (status) => {
if (props.unknown) {
isPassword.value = status; //
lastClickedButton.value = "edit";
} else {
isEditTextarea.value = status; //
}
};
//
const submitComment = (newComment) => {
emit('submitComment', { parentId: props.comment.commentId, ...newComment });
@ -118,21 +137,109 @@ const submitComment = (newComment) => {
const handleUpdateReaction = (reactionData) => {
emit('updateReaction', {
boardId: props.comment.boardId,
commentId: props.comment.commentId,
...reactionData
commentId: props.comment.commentId || reactionData.commentId,
...reactionData,
});
};
//
const editClick = (data) => {
emit('editClick', data);
//
const editClick = () => {
if (props.unknown) {
console.log('수정')
togglePassword("edit");
}
};
const deleteClick = () => {
if (props.unknown) {
console.log('삭제')
togglePassword("delete");
}
};
const togglePassword = (button) => {
if (lastClickedButton.value === button) {
isPassword.value = !isPassword.value;
} else {
isPassword.value = true;
}
lastClickedButton.value = button;
};
// const deleteComment = async () => {
// if (!confirm(" ?")) return;
// try {
// console.log(" :");
// console.log(" ID:", props.comment.commentId);
// console.log(" ID:", props.comment.boardId);
// console.log(" :", props.unknown ? " ( )" : " ( )");
// const response = await axios.delete(`board/${props.comment.commentId}`, {
// data: { LOCCMTSEQ: props.comment.commentId }
// });
// console.log("📌 :", response.data);
// if (response.data.code === 200) {
// console.log(" !");
// // emit("commentDeleted", props.comment.commentId);
// } else {
// console.log(" :", response.data.message);
// // alert(" .");
// }
// } catch (error) {
// console.log("🚨 :", error);
// alert(" .");
// }
// };
//
const submitPassword = async () => {
if (!password.value) {
passwordAlert.value = "비밀번호를 입력해주세요.";
return;
}
console.log(props.comment.commentId)
try {
const response = await axios.post(`board/${props.comment.commentId}/password`, {
LOCCMTPWD: password.value,
LOCCMTSEQ: 288,
});
console.log("응답!!!!!!!!", response); //
console.log("응답 데이터:", response.data);
if (response.data.code === 200 && response.data.data === true) {
console.log('되는거니')
// deleteComment()
// // password.value = '';
// // isPassword.value = false;
// // isEditTextarea.value = true;
} else {
passwordAlert.value = "비밀번호가 일치하지 않습니다.";
}
} catch (error) {
passwordAlert.value = "비밀번호 검증 중 오류가 발생했습니다.";
}
};
//
const editedContent = ref(props.comment.content);
//
const cancelEdit = () => {
isEditTextarea.value = false;
};
//
const submitEdit = () => {
emit('submitComment', { commentId: props.comment.commentId, content: editedContent.value });
emit('toggleEdit', props.comment.commentId, false); //
isEditTextarea.value = false; //
};
</script>

View File

@ -43,6 +43,7 @@
class="form-control flex-grow-1"
v-model="password"
/>
<!-- <span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span> -->
</div>
</div>
@ -74,20 +75,18 @@ const props = defineProps({
const comment = ref('');
const password = ref('');
const isCheck = ref(false);
const isCheck = ref(props.unknown);
const emit = defineEmits(['submitComment']);
watch(() => props.unknown, (newVal) => {
if (!newVal) {
isCheck.value = false;
}
isCheck.value = newVal;
});
function handleCommentSubmit() {
emit('submitComment', {
comment: comment.value,
password: password.value,
password: isCheck.value ? password.value : '',
});
comment.value = '';

View File

@ -9,13 +9,14 @@
:unknown="unknown"
:comment="comment"
:isPassword="isPassword"
:isEditTextarea="isEditTextarea"
@editClick="editClick"
@deleteClick="deleteClick"
@submitPassword="submitPassword"
@submitComment="submitComment"
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId)"
@commentDeleted="handleCommentDeleted"
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
/>
<!-- @updateReaction="handleUpdateReaction" -->
</li>
</ul>
</template>
@ -38,6 +39,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
isEditTextarea: {
type: Boolean,
default: false,
}
});
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick']);
@ -46,17 +51,13 @@ const submitComment = (replyData) => {
emit('submitComment', replyData);
};
const handleUpdateReaction = (reactionData, commentId) => {
// console.log('📢 BoardCommentList :', reactionData);
// console.log('📌 ID>>>>:', commentId);
const handleUpdateReaction = (reactionData, commentId, boardId) => {
const updatedReactionData = {
...reactionData,
commentId: commentId
commentId: commentId || reactionData.commentId,
boardId: boardId || reactionData.boardId,
};
// console.log('🚀 :', updatedReactionData);
emit('updateReaction', updatedReactionData);
}

View File

@ -1,53 +1,52 @@
<template>
<div class="container flex-grow-1 container-p-y">
<div class="row mb-4">
<div class="card">
<div class="card-header">
<!-- 검색창 -->
<div class="container col-8 px-3">
<div class="container col-6 mt-12 mb-8">
<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="card-datatable">
<div class="row mx-6 my-6 justify-content-between g-3 align-items-center">
<div class="col-md-6 d-flex flex-column flex-md-row align-items-md-center gap-2 mt-0">
<!-- 리스트 갯수 선택 -->
<div class="container col-1 px-0 py-2">
<select class="form-select" v-model="selectedSize" @change="handleSizeChange">
<select class="form-select w-25 w-md-100" 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>
<!-- 셀렉트 박스 -->
<select class="form-select w-25 w-md-100" v-model="selectedOrder" @change="handleSortChange">
<option value="date">최신날짜</option>
<option value="views">조회수</option>
</select>
<!-- 공지 접기 기능 -->
<div class="form-check mb-0 ms-2">
<input class="form-check-input" type="checkbox" v-model="showNotices" id="hideNotices" />
<label class="form-check-label" for="hideNotices">공지 숨기기</label>
</div>
</div>
<div class="col-md-6 d-flex flex-column flex-md-row align-items-md-center justify-content-md-end gap-2">
<!-- 글쓰기 -->
<router-link to="/board/write" class="ms-2">
<WriteButton class="btn add-new btn-primary"/>
</router-link>
</div>
</div>
<br>
<!-- 게시판 -->
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-light">
<table class="datatables-users table border-top dataTable dtr-column">
<thead>
<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>
<th style="width: 11%;" class="text-center">번호</th>
<th style="width: 45%;" class="text-center">제목</th>
<th style="width: 10%;" class="text-center">작성자</th>
<th style="width: 15%;" class="text-center">작성일</th>
<th style="width: 9%;" class="text-center">조회수</th>
</tr>
</thead>
<tbody>
@ -57,16 +56,16 @@
:key="'notice-' + index"
class="bg-label-gray"
@click="goDetail(notice.id)">
<td>공지</td>
<td class="text-center">공지</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 fs-tiny">N</span>
</td>
<td>{{ notice.author }}</td>
<td>{{ notice.date }}</td>
<td>{{ notice.views }}</td>
<td class="text-center">{{ notice.author }}</td>
<td class="text-center">{{ notice.date }}</td>
<td class="text-center">{{ notice.views }}</td>
</tr>
</template>
<!-- 일반 게시물 -->
@ -74,21 +73,22 @@
:key="'post-' + index"
class="invert-bg-white"
@click="goDetail(post.realId)">
<td>{{ post.id }}</td>
<td class="text-center">{{ 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 fs-tiny">N</span>
</td>
<td>{{ post.author }}</td>
<td>{{ post.date }}</td>
<td>{{ post.views }}</td>
<td class="text-center">{{ post.author }}</td>
<td class="text-center">{{ post.date }}</td>
<td class="text-center">{{ post.views }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<!-- 페이지네이션 -->
<div class="row g-3">
<div class="mt-8">
@ -100,6 +100,8 @@
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
@ -262,4 +264,9 @@ onMounted(() => {
</script>
<style scoped>
@media (max-width: 768px) {
.w-md-100 {
width: 100% !important;
}
}
</style>

View File

@ -5,6 +5,7 @@
<div class="card">
<!-- 프로필 헤더 -->
<div class="card-header">
<div class="pb-5 border-bottom">
<BoardProfile
:boardId="currentBoardId"
:profileName="profileName"
@ -14,13 +15,24 @@
:commentNum="commentNum"
:date="formattedBoardDate"
:isLike="false"
:isPassword="isPassword"
:passwordAlert="passwordAlert"
@editClick="editClick"
@deleteClick="deleteClick"
@submitPassword="submitPassword"
class="pb-6 border-bottom"
/>
<!-- 비밀번호 입력창 (익명일 경우) -->
<div v-if="isPassword && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group">
<input
type="password"
class="form-control"
v-model="password"
placeholder="비밀번호 입력"
/>
<button class="btn btn-primary" @click="submitPassword">확인</button>
</div>
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
</div>
</div>
</div>
<!-- 게시글 내용 -->
<div class="card-body">
@ -86,13 +98,9 @@
<BoardCommentList
:unknown="unknown"
:comments="comments"
:isEditTextarea="isEditTextarea"
:isPassword="isPassword"
@editClick="editClick"
@deleteClick="deleteClick"
@submitPassword="submitPassword"
@updateReaction="handleUpdateReaction"
@updateReaction="handleCommentReaction"
@submitComment="handleCommentReply"
@commentDeleted="handleCommentDeleted"
/>
<Pagination
v-if="pagination.pages"
@ -139,8 +147,8 @@ const authorId = ref(null); // 작성자 id
const isAuthor = computed(() => currentUserId.value === authorId.value);
const isEditTextarea = ref({});
const password = ref('');
const passwordAlert = ref("");
const isPassword = ref(false);
const lastClickedButton = ref("");
@ -195,15 +203,13 @@ const fetchBoardDetails = async () => {
// ,
const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => {
try {
const aa = await axios.post(`/board/${boardId}/${commentId}/reaction`, {
await axios.post(`/board/${boardId}/${commentId}/reaction`, {
LOCBRDSEQ: boardId, // id
LOCCMTSEQ: commentId, // id
// MEMBERSEQ: 1, // 1
LOCGOBGOD: isLike ? 'T' : 'F',
LOCGOBBAD: isDislike ? 'T' : 'F'
});
console.log("좋아요 API 응답 데이터:", aa.data);
const response = await axios.get(`board/${boardId}`);
const updatedData = response.data.data;
@ -215,52 +221,91 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
dislikeClicked.value = isDislike;
// console.log(updatedData)
// console.log(" :", updatedData);
} catch (error) {
alert('반응을 업데이트하는 중 오류 발생');
}
};
//
const handleCommentReaction = async ({ boardId, commentId, isLike, isDislike }) => {
if (!commentId) return; // ID
try {
const response = await axios.post(`/board/${boardId}/${commentId}/reaction`, {
LOCBRDSEQ: boardId, // ID
LOCCMTSEQ: commentId, // ID
LOCGOBGOD: isLike ? 'T' : 'F',
LOCGOBBAD: isDislike ? 'T' : 'F'
});
console.log("댓글 좋아요 API 응답 데이터:", response.data);
// /
await fetchComments();
} catch (error) {
alert('댓글 반응을 업데이트하는 중 오류 발생');
}
};
//
const fetchComments = async (page = 1) => {
try {
//
const response = await axios.get(`board/${currentBoardId.value}/comments`, {
params: {
LOCBRDSEQ: currentBoardId.value,
page
}
});
console.log("목록 API 응답 데이터:", response.data);
let allComments = response.data.data.list.map(comment => ({
commentId: comment.LOCCMTSEQ, // id
const commentsList = response.data.data.list.map(comment => ({
commentId: comment.LOCCMTSEQ, // ID
boardId: comment.LOCBRDSEQ,
parentId: comment.LOCCMTPNT, // id
author: comment.author || "익명 사용자", //
content: comment.LOCCMTRPY, //
createdAt: formattedDate(comment.LOCCMTRDT), //
children: []
parentId: comment.LOCCMTPNT, // ID
author: comment.author || "익명 사용자",
content: comment.LOCCMTRPY,
likeCount: comment.likeCount || 0,
dislikeCount: comment.dislikeCount || 0,
likeClicked: comment.likeClicked || false,
dislikeClicked: comment.dislikeClicked || false,
createdAtRaw: new Date(comment.LOCCMTRDT), //
createdAt: formattedDate(comment.LOCCMTRDT), //
children: [] //
}));
allComments.sort((a, b) => b.commentId - a.commentId);
for (const comment of commentsList) {
if (!comment.commentId) continue;
let commentMap = {};
let rootComments = [];
allComments.forEach(comment => {
commentMap[comment.commentId] = comment;
const replyResponse = await axios.get(`board/${currentBoardId.value}/reply`, {
params: { LOCCMTPNT: comment.commentId }
});
allComments.forEach(comment => {
if (comment.parentId && commentMap[comment.parentId]) {
commentMap[comment.parentId].children.push(comment);
// console.log(`📌 (${comment.commentId} ):`, replyResponse.data);
if (replyResponse.data.data) {
comment.children = replyResponse.data.data.map(reply => ({
commentId: reply.LOCCMTSEQ,
boardId: reply.LOCBRDSEQ,
parentId: reply.LOCCMTPNT, // ID
content: reply.LOCCMTRPY || "내용 없음",
createdAtRaw: new Date(reply.LOCCMTRDT),
createdAt: formattedDate(reply.LOCCMTRDT),
likeCount: reply.likeCount || 0,
dislikeCount: reply.dislikeCount || 0,
likeClicked: false,
dislikeClicked: false
}));
} else {
rootComments.push(comment);
comment.children = []; //
}
}
});
comments.value = rootComments;
// console.log(" comments :", comments.value);
//
comments.value = commentsList;
pagination.value = {
...pagination.value,
@ -278,34 +323,57 @@ const fetchComments = async (page = 1) => {
navigateLastPage: response.data.data.navigateLastPage //
};
console.log("📌 댓글 목록:", comments.value);
} catch (error) {
console.error('댓글 목록 불러오기 오류:', error);
console.log('댓글 목록 불러오기 오류:', error);
}
};
//
const handleCommentSubmit = async ({ comment, password }) => {
// if (unknown.value && !password) {
// passwordAlert.value = " ."; // UI
// return;
// }
// if (!password) {
// passwordAlert.value = " ."; // UI
// return;
// }
try {
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
LOCBRDSEQ: currentBoardId.value,
LOCCMTRPY: comment,
LOCCMTPWD: password || null,
LOCCMTPWD: password,
LOCCMTPNT: 1
});
// console.log('📥 :', response.data);
// console.log(' 1212121212:', response.data);
if (response.status === 200) {
console.log('댓글 작성 성공:', response.data.message);
await fetchComments();
} else {
console.error('댓글 작성 실패:', response.data.message);
console.log('댓글 작성 실패:', response.data.message);
}
} catch (error) {
console.error('댓글 작성 중 오류 발생:', error);
console.log('댓글 작성 중 오류 발생:', error);
}
};
//
const handleCommentReply = async (reply) => {
try {
console.log('대댓글 작성 요청 데이터:', {
LOCBRDSEQ: currentBoardId.value,
LOCCMTRPY: reply.comment,
LOCCMTPWD: reply.password || null,
LOCCMTPNT: reply.parentId
});
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
LOCBRDSEQ: currentBoardId.value,
LOCCMTRPY: reply.comment,
@ -313,16 +381,32 @@ const handleCommentReply = async (reply) => {
LOCCMTPNT: reply.parentId
});
//
console.log('대댓글 작성 응답:', {
status: response.status,
data: response.data,
headers: response.headers
});
if (response.status === 200) {
console.log('대댓글 작성 성공:', response.data.message);
await fetchComments();
if (response.data.code === 200) { //
console.log('대댓글 작성 성공:', response.data);
await fetchComments(); //
} else {
console.error('대댓글 작성 실패:', response.data.message);
console.log('대댓글 작성 실패 - 서버 응답:', response.data);
alert('대댓글 작성에 실패했습니다.');
}
}
} catch (error) {
console.error('대댓글 작성 중 오류 발생:', error);
if (error.response) {
console.error('서버 응답 에러:', error.response.data);
}
alert('대댓글 작성 중 오류가 발생했습니다.');
}
}
const editClick = (unknown) => {
if (unknown) {
togglePassword("edit");
} else {
@ -348,24 +432,23 @@ const togglePassword = (button) => {
};
const submitPassword = async (inputPassword) => {
console.log(inputPassword)
if (!inputPassword) {
const submitPassword = async () => {
if (!password.value) {
passwordAlert.value = "비밀번호를 입력해주세요.";
return;
}
// console.log("📌 : submitPassword ");
try {
const requestData = {
LOCBRDPWD: inputPassword,
LOCBRDSEQ: 288
};
const response = await axios.post(`board/${currentBoardId.value}/password`, {
LOCBRDPWD: password.value,
LOCBRDSEQ: 288, // ID
});
const postResponse = await axios.post(`board/${currentBoardId.value}/password`, requestData);
if (postResponse.data.code === 200 && postResponse.data.data === true) {
if (response.data.code === 200 && response.data.data === true) {
password.value = '';
isPassword.value = false;
passwordAlert.value = "";
if (lastClickedButton.value === "edit") {
router.push({ name: "BoardEdit", params: { id: currentBoardId.value } });
@ -374,15 +457,21 @@ const submitPassword = async (inputPassword) => {
}
lastClickedButton.value = null;
} else {
passwordAlert.value = "비밀번호가 일치하지 않습니다.";
passwordAlert.value = "비밀번호가 일치하지 않습니다.????";
}
} catch (error) {
if (error.response && error.response.status === 401) {
// console.log("📌 :", error);
if (error.response) {
if (error.response.status === 401) {
passwordAlert.value = "비밀번호가 일치하지 않습니다.";
} else if (error.response) {
alert(`오류 발생: ${error.response.data.message || "서버 오류"}`);
} else {
alert("네트워크 오류가 발생했습니다. 다시 시도해주세요.");
passwordAlert.value = error.response.data?.message || "서버 오류가 발생했습니다.";
}
} else if (error.request) {
passwordAlert.value = "네트워크 오류가 발생했습니다. 다시 시도해주세요.";
} else {
passwordAlert.value = "요청 중 알 수 없는 오류가 발생했습니다.";
}
}
};
@ -418,6 +507,10 @@ const handlePageChange = (page) => {
}
};
const handleCommentDeleted = (deletedCommentId) => {
comments.value = comments.value.filter(comment => comment.commentId !== deletedCommentId);
};
//
const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음";

View File

@ -195,9 +195,10 @@ function updateCalendarEvents() {
.map(([date, type]) => ({
title: getVacationType(type),
start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)",
backgroundColor: "rgb(113 212 243 / 76%)",
textColor: "#fff", //
display: "background",
classNames: [getVacationTypeClass(type)]
classNames: [getVacationTypeClass(type), "selected-event"]
}));
// ,
@ -340,10 +341,6 @@ halfDayType.value = halfDayType.value === type ? null : type;
return typeof id === "number" ? Number(id) : id;
});
console.log("vacationsToAdd:", vacationsToAdd);
console.log("vacationsToDelete:", vacationsToDelete);
try {
const response = await axios.post("vacation/batchUpdate", {
add: vacationsToAdd,
@ -424,5 +421,7 @@ await loadCalendarData(year, month);
</script>
<style>
.fc-bg-event{
}
</style>