게시판 동영상 수정

This commit is contained in:
dyhj625 2025-04-10 13:28:50 +09:00
parent 5c7f7c6346
commit ba9a752250
2 changed files with 76 additions and 83 deletions

View File

@ -99,7 +99,7 @@
// //
const title = ref(''); const title = ref('');
const content = ref(''); const content = ref({ ops: [] });
const autoIncrement = ref(0); const autoIncrement = ref(0);
// //
@ -130,10 +130,9 @@
// //
const isFirstContentUpdate = ref(true); const isFirstContentUpdate = ref(true);
// // ( )
const handleEditorDataUpdate = data => { const handleEditorDataUpdate = data => {
content.value = data; content.value = data;
if (isFirstContentUpdate.value) { if (isFirstContentUpdate.value) {
originalContent.value = structuredClone(data); originalContent.value = structuredClone(data);
isFirstContentUpdate.value = false; isFirstContentUpdate.value = false;
@ -141,23 +140,28 @@
} }
}; };
// isDeltaChanged ( diff , , )
function isDeltaChanged(current, original) { function isDeltaChanged(current, original) {
const Delta = Quill.import('delta'); const Delta = Quill.import('delta');
const currentDelta = new Delta(current || []); const currentDelta = new Delta(current || []);
const originalDelta = new Delta(original || []); const originalDelta = new Delta(original || []);
const diff = originalDelta.diff(currentDelta); //
if (!diff || diff.ops.length === 0) return false;
//
const getPlainText = delta => const getPlainText = delta =>
(delta.ops || []) (delta.ops || [])
.filter(op => typeof op.insert === 'string') .filter(op => typeof op.insert === 'string')
.map(op => op.insert) .map(op => op.insert)
.join(''); .join('');
// URL
const getImages = delta => const getImages = delta =>
(delta.ops || []).filter(op => typeof op.insert === 'object' && op.insert.image).map(op => op.insert.image); (delta.ops || [])
.filter(op => typeof op.insert === 'object' && op.insert.image)
.map(op => op.insert.image);
// URL
const getVideos = delta =>
(delta.ops || [])
.filter(op => typeof op.insert === 'object' && op.insert.video)
.map(op => op.insert.video);
const textCurrent = getPlainText(currentDelta); const textCurrent = getPlainText(currentDelta);
const textOriginal = getPlainText(originalDelta); const textOriginal = getPlainText(originalDelta);
@ -165,22 +169,27 @@
const imgsCurrent = getImages(currentDelta); const imgsCurrent = getImages(currentDelta);
const imgsOriginal = getImages(originalDelta); const imgsOriginal = getImages(originalDelta);
const textEqual = textCurrent === textOriginal; const vidsCurrent = getVideos(currentDelta);
const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal); const vidsOriginal = getVideos(originalDelta);
return !(textEqual && imageEqual); // false const textEqual = textCurrent === textOriginal;
const imageEqual = imgsCurrent.length === imgsOriginal.length && imgsCurrent.every((val, idx) => val === imgsOriginal[idx]);
const videoEqual = vidsCurrent.length === vidsOriginal.length && vidsCurrent.every((val, idx) => val === vidsOriginal[idx]);
return !(textEqual && imageEqual && videoEqual);
} }
//
const isChanged = computed(() => { const isChanged = computed(() => {
if (!contentInitialized.value) return false; if (!contentInitialized.value) return false;
const isTitleChanged = title.value !== originalTitle.value; const isTitleChanged = title.value !== originalTitle.value;
const isContentChanged = isDeltaChanged(content.value, originalContent.value); const isContentChanged = isDeltaChanged(content.value, originalContent.value);
const isFilesChanged = const isFilesChanged =
attachFiles.value.some(f => !f.id) || // id attachFiles.value.some(f => !f.id) || //
delFileIdx.value.length > 0 || // delFileIdx.value.length > 0 || //
!isSameFiles( !isSameFiles(
attachFiles.value.filter(f => f.id), // (id ) attachFiles.value.filter(f => f.id), //
originalFiles.value, originalFiles.value
); );
return isTitleChanged || isContentChanged || isFilesChanged; return isTitleChanged || isContentChanged || isFilesChanged;
}); });
@ -188,10 +197,8 @@
// //
function isSameFiles(current, original) { function isSameFiles(current, original) {
if (current.length !== original.length) return false; if (current.length !== original.length) return false;
const sortedCurrent = [...current].sort((a, b) => a.id - b.id); const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
const sortedOriginal = [...original].sort((a, b) => a.id - b.id); const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
return sortedCurrent.every((file, idx) => { return sortedCurrent.every((file, idx) => {
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name; return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
}); });
@ -199,31 +206,24 @@
// //
const fetchBoardDetails = async () => { const fetchBoardDetails = async () => {
//
let password = accessStore.password; let password = accessStore.password;
const params = { const params = {
password: `${password}` || '', password: `${password}` || '',
}; };
//const response = await axios.get(`board/${currentBoardId.value}`);
const { data } = await axios.post(`board/${currentBoardId.value}`, params); const { data } = await axios.post(`board/${currentBoardId.value}`, params);
if (data.code !== 200) { if (data.code !== 200) {
//toastStore.onToast(data.message, 'e');
alert(data.message, 'e'); alert(data.message, 'e');
router.back(); router.back();
return; return;
} }
const boardData = data.data; const boardData = data.data;
//
if (boardData.hasAttachment && boardData.attachments.length > 0) { if (boardData.hasAttachment && boardData.attachments.length > 0) {
const formatted = addDisplayFileName([...boardData.attachments]); const formatted = addDisplayFileName([...boardData.attachments]);
attachFiles.value = formatted; attachFiles.value = formatted;
originalFiles.value = formatted; originalFiles.value = formatted;
} }
//
title.value = boardData.title || '제목 없음'; title.value = boardData.title || '제목 없음';
content.value = boardData.content || '내용 없음'; content.value = boardData.content || { ops: [] };
originalTitle.value = title.value; originalTitle.value = title.value;
originalContent.value = structuredClone(boardData.content); originalContent.value = structuredClone(boardData.content);
contentInitialized.value = true; contentInitialized.value = true;
@ -242,38 +242,34 @@
const addDisplayFileName = fileInfos => const addDisplayFileName = fileInfos =>
fileInfos.map(file => ({ fileInfos.map(file => ({
...file, ...file,
name: `${file.originalName}.${file.extension}`, name: `${file.originalName}.${file.extension}`
})); }));
// //
const goList = () => { const goList = () => {
accessStore.$reset(); 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(); router.back();
}; };
// //
const goBack = () => { const goBack = () => {
accessStore.$reset(); accessStore.$reset();
router.back(); router.back();
}; };
// // ( : , , )
const checkValidation = () => { const isNotValidContent = delta => {
contentAlert.value = $common.isNotValidContent(content); if (!delta?.ops?.length) return true;
titleAlert.value = $common.isNotValidInput(title.value); const hasText = delta.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
const hasImage = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
const hasVideo = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.video);
return !(hasText || hasImage || hasVideo);
};
//
const checkValidation = () => {
contentAlert.value = isNotValidContent(content.value);
titleAlert.value = $common.isNotValidInput(title.value);
if (titleAlert.value || contentAlert.value || !isFileValid.value) { if (titleAlert.value || contentAlert.value || !isFileValid.value) {
if (titleAlert.value) { if (titleAlert.value) {
title.value = ''; title.value = '';
@ -289,7 +285,6 @@
const handleFileUpload = files => { const handleFileUpload = files => {
const validFiles = files.filter(file => file.size <= maxSize); const validFiles = files.filter(file => file.size <= maxSize);
if (files.some(file => file.size > maxSize)) { if (files.some(file => file.size > maxSize)) {
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.'; fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
return; return;
@ -300,13 +295,11 @@
} }
fileError.value = ''; fileError.value = '';
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles); attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
autoIncrement.value++; autoIncrement.value++;
}; };
const removeFile = (index, file) => { const removeFile = (index, file) => {
if (file.id) delFileIdx.value.push(file.id); if (file.id) delFileIdx.value.push(file.id);
attachFiles.value.splice(index, 1); attachFiles.value.splice(index, 1);
if (attachFiles.value.length <= maxFiles) { if (attachFiles.value.length <= maxFiles) {
fileError.value = ''; fileError.value = '';
@ -324,55 +317,41 @@
}; };
////////////////// fileSection[E] //////////////////// ////////////////// fileSection[E] ////////////////////
/** `content` 변경 감지하여 자동 유효성 검사 실행 */ /** content 변경 감지 (deep 옵션 추가) */
watch(content, () => { watch(content, () => {
contentAlert.value = $common.isNotValidContent(content); contentAlert.value = isNotValidContent(content.value);
}); }, { deep: true });
// //
const validateTitle = () => { const validateTitle = () => {
titleAlert.value = title.value.trim().length === 0; titleAlert.value = title.value.trim().length === 0;
}; };
// //
const updateBoard = async () => { const updateBoard = async () => {
if (checkValidation()) return; if (checkValidation()) return;
//
const boardData = { const boardData = {
LOCBRDTTL: title.value.trim(), LOCBRDTTL: title.value.trim(),
LOCBRDCON: JSON.stringify(content.value), LOCBRDCON: JSON.stringify(content.value),
LOCBRDSEQ: currentBoardId.value, LOCBRDSEQ: currentBoardId.value
}; };
//
if (delFileIdx.value && delFileIdx.value.length > 0) { if (delFileIdx.value && delFileIdx.value.length > 0) {
boardData.delFileIdx = [...delFileIdx.value]; boardData.delFileIdx = [...delFileIdx.value];
} }
//
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) { if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
boardData.editorUploadedImgList = [...editorUploadedImgList.value]; boardData.editorUploadedImgList = [...editorUploadedImgList.value];
} }
//
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) { if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
boardData.editorDeleteImgList = [...editorDeleteImgList.value]; boardData.editorDeleteImgList = [...editorDeleteImgList.value];
} }
const fileArray = newFileFilter(attachFiles); const fileArray = newFileFilter(attachFiles);
const formData = new FormData(); const formData = new FormData();
// formData boardData
Object.entries(boardData).forEach(([key, value]) => { Object.entries(boardData).forEach(([key, value]) => {
formData.append(key, value); formData.append(key, value);
}); });
// formData
fileArray.forEach((file, idx) => { fileArray.forEach((file, idx) => {
formData.append('files', file); formData.append('files', file);
}); });
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true }); const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
if (data.code === 200) { if (data.code === 200) {
toastStore.onToast('게시물이 수정되었습니다.', 's'); toastStore.onToast('게시물이 수정되었습니다.', 's');

View File

@ -37,7 +37,9 @@
</label> </label>
</div> </div>
</div> </div>
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">카테고리를 선택해주세요.</div> <div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
카테고리를 선택해주세요.
</div>
</div> </div>
<!-- 비밀번호 필드 (익명게시판 선택 활성화) --> <!-- 비밀번호 필드 (익명게시판 선택 활성화) -->
@ -101,11 +103,14 @@
@update:deleteImgIndexList="handleDeleteEditorImg" @update:deleteImgIndexList="handleDeleteEditorImg"
/> />
</div> </div>
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div> <div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
내용을 입력해주세요.
</div>
</div> </div>
<div class="mb-4 d-flex justify-content-end"> <div class="mb-4 d-flex justify-content-end">
<BackButton @click="goList" /> <BackButton @click="goList" />
<!-- 저장 버튼은 항상 활성화 -->
<SaveButton @click="write" :isEnabled="isFileValid" /> <SaveButton @click="write" :isEnabled="isFileValid" />
</div> </div>
</div> </div>
@ -115,7 +120,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue'; import { ref, onMounted, watch, computed } from 'vue';
import QEditor from '@c/editor/QEditor.vue'; import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue'; import FormFile from '@c/input/FormFile.vue';
@ -169,10 +174,12 @@
const fileCount = computed(() => attachFiles.value.length); const fileCount = computed(() => attachFiles.value.length);
//
const handleUpdateEditorImg = item => { const handleUpdateEditorImg = item => {
editorUploadedImgList.value = item; editorUploadedImgList.value = item;
}; };
//
const handleDeleteEditorImg = item => { const handleDeleteEditorImg = item => {
editorDeleteImgList.value = item; editorDeleteImgList.value = item;
}; };
@ -187,10 +194,8 @@
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`; fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
return; return;
} }
fileError.value = ''; fileError.value = '';
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles); attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
autoIncrement.value++; autoIncrement.value++;
}; };
@ -213,7 +218,7 @@
const validateNickname = () => { const validateNickname = () => {
if (categoryValue.value === 300102) { if (categoryValue.value === 300102) {
nickname.value = nickname.value.replace(/\s/g, ''); // nickname.value = nickname.value.replace(/\s/g, ''); //
nicknameAlert.value = nickname.value.length === 0 ; nicknameAlert.value = nickname.value.length === 0;
} else { } else {
nicknameAlert.value = false; nicknameAlert.value = false;
} }
@ -228,19 +233,28 @@
} }
}; };
/**
* validateContent:
* - 내용이 없으면 contentAlert를 true로 설정
* - 텍스트, 이미지, 비디오 하나라도 존재하면 유효한 콘텐츠로 판단
*/
const validateContent = () => { const validateContent = () => {
if (!content.value?.ops?.length) { if (!content.value?.ops?.length) {
contentAlert.value = true; contentAlert.value = true;
return; return;
} }
// const hasText = content.value.ops.some(
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image); op => typeof op.insert === 'string' && op.insert.trim().length > 0
// );
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0); const hasImage = content.value.ops.some(
op => op.insert && typeof op.insert === 'object' && op.insert.image
);
const hasVideo = content.value.ops.some(
op => op.insert && typeof op.insert === 'object' && op.insert.video
);
// contentAlert.value = !(hasText || hasImage || hasVideo);
contentAlert.value = !(hasText || hasImage);
}; };
/** 글쓰기 */ /** 글쓰기 */
@ -294,10 +308,10 @@
formData.append('CMNFLEORG', fileNameWithoutExt); formData.append('CMNFLEORG', fileNameWithoutExt);
formData.append('CMNFLEEXT', file.name.split('.').pop()); formData.append('CMNFLEEXT', file.name.split('.').pop());
formData.append('CMNFLESIZ', file.size); formData.append('CMNFLESIZ', file.size);
formData.append('file', file); // 📌 formData.append('file', file);
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true }); await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
}), })
); );
} }
@ -313,8 +327,8 @@
router.push('/board'); router.push('/board');
}; };
/** `content` 변경 감지하여 자동 유효성 검사 실행 */ /** content 변경 감지 (deep 옵션 추가) */
watch(content, () => { watch(content, () => {
validateContent(); validateContent();
}); }, { deep: true });
</script> </script>