Merge branch 'main' into login

This commit is contained in:
yoon 2025-02-28 13:39:48 +09:00
commit 6a7ed19d54
11 changed files with 230 additions and 206 deletions

View File

@ -3,22 +3,33 @@
/* 휴가 */ /* 휴가 */
.fc-daygrid-day-events {
max-height: 100px !important;
overflow-y: auto !important;
}
/* 이벤트 선 없게 */ /* 이벤트 선 없게 */
.fc-event { .fc-event {
border: none; border: none;
} }
/* 오전전반차 그래프 */ /* 오전 반차 그래프 (왼쪽 절반) */
.fc-daygrid-event.half-day-am { .fc-daygrid-event.half-day-am {
width: calc(50% - 4px) !important; width: 50% !important;
height: 8px !important;
border-radius: 2px !important;
font-size: 0px !important;
} }
/* 오후반차 그래프프 */ /* 오후 반차 그래프 (오른쪽 절반) */
.fc-daygrid-event.half-day-pm { .fc-daygrid-event.half-day-pm {
width: calc(50% - 4px) !important; width: 50% !important;
margin-left: auto !important height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
}
/* 연차 그래프 (풀풀) */
.fc-daygrid-event.full-day {
width: 100% !important;
height: 8px !important;
margin-left: auto !important;
border-radius: 2px !important;
font-size: 0px !important;
} }
/* 공휴일,일요일 색상 */ /* 공휴일,일요일 색상 */
.fc-day-sun .fc-daygrid-day-number, .fc-day-sun .fc-daygrid-day-number,
@ -39,8 +50,8 @@
.flatpickr-calendar:after { .flatpickr-calendar:after {
display: none !important; display: none !important;
} }
/* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */ /* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title { .fc-toolbar-title {
cursor: pointer; cursor: pointer;
} }
/* 클릭 가능한 날짜 (오늘 + 미래) */ /* 클릭 가능한 날짜 (오늘 + 미래) */
@ -84,7 +95,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 본인 모달 */ /* 본인 모달 */
/* 닫기 버튼 */ /* 닫기 버튼 */
.close-btn { .close-btn {
position: absolute; position: absolute;
@ -109,7 +119,6 @@ opacity: 0.6; /* 흐려 보이게 */
/* 선물하기 모달 */ /* 선물하기 모달 */
/* 연차 개수 버튼 */ /* 연차 개수 버튼 */
.count-btn { .count-btn {
font-size: 18px; font-size: 18px;
@ -127,7 +136,6 @@ opacity: 0.6; /* 흐려 보이게 */
background: #cccccc; background: #cccccc;
cursor: not-allowed; cursor: not-allowed;
} }
/* 버튼 컨테이너 (우측 정렬) */ /* 버튼 컨테이너 (우측 정렬) */
.custom-button-container { .custom-button-container {
display: flex; display: flex;
@ -141,18 +149,17 @@ opacity: 0.6; /* 흐려 보이게 */
padding: 10px; /* 크기 조정 */ padding: 10px; /* 크기 조정 */
cursor: pointer; /* 클릭 가능하도록 변경 */ cursor: pointer; /* 클릭 가능하도록 변경 */
} }
/* 아이콘 색상 변경 (기본) */ /* 아이콘 색상 변경 (기본) */
.custom-button i { .custom-button i {
color: #282538; /* 기본 아이콘 색상 */ color: #282538; /* 기본 아이콘 색상 */
font-size: 25px; /* 아이콘 크기 */ font-size: 25px; /* 아이콘 크기 */
} }
/* 버튼 호버 효과 */ /* 버튼 호버 효과 */
.custom-button:hover i { .custom-button:hover i {
color: #ff0800; /* 호버 시 아이콘 색상 변경 */ color: #ff0800; /* 호버 시 아이콘 색상 변경 */
} }
.grayscaleImg { .grayscaleImg {
filter: grayscale(100%); filter: grayscale(100%);
} }

View File

@ -11,35 +11,29 @@
:isLike="!isLike" :isLike="!isLike"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
:isCommentProfile="true" :isCommentProfile="true"
@editClick="aaaa" @editClick="handleEditClick"
@deleteClick="$emit('deleteClick', comment)" @deleteClick="$emit('deleteClick', comment)"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
<!-- <P>Commentpassssss: {{isCommentPassword}}</P> -->
<!-- :author="true" -->
<!-- 댓글 비밀번호 입력창 (익명일 경우) --> <!-- 댓글 비밀번호 입력창 (익명일 경우) -->
<div v-if="isCommentPassword === comment.commentId && unknown" class="mt-3 w-25 ms-auto"> <div v-if="currentPasswordCommentId === comment.commentId && unknown" class="mt-3 w-25 ms-auto">
<div class="input-group"> <div class="input-group">
<input <input
type="password" type="password"
class="form-control" class="form-control"
v-model="password" :value="password"
placeholder="비밀번호 입력" placeholder="비밀번호 입력"
@input="$emit('update:password', $event.target.value.trim())"
/> />
<button class="btn btn-primary" @click="logPasswordAndEmit">확인</button> <button class="btn btn-primary" @click="logPasswordAndEmit">확인</button>
</div> </div>
<span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span> <span v-if="passwordCommentAlert" class="invalid-feedback d-block text-start">{{ passwordCommentAlert }}</span>
</div> </div>
<!-- <p>authorId:{{ comment.authorId }}</p>
<p>코멘트 비교: {{comment.isCommentAuthor}}</p> -->
<div class="mt-6"> <div class="mt-6">
<template v-if="comment.isEditTextarea"> <template v-if="comment.isEditTextarea">
<textarea v-model="localEditedContent" class="form-control"></textarea> <textarea v-model="localEditedContent" class="form-control"></textarea>
<div class="mt-2 d-flex justify-content-end"> <div class="mt-2 d-flex justify-content-end">
<!-- <button class="btn btn-secondary me-2" @click="$emit('cancelEdit', comment)">취소</button> -->
<!-- <button class="btn btn-primary" @click="submitEdit">수정</button> -->
<SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn> <SaveBtn class="btn btn-primary" @click="submitEdit"></SaveBtn>
</div> </div>
</template> </template>
@ -58,10 +52,6 @@
:key="child.commentId" :key="child.commentId"
class="mt-8 pt-6 ps-10 border-top" class="mt-8 pt-6 ps-10 border-top"
> >
<!-- <p>대댓글 데이터(JSON): {{ JSON.stringify(child, null, 2) }}</p> -->
<!-- <p>comment child: {{ comment.children }}</p> -->
<!-- :unknown="child.author === '익명'" -->
<p>child.isCommentPassword: {{ child.isCommentPassword }}</p>
<BoardComment <BoardComment
:comment="child" :comment="child"
:unknown="child.author === '익명'" :unknown="child.author === '익명'"
@ -70,12 +60,17 @@
:isCommentProfile="true" :isCommentProfile="true"
:isCommentAuthor="child.isCommentAuthor" :isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
@editClick="$emit('editClick', $event)" :currentPasswordCommentId="currentPasswordCommentId"
:passwordCommentAlert="passwordCommentAlert"
:password="password"
@editClick="handleReplyEditClick"
@deleteClick="$emit('deleteClick', child)" @deleteClick="$emit('deleteClick', child)"
@submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)" @submitEdit="(comment, editedContent) => $emit('submitEdit', comment, editedContent)"
@cancelEdit="$emit('cancelEdit', child)" @cancelEdit="$emit('cancelEdit', child)"
@submitComment="submitComment" @submitComment="submitComment"
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
@submitPassword="$emit('submitPassword', child, password)"
@update:password="$emit('update:password', $event)"
/> />
</li> </li>
</ul> </ul>
@ -120,14 +115,19 @@ const props = defineProps({
}, },
passwordCommentAlert: { passwordCommentAlert: {
type: String, type: String,
default: false default: ''
} },
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
}); });
// emits // emits
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'submitEdit', 'cancelEdit']); const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'submitEdit', 'cancelEdit', 'update:password']);
const password = ref('');
const localEditedContent = ref(props.comment.content); const localEditedContent = ref(props.comment.content);
// //
@ -154,8 +154,8 @@ const handleUpdateReaction = (reactionData) => {
// //
const logPasswordAndEmit = () => { const logPasswordAndEmit = () => {
emit('submitPassword', props.comment, password.value); console.log('비밀번호 확인',props.password)
password.value = ""; emit('submitPassword', props.comment, props.password);
}; };
watch(() => props.comment.isEditTextarea, (newVal) => { watch(() => props.comment.isEditTextarea, (newVal) => {
@ -169,8 +169,12 @@ const submitEdit = () => {
emit('submitEdit', props.comment, localEditedContent.value); emit('submitEdit', props.comment, localEditedContent.value);
}; };
const aaaa = () => { const handleEditClick = () => {
emit('editClick', props.comment); emit('editClick', props.comment);
} }
const handleReplyEditClick = (comment) => {
emit('editClick', comment);
}
</script> </script>

View File

@ -51,10 +51,7 @@
<!-- 답변 쓰기 버튼 --> <!-- 답변 쓰기 버튼 -->
<div class="ms-auto mt-3 mt-md-0"> <div class="ms-auto mt-3 mt-md-0">
<button class="btn btn-primary" @click="handleCommentSubmit"> <SaveBtn class="btn btn-primary" @click="handleCommentSubmit"></SaveBtn>
<!-- <i class="icon-base bx bx-check"></i> -->
확인
</button>
</div> </div>
</div> </div>
</div> </div>
@ -63,6 +60,7 @@
<script setup> <script setup>
import { ref, defineEmits, defineProps, computed, watch } from 'vue'; import { ref, defineEmits, defineProps, computed, watch } from 'vue';
import SaveBtn from '../button/SaveBtn.vue';
const props = defineProps({ const props = defineProps({
unknown: { unknown: {
@ -75,11 +73,11 @@ const props = defineProps({
}, },
passwordAlert: { passwordAlert: {
type: String, type: String,
default: false default: ''
}, },
commentAlert: { commentAlert: {
type: String, type: String,
default: false default: ''
} }
}); });

View File

@ -11,15 +11,17 @@
:isCommentAuthor="comment.isCommentAuthor" :isCommentAuthor="comment.isCommentAuthor"
:isEditTextarea="comment.isEditTextarea" :isEditTextarea="comment.isEditTextarea"
:isCommentPassword="isCommentPassword" :isCommentPassword="isCommentPassword"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert || ''"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
@editClick="handleEditClick" @editClick="handleEditClick"
@deleteClick="handleDeleteClick" @deleteClick="handleDeleteClick"
@submitPassword="submitPassword" @submitPassword="submitPassword"
@submitComment="submitComment" @submitComment="submitComment"
@commentDeleted="handleCommentDeleted"
@submitEdit="handleSubmitEdit" @submitEdit="handleSubmitEdit"
@cancelEdit="handleCancelEdit" @cancelEdit="handleCancelEdit"
@updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)" @updateReaction="(reactionData) => handleUpdateReaction(reactionData, comment.commentId, comment.boardId)"
@update:password="updatePassword"
/> />
</li> </li>
</ul> </ul>
@ -53,11 +55,17 @@ const props = defineProps({
}, },
passwordCommentAlert: { passwordCommentAlert: {
type: String, type: String,
default: false default: ''
} },
currentPasswordCommentId: {
type: Number
},
password:{
type: String
},
}); });
const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword']); const emit = defineEmits(['submitComment', 'updateReaction', 'editClick', 'deleteClick', 'submitPassword', 'clearPassword','submitEdit', 'update:password']);
const submitComment = (replyData) => { const submitComment = (replyData) => {
emit('submitComment', replyData); emit('submitComment', replyData);
@ -78,7 +86,11 @@ const submitPassword = (comment, password) => {
}; };
const handleEditClick = (comment) => { const handleEditClick = (comment) => {
emit('editClick', comment); if (comment.parentId) {
emit('editClick', comment); //
} else {
emit('editClick', comment); //
}
}; };
const handleSubmitEdit = (comment, editedContent) => { const handleSubmitEdit = (comment, editedContent) => {
@ -100,4 +112,8 @@ const handleCancelEdit = (comment) => {
emit('cancelEdit', comment); // emit('cancelEdit', comment); //
} }
}; };
const updatePassword = (newPassword) => {
emit('update:password', newPassword);
};
</script> </script>

View File

@ -4,6 +4,7 @@
<div v-if="!unknown" class="avatar me-2"> <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">
<h6 class="mb-0">{{ profileName }}</h6> <h6 class="mb-0">{{ profileName }}</h6>
<div class="profile-detail"> <div class="profile-detail">
@ -81,7 +82,7 @@ const props = defineProps({
isCommentProfile: Boolean, // isCommentProfile: Boolean, //
date: { date: {
type: String, type: String,
required: true, required: '',
}, },
views: { views: {
type: Number, type: Number,
@ -117,6 +118,10 @@ const handleUpdateReaction = (reactionData) => {
}); });
}; };
const getProfileImage = (profilePath) => {
return profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
};
</script> </script>

View File

@ -13,7 +13,7 @@ import { ref, computed } from 'vue';
const props = defineProps({ const props = defineProps({
comment: { comment: {
type: Object, type: Object,
required: true, default: () => ({}),
}, },
likeClicked : { likeClicked : {
type : Boolean, type : Boolean,
@ -36,7 +36,7 @@ const props = defineProps({
required: true, required: true,
}, },
commentId: { commentId: {
type: Number, type: [Number, null],
default: null, default: null,
}, },
likeCount: { likeCount: {

View File

@ -28,10 +28,11 @@
</template> </template>
<script setup> <script setup>
import { defineEmits, ref, defineProps } from "vue"; import { defineEmits, ref, defineProps, watch } from "vue";
const props = defineProps({ const props = defineProps({
isDisabled: Boolean isDisabled: Boolean,
selectedDate: String // props
}); });
const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]); const emit = defineEmits(["toggleHalfDay", "addVacationRequests", "resetHalfDay"]);
@ -39,15 +40,16 @@ const halfDayType = ref(null);
const toggleHalfDay = (type) => { const toggleHalfDay = (type) => {
halfDayType.value = type; halfDayType.value = type;
emit("toggleHalfDay", halfDayType.value); emit("toggleHalfDay", halfDayType.value);
// 1
setTimeout(() => {
halfDayType.value = null;
}, 1000);
}; };
// `selectedDate`
watch(() => props.selectedDate, (newDate) => {
if (newDate) {
resetHalfDay();
}
});
// //
const resetHalfDay = () => { const resetHalfDay = () => {
halfDayType.value = null; halfDayType.value = null;
@ -89,20 +91,20 @@ defineExpose({ resetHalfDay });
/* 선택된 (눌린) 버튼 */ /* 선택된 (눌린) 버튼 */
.btn.active { .btn.active {
border: 3px solid #fff; /* 흰색 테두리 강조 */ border: 3px solid #fff;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3); box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.3);
transform: scale(1.1); transform: scale(1.1);
} }
/* AM 버튼 (선택된 상태) */ /* AM 버튼 (선택된 상태) */
.btn-warning.active { .btn-warning.active {
background-color: #ffca2c !important; /* 진한 노란색 */ background-color: #ffca2c !important;
color: black; color: black;
} }
/* PM 버튼 (선택된 상태) */ /* PM 버튼 (선택된 상태) */
.btn-info.active { .btn-info.active {
background-color: #0b5ed7 !important; /* 진한 파란색 */ background-color: #0b5ed7 !important;
color: white; color: white;
} }

View File

@ -1,11 +1,12 @@
<template> <template>
<div class="mb-4 row"> <div class="mb-4 row">
<label :for="name" class="col-md-2 col-form-label">{{ title }}</label> <label :for="inputId" class="col-md-2 col-form-label">{{ title }}</label>
<div class="col-md-10"> <div class="col-md-10">
<input <input
class="form-control" class="form-control"
type="file" type="file"
:id="name" :id="inputId"
ref="fileInput"
@change="changeHandler" @change="changeHandler"
multiple multiple
/> />
@ -21,7 +22,7 @@ import { ref ,computed} from 'vue';
import { fileMsg } from '@/common/msgEnum'; import { fileMsg } from '@/common/msgEnum';
// Props // Props
const prop = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '라벨', default: '라벨',
@ -29,7 +30,7 @@ const prop = defineProps({
}, },
name: { name: {
type: String, type: String,
default: 'nameplz', default: 'fileInput',
required: true, required: true,
}, },
isAlert: { isAlert: {
@ -38,12 +39,13 @@ const prop = defineProps({
required: false, required: false,
}, },
}); });
const inputId = computed(() => props.name || 'defaultFileInput');
const emits = defineEmits(['update:data', 'update:isValid']); const emits = defineEmits(['update:data', 'update:isValid']);
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILE_COUNT = 5; // const MAX_FILE_COUNT = 5; //
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; // const ALLOWED_FILE_TYPES = []; //
const showError = ref(false); const showError = ref(false);
const fileMsgKey = ref(''); // const fileMsgKey = ref(''); //
@ -51,9 +53,12 @@ const fileMsgKey = ref(''); // 에러 메시지 키
const changeHandler = (event) => { const changeHandler = (event) => {
const files = Array.from(event.target.files); const files = Array.from(event.target.files);
const totalSize = files.reduce((sum, file) => sum + file.size, 0); const totalSize = files.reduce((sum, file) => sum + file.size, 0);
const invalidFiles = files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type));
// // ALLOWED_FILE_TYPES
const invalidFiles = ALLOWED_FILE_TYPES.length > 0
? files.filter(file => !ALLOWED_FILE_TYPES.includes(file.type))
: [];
if (totalSize > MAX_TOTAL_SIZE) { if (totalSize > MAX_TOTAL_SIZE) {
showError.value = true; showError.value = true;
fileMsgKey.value = 'FileMaxSizeMsg'; fileMsgKey.value = 'FileMaxSizeMsg';

View File

@ -195,10 +195,7 @@ const fetchGeneralPosts = async (page = 1) => {
}); });
if (data?.data) { if (data?.data) {
// console.log(data) const totalPosts = data.data.total;
const totalPosts = data.data.total; //
// console.log('📌 API :', data.data);
generalList.value = data.data.list.map((post, index) => ({ generalList.value = data.data.list.map((post, index) => ({
realId: post.id, realId: post.id,

View File

@ -27,6 +27,7 @@
class="form-control" class="form-control"
v-model="password" v-model="password"
placeholder="비밀번호 입력" placeholder="비밀번호 입력"
@input="password = password.replace(/\s/g, '')"
/> />
<button class="btn btn-primary" @click="submitPassword">확인</button> <button class="btn btn-primary" @click="submitPassword">확인</button>
</div> </div>
@ -76,12 +77,6 @@
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
</div> </div>
<!-- <p>현재 로그인한 사용자 ID: {{ currentUserId }}</p>
<p>게시글 작성자: {{ authorId }}</p>
<p>isAuthor : {{ isAuthor }}</p> -->
<!-- <p>use이미지:{{userStore.user.img}}</p> -->
<!-- <img :src="`http://localhost:10325/upload/img/profile/${userStore.user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle"/> -->
<!-- 첨부파일 목록 --> <!-- 첨부파일 목록 -->
<!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled"> <!-- <ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
@ -98,7 +93,6 @@
:passwordAlert="passwordAlert" :passwordAlert="passwordAlert"
@submitComment="handleCommentSubmit" @submitComment="handleCommentSubmit"
/> />
<!-- <BoardCommentArea :profileName="profileName" :unknown="unknown" /> -->
</div> </div>
<!-- 댓글 목록 --> <!-- 댓글 목록 -->
@ -106,9 +100,11 @@
<BoardCommentList <BoardCommentList
:unknown="unknown" :unknown="unknown"
:comments="commentsWithAuthStatus" :comments="commentsWithAuthStatus"
:isCommentPassword="Boolean(isCommentPassword)" :isCommentPassword="isCommentPassword"
:isEditTextarea="isEditTextarea" :isEditTextarea="isEditTextarea"
:passwordCommentAlert="passwordCommentAlert" :passwordCommentAlert="passwordCommentAlert"
:currentPasswordCommentId="currentPasswordCommentId"
:password="password"
@editClick="editComment" @editClick="editComment"
@deleteClick="deleteComment" @deleteClick="deleteComment"
@updateReaction="handleCommentReaction" @updateReaction="handleCommentReaction"
@ -117,6 +113,7 @@
@commentDeleted="handleCommentDeleted" @commentDeleted="handleCommentDeleted"
@cancelEdit="handleCancelEdit" @cancelEdit="handleCancelEdit"
@submitEdit="handleSubmitEdit" @submitEdit="handleSubmitEdit"
@update:password="updatePassword"
/> />
<Pagination <Pagination
v-if="pagination.pages" v-if="pagination.pages"
@ -173,7 +170,6 @@ const commentsWithAuthStatus = computed(() => {
isCommentAuthor: reply.authorId === currentUserId.value, isCommentAuthor: reply.authorId === currentUserId.value,
})) }))
})); }));
// console.log(" commentsWithAuthStatus :", updatedComments);
return updatedComments; return updatedComments;
}); });
@ -183,10 +179,15 @@ const passwordAlert = ref("");
const passwordCommentAlert = ref(""); const passwordCommentAlert = ref("");
const isPassword = ref(false); const isPassword = ref(false);
const isCommentPassword = ref(false); const isCommentPassword = ref(false);
const currentPasswordCommentId = ref(null);
const lastClickedButton = ref(""); const lastClickedButton = ref("");
const lastCommentClickedButton = ref(""); const lastCommentClickedButton = ref("");
const isEditTextarea = ref(false); const isEditTextarea = ref(false);
const commentAlert = ref('') const commentAlert = ref('');
const updatePassword = (newPassword) => {
password.value = newPassword;
};
const pagination = ref({ const pagination = ref({
currentPage: 1, currentPage: 1,
@ -210,20 +211,12 @@ const fetchBoardDetails = async () => {
const response = await axios.get(`board/${currentBoardId.value}`); const response = await axios.get(`board/${currentBoardId.value}`);
const data = response.data.data; const data = response.data.data;
// console.log(data)
// API // API
// const boardDetail = data.boardDetail || {}; // const boardDetail = data.boardDetail || {};
profileName.value = data.author || '익명'; profileName.value = data.author || '익명';
//
// profileName.value = '';
// console.log("📌 :", profileName.value); //
// console.log("🔍 (unknown.value):", unknown.value); //
authorId.value = data.authorId; // id authorId.value = data.authorId; // id
boardTitle.value = data.title || '제목 없음'; boardTitle.value = data.title || '제목 없음';
boardContent.value = data.content || ''; boardContent.value = data.content || '';
@ -245,7 +238,6 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
await axios.post(`/board/${boardId}/${commentId}/reaction`, { await axios.post(`/board/${boardId}/${commentId}/reaction`, {
LOCBRDSEQ: boardId, // id LOCBRDSEQ: boardId, // id
LOCCMTSEQ: commentId, // id LOCCMTSEQ: commentId, // id
// MEMBERSEQ: 1, // 1
LOCGOBGOD: isLike ? 'T' : 'F', LOCGOBGOD: isLike ? 'T' : 'F',
LOCGOBBAD: isDislike ? 'T' : 'F' LOCGOBBAD: isDislike ? 'T' : 'F'
}); });
@ -258,13 +250,9 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
likeClicked.value = isLike; likeClicked.value = isLike;
dislikeClicked.value = isDislike; dislikeClicked.value = isDislike;
// console.log(updatedData)
// console.log(" :", updatedData);
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// console.log(' .');
} }
}; };
@ -281,13 +269,10 @@ const handleCommentReaction = async ({ boardId, commentId, isLike, isDislike })
LOCGOBBAD: isDislike ? 'T' : 'F' LOCGOBBAD: isDislike ? 'T' : 'F'
}); });
// console.log(" API :", response.data);
await fetchComments(); await fetchComments();
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// console.log(' ');
} }
}; };
@ -302,8 +287,6 @@ const fetchComments = async (page = 1) => {
} }
}); });
// console.log(response.data.data)
const commentsList = response.data.data.list.map(comment => ({ const commentsList = response.data.data.list.map(comment => ({
commentId: comment.LOCCMTSEQ, // ID commentId: comment.LOCCMTSEQ, // ID
boardId: comment.LOCBRDSEQ, boardId: comment.LOCBRDSEQ,
@ -320,6 +303,8 @@ const fetchComments = async (page = 1) => {
children: [], // children: [], //
})); }));
commentsList.sort((a, b) => b.createdAtRaw - a.createdAtRaw);
for (const comment of commentsList) { for (const comment of commentsList) {
if (!comment.commentId) continue; if (!comment.commentId) continue;
@ -327,8 +312,6 @@ const fetchComments = async (page = 1) => {
params: { LOCCMTPNT: comment.commentId } params: { LOCCMTPNT: comment.commentId }
}); });
// console.log(` (${comment.commentId} ):`, replyResponse.data);
if (replyResponse.data.data) { if (replyResponse.data.data) {
comment.children = replyResponse.data.data.map(reply => ({ comment.children = replyResponse.data.data.map(reply => ({
author: reply.author || '익명', author: reply.author || '익명',
@ -368,23 +351,19 @@ const fetchComments = async (page = 1) => {
navigateLastPage: response.data.data.navigateLastPage // navigateLastPage: response.data.data.navigateLastPage //
}; };
// console.log("📌 :", comments.value);
} catch (error) { } catch (error) {
alert('오류가 발생했습니다.'); alert('오류가 발생했습니다.');
// alert(' :', error);
} }
}; };
// //
const handleCommentSubmit = async (data, isCheck) => { const handleCommentSubmit = async (data) => {
if (!data) { if (!data) {
console.error("handleCommentSubmit: data가 undefined입니다.");
return; return;
} }
const { comment, password } = data; const { comment, password, isCheck } = data;
if (!comment || comment.trim() === "") { if (!comment || comment.trim() === "") {
commentAlert.value = '댓글을 입력해주세요.'; commentAlert.value = '댓글을 입력해주세요.';
@ -404,25 +383,22 @@ const handleCommentSubmit = async (data, isCheck) => {
LOCCMTRPY: comment, LOCCMTRPY: comment,
LOCCMTPWD: isCheck ? password : '', LOCCMTPWD: isCheck ? password : '',
LOCCMTPNT: 1, LOCCMTPNT: 1,
LOCBRDTYP: unknown.value ? "300102" : null LOCBRDTYP: isCheck ? "300102" : null
}); });
if (response.status === 200) { if (response.status === 200) {
// console.log(' :', response.data.message);
passwordAlert.value = ''; passwordAlert.value = '';
commentAlert.value = ''; commentAlert.value = '';
await fetchComments(); await fetchComments();
} else { } else {
// console.error(' :', response.data.message);
alert("댓글 작성을 실패했습니다.") alert("댓글 작성을 실패했습니다.")
} }
} catch (error) { } catch (error) {
// console.error(' :', error);
alert("오류가 발생했습니다.") alert("오류가 발생했습니다.")
} }
}; };
// ( `BoardCommentList` ) //
const handleCommentReply = async (reply) => { const handleCommentReply = async (reply) => {
try { try {
const response = await axios.post(`board/${currentBoardId.value}/comment`, { const response = await axios.post(`board/${currentBoardId.value}/comment`, {
@ -434,22 +410,17 @@ const handleCommentReply = async (reply) => {
}); });
if (response.status === 200) { if (response.status === 200) {
if (response.data.code === 200) { // if (response.data.code === 200) {
// console.log(' :', response.data); await fetchComments();
await fetchComments(); //
} else { } else {
// console.log(' - :', response.data);
alert('대댓글 작성을 실패했습니다.'); alert('대댓글 작성을 실패했습니다.');
} }
} }
} catch (error) { } catch (error) {
// console.error(' :', error);
if (error.response) { if (error.response) {
// console.error(' :', error.response.data);
alert("오류가 발생했습니다."); alert("오류가 발생했습니다.");
} }
alert("오류가 발생했습니다."); alert("오류가 발생했습니다.");
} }
} }
@ -486,45 +457,41 @@ const findCommentById = (commentId, commentsList) => {
return null; return null;
}; };
// // ( )
const editComment = (comment) => { const editComment = (comment) => {
password.value = '';
passwordCommentAlert.value = '';
currentPasswordCommentId.value = null;
//
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
if (!targetComment) { if (!targetComment) {
return; return;
} }
//
const isMyComment = comment.authorId === currentUserId.value; const isMyComment = comment.authorId === currentUserId.value;
const isAnonymous = comment.author === "익명"; const isAnonymous = comment.author === "익명";
if (isMyComment) { if (isMyComment) {
// targetComment.isEditTextarea = !targetComment.isEditTextarea;
targetComment.isEditTextarea = true; lastCommentClickedButton.value = "edit";
} else if (isAnonymous) { } else if (isAnonymous) {
// if (targetComment.isEditTextarea) return;
toggleCommentPassword(comment, "edit"); toggleCommentPassword(comment, "edit");
} else { } else {
// console.log(" - ");
alert("수정이 불가능합니다"); alert("수정이 불가능합니다");
} }
} }
// //
const deleteComment = async (comment) => { const deleteComment = async (comment) => {
//
const isMyComment = comment.authorId === currentUserId.value; const isMyComment = comment.authorId === currentUserId.value;
if (unknown.value && !isMyComment) { if (unknown.value && !isMyComment) {
if (comment.isEditTextarea) { if (comment.isEditTextarea) {
// ,
comment.isEditTextarea = false; comment.isEditTextarea = false;
comment.isCommentPassword = true; comment.isCommentPassword = true;
} else { } else {
//
toggleCommentPassword(comment, "delete"); toggleCommentPassword(comment, "delete");
} }
} else { } else {
@ -532,19 +499,21 @@ const deleteComment = async (comment) => {
} }
}; };
// //
const toggleCommentPassword = (comment, button) => { const toggleCommentPassword = (comment, button) => {
if (lastCommentClickedButton.value === button && isCommentPassword.value === comment.commentId) { if (lastCommentClickedButton.value === button && currentPasswordCommentId.value === comment.commentId) {
isCommentPassword.value = false; // currentPasswordCommentId.value = null; //
password.value = '';
passwordCommentAlert.value = '';
} else { } else {
isCommentPassword.value = comment.commentId; // currentPasswordCommentId.value = comment.commentId; //
password.value = '';
passwordCommentAlert.value = '';
} }
lastCommentClickedButton.value = button; lastCommentClickedButton.value = button;
}; };
const togglePassword = (button) => { const togglePassword = (button) => {
if (lastClickedButton.value === button) { if (lastClickedButton.value === button) {
isPassword.value = !isPassword.value; isPassword.value = !isPassword.value;
@ -556,16 +525,15 @@ const togglePassword = (button) => {
// //
const submitPassword = async () => { const submitPassword = async () => {
if (!password.value) { if (!password.value.trim()) {
passwordAlert.value = "비밀번호를 입력해주세요."; passwordAlert.value = "비밀번호를 입력해주세요.";
return; return;
} }
// console.log("📌 : submitPassword ");
try { try {
const response = await axios.post(`board/${currentBoardId.value}/password`, { const response = await axios.post(`board/${currentBoardId.value}/password`, {
LOCBRDPWD: password.value, LOCBRDPWD: password.value,
LOCBRDSEQ: 288, // ID LOCBRDSEQ: 288,
}); });
if (response.data.code === 200 && response.data.data === true) { if (response.data.code === 200 && response.data.data === true) {
@ -582,8 +550,6 @@ const submitPassword = async () => {
passwordAlert.value = "비밀번호가 일치하지 않습니다."; passwordAlert.value = "비밀번호가 일치하지 않습니다.";
} }
} catch (error) { } catch (error) {
// console.log("📌 :", error);
if (error.response) { if (error.response) {
if (error.response.status === 401) { if (error.response.status === 401) {
passwordAlert.value = "비밀번호가 일치하지 않습니다."; passwordAlert.value = "비밀번호가 일치하지 않습니다.";
@ -598,34 +564,38 @@ const submitPassword = async () => {
} }
}; };
// ( ) // ( )
const submitCommentPassword = async (comment, password) => { const submitCommentPassword = async (comment, password) => {
// console.log(" :", password);
// console.log(" ID:", comment.commentId);
if (!password) { if (!password) {
passwordCommentAlert.value = "비밀번호를 입력해주세요."; passwordCommentAlert.value = "비밀번호를 입력해주세요.";
return; return;
} }
const targetComment = findCommentById(comment.commentId, comments.value);
try { try {
// console.log(' ')
const response = await axios.post(`board/comment/${comment.commentId}/password`, { const response = await axios.post(`board/comment/${comment.commentId}/password`, {
LOCCMTPWD: password, LOCCMTPWD: password,
LOCCMTSEQ: comment.commentId, LOCCMTSEQ: comment.commentId,
}); });
// console.log(" :", response.data);
if (response.data.code === 200 && response.data.data === true) { if (response.data.code === 200 && response.data.data === true) {
passwordCommentAlert.value = ""; passwordCommentAlert.value = "";
comment.isCommentPassword = false; comment.isCommentPassword = false;
//
if (lastCommentClickedButton.value === "edit") { if (lastCommentClickedButton.value === "edit") {
comment.isEditTextarea = true;
passwordCommentAlert.value = "";
// handleSubmitEdit(comment, comment.content); if (targetComment) {
// input
targetComment.isEditTextarea = true;
passwordCommentAlert.value = "";
currentPasswordCommentId.value = null;
} else {
alert("수정 취소를 실패했습니다.");
}
//
} else if (lastCommentClickedButton.value === "delete") { } else if (lastCommentClickedButton.value === "delete") {
passwordCommentAlert.value = ""; passwordCommentAlert.value = "";
@ -633,14 +603,12 @@ const submitCommentPassword = async (comment, password) => {
} }
lastCommentClickedButton.value = null; lastCommentClickedButton.value = null;
} else { } else {
// console.log(" ");
passwordCommentAlert.value = "비밀번호가 일치하지 않습니다."; passwordCommentAlert.value = "비밀번호가 일치하지 않습니다.";
} }
} catch (error) { } catch (error) {
// console.log("🚨 :", error.response?.data || error); if (error.response?.status === 401) {
// if (error.response?.status === 401) { passwordCommentAlert.value = "비밀번호가 일치하지 않습니다";
// console.log(" 401 : ( )"); }
// }
passwordCommentAlert.value = "비밀번호가 일치하지 않습니다"; passwordCommentAlert.value = "비밀번호가 일치하지 않습니다";
} }
}; };
@ -672,24 +640,18 @@ const deletePost = async () => {
// ( ) // ( )
const deleteReplyComment = async (comment) => { const deleteReplyComment = async (comment) => {
if (!confirm("정말 이 댓글을 삭제하시겠습니까?")) return; if (!confirm("정말 이 댓글을 삭제하시겠습니까?")) return;
// console.log(" ID:", comment);
try { try {
const response = await axios.delete(`board/comment/${comment.commentId}`, { const response = await axios.delete(`board/comment/${comment.commentId}`, {
data: { LOCCMTSEQ: comment.commentId } data: { LOCCMTSEQ: comment.commentId }
}); });
// console.log(" :", response.data);
if (response.data.code === 200) { if (response.data.code === 200) {
// console.log(" !");
await fetchComments(); await fetchComments();
} else { } else {
// console.log(" :", response.data.message);
alert("댓글 삭제에 실패했습니다."); alert("댓글 삭제에 실패했습니다.");
} }
} catch (error) { } catch (error) {
// console.log(" :", error);
alert("댓글 삭제 중 오류가 발생했습니다."); alert("댓글 삭제 중 오류가 발생했습니다.");
} }
}; };
@ -702,9 +664,6 @@ const handleSubmitEdit = async (comment, editedContent) => {
LOCCMTRPY: editedContent LOCCMTRPY: editedContent
}); });
//
// comment.content = editedContent;
// comment.isEditTextarea = false; f
if (response.status === 200) { if (response.status === 200) {
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
@ -712,15 +671,12 @@ const handleSubmitEdit = async (comment, editedContent) => {
targetComment.content = editedContent; // targetComment.content = editedContent; //
targetComment.isEditTextarea = false; // targetComment.isEditTextarea = false; //
} else { } else {
// console.warn(" ");
alert("수정할 댓글을 찾을 수 없습니다."); alert("수정할 댓글을 찾을 수 없습니다.");
} }
} else { } else {
// console.log(" :", response.data);
alert("댓글 수정 실패했습니다."); alert("댓글 수정 실패했습니다.");
} }
} catch (error) { } catch (error) {
// console.error(" :", error);
alert("댓글 수정 중 오류 발생했습니다."); alert("댓글 수정 중 오류 발생했습니다.");
} }
}; };
@ -730,10 +686,8 @@ const handleCancelEdit = (comment) => {
const targetComment = findCommentById(comment.commentId, comments.value); const targetComment = findCommentById(comment.commentId, comments.value);
if (targetComment) { if (targetComment) {
// console.log(" , :", targetComment);
targetComment.isEditTextarea = false; targetComment.isEditTextarea = false;
} else { } else {
// console.error(" , ");
alert("수정 취소를 실패했습니다."); alert("수정 취소를 실패했습니다.");
} }
}; };
@ -756,7 +710,7 @@ const handleCommentDeleted = (deletedCommentId) => {
return; return;
} }
// 2 //
for (let parent of comments.value) { for (let parent of comments.value) {
const childIndex = parent.children.findIndex(child => child.commentId === deletedCommentId); const childIndex = parent.children.findIndex(child => child.commentId === deletedCommentId);
if (childIndex !== -1) { if (childIndex !== -1) {
@ -764,11 +718,8 @@ const handleCommentDeleted = (deletedCommentId) => {
return; return;
} }
} }
// console.error(" :", deletedCommentId);
}; };
// //
const formattedDate = (dateString) => { const formattedDate = (dateString) => {
if (!dateString) return "날짜 없음"; if (!dateString) return "날짜 없음";

View File

@ -7,9 +7,11 @@
<div class="sidebar-content"> <div class="sidebar-content">
<div class="sidebar-actions text-center my-3"> <div class="sidebar-actions text-center my-3">
<HalfDayButtons <HalfDayButtons
ref="halfDayButtonsRef"
@toggleHalfDay="toggleHalfDay" @toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges" @addVacationRequests="saveVacationChanges"
:isDisabled="!hasChanges" :isDisabled="!hasChanges"
:selectedDate="selectedDate"
/> />
</div> </div>
<ProfileList <ProfileList
@ -58,28 +60,31 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue"; import { reactive, ref, onMounted, nextTick, computed, watch, onBeforeUnmount } from "vue";
import axios from "@api"; import axios from "@api";
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect // Flatpickr MonthSelect
import flatpickr from "flatpickr"; import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index"; import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css"; import "flatpickr/dist/plugins/monthSelect/style.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue"; import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@c/vacation/ProfileList.vue"; import ProfileList from "@c/vacation/ProfileList.vue";
import VacationModal from "@c/modal/VacationModal.vue"; import VacationModal from "@c/modal/VacationModal.vue";
import VacationGrantModal from "@c/modal/VacationGrantModal.vue"; import VacationGrantModal from "@c/modal/VacationGrantModal.vue";
import { useUserStore } from "@s/userList"; import { useUserStore } from "@s/userList";
import { useUserInfoStore } from "@s/useUserInfoStore"; import { useUserInfoStore } from "@s/useUserInfoStore";
import { fetchHolidays } from "@c/calendar/holiday.js"; import { fetchHolidays } from "@c/calendar/holiday.js";
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useRouter } from "vue-router";
const router = useRouter();
const toastStore = useToastStore(); const toastStore = useToastStore();
const userStore = useUserInfoStore(); const userStore = useUserInfoStore();
@ -90,7 +95,7 @@
const receivedVacations = ref([]); const receivedVacations = ref([]);
const isModalOpen = ref(false); const isModalOpen = ref(false);
const remainingVacationData = ref({}); const remainingVacationData = ref({});
const selectedDate = ref(null);
const lastRemainingYear = ref(new Date().getFullYear()); const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0")); const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false); const isGrantModalOpen = ref(false);
@ -106,6 +111,39 @@
const fetchedEvents = ref([]); const fetchedEvents = ref([]);
const halfDayButtonsRef = ref(null); const halfDayButtonsRef = ref(null);
//
router.beforeEach((to, from, next) => {
if (hasChanges.value) {
const answer = window.confirm("저장하지 않은 변경 사항이 있습니다. 이동하시겠습니까?");
if (!answer) {
return next(false); //
}
}
next();
});
onBeforeUnmount(() => {
window.removeEventListener("beforeunload", preventUnsavedChanges);
});
function preventUnsavedChanges(event) {
if (hasChanges.value) {
event.preventDefault();
event.returnValue = ""; //
}
}
// `selectedDates`
watch(
() => Array.from(selectedDates.value.keys()), //
(newKeys) => {
if (halfDayButtonsRef.value) {
halfDayButtonsRef.value.resetHalfDay();
}
},
{ deep: true }
);
// ref // ref
const calendarDatepicker = ref(null); const calendarDatepicker = ref(null);
let fpInstance = null; let fpInstance = null;
@ -192,6 +230,7 @@ function handleDateClick(info) {
await fetchRemainingVacation(); await fetchRemainingVacation();
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
await fetchVacationHistory(currentYear); await fetchVacationHistory(currentYear);
window.addEventListener("beforeunload", preventUnsavedChanges);
// Flatpickr ( ) // Flatpickr ( )
fpInstance = flatpickr(calendarDatepicker.value, { fpInstance = flatpickr(calendarDatepicker.value, {