370 lines
13 KiB
Vue
370 lines
13 KiB
Vue
<template>
|
|
<div class="container-xxl flex-grow-1 container-p-y">
|
|
<div class="card">
|
|
<div class="pb-4 rounded-top">
|
|
<div class="container py-12 px-xl-10 px-4" style="padding-bottom: 0px !important">
|
|
<h3 class="text-center mb-2 mt-4">글 수정</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-12">
|
|
<div class="card-body">
|
|
<!-- 제목 입력 -->
|
|
<FormInput
|
|
title="제목"
|
|
name="title"
|
|
:is-essential="true"
|
|
:is-alert="titleAlert"
|
|
v-model="title"
|
|
@update:alert="titleAlert = $event"
|
|
@input.once="validateTitle"
|
|
/>
|
|
|
|
<!-- 첨부파일 업로드 -->
|
|
<FormFile
|
|
title="첨부파일"
|
|
name="files"
|
|
:is-alert="attachFilesAlert"
|
|
@update:data="handleFileUpload"
|
|
@update:isValid="isFileValid = $event"
|
|
/>
|
|
|
|
<!-- 실시간 반영된 파일 개수 표시 -->
|
|
<div>
|
|
<p class="text-muted mt-1">첨부파일: {{ fileCount }} / 5개</p>
|
|
<p v-if="fileError" class="text-danger">{{ fileError }}</p>
|
|
|
|
<ul class="list-group mb-2" v-if="attachFiles.length">
|
|
<li
|
|
v-for="(file, index) in attachFiles"
|
|
:key="index"
|
|
class="list-group-item d-flex justify-content-between align-items-center"
|
|
>
|
|
{{ file.name }}
|
|
<button class="close-btn" @click="removeFile(index, file)">✖</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- 내용 입력 -->
|
|
<div class="mb-4">
|
|
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
|
내용
|
|
<span class="text-danger">*</span>
|
|
</label>
|
|
<div class="col-md-12">
|
|
<QEditor
|
|
v-if="contentLoaded"
|
|
@update:data="content = $event"
|
|
@update:imageUrls="imageUrls = $event"
|
|
@update:uploadedImgList="handleUpdateEditorImg"
|
|
@update:deleteImgIndexList="handleDeleteEditorImg"
|
|
:initialData="content"
|
|
/>
|
|
</div>
|
|
<div v-if="contentAlert" class="invalid-feedback d-block">내용을 확인해주세요.</div>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="mb-4 d-flex justify-content-end">
|
|
<button type="button" class="btn btn-info right" @click="goBack">
|
|
<i class="bx bx-left-arrow-alt"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-primary ms-1" :disabled="!isChanged" @click="updateBoard">
|
|
<i class="bx bx-check"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import QEditor from '@c/editor/QEditor.vue';
|
|
import FormInput from '@c/input/FormInput.vue';
|
|
import FormFile from '@c/input/FormFile.vue';
|
|
import { ref, onMounted, computed, watch, inject } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useToastStore } from '@s/toastStore';
|
|
import { useBoardAccessStore } from '@s/useBoardAccessStore';
|
|
import axios from '@api';
|
|
|
|
// 공통
|
|
const $common = inject('common');
|
|
const toastStore = useToastStore();
|
|
const accessStore = useBoardAccessStore();
|
|
|
|
// 상태 변수
|
|
const title = ref('');
|
|
const content = ref('');
|
|
|
|
// 경고 상태
|
|
const titleAlert = ref(false);
|
|
const contentAlert = ref(false);
|
|
const contentLoaded = ref(false);
|
|
|
|
// 라우터
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const currentBoardId = ref(route.params.id); // 라우트에서 ID 가져오기
|
|
|
|
// 파일
|
|
const maxFiles = 5;
|
|
const maxSize = 10 * 1024 * 1024;
|
|
const attachFiles = ref([]);
|
|
const fileError = ref('');
|
|
const attachFilesAlert = ref(false);
|
|
const isFileValid = ref(true);
|
|
const delFileIdx = ref([]); // 제외할 기존 첨부파일 ID
|
|
const editorUploadedImgList = ref([]);
|
|
const editorDeleteImgList = ref([]);
|
|
|
|
const originalTitle = ref('');
|
|
const originalPlainText = ref('');
|
|
const originalFiles = ref([]);
|
|
|
|
|
|
function extractPlainText(delta) {
|
|
if (!delta || !Array.isArray(delta.ops)) return '';
|
|
return delta.ops
|
|
.filter(op => typeof op.insert === 'string')
|
|
.map(op => op.insert.trim())
|
|
.join(' ')
|
|
.trim();
|
|
}
|
|
|
|
const isChanged = computed(() => {
|
|
const isTitleChanged = title.value !== originalTitle.value;
|
|
const currentPlainText = extractPlainText(content.value);
|
|
const isContentChanged = currentPlainText !== originalPlainText.value;
|
|
|
|
const currentAttachedFiles = attachFiles.value.filter(f => f.id);
|
|
const isFilesChanged =
|
|
attachFiles.value.some(f => !f.id) || // id 없는 새 파일이 있는 경우
|
|
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
|
!isSameFiles(
|
|
attachFiles.value.filter(f => f.id), // 기존 파일(id 있는 것만)
|
|
originalFiles.value
|
|
);
|
|
console.log(isTitleChanged);
|
|
console.log(isContentChanged);
|
|
console.log(isFilesChanged);
|
|
return isTitleChanged || isContentChanged || isFilesChanged;
|
|
});
|
|
watch(isChanged, (val) => {
|
|
console.log('🔄 isChanged changed:', val);
|
|
});
|
|
|
|
|
|
// 파일 비교 함수
|
|
function isSameFiles(current, original) {
|
|
if (current.length !== original.length) return false;
|
|
|
|
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
|
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
|
|
|
return sortedCurrent.every((file, idx) => {
|
|
return (
|
|
file.id === sortedOriginal[idx].id &&
|
|
file.name === sortedOriginal[idx].name
|
|
);
|
|
});
|
|
}
|
|
|
|
// 게시물 데이터 로드
|
|
const fetchBoardDetails = async () => {
|
|
// 수정 데이터 전송
|
|
let password = accessStore.password;
|
|
const params = {
|
|
password: `${password}` || '',
|
|
};
|
|
//const response = await axios.get(`board/${currentBoardId.value}`);
|
|
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
|
|
|
if (data.code !== 200) {
|
|
//toastStore.onToast(data.message, 'e');
|
|
alert(data.message, 'e');
|
|
router.back();
|
|
return;
|
|
}
|
|
const boardData = data.data;
|
|
// 기존 첨부파일 추가
|
|
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
|
const formatted = addDisplayFileName([...boardData.attachments]);
|
|
attachFiles.value = formatted;
|
|
originalFiles.value = formatted;
|
|
}
|
|
|
|
// 데이터 설정
|
|
title.value = boardData.title || '제목 없음';
|
|
content.value = boardData.content || '내용 없음';
|
|
originalTitle.value = title.value;
|
|
contentLoaded.value = true;
|
|
};
|
|
|
|
watch(content, (val) => {
|
|
if (contentLoaded.value && !originalPlainText.value) {
|
|
originalPlainText.value = extractPlainText(val);
|
|
}
|
|
}, { immediate: true });
|
|
|
|
const handleUpdateEditorImg = item => {
|
|
editorUploadedImgList.value = item;
|
|
};
|
|
|
|
const handleDeleteEditorImg = item => {
|
|
editorDeleteImgList.value = item;
|
|
};
|
|
|
|
// 기존 첨부파일명을 노출
|
|
const addDisplayFileName = fileInfos =>
|
|
fileInfos.map(file => ({
|
|
...file,
|
|
name: `${file.originalName}.${file.extension}`,
|
|
}));
|
|
|
|
// 상세 페이지로 이동
|
|
const goList = () => {
|
|
accessStore.$reset();
|
|
|
|
// 목록으로 바로 이동시 필터 유지
|
|
// const getFilter = localStorage.getItem(`boardList_${currentBoardId.value}`);
|
|
// if (getFilter) {
|
|
// router.push({
|
|
// path: '/board',
|
|
// query: JSON.parse(getFilter),
|
|
// });
|
|
// } else {
|
|
// router.push('/board');
|
|
// }
|
|
|
|
router.back();
|
|
};
|
|
|
|
// 전 페이지로 이동
|
|
const goBack = () => {
|
|
accessStore.$reset();
|
|
router.back();
|
|
};
|
|
|
|
// 유효성 확인
|
|
const checkValidation = () => {
|
|
contentAlert.value = $common.isNotValidContent(content);
|
|
titleAlert.value = $common.isNotValidInput(title.value);
|
|
|
|
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
|
if (titleAlert.value) {
|
|
title.value = '';
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
////////////////// fileSection[S] ////////////////////
|
|
const fileCount = computed(() => attachFiles.value.length);
|
|
|
|
const handleFileUpload = files => {
|
|
const validFiles = files.filter(file => file.size <= maxSize);
|
|
|
|
if (files.some(file => file.size > maxSize)) {
|
|
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
|
return;
|
|
}
|
|
if (attachFiles.value.length + validFiles.length > maxFiles) {
|
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
|
return;
|
|
}
|
|
fileError.value = '';
|
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
|
};
|
|
|
|
const removeFile = (index, file) => {
|
|
if (file.id) delFileIdx.value.push(file.id);
|
|
|
|
attachFiles.value.splice(index, 1);
|
|
if (attachFiles.value.length <= maxFiles) {
|
|
fileError.value = '';
|
|
}
|
|
};
|
|
|
|
watch(attachFiles, () => {
|
|
isFileValid.value = attachFiles.value.length <= maxFiles;
|
|
});
|
|
|
|
const newFileFilter = attachFiles => {
|
|
const copyFiles = [...attachFiles.value];
|
|
return copyFiles.filter(item => !item.id);
|
|
};
|
|
////////////////// fileSection[E] ////////////////////
|
|
|
|
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
|
watch(content, () => {
|
|
contentAlert.value = $common.isNotValidContent(content);
|
|
});
|
|
|
|
// 글 제목 유효성
|
|
const validateTitle = () => {
|
|
titleAlert.value = title.value.trim().length === 0;
|
|
};
|
|
|
|
// 게시물 수정
|
|
const updateBoard = async () => {
|
|
if (checkValidation()) return;
|
|
|
|
// 수정 데이터 전송
|
|
const boardData = {
|
|
LOCBRDTTL: title.value.trim(),
|
|
LOCBRDCON: JSON.stringify(content.value),
|
|
LOCBRDSEQ: currentBoardId.value,
|
|
};
|
|
|
|
// 업로드 된 첨부파일의 삭제목록
|
|
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
|
boardData.delFileIdx = [...delFileIdx.value];
|
|
}
|
|
|
|
// 에디터에 업로드 된 이미지 인덱스 목록
|
|
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
|
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
|
}
|
|
|
|
// 삭제할 에디터 이미지 인덱스
|
|
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
|
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
|
}
|
|
|
|
const fileArray = newFileFilter(attachFiles);
|
|
const formData = new FormData();
|
|
|
|
// formData에 boardData 추가
|
|
Object.entries(boardData).forEach(([key, value]) => {
|
|
formData.append(key, value);
|
|
});
|
|
|
|
// formData에 새로 추가한 파일 추가
|
|
fileArray.forEach((file, idx) => {
|
|
formData.append('files', file);
|
|
});
|
|
|
|
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
|
if (data.code === 200) {
|
|
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
|
goList();
|
|
} else {
|
|
toastStore.onToast('게시물 수정에 실패했습니다.', 'e');
|
|
}
|
|
};
|
|
|
|
// 컴포넌트 마운트 시 데이터 로드
|
|
onMounted(async () => {
|
|
if (currentBoardId.value) {
|
|
fetchBoardDetails();
|
|
} else {
|
|
console.error('잘못된 게시물 ID:', currentBoardId.value);
|
|
}
|
|
});
|
|
</script>
|