Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
This commit is contained in:
commit
ac97d5fcfb
@ -81,3 +81,9 @@
|
|||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* scrollbar 안보이게 */
|
||||||
|
.scrollbar-none {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,60 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BoardProfile :profileName="comment.author" :showDetail="false" :author="true" :isChild="isChild" />
|
<BoardProfile
|
||||||
|
:unknown="unknown"
|
||||||
|
:boardId="comment.boardId"
|
||||||
|
:profileName="comment.author"
|
||||||
|
:date="comment.createdAt"
|
||||||
|
:comment="comment"
|
||||||
|
:showDetail="false"
|
||||||
|
:author="true"
|
||||||
|
:isLike="!isLike"
|
||||||
|
:isPassword="isPassword"
|
||||||
|
@editClick="editClick"
|
||||||
|
@deleteClick="deleteClick"
|
||||||
|
@submitPassword="submitPassword"
|
||||||
|
@updateReaction="handleUpdateReaction"
|
||||||
|
@toggleEdit="emit('toggleEdit', comment.commentId, true)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<p class="m-0">{{ comment.content }}</p>
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="m-0">{{ comment.content }}</p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
|
<PlusButton v-if="isPlusButton" @click="toggleComment" class="mt-6"/>
|
||||||
<BoardComentArea v-if="isComment" @submit="submitComment"/>
|
<BoardCommentArea v-if="isComment" @submitComment="submitComment"/>
|
||||||
|
|
||||||
<!-- 대댓글 -->
|
<!-- 대댓글 -->
|
||||||
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
|
<ul v-if="comment.children && comment.children.length" class="list-unstyled">
|
||||||
<li
|
<li
|
||||||
v-for="child in comment.children"
|
v-for="child in comment.children"
|
||||||
:key="child.id"
|
:key="child.commentId"
|
||||||
class="pt-8 ps-10"
|
class="mt-8 pt-6 ps-10 border-top"
|
||||||
>
|
>
|
||||||
<BoardComment :comment="child" :isPlusButton="false" :isChild="true" @submitComment="addChildComment" />
|
<BoardComment
|
||||||
|
:comment="child"
|
||||||
|
:unknown="unknown"
|
||||||
|
:isPlusButton="false"
|
||||||
|
:isLike="true"
|
||||||
|
@submitComment="submitComment"
|
||||||
|
@updateReaction="handleUpdateReaction"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- <ul class="list-unstyled twoDepth">
|
<!-- <ul class="list-unstyled twoDepth">
|
||||||
<li>
|
<li>
|
||||||
<BoardProfile profileName=곤데리2 :showDetail="false" />
|
<BoardProfile profileName=곤데리2 :showDetail="false" />
|
||||||
<div class="mt-2">저도 궁금합니다.</div>
|
<div class="mt-2">저도 궁금합니다.</div>
|
||||||
<BoardComentArea v-if="comment" />
|
<BoardCommentArea v-if="comment" />
|
||||||
</li>
|
</li>
|
||||||
</ul> -->
|
</ul> -->
|
||||||
<!-- <BoardProfile profileName=곤데리 :showDetail="false" />
|
<!-- <BoardProfile profileName=곤데리 :showDetail="false" />
|
||||||
<div class="mt-2">저도 궁금합니다.</div>
|
<div class="mt-2">저도 궁금합니다.</div>
|
||||||
<PlusButton @click="toggleComment"/>
|
<PlusButton @click="toggleComment"/>
|
||||||
<BoardComentArea v-if="comment" /> -->
|
<BoardCommentArea v-if="comment" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, ref } from 'vue';
|
||||||
import BoardProfile from './BoardProfile.vue';
|
import BoardProfile from './BoardProfile.vue';
|
||||||
import BoardComentArea from './BoardComentArea.vue';
|
import BoardCommentArea from './BoardCommentArea.vue';
|
||||||
import PlusButton from '../button/PlusBtn.vue';
|
import PlusButton from '../button/PlusBtn.vue';
|
||||||
import { ref } from 'vue';
|
|
||||||
import { defineEmits } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
comment: {
|
comment: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
unknown: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
isPlusButton: {
|
isPlusButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
isChild: {
|
isLike: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isEditTextarea: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isPassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// emits 정의
|
// emits 정의
|
||||||
const emit = defineEmits(['submitComment']);
|
const emit = defineEmits(['submitComment', 'updateReaction', 'toggleEdit', 'editClick']);
|
||||||
|
|
||||||
// 댓글 입력 창 토글
|
// 댓글 입력 창 토글
|
||||||
const isComment = ref(false);
|
const isComment = ref(false);
|
||||||
@ -63,29 +108,31 @@ const toggleComment = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 부모 컴포넌트에 대댓글 추가 요청
|
// 부모 컴포넌트에 대댓글 추가 요청
|
||||||
const addChildComment = (parentId, newComment) => {
|
const submitComment = (newComment) => {
|
||||||
emit('submitComment', parentId, newComment);
|
emit('submitComment', { parentId: props.comment.commentId, ...newComment });
|
||||||
|
|
||||||
|
isComment.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 좋아요, 싫어요
|
||||||
|
const handleUpdateReaction = (reactionData) => {
|
||||||
|
emit('updateReaction', {
|
||||||
|
boardId: props.comment.boardId,
|
||||||
|
commentId: props.comment.commentId,
|
||||||
|
...reactionData
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 수정
|
||||||
|
const editClick = (data) => {
|
||||||
|
emit('editClick', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 수정
|
||||||
|
const editedContent = ref(props.comment.content);
|
||||||
|
const submitEdit = () => {
|
||||||
|
emit('submitComment', { commentId: props.comment.commentId, content: editedContent.value });
|
||||||
|
emit('toggleEdit', props.comment.commentId, false); // 수정 모드 종료
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* .twoDepth {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-unstyled > li ~ li {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-text-primary {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
.btn-text-primary:hover,
|
|
||||||
.btn-text-primary:active,
|
|
||||||
.btn-text-primary:focus {
|
|
||||||
background-color: transparent
|
|
||||||
} */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -2,47 +2,66 @@
|
|||||||
<ul class="list-unstyled mt-10">
|
<ul class="list-unstyled mt-10">
|
||||||
<li
|
<li
|
||||||
v-for="comment in comments"
|
v-for="comment in comments"
|
||||||
:key="comment.id"
|
:key="comment.commentId"
|
||||||
class="mt-8"
|
class="mt-6 border-bottom pb-6"
|
||||||
>
|
>
|
||||||
<BoardComment :comment="comment" @submitComment="addComment" />
|
<BoardComment
|
||||||
|
:unknown="unknown"
|
||||||
|
:comment="comment"
|
||||||
|
:isPassword="isPassword"
|
||||||
|
@editClick="editClick"
|
||||||
|
@deleteClick="deleteClick"
|
||||||
|
@submitPassword="submitPassword"
|
||||||
|
@submitComment="submitComment"
|
||||||
|
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId)"
|
||||||
|
/>
|
||||||
|
<!-- @updateReaction="handleUpdateReaction" -->
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { defineProps, defineEmits } from 'vue';
|
||||||
import BoardComment from './BoardComment.vue'
|
import BoardComment from './BoardComment.vue'
|
||||||
|
|
||||||
const comments = ref([
|
const props = defineProps({
|
||||||
{
|
comments: {
|
||||||
id: 1,
|
type: Array,
|
||||||
author: '홍길동',
|
required: true,
|
||||||
content: '저도 궁금합니다.',
|
default: () => []
|
||||||
children: [
|
},
|
||||||
{
|
unknown: {
|
||||||
id: 2,
|
type: Boolean,
|
||||||
author: '사용자1',
|
default: true,
|
||||||
content: '저도요!',
|
},
|
||||||
},
|
isPassword: {
|
||||||
{
|
type: Boolean,
|
||||||
id: 3,
|
default: false,
|
||||||
author: '사용자2',
|
},
|
||||||
content: '저도..',
|
});
|
||||||
},
|
|
||||||
],
|
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick']);
|
||||||
},
|
|
||||||
{
|
const submitComment = (replyData) => {
|
||||||
id: 4,
|
emit('submitComment', replyData);
|
||||||
author: '사용자4',
|
};
|
||||||
content: '흥미로운 주제네요.',
|
|
||||||
children: [],
|
const handleUpdateReaction = (reactionData, commentId) => {
|
||||||
},
|
// console.log('📢 BoardCommentList에서 이벤트 수신:', reactionData);
|
||||||
{
|
// console.log('📌 전달할 댓글 ID>>>>:', commentId);
|
||||||
id: 5,
|
|
||||||
author: '사용자5',
|
const updatedReactionData = {
|
||||||
content: '우오아아아아아앙',
|
...reactionData,
|
||||||
children: [],
|
commentId: commentId
|
||||||
},
|
};
|
||||||
]);
|
|
||||||
|
// console.log('🚀 최종 전달할 데이터:', updatedReactionData);
|
||||||
|
|
||||||
|
emit('updateReaction', updatedReactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editClick = (data) => {
|
||||||
|
emit('editClick', data);
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex align-items-center flex-wrap">
|
<div class="d-flex align-items-center flex-wrap">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar me-2" v-if="unknown">
|
<div v-if="!unknown" class="avatar me-2">
|
||||||
<img src="/img/avatars/2.png" alt="Avatar" class="rounded-circle" />
|
<img src="/img/avatars/2.png" alt="Avatar" class="rounded-circle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="me-2">
|
<div class="me-2">
|
||||||
@ -13,73 +13,71 @@
|
|||||||
<i class="fa-regular fa-eye"></i> {{ views }}
|
<i class="fa-regular fa-eye"></i> {{ views }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="bx bx-comment"></i> {{ comments }}
|
<i class="bx bx-comment"></i> {{ commentNum }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto btn-area">
|
<!-- 버튼 영역 -->
|
||||||
<template v-if="showDetail">
|
<div class="ms-auto text-end">
|
||||||
<div class="text-end">
|
<!-- 수정, 삭제 버튼 -->
|
||||||
<EditButton @click="handleEdit" />
|
<template v-if="author || showDetail">
|
||||||
<DeleteButton @click="handleDelete" />
|
<EditButton @click.stop="editClick" />
|
||||||
|
<DeleteButton @click.stop="deleteClick" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="mt-3" v-if="isPassword && unknown">
|
<!-- 좋아요, 싫어요 버튼 (댓글에서만 표시) -->
|
||||||
<div class="input-group">
|
<BoardRecommendBtn
|
||||||
<input
|
v-if="isLike"
|
||||||
type="password"
|
:boardId="boardId"
|
||||||
class="form-control"
|
:comment="props.comment"
|
||||||
v-model="password"
|
@updateReaction="handleUpdateReaction"
|
||||||
placeholder="비밀번호 입력"
|
/>
|
||||||
/>
|
|
||||||
<button class="btn btn-primary" type="button" @click="handleSubmit">확인</button>
|
<!-- 비밀번호 입력창 (익명일 경우) -->
|
||||||
</div>
|
<div v-if="isPassword && unknown" class="mt-3">
|
||||||
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
|
<div class="input-group">
|
||||||
</div>
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="비밀번호 입력"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" @click="$emit('submitPassword', password)">확인</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<span v-if="props.passwordAlert" class="invalid-feedback d-block text-start">{{ props.passwordAlert }}</span>
|
||||||
<template v-else>
|
</div>
|
||||||
<template v-if="author">
|
|
||||||
<EditButton />
|
|
||||||
<DeleteButton />
|
|
||||||
<!-- <button class="btn author btn-label-primary btn-icon" @click="handleEdit">
|
|
||||||
<i class='bx bx-edit-alt'></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn author btn-label-primary btn-icon" @click="handleDelete">
|
|
||||||
<i class='bx bx-trash'></i>
|
|
||||||
</button> -->
|
|
||||||
</template>
|
|
||||||
<BoardRecommendBtn v-if="!isChild" :isRecommend="false" />
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, defineProps } from 'vue';
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import axios from '@api';
|
|
||||||
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 인스턴스
|
// Vue Router 인스턴스
|
||||||
const router = useRouter();
|
|
||||||
const isPassword = ref(false);
|
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const passwordAlert = ref(false);
|
|
||||||
const lastClickedButton = ref('');
|
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
boardId: {
|
boardId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
|
},
|
||||||
|
commentId: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
profileName: {
|
profileName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '익명',
|
default: '익명 사용자',
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -89,6 +87,7 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
// 게시글의 작성자 여부를 확인 : 현재 로그인한 사용자가 이 게시글의 작성자인지 여부
|
||||||
author: {
|
author: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -101,113 +100,49 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
comments: {
|
commentNum: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
isChild: {
|
isLike: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
},
|
||||||
|
isPassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
passwordAlert: {
|
||||||
|
type: String,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['togglePasswordInput']);
|
const emit = defineEmits(['togglePasswordInput', 'updateReaction', 'editClick', 'deleteClick', 'updatePasswordAlert']);
|
||||||
|
|
||||||
// 수정 버튼
|
// 수정
|
||||||
const handleEdit = () => {
|
const editClick = () => {
|
||||||
if (props.unknown) {
|
emit('editClick', props.unknown);
|
||||||
togglePassword('edit');
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'BoardEdit', params: { id: props.boardId } });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 삭제 버튼
|
// 삭제
|
||||||
const handleDelete = () => {
|
const deleteClick = () => {
|
||||||
if (props.unknown) {
|
emit('deleteClick', props.unknown);
|
||||||
togglePassword('delete');
|
|
||||||
} else {
|
|
||||||
deletePost();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 비밀번호 입력 토글
|
const handleUpdateReaction = (reactionData) => {
|
||||||
const togglePassword = (button) => {
|
// console.log("🔥 BoardProfile에서 좋아요/싫어요 이벤트 발생");
|
||||||
if (lastClickedButton.value === button) {
|
// console.log("📌 게시글 ID:", props.boardId);
|
||||||
isPassword.value = !isPassword.value;
|
// console.log("📌 댓글 ID (수정 후):", props.comment?.commentId);
|
||||||
} else {
|
// console.log("📌 reactionData:", reactionData);
|
||||||
isPassword.value = true;
|
|
||||||
}
|
emit("updateReaction", {
|
||||||
lastClickedButton.value = button;
|
boardId: props.boardId,
|
||||||
|
commentId: props.comment?.commentId,
|
||||||
|
...reactionData,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 비밀번호 확인
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!password.value) {
|
|
||||||
passwordAlert.value = '비밀번호를 입력해주세요.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestData = {
|
|
||||||
LOCBRDPWD: password.value,
|
|
||||||
LOCBRDSEQ: 288
|
|
||||||
}
|
|
||||||
|
|
||||||
const postResponse = await axios.post(`board/${props.boardId}/password`, requestData);
|
|
||||||
|
|
||||||
if (postResponse.data.code === 200) {
|
|
||||||
if (postResponse.data.data === true) {
|
|
||||||
isPassword.value = false;
|
|
||||||
|
|
||||||
|
|
||||||
if (lastClickedButton.value === 'edit') {
|
|
||||||
router.push({ name: 'BoardEdit', params: { id: props.boardId } });
|
|
||||||
} else if (lastClickedButton.value === 'delete') {
|
|
||||||
await deletePost();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastClickedButton.value = null;
|
|
||||||
} else {
|
|
||||||
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 401 오류
|
|
||||||
if (error.response && error.response.status === 401) {
|
|
||||||
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
|
|
||||||
} else if (error.response) {
|
|
||||||
alert(`오류 발생: ${error.response.data.message || '서버 오류'}`);
|
|
||||||
} else {
|
|
||||||
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deletePost = async () => {
|
|
||||||
if (confirm('정말 삭제하시겠습니까?')) {
|
|
||||||
try {
|
|
||||||
const response = await axios.delete(`board/${props.boardId}`, {
|
|
||||||
data: { LOCBRDSEQ: props.boardId }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
alert('게시물이 삭제되었습니다.');
|
|
||||||
router.push({ name: 'BoardList' });
|
|
||||||
} else {
|
|
||||||
alert('삭제 실패: ' + response.data.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response) {
|
|
||||||
alert(`삭제 실패: ${error.response.data.message || '서버 오류'}`);
|
|
||||||
} else {
|
|
||||||
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="cate-list list-unstyled d-flex flex-wrap mb-0">
|
<ul class="cate-list list-unstyled d-flex flex-wrap mb-0">
|
||||||
<li v-for="category in lists" :key="category.value" class="mt-2 mx-1">
|
<li v-for="category in lists" :key="category.value" class="mt-2 me-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
@ -39,9 +39,7 @@ const selectCategory = (cate) => {
|
|||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.cate-list {
|
|
||||||
margin-left: -0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.cate-list {
|
.cate-list {
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-2">
|
<div class="mb-2 row" >
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex">
|
||||||
<label :for="name" class="col-md-2 col-form-label">
|
<label :for="name" class="col-md-2 col-form-label">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
|
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="btn btn-sm btn-primary" @click="openAddressSearch">주소찾기</button>
|
<div class="align-content-center col-md-10 text-end ms-auto">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" :class="isRow ? '' : 'ms-auto'" @click="openAddressSearch">주소찾기</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12">
|
<div :class="isRow ? 'col-md-10 ms-auto' : 'col-md-12'">
|
||||||
<div class="d-flex mb-3">
|
<div class="d-flex mb-3">
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name"
|
||||||
@ -48,6 +50,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const postcode = ref('');
|
||||||
|
const address = ref('');
|
||||||
|
const detailAddress = ref('');
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -69,19 +75,33 @@ const props = defineProps({
|
|||||||
default: 30,
|
default: 30,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
isRow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
isAlert: {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
required: false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:data', 'update:alert']);
|
// watch 설정 수정
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
postcode.value = newValue.PROJCTZIP || '';
|
||||||
|
address.value = newValue.PROJCTARR || '';
|
||||||
|
detailAddress.value = newValue.PROJCTDTL || '';
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
// 주소 관련 상태 관리
|
const emits = defineEmits(['update:data', 'update:alert']);
|
||||||
const postcode = ref('');
|
|
||||||
const address = ref('');
|
|
||||||
const detailAddress = ref('');
|
|
||||||
|
|
||||||
// 주소 검색 팝업 열기
|
// 주소 검색 팝업 열기
|
||||||
const openAddressSearch = () => {
|
const openAddressSearch = () => {
|
||||||
|
|||||||
@ -40,26 +40,26 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: [String, Number],
|
||||||
default: '0',
|
default: '0',
|
||||||
require: false,
|
require: false,
|
||||||
},
|
},
|
||||||
isAlert : {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isLabel : {
|
isLabel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isRow : {
|
isRow: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isCommon : {
|
isCommon: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
@ -69,11 +69,19 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:data']);
|
const emit = defineEmits(['update:data']);
|
||||||
const selectData = ref(props.value);
|
const selectData = ref(props.value);
|
||||||
|
|
||||||
// data 변경 감지
|
// props.value의 변경을 감지하는 watch 추가
|
||||||
|
watch(() => props.value, (newValue) => {
|
||||||
|
selectData.value = newValue;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// data 변경 감지 수정
|
||||||
watch(() => props.data, (newData) => {
|
watch(() => props.data, (newData) => {
|
||||||
if (props.isCommon && newData.length > 0) {
|
if (props.isCommon && newData.length > 0) {
|
||||||
selectData.value = newData[0].value;
|
// value prop이 '0'(기본값)일 때만 첫번째 아이템 선택
|
||||||
emit('update:data', selectData.value);
|
if (props.value === '0') {
|
||||||
|
selectData.value = newData[0].value;
|
||||||
|
emit('update:data', selectData.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card mb-3 shadow-sm border">
|
<div class="card mb-3 shadow-sm border">
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<!-- 게시물 내용 섹션 -->
|
<div class="card-body">
|
||||||
<div>
|
<!-- 제목 -->
|
||||||
<div class="card-body">
|
<h5 class="card-title">
|
||||||
<!-- 제목 -->
|
{{ title }}
|
||||||
<h5 class="card-title">
|
</h5>
|
||||||
{{ title }}
|
<!-- 날짜 -->
|
||||||
<span class="text-muted me-3" v-if="attachment">
|
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||||
<i class="fa-solid fa-paperclip"></i>
|
<i class="bx bx-calendar"></i>
|
||||||
</span>
|
<div class="ms-2">날짜</div>
|
||||||
</h5>
|
<div class="ms-12">{{ strdate }} ~ {{ enddate }}</div>
|
||||||
<!-- 본문 -->
|
</div>
|
||||||
<div class="card-text line-clamp-2 my-5">{{ content }}</div>
|
<!-- 참여자 -->
|
||||||
<!-- 날짜 -->
|
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start">
|
<i class="bx bxs-user"></i>
|
||||||
<small class="text-muted">{{ formattedDate }}</small>
|
<div class="ms-2">참여자</div>
|
||||||
</div>
|
<UserList :projctSeq="projctSeq" class="ms-8 mb-0" />
|
||||||
|
</div>
|
||||||
|
<!-- 설명 -->
|
||||||
|
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||||
|
<i class="bx bx-detail"></i>
|
||||||
|
<div class="ms-2">설명</div>
|
||||||
|
<div class="ms-12">{{ description }}</div>
|
||||||
|
</div>
|
||||||
|
<!-- 주소 -->
|
||||||
|
<div class="d-flex flex-column flex-sm-row align-items-center pb-2">
|
||||||
|
<i class="bx bxs-map"></i>
|
||||||
|
<div class="ms-2">주소</div>
|
||||||
|
<div class="ms-12">{{ address }}</div>
|
||||||
|
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctCol} !important;`" @click.stop="openModal">log</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title> Log </template>
|
||||||
|
<template #body>
|
||||||
|
<div class="ms-4 mt-2" v-if="logData">
|
||||||
|
<p class="mb-1">{{ logData.createDate }}</p>
|
||||||
|
<strong class="">[{{ logData.creator }}] 프로젝트 등록</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-item" v-if="logData?.updateDate">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="bx bx-edit me-2"></i>
|
||||||
|
<strong>수정 정보</strong>
|
||||||
|
</div>
|
||||||
|
<div class="ms-4 mt-2">
|
||||||
|
<p class="mb-1">{{ logData.updateDate }}</p>
|
||||||
|
<p class="mb-0 text-muted">[{{ logData.updater }}] 프로젝트 수정</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">닫기</button>
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { defineProps, ref } from 'vue';
|
||||||
import { defineProps } from 'vue';
|
import UserList from '@c/user/UserList.vue';
|
||||||
|
import CenterModal from '../modal/CenterModal.vue';
|
||||||
|
import $api from '@api';
|
||||||
|
|
||||||
// Props 정의
|
// Props 정의
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
category: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
content: {
|
strdate: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
date: {
|
enddate: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
address: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
attachment: {
|
projctSeq: {
|
||||||
type: Boolean,
|
type: Number,
|
||||||
default: false,
|
required: false
|
||||||
}
|
},
|
||||||
|
projctCol: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// formattedDate을 computed로 정의
|
defineEmits(['click']);
|
||||||
const formattedDate = computed(() => {
|
|
||||||
const date = new Date(props.date);
|
const isModalOpen = ref(false);
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
|
const logData = ref(null);
|
||||||
date.getDate()
|
|
||||||
).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
|
const fetchLogData = async () => {
|
||||||
date.getMinutes()
|
try {
|
||||||
).padStart(2, "0")}`;
|
const response = await $api.get(`project/log/${props.projctSeq}`);
|
||||||
});
|
logData.value = response.data.data.length > 0 ? response.data.data[0] : {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('로그 정보 조회 실패:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = async () => {
|
||||||
|
await fetchLogData();
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,49 +1,181 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div v-if="posts.length === 0" class="text-center">
|
<div v-if="projectList.length === 0" class="text-center">
|
||||||
<p class="text-muted mt-4">게시물이 없습니다.</p>
|
<p class="text-muted mt-4">게시물이 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="post in posts" :key="post.id" @click="goDetail(post.id)">
|
<div v-for="post in projectList" :key="post.PROJCTSEQ" @click="openModal(post)" class="cursor-pointer">
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
:title="post.title"
|
:title="post.PROJCTNAM"
|
||||||
:content="post.content"
|
:description="post.PROJCTDES"
|
||||||
:date="post.date"
|
:strdate="post.PROJCTSTR"
|
||||||
:attachment="post.attachment || false"
|
:enddate="post.PROJCTEND"
|
||||||
|
:address="post.PROJCTARR + ' ' + post.PROJCTDTL"
|
||||||
|
:projctSeq="post.PROJCTSEQ"
|
||||||
|
:projctCol="post.projctcolor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title> 프로젝트 수정 </template>
|
||||||
|
<template #body>
|
||||||
|
<FormInput
|
||||||
|
title="이름"
|
||||||
|
name="name"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-alert="nameAlert"
|
||||||
|
v-model="selectedProject.PROJCTNAM"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSelect
|
||||||
|
title="컬러"
|
||||||
|
name="color"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-label="true"
|
||||||
|
:is-common="true"
|
||||||
|
:data="allColors"
|
||||||
|
v-model="selectedProject.projctcolor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="시작일"
|
||||||
|
type="date"
|
||||||
|
name="startDay"
|
||||||
|
v-model="selectedProject.PROJCTSTR"
|
||||||
|
:is-essential="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="종료일"
|
||||||
|
type="date"
|
||||||
|
name="endDay"
|
||||||
|
v-model="selectedProject.PROJCTEND"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="설명"
|
||||||
|
name="description"
|
||||||
|
v-model="selectedProject.PROJCTDES"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ArrInput
|
||||||
|
title="주소"
|
||||||
|
name="address"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-row="true"
|
||||||
|
v-model="selectedProject"
|
||||||
|
@update:data="updateAddress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button class="btn btn-secondary" @click="closeModal">Close</button>
|
||||||
|
<button class="btn btn-primary" @click="handleSubmit">Save</button>
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import ProjectCard from './ProjectCard.vue';
|
import ProjectCard from './ProjectCard.vue';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
|
import FormSelect from '@c/input/FormSelect.vue';
|
||||||
|
import commonApi from '@/common/commonApi';
|
||||||
|
import ArrInput from '../input/ArrInput.vue';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
|
||||||
// 테스트용 데이터
|
const projectList = ref([]);
|
||||||
const posts = ref([
|
const isModalOpen = ref(false);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Vue 3 Composition API 소개',
|
|
||||||
content: 'Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다. Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다. Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다. Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다. Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다. Composition API를 사용하여 Vue 3에서 효율적으로 개발하는 방법을 알아봅니다.',
|
|
||||||
date: '2025-02-10',
|
|
||||||
comments: 4,
|
|
||||||
attachment: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Spring Boot로 REST API 만들기',
|
|
||||||
content: 'Spring Boot를 사용하여 간단한 RESTful API를 구현하는 방법을 다룹니다.',
|
|
||||||
date: '2025-02-09',
|
|
||||||
comments: 2,
|
|
||||||
attachment: false
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const emit = defineEmits(['click']);
|
const userStore = useUserInfoStore();
|
||||||
|
const user = ref(null);
|
||||||
|
|
||||||
// 상세 페이지 이동
|
const nameAlert = ref(false);
|
||||||
const goDetail = (id) => {
|
const selectedProject = ref({
|
||||||
emit('click', id);
|
PROJCTNAM: '',
|
||||||
|
projctcolor: '',
|
||||||
|
PROJCTSTR: '',
|
||||||
|
PROJCTEND: '',
|
||||||
|
PROJCTZIP: '',
|
||||||
|
PROJCTARR: '',
|
||||||
|
PROJCTDTL: '',
|
||||||
|
PROJCTDES: '',
|
||||||
|
PROJCTCOL: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { colorList } = commonApi({
|
||||||
|
loadColor: true,
|
||||||
|
colorType: 'YNP',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getProjectList();
|
||||||
|
|
||||||
|
await userStore.userInfo(); // 로그인한 사용자 정보
|
||||||
|
user.value = userStore.user;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 프로젝트 목록 불러오기
|
||||||
|
const getProjectList = () => {
|
||||||
|
$api.get('project/select').then(res => {
|
||||||
|
projectList.value = res.data.data.projectList;
|
||||||
|
console.log(projectList.value);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const openModal = (post) => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
selectedProject.value = { ...post };
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 현재 프로젝트 색상 + 사용하지 않은 색상
|
||||||
|
const allColors = computed(() => {
|
||||||
|
// 기존 색상 추가
|
||||||
|
const existingColor = { value: selectedProject.value.PROJCTCOL, label: selectedProject.value.projctcolor };
|
||||||
|
|
||||||
|
// colorList에 기존 색상을 추가
|
||||||
|
return [existingColor, ...colorList.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAddress = (addressData) => {
|
||||||
|
selectedProject.value = {
|
||||||
|
...selectedProject.value,
|
||||||
|
PROJCTZIP: addressData.postcode,
|
||||||
|
PROJCTARR: addressData.address,
|
||||||
|
PROJCTDTL: addressData.detailAddress
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(projectList.PROJCTSEQ)
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
$api.patch('project/update', {
|
||||||
|
projctSeq: projectList.PROJCTSEQ,
|
||||||
|
projctNam: selectedProject.value.PROJCTNAM,
|
||||||
|
projctCol: selectedProject.value.projctcolor,
|
||||||
|
projctArr: selectedProject.value.PROJCTARR,
|
||||||
|
projctDtl: selectedProject.value.PROJCTDTL,
|
||||||
|
projctZip: selectedProject.value.PROJCTZIP,
|
||||||
|
projctStr: selectedProject.value.PROJCTSTR,
|
||||||
|
projctEnd: selectedProject.value.PROJCTEND,
|
||||||
|
projctDes: selectedProject.value.PROJCTDES,
|
||||||
|
projctUmb: user.value.name,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
||||||
|
closeModal();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div @click="closeModal" class="modal fade" :class="{ 'show': display, 'display-block': display , 'modal-back' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
|
<div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display, 'display-block': display , 'modal-back' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
|
||||||
<div @click.stop class="modal-dialog modal-dialog-centered" role="document">
|
<div @click.stop class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="modalCenterTitle">
|
<h5 class="modal-title m-auto fw-bold" id="modalCenterTitle">
|
||||||
<slot name="title">Modal Title</slot>
|
<slot name="title">Modal Title</slot>
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close" @click="closeModal" aria-label="Close"></button>
|
<button type="button" class="btn-close" @click="closeModal" aria-label="Close"></button>
|
||||||
|
|||||||
@ -1,19 +1,163 @@
|
|||||||
<template>
|
<template>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<CategoryBtn :lists="yearCategory" />
|
<div class="d-flex align-items-center">
|
||||||
|
<CategoryBtn :lists="yearCategory" v-model:selectedCategory="selectedCategory" />
|
||||||
|
<WriteBtn class="mt-2 ms-auto" @click="openModal" />
|
||||||
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
||||||
|
<template #title> 프로젝트 등록 </template>
|
||||||
|
<template #body>
|
||||||
|
<FormInput
|
||||||
|
title="이름"
|
||||||
|
name="name"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-alert="nameAlert"
|
||||||
|
@update:modelValue="name = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSelect
|
||||||
|
title="컬러"
|
||||||
|
name="color"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-label="true"
|
||||||
|
:is-common="true"
|
||||||
|
:data="colorList"
|
||||||
|
@update:data="color = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="시작 일"
|
||||||
|
type="date"
|
||||||
|
name="startDay"
|
||||||
|
v-model="startDay"
|
||||||
|
:is-essential="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="종료 일"
|
||||||
|
name="endDay"
|
||||||
|
:type="'date'"
|
||||||
|
@update:modelValue="endDay = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInput
|
||||||
|
title="설명"
|
||||||
|
name="description"
|
||||||
|
@update:modelValue="description = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ArrInput
|
||||||
|
title="주소"
|
||||||
|
name="address"
|
||||||
|
:isEssential="true"
|
||||||
|
:is-row="true"
|
||||||
|
:is-alert="addressAlert"
|
||||||
|
@update:data="handleAddressUpdate"
|
||||||
|
@update:alert="addressAlert = $event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button class="btn btn-secondary" @click="closeModal">Close</button>
|
||||||
|
<button class="btn btn-primary" @click="handleSubmit">Save</button>
|
||||||
|
</template>
|
||||||
|
</CenterModal>
|
||||||
|
</div>
|
||||||
<ProjectCardList :category="selectedCategory" />
|
<ProjectCardList :category="selectedCategory" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import SearchBar from '@c/search/SearchBar.vue';
|
import SearchBar from '@c/search/SearchBar.vue';
|
||||||
import ProjectCardList from '@c/list/ProjectCardList.vue';
|
import ProjectCardList from '@c/list/ProjectCardList.vue';
|
||||||
import CategoryBtn from '@c/category/CategoryBtn.vue';
|
import CategoryBtn from '@c/category/CategoryBtn.vue';
|
||||||
import commonApi from '@/common/commonApi'
|
import commonApi from '@/common/commonApi';
|
||||||
import { ref } from 'vue';
|
import { inject, onMounted, ref } from 'vue';
|
||||||
|
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||||
|
import CenterModal from '@c/modal/CenterModal.vue';
|
||||||
|
import FormSelect from '@c/input/FormSelect.vue';
|
||||||
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
|
import ArrInput from '@c/input/ArrInput.vue';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
import $api from '@api';
|
||||||
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
|
||||||
const selectedCategory = ref(null);
|
const dayjs = inject('dayjs');
|
||||||
|
|
||||||
const { yearCategory } = commonApi();
|
const today = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
const userStore = useUserInfoStore();
|
||||||
|
|
||||||
|
const user = ref(null);
|
||||||
|
|
||||||
|
const name = ref('');
|
||||||
|
const color = ref('');
|
||||||
|
const address = ref('');
|
||||||
|
const detailAddress = ref('');
|
||||||
|
const postcode = ref('');
|
||||||
|
const startDay = ref(today);
|
||||||
|
const endDay = ref('');
|
||||||
|
const description = ref('');
|
||||||
|
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
const nameAlert = ref(false);
|
||||||
|
const addressAlert = ref(false);
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const selectedCategory = ref(null);
|
||||||
|
|
||||||
|
const { yearCategory, colorList } = commonApi({
|
||||||
|
loadColor: true,
|
||||||
|
colorType: 'YNP',
|
||||||
|
loadYearCategory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 주소 업데이트 핸들러
|
||||||
|
const handleAddressUpdate = addressData => {
|
||||||
|
address.value = addressData.address;
|
||||||
|
detailAddress.value = addressData.detailAddress;
|
||||||
|
postcode.value = addressData.postcode;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await userStore.userInfo(); // 로그인한 사용자 정보
|
||||||
|
user.value = userStore.user;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
|
nameAlert.value = name.value.trim() === '';
|
||||||
|
addressAlert.value = address.value.trim() === '';
|
||||||
|
|
||||||
|
if (nameAlert.value || addressAlert.value ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api.post('project/insert', {
|
||||||
|
projctNam: name.value,
|
||||||
|
projctCol: color.value,
|
||||||
|
projctStr: startDay.value,
|
||||||
|
projctEnd: endDay.value || null,
|
||||||
|
projctDes: description.value || null,
|
||||||
|
projctArr: address.value,
|
||||||
|
projctDtl: detailAddress.value,
|
||||||
|
projctZip: postcode.value,
|
||||||
|
projctCmb: user.value.name,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
|
||||||
|
closeModal();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleSubmit">
|
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
|
<UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
|
||||||
|
|
||||||
@ -13,7 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-grid gap-2 mt-7 mb-5">
|
<div class="d-grid gap-2 mt-7 mb-5">
|
||||||
<button type="submit" class="btn btn-primary">로그인</button>
|
<button type="submit" @click="handleSubmit" class="btn btn-primary">로그인</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 d-flex justify-content-around">
|
<div class="mb-3 d-flex justify-content-around">
|
||||||
@ -25,7 +24,6 @@
|
|||||||
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink>
|
<RouterLink class="text-dark fw-bold" to="/pw">비밀번호 찾기</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -34,7 +32,7 @@
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
import UserFormInput from '@c/input/UserFormInput.vue';
|
||||||
import { useUserStore } from '@s/useUserStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
|
|
||||||
const id = ref('');
|
const id = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
@ -42,7 +40,7 @@
|
|||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const remember = ref(false);
|
const remember = ref(false);
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserInfoStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const handleIdChange = value => {
|
const handleIdChange = value => {
|
||||||
@ -55,21 +53,21 @@
|
|||||||
passwordAlert.value = false;
|
passwordAlert.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
$api.post(
|
idAlert.value = id.value.trim() === '';
|
||||||
'user/login',
|
passwordAlert.value = password.value.trim() === '';
|
||||||
{
|
|
||||||
loginId: id.value,
|
|
||||||
password: password.value,
|
|
||||||
remember: remember.value,
|
|
||||||
},
|
|
||||||
{ headers: { 'X-Page-Route': route.path } },
|
|
||||||
).then(res => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
// const sessionCookie = res.data.data;
|
|
||||||
// document.cookie = `JSESSIONID=${sessionCookie};path=/;expires=-1;`;
|
|
||||||
// document.cookie = `JSESSIONID=${sessionCookie};path=/;HttpOnly=true;samesite=lax`;
|
|
||||||
|
|
||||||
|
if (idAlert.value || passwordAlert.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api.post('user/login', {
|
||||||
|
loginId: id.value,
|
||||||
|
password: password.value,
|
||||||
|
remember: remember.value,
|
||||||
|
}, { headers: { 'X-Page-Route': route.path } })
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
userStore.userInfo();
|
userStore.userInfo();
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,7 +252,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 컬러, mbti, 비밀번호 힌트 목록 불러오기
|
// 컬러, mbti, 비밀번호 힌트 목록 불러오기
|
||||||
const { colorList, mbtiList, pwhintList } = commonApi();
|
const { colorList, mbtiList, pwhintList } = commonApi({
|
||||||
|
loadColor: true, colorType: 'YON',
|
||||||
|
loadMbti: true,
|
||||||
|
loadPwhint: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 주소 업데이트 핸들러
|
// 주소 업데이트 핸들러
|
||||||
const handleAddressUpdate = (addressData) => {
|
const handleAddressUpdate = (addressData) => {
|
||||||
|
|||||||
@ -4,23 +4,22 @@
|
|||||||
v-for="(user, index) in userList"
|
v-for="(user, index) in userList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="avatar pull-up"
|
class="avatar pull-up"
|
||||||
:class="{ 'opacity-100': user.disabled }"
|
:class="{ 'opacity-100': isUserDisabled(user) }"
|
||||||
@click="toggleDisable(index)"
|
@click="toggleDisable(index)"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-popup="tooltip-custom"
|
data-popup="tooltip-custom"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
:aria-label="user.MEMBERSEQ"
|
:aria-label="user.MEMBERSEQ"
|
||||||
:data-bs-original-title="getTooltipTitle(user)"
|
:data-bs-original-title="getTooltipTitle(user)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="rounded-circle user-avatar border border-3"
|
class="rounded-circle user-avatar border border-3"
|
||||||
:class="{ 'grayscaleImg': user.disabled }"
|
:class="{ 'grayscaleImg': isUserDisabled(user) }"
|
||||||
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
||||||
:style="`border-color: ${user.usercolor} !important;`"
|
:style="`border-color: ${user.usercolor} !important;`"
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -29,38 +28,83 @@ import { onMounted, ref, nextTick } from 'vue';
|
|||||||
import { useUserStore } from '@s/userList';
|
import { useUserStore } from '@s/userList';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
|
||||||
const emit = defineEmits();
|
const emit = defineEmits(['user-list-update']);
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const userList = ref([]);
|
const userList = ref([]);
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
projctSeq: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 사용자의 프로젝트 참여 상태 확인
|
||||||
|
const fetchProjectParticipation = async () => {
|
||||||
|
if (props.projctSeq) {
|
||||||
|
const response = await $api.get(`project/members/${props.projctSeq}`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
const projectMembers = response.data.data;
|
||||||
|
userList.value = userList.value.map(user => ({
|
||||||
|
...user,
|
||||||
|
PROJCTYON: projectMembers.find(pm => pm.MEMBERSEQ === user.MEMBERSEQ)?.PROJCTYON ?? '1'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 사용자 목록 호출
|
// 사용자 목록 호출
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await userStore.fetchUserList();
|
await userStore.fetchUserList();
|
||||||
userList.value = userStore.userList;
|
userList.value = userStore.userList;
|
||||||
|
|
||||||
|
if (props.projctSeq) {
|
||||||
|
await fetchProjectParticipation();
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
tooltips.forEach((tooltip) => {
|
tooltips.forEach((tooltip) => {
|
||||||
new bootstrap.Tooltip(tooltip);
|
new bootstrap.Tooltip(tooltip);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 클릭 시 활성화/비활성화
|
// 사용자 비활성화 상태 확인
|
||||||
const toggleDisable = (index) => {
|
const isUserDisabled = (user) => {
|
||||||
|
return props.projctSeq ? user.PROJCTYON === '0' : user.disabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 클릭 시 활성화/비활성화 및 DB 업데이트
|
||||||
|
const toggleDisable = async (index) => {
|
||||||
const user = userList.value[index];
|
const user = userList.value[index];
|
||||||
if (user) {
|
if (user) {
|
||||||
user.disabled = !user.disabled;
|
const newParticipationStatus = props.projctSeq
|
||||||
emitUserListUpdate();
|
? user.PROJCTYON === '1'
|
||||||
|
: !user.disabled;
|
||||||
|
|
||||||
|
if (props.projctSeq) {
|
||||||
|
const response = await $api.patch('project/updateYon', {
|
||||||
|
memberSeq: user.MEMBERSEQ,
|
||||||
|
projctSeq: props.projctSeq,
|
||||||
|
projctYon: newParticipationStatus ? '0' : '1'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
user.PROJCTYON = newParticipationStatus ? '0' : '1';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.disabled = newParticipationStatus;
|
||||||
|
emitUserListUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// emit
|
// emit
|
||||||
const emitUserListUpdate = () => {
|
const emitUserListUpdate = () => {
|
||||||
const activeUsers = userList.value.filter((user) => !user.disabled);
|
const activeUsers = userList.value.filter(user => !isUserDisabled(user));
|
||||||
const disabledUsers = userList.value.filter((user) => user.disabled);
|
const disabledUsers = userList.value.filter(user => isUserDisabled(user));
|
||||||
|
|
||||||
emit('user-list-update', { activeUsers, disabledUsers });
|
emit('user-list-update', { activeUsers, disabledUsers });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +112,3 @@ const getTooltipTitle = (user) => {
|
|||||||
return user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
|
return user.MEMBERSEQ === userStore.userInfo.id ? '나' : user.MEMBERNAM;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
remainingVacationData: Object,
|
remainingVacationData: Object,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserInfoStore();
|
||||||
const userListStore = useUserListStore();
|
const userListStore = useUserListStore();
|
||||||
|
|
||||||
const userList = ref([]);
|
const userList = ref([]);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
||||||
<li v-for="char in koreanChars" :key="char" class="mt-2 mx-1">
|
<li v-for="char in koreanChars" :key="char" class="mt-2 me-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
|
||||||
<li v-for="char in englishChars" :key="char" class="mt-2 mx-1">
|
<li v-for="char in englishChars" :key="char" class="mt-2 me-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
@ -43,9 +43,6 @@ const selectAlphabet = (alphabet) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.alphabet-list {
|
|
||||||
margin-left: -0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.alphabet-list {
|
.alphabet-list {
|
||||||
|
|||||||
@ -1,69 +1,146 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="mt-5 card p-5">
|
<li class="mt-5 card p-5">
|
||||||
<div class="d-flex align-items-center">
|
<DictWrite
|
||||||
<div class="w-100 d-flex align-items-center">
|
v-if="isWriteVisible"
|
||||||
<span class="btn btn-primary pe-none">{{ item.category }}</span>
|
@close="isWriteVisible = false"
|
||||||
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
|
:dataList="cateList"
|
||||||
</div>
|
@addCategory="addCategory"
|
||||||
<EditBtn />
|
:formValue="item.WRDDICCAT"
|
||||||
</div>
|
:titleValue="item.WRDDICTTL"
|
||||||
<p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
|
:contentValue="$common.contentToHtml(item.WRDDICCON)"
|
||||||
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
/>
|
||||||
<div class="d-flex flex-wrap align-items-center mb-50">
|
|
||||||
<div class="avatar avatar-sm me-2">
|
<div v-else>
|
||||||
<img
|
<div class="d-flex align-items-center">
|
||||||
class="rounded-circle user-avatar"
|
<div class="w-100 d-flex align-items-center">
|
||||||
:src="getProfileImage(item.author.profileImage)"
|
<span class="btn btn-primary pe-none">{{ item.category }}</span>
|
||||||
alt="최초 작성자"
|
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
|
||||||
:style="{ borderColor: item.author.color}"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p>
|
<p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
|
||||||
|
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
||||||
|
<div class="d-flex flex-wrap align-items-center mb-50">
|
||||||
|
<div class="avatar avatar-sm me-2">
|
||||||
|
<img
|
||||||
|
class="rounded-circle user-avatar"
|
||||||
|
:src="getProfileImage(item.author.profileImage)"
|
||||||
|
alt="최초 작성자"
|
||||||
|
:style="{ borderColor: item.author.color}"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 small fw-medium">{{ formatDate(item.author.createdAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
||||||
|
<div class="d-flex flex-wrap align-items-center mb-50">
|
||||||
|
<div class="avatar avatar-sm me-2">
|
||||||
|
<img
|
||||||
|
class="rounded-circle user-avatar"
|
||||||
|
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||||
|
alt="최근 작성자"
|
||||||
|
:style="{ borderColor: item.lastEditor.color}"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
|
|
||||||
<div class="d-flex flex-wrap align-items-center mb-50">
|
<div class="edit-btn">
|
||||||
<div class="avatar avatar-sm me-2">
|
<EditBtn @click="toggleWriteVisible" />
|
||||||
<img
|
|
||||||
class="rounded-circle user-avatar"
|
|
||||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
|
||||||
alt="최근 작성자"
|
|
||||||
:style="{ borderColor: item.lastEditor.color}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="mb-0 small fw-medium">{{ formatDate(item.lastEditor.updatedAt) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import axios from "@api";
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
import { ref, toRefs } from 'vue';
|
||||||
import EditBtn from '@/components/button/EditBtn.vue';
|
import EditBtn from '@/components/button/EditBtn.vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
import DictWrite from './DictWrite.vue';
|
||||||
|
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
|
cateList: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 카테고리
|
||||||
|
const localCateList = ref([...props.cateList]);
|
||||||
|
// 선택 카테고리
|
||||||
|
const selectedCategory = ref('');
|
||||||
|
|
||||||
|
// cateList emit
|
||||||
|
const emit = defineEmits(['update:cateList']);
|
||||||
|
|
||||||
|
// 글 수정 상태
|
||||||
|
const isWriteVisible = ref(false);
|
||||||
|
|
||||||
|
|
||||||
|
// 글 수정 toggle
|
||||||
|
const toggleWriteVisible = () => {
|
||||||
|
isWriteVisible.value = !isWriteVisible.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
//카테고리 등록 수정
|
||||||
|
const addCategory = (data) => {
|
||||||
|
try {
|
||||||
|
const lastCategory = localCateList.value.length > 0
|
||||||
|
? localCateList.value[localCateList.value.length - 1]
|
||||||
|
: null;
|
||||||
|
const newValue = lastCategory ? lastCategory.value + 1 : 600101;
|
||||||
|
|
||||||
|
axios.post('worddict/insertCategory', {
|
||||||
|
CMNCODNAM: data
|
||||||
|
}).then(res => {
|
||||||
|
if(res.data.data === 1){
|
||||||
|
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
|
||||||
|
const newCategory = { label: data, value: newValue };
|
||||||
|
localCateList.value = [newCategory, ...localCateList.value];
|
||||||
|
selectedCategory.value = newCategory.value;
|
||||||
|
|
||||||
|
// 부모에게 전달
|
||||||
|
emit('update:cateList', localCateList.value);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('카테고리 추가 중 오류:', err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('카테고리 추가 함수 오류:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||||
// 날짜
|
|
||||||
|
// 날짜 포맷
|
||||||
const formatDate = (dateString) => new Date(dateString).toLocaleString();
|
const formatDate = (dateString) => new Date(dateString).toLocaleString();
|
||||||
// 이미지
|
|
||||||
|
// 프로필 이미지
|
||||||
const getProfileImage = (imagePath) =>
|
const getProfileImage = (imagePath) =>
|
||||||
imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg';
|
imagePath ? `${baseUrl}upload/img/profile/${imagePath}` : '/img/avatars/default-Profile.jpg';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 동그란 테두리 설정 */
|
/* 동그란 테두리 설정 */
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
border: 3px solid; /* 테두리 */
|
border: 3px solid; /* 테두리 */
|
||||||
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */
|
padding: 0.1px; /* 테두리와 이미지 사이의 간격 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.7rem;
|
||||||
|
top: 1.2rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -9,6 +9,7 @@
|
|||||||
:is-common="true"
|
:is-common="true"
|
||||||
@update:data="selectCategory = $event"
|
@update:data="selectCategory = $event"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
|
:value="formValue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 btn-margin">
|
<div class="col-2 btn-margin">
|
||||||
@ -34,11 +35,13 @@
|
|||||||
name="word"
|
name="word"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="wordTitleAlert"
|
:is-alert="wordTitleAlert"
|
||||||
|
:modelValue="titleValue"
|
||||||
@update:modelValue="wordTitle = $event"
|
@update:modelValue="wordTitle = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" />
|
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" />
|
||||||
|
{{ contentValue }}
|
||||||
<div class="text-end mt-5">
|
<div class="text-end mt-5">
|
||||||
<button class="btn btn-primary" @click="saveWord">
|
<button class="btn btn-primary" @click="saveWord">
|
||||||
<i class="bx bx-check"></i>
|
<i class="bx bx-check"></i>
|
||||||
@ -75,17 +78,17 @@ const props = defineProps({
|
|||||||
dataList: {
|
dataList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
formValue : {
|
||||||
|
type:[String, Number]
|
||||||
|
},
|
||||||
|
titleValue : {
|
||||||
|
type:String,
|
||||||
|
},contentValue : {
|
||||||
|
type:String
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 데이터 포맷 수정
|
|
||||||
// const formattedDataList = computed(() =>
|
|
||||||
// props.dataList.map(item => ({
|
|
||||||
// label: item.CMNCODNAM,
|
|
||||||
// value: item.CMNCODVAL
|
|
||||||
// }))
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 카테고리 입력 창
|
// 카테고리 입력 창
|
||||||
const showInput = ref(false);
|
const showInput = ref(false);
|
||||||
|
|
||||||
|
|||||||
@ -228,7 +228,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from '@s/useAuthStore';
|
import { useAuthStore } from '@s/useAuthStore';
|
||||||
import { useUserStore } from '@s/useUserStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useThemeStore } from '@s/darkmode';
|
import { useThemeStore } from '@s/darkmode';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
@ -236,7 +236,7 @@ import { onMounted, ref } from 'vue';
|
|||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserInfoStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
|
/*
|
||||||
|
작성자 : 박지윤
|
||||||
|
작성일 : 2025-02-04
|
||||||
|
수정자 :
|
||||||
|
수정일 :
|
||||||
|
설명 : 로그인 한 사용자 정보
|
||||||
|
*/
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import $api from "@api";
|
import $api from "@api";
|
||||||
|
|
||||||
export const useUserStore = defineStore('userInfo', () => {
|
export const useUserInfoStore = defineStore('userInfo', () => {
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
|
|
||||||
// 사용자 정보 가져오기
|
// 사용자 정보 가져오기
|
||||||
@ -8,9 +8,17 @@
|
|||||||
<BoardProfile
|
<BoardProfile
|
||||||
:boardId="currentBoardId"
|
:boardId="currentBoardId"
|
||||||
:profileName="profileName"
|
:profileName="profileName"
|
||||||
|
:unknown="unknown"
|
||||||
|
:author="isAuthor"
|
||||||
:views="views"
|
:views="views"
|
||||||
:comments="comments"
|
:commentNum="commentNum"
|
||||||
:date="formattedBoardDate"
|
:date="formattedBoardDate"
|
||||||
|
:isLike="false"
|
||||||
|
:isPassword="isPassword"
|
||||||
|
:passwordAlert="passwordAlert"
|
||||||
|
@editClick="editClick"
|
||||||
|
@deleteClick="deleteClick"
|
||||||
|
@submitPassword="submitPassword"
|
||||||
class="pb-6 border-bottom"
|
class="pb-6 border-bottom"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -64,17 +72,28 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul> -->
|
</ul> -->
|
||||||
|
|
||||||
<!-- 댓글 영역 -->
|
<!-- 댓글 입력 영역 -->
|
||||||
<BoardComentArea />
|
<BoardCommentArea
|
||||||
|
:profileName="profileName"
|
||||||
<!-- 수정 버튼 -->
|
:unknown="unknown"
|
||||||
<!-- <button class="btn btn-primary" @click="goToEditPage">
|
@submitComment="handleCommentSubmit"
|
||||||
글 수정
|
/>
|
||||||
</button> -->
|
<!-- <BoardCommentArea :profileName="profileName" :unknown="unknown" /> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
|
||||||
<BoardCommentList/>
|
|
||||||
|
|
||||||
|
<!-- 댓글 목록 -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<BoardCommentList
|
||||||
|
:unknown="unknown"
|
||||||
|
:comments="comments"
|
||||||
|
:isEditTextarea="isEditTextarea"
|
||||||
|
:isPassword="isPassword"
|
||||||
|
@editClick="editClick"
|
||||||
|
@deleteClick="deleteClick"
|
||||||
|
@submitPassword="submitPassword"
|
||||||
|
@updateReaction="handleUpdateReaction"
|
||||||
|
@submitComment="handleCommentReply"
|
||||||
|
/>
|
||||||
<Pagination/>
|
<Pagination/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -84,7 +103,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import BoardComentArea from '@c/board/BoardComentArea.vue';
|
import BoardCommentArea from '@/components/board/BoardCommentArea.vue';
|
||||||
import BoardProfile from '@c/board/BoardProfile.vue';
|
import BoardProfile from '@c/board/BoardProfile.vue';
|
||||||
import BoardCommentList from '@/components/board/BoardCommentList.vue';
|
import BoardCommentList from '@/components/board/BoardCommentList.vue';
|
||||||
import BoardRecommendBtn from '@/components/button/BoardRecommendBtn.vue';
|
import BoardRecommendBtn from '@/components/button/BoardRecommendBtn.vue';
|
||||||
@ -94,7 +113,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('익명 사용자');
|
const profileName = ref('');
|
||||||
const boardTitle = ref('제목 없음');
|
const boardTitle = ref('제목 없음');
|
||||||
const boardContent = ref('');
|
const boardContent = ref('');
|
||||||
const date = ref('');
|
const date = ref('');
|
||||||
@ -103,17 +122,25 @@ const likes = ref(0);
|
|||||||
const dislikes = ref(0);
|
const dislikes = ref(0);
|
||||||
const likeClicked = ref(false);
|
const likeClicked = ref(false);
|
||||||
const dislikeClicked = ref(false);
|
const dislikeClicked = ref(false);
|
||||||
const comments = ref(0);
|
const commentNum = ref(0);
|
||||||
const attachment = ref(false);
|
const attachment = ref(false);
|
||||||
|
const comments = ref([]);
|
||||||
|
|
||||||
// 라우트에서 ID 가져오기
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const currentBoardId = ref(Number(route.params.id));
|
const currentBoardId = ref(Number(route.params.id));
|
||||||
|
const unknown = computed(() => profileName.value === '익명 사용자');
|
||||||
|
const currentUserId = ref('김자바'); // 현재 로그인한 사용자 id
|
||||||
|
const authorId = ref(null); // 작성자 id
|
||||||
|
|
||||||
|
const isAuthor = computed(() => currentUserId.value === authorId.value);
|
||||||
|
|
||||||
|
const isEditTextarea = ref({});
|
||||||
|
|
||||||
|
const passwordAlert = ref("");
|
||||||
|
const isPassword = ref(false);
|
||||||
|
const lastClickedButton = ref("");
|
||||||
|
|
||||||
// 글 수정 페이지로 이동
|
|
||||||
const goToEditPage = () => {
|
|
||||||
router.push({ name: 'BoardEdit', params: { id: currentBoardId.value } });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 게시물 상세 데이터 불러오기
|
// 게시물 상세 데이터 불러오기
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
@ -125,6 +152,13 @@ const fetchBoardDetails = async () => {
|
|||||||
// const boardDetail = data.boardDetail || {};
|
// const boardDetail = data.boardDetail || {};
|
||||||
|
|
||||||
profileName.value = data.author || '익명 사용자';
|
profileName.value = data.author || '익명 사용자';
|
||||||
|
|
||||||
|
// 익명확인하고 싶을때
|
||||||
|
profileName.value = '익명 사용자';
|
||||||
|
|
||||||
|
// 게시글의 작성자 여부를 확인 : 현재 로그인한 사용자가 이 게시글의 작성자인지 여부
|
||||||
|
authorId.value = data.author;
|
||||||
|
|
||||||
boardTitle.value = data.title || '제목 없음';
|
boardTitle.value = data.title || '제목 없음';
|
||||||
boardContent.value = data.content || '';
|
boardContent.value = data.content || '';
|
||||||
date.value = data.date || '';
|
date.value = data.date || '';
|
||||||
@ -132,10 +166,7 @@ const fetchBoardDetails = async () => {
|
|||||||
likes.value = data.likeCount || 0;
|
likes.value = data.likeCount || 0;
|
||||||
dislikes.value = data.dislikeCount || 0;
|
dislikes.value = data.dislikeCount || 0;
|
||||||
attachment.value = data.hasAttachment || null;
|
attachment.value = data.hasAttachment || null;
|
||||||
comments.value = data.commentCount || 0;
|
commentNum.value = data.commentCount || 0;
|
||||||
|
|
||||||
// const response2 = await axios.post(`board/${currentBoardId.value}/password`);
|
|
||||||
// console.log(response2)
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
alert('게시물 데이터를 불러오는 중 오류가 발생했습니다.');
|
||||||
@ -144,50 +175,215 @@ const fetchBoardDetails = async () => {
|
|||||||
|
|
||||||
// 좋아요, 싫어요
|
// 좋아요, 싫어요
|
||||||
const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => {
|
const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) => {
|
||||||
// console.log(type, boardId)
|
|
||||||
try {
|
try {
|
||||||
|
const aa = await axios.post(`/board/${boardId}/${commentId}/reaction`, {
|
||||||
const requestData = {
|
LOCBRDSEQ: boardId, // 게시글 id
|
||||||
LOCBRDSEQ: boardId,
|
LOCCMTSEQ: commentId, //댓글 id
|
||||||
LOCCMTSEQ: commentId,
|
// MEMBERSEQ: 1, // 멤버아이디 지금은 1 나중에 수정해야함
|
||||||
MEMBERSEQ: 1, // 멤버아이디 지금은 1 나중에 수정해야함
|
|
||||||
LOCGOBGOD: isLike ? 'T' : 'F',
|
LOCGOBGOD: isLike ? 'T' : 'F',
|
||||||
LOCGOBBAD: isDislike ? 'T' : 'F'
|
LOCGOBBAD: isDislike ? 'T' : 'F'
|
||||||
};
|
});
|
||||||
|
console.log("좋아요 API 응답 데이터:", aa.data);
|
||||||
|
|
||||||
console.log(requestData)
|
|
||||||
|
|
||||||
const postResponse = await axios.post(`/board/${boardId}/${commentId}/reaction`, requestData);
|
|
||||||
// await axios.post(`board/${boardId}/${commentId}/reaction`, { type });
|
|
||||||
|
|
||||||
const response = await axios.get(`board/${boardId}`);
|
const response = await axios.get(`board/${boardId}`);
|
||||||
const updatedData = response.data.data;
|
const updatedData = response.data.data;
|
||||||
|
|
||||||
console.log('post요청 결과:', postResponse.data);
|
|
||||||
console.log('get요청 결과(좋아요):', response.data.data.likeCount);
|
|
||||||
|
|
||||||
likes.value = updatedData.likeCount;
|
likes.value = updatedData.likeCount;
|
||||||
dislikes.value = updatedData.dislikeCount;
|
dislikes.value = updatedData.dislikeCount;
|
||||||
|
|
||||||
likeClicked.value = isLike;
|
likeClicked.value = isLike;
|
||||||
dislikeClicked.value = isDislike;
|
dislikeClicked.value = isDislike;
|
||||||
|
// console.log(updatedData)
|
||||||
console.log('반응 결과:', postResponse.data);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('반응을 업데이트하는 중 오류 발생');
|
alert('반응을 업데이트하는 중 오류 발생');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 날짜
|
// 댓글 목록 조회
|
||||||
const formattedBoardDate = computed(() => {
|
const fetchComments = async () => {
|
||||||
const dateObj = new Date(date.value);
|
try {
|
||||||
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')}`;
|
const response = await axios.get(`board/${currentBoardId.value}/comments`, {
|
||||||
});
|
params: { LOCBRDSEQ: currentBoardId.value }
|
||||||
|
});
|
||||||
|
console.log("목록 API 응답 데이터:", response.data);
|
||||||
|
|
||||||
|
let allComments = 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: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
allComments.sort((a, b) => b.commentId - a.commentId);
|
||||||
|
|
||||||
|
let commentMap = {};
|
||||||
|
let rootComments = [];
|
||||||
|
|
||||||
|
allComments.forEach(comment => {
|
||||||
|
commentMap[comment.commentId] = comment;
|
||||||
|
});
|
||||||
|
|
||||||
|
allComments.forEach(comment => {
|
||||||
|
if (comment.parentId && commentMap[comment.parentId]) {
|
||||||
|
commentMap[comment.parentId].children.push(comment);
|
||||||
|
} else {
|
||||||
|
rootComments.push(comment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
comments.value = rootComments;
|
||||||
|
|
||||||
|
// console.log("변환된 comments 데이터:", comments.value);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('댓글 목록 불러오기 오류:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 댓글 작성
|
||||||
|
const handleCommentSubmit = async ({ comment, password }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||||
|
LOCBRDSEQ: currentBoardId.value,
|
||||||
|
LOCCMTRPY: comment,
|
||||||
|
LOCCMTPWD: password || null,
|
||||||
|
LOCCMTPNT: 1
|
||||||
|
});
|
||||||
|
// console.log('📥 서버 응답 전체:', response.data);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('댓글 작성 성공:', response.data.message);
|
||||||
|
await fetchComments();
|
||||||
|
} else {
|
||||||
|
console.error('댓글 작성 실패:', response.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('댓글 작성 중 오류 발생:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommentReply = async (reply) => {
|
||||||
|
const response = await axios.post(`board/${currentBoardId.value}/comment`, {
|
||||||
|
LOCBRDSEQ: currentBoardId.value,
|
||||||
|
LOCCMTRPY: reply.comment,
|
||||||
|
LOCCMTPWD: reply.password || null,
|
||||||
|
LOCCMTPNT: reply.parentId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('대댓글 작성 성공:', response.data.message);
|
||||||
|
await fetchComments();
|
||||||
|
} else {
|
||||||
|
console.error('대댓글 작성 실패:', response.data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editClick = (unknown) => {
|
||||||
|
|
||||||
|
if (unknown) {
|
||||||
|
togglePassword("edit");
|
||||||
|
} else {
|
||||||
|
router.push({ name: "BoardEdit", params: { id: currentBoardId.value } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteClick = (unknown) => {
|
||||||
|
if (unknown) {
|
||||||
|
togglePassword("delete");
|
||||||
|
} else {
|
||||||
|
deletePost();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePassword = (button) => {
|
||||||
|
if (lastClickedButton.value === button) {
|
||||||
|
isPassword.value = !isPassword.value;
|
||||||
|
} else {
|
||||||
|
isPassword.value = true;
|
||||||
|
}
|
||||||
|
lastClickedButton.value = button;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const submitPassword = async (inputPassword) => {
|
||||||
|
console.log(inputPassword)
|
||||||
|
if (!inputPassword) {
|
||||||
|
passwordAlert.value = "비밀번호를 입력해주세요.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
LOCBRDPWD: inputPassword,
|
||||||
|
LOCBRDSEQ: 288
|
||||||
|
};
|
||||||
|
|
||||||
|
const postResponse = await axios.post(`board/${currentBoardId.value}/password`, requestData);
|
||||||
|
|
||||||
|
if (postResponse.data.code === 200 && postResponse.data.data === true) {
|
||||||
|
isPassword.value = false;
|
||||||
|
passwordAlert.value = "";
|
||||||
|
|
||||||
|
if (lastClickedButton.value === "edit") {
|
||||||
|
router.push({ name: "BoardEdit", params: { id: currentBoardId.value } });
|
||||||
|
} else if (lastClickedButton.value === "delete") {
|
||||||
|
await deletePost();
|
||||||
|
}
|
||||||
|
lastClickedButton.value = null;
|
||||||
|
} else {
|
||||||
|
passwordAlert.value = "비밀번호가 일치하지 않습니다.";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
passwordAlert.value = "비밀번호가 일치하지 않습니다.";
|
||||||
|
} else if (error.response) {
|
||||||
|
alert(`오류 발생: ${error.response.data.message || "서버 오류"}`);
|
||||||
|
} else {
|
||||||
|
alert("네트워크 오류가 발생했습니다. 다시 시도해주세요.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePost = async () => {
|
||||||
|
if (confirm("정말 삭제하시겠습니까?")) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`board/${currentBoardId.value}`, {
|
||||||
|
data: { LOCBRDSEQ: currentBoardId.value }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
alert("게시물이 삭제되었습니다.");
|
||||||
|
router.push({ name: "BoardList" });
|
||||||
|
} else {
|
||||||
|
alert("삭제 실패: " + response.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
alert(`삭제 실패: ${error.response.data.message || "서버 오류"}`);
|
||||||
|
} else {
|
||||||
|
alert("네트워크 오류가 발생했습니다. 다시 시도해주세요.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜
|
||||||
|
const formattedDate = (dateString) => {
|
||||||
|
if (!dateString) return "날짜 없음";
|
||||||
|
const dateObj = new Date(dateString);
|
||||||
|
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')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedBoardDate = computed(() => formattedDate(date.value));
|
||||||
|
|
||||||
// 컴포넌트 마운트 시 데이터 로드
|
// 컴포넌트 마운트 시 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchBoardDetails();
|
fetchBoardDetails()
|
||||||
|
fetchComments()
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
v-for="item in wordList"
|
v-for="item in wordList"
|
||||||
:key="item.WRDDICSEQ"
|
:key="item.WRDDICSEQ"
|
||||||
:item="item"
|
:item="item"
|
||||||
|
:cateList="cateList"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -91,6 +92,7 @@
|
|||||||
const { cateList } = commonApi({
|
const { cateList } = commonApi({
|
||||||
loadCateList: true
|
loadCateList: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedCategory = ref('');
|
const selectedCategory = ref('');
|
||||||
const selectCategory = ref('');
|
const selectCategory = ref('');
|
||||||
|
|
||||||
@ -167,8 +169,8 @@
|
|||||||
if(res.data.data == '1'){
|
if(res.data.data == '1'){
|
||||||
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
|
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
|
||||||
const newCategory = { label: data, value: newValue };
|
const newCategory = { label: data, value: newValue };
|
||||||
cateList.value.unshift(newCategory);
|
cateList.value = [newCategory, ...cateList.value];
|
||||||
selectCategory.value = newCategory.CMNCODVAL;
|
selectedCategory.value = newCategory.value;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user