From ba9a752250b3a2d54663f2b71b0b7589ad3ea09b Mon Sep 17 00:00:00 2001 From: dyhj625 Date: Thu, 10 Apr 2025 13:28:50 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EB=8F=99?= =?UTF-8?q?=EC=98=81=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/board/BoardEdit.vue | 113 ++++++++++++++------------------- src/views/board/BoardWrite.vue | 46 +++++++++----- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/src/views/board/BoardEdit.vue b/src/views/board/BoardEdit.vue index bb6d6ab..c333508 100644 --- a/src/views/board/BoardEdit.vue +++ b/src/views/board/BoardEdit.vue @@ -99,7 +99,7 @@ // 상태 변수 const title = ref(''); - const content = ref(''); + const content = ref({ ops: [] }); const autoIncrement = ref(0); // 경고 상태 @@ -130,10 +130,9 @@ // 최초 업데이트 감지 여부 const isFirstContentUpdate = ref(true); - // 에디터에서 데이터 업데이트 시 + // 에디터 데이터 업데이트 시 처리 (최초 데이터 저장) const handleEditorDataUpdate = data => { content.value = data; - if (isFirstContentUpdate.value) { originalContent.value = structuredClone(data); isFirstContentUpdate.value = false; @@ -141,23 +140,28 @@ } }; + // isDeltaChanged 함수 수정 (내장 diff 대신 텍스트, 이미지, 비디오 각각을 직접 비교) function isDeltaChanged(current, original) { const Delta = Quill.import('delta'); const currentDelta = new Delta(current || []); const originalDelta = new Delta(original || []); - const diff = originalDelta.diff(currentDelta); - if (!diff || diff.ops.length === 0) return false; - - // 텍스트만 비교해서 완전 동일한지 확인 + // 텍스트 추출 const getPlainText = delta => (delta.ops || []) .filter(op => typeof op.insert === 'string') .map(op => op.insert) .join(''); - + // 이미지 URL 추출 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 textOriginal = getPlainText(originalDelta); @@ -165,22 +169,27 @@ const imgsCurrent = getImages(currentDelta); const imgsOriginal = getImages(originalDelta); - const textEqual = textCurrent === textOriginal; - const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal); + const vidsCurrent = getVideos(currentDelta); + 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(() => { if (!contentInitialized.value) return false; const isTitleChanged = title.value !== originalTitle.value; const isContentChanged = isDeltaChanged(content.value, originalContent.value); const isFilesChanged = - attachFiles.value.some(f => !f.id) || // id 없는 새 파일이 있는 경우 + attachFiles.value.some(f => !f.id) || // 신규 파일 존재 delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우 !isSameFiles( - attachFiles.value.filter(f => f.id), // 기존 파일(id 있는 것만) - originalFiles.value, + attachFiles.value.filter(f => f.id), // 기존 파일만 비교 + originalFiles.value ); return isTitleChanged || isContentChanged || isFilesChanged; }); @@ -188,10 +197,8 @@ // 파일 비교 함수 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; }); @@ -199,31 +206,24 @@ // 게시물 데이터 로드 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 || '내용 없음'; + content.value = boardData.content || { ops: [] }; originalTitle.value = title.value; originalContent.value = structuredClone(boardData.content); contentInitialized.value = true; @@ -242,38 +242,34 @@ const addDisplayFileName = fileInfos => fileInfos.map(file => ({ ...file, - name: `${file.originalName}.${file.extension}`, + 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); + // 로컬 유효성 검사 함수 (에디터 내용: 텍스트, 이미지, 비디오 중 하나라도 있으면 유효) + const isNotValidContent = delta => { + if (!delta?.ops?.length) return true; + 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) { title.value = ''; @@ -289,7 +285,6 @@ const handleFileUpload = files => { const validFiles = files.filter(file => file.size <= maxSize); - if (files.some(file => file.size > maxSize)) { fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.'; return; @@ -300,13 +295,11 @@ } fileError.value = ''; attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles); - autoIncrement.value++; }; const removeFile = (index, file) => { if (file.id) delFileIdx.value.push(file.id); - attachFiles.value.splice(index, 1); if (attachFiles.value.length <= maxFiles) { fileError.value = ''; @@ -324,55 +317,41 @@ }; ////////////////// fileSection[E] //////////////////// - /** `content` 변경 감지하여 자동 유효성 검사 실행 */ + /** content 변경 감지 (deep 옵션 추가) */ watch(content, () => { - contentAlert.value = $common.isNotValidContent(content); - }); + contentAlert.value = isNotValidContent(content.value); + }, { deep: true }); - // 글 제목 유효성 + // 글 제목 유효성 검사 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, + 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'); diff --git a/src/views/board/BoardWrite.vue b/src/views/board/BoardWrite.vue index c44e3a3..b5c9c6f 100644 --- a/src/views/board/BoardWrite.vue +++ b/src/views/board/BoardWrite.vue @@ -37,7 +37,9 @@ -
카테고리를 선택해주세요.
+
+ 카테고리를 선택해주세요. +
@@ -101,11 +103,14 @@ @update:deleteImgIndexList="handleDeleteEditorImg" /> -
내용을 입력해주세요.
+
+ 내용을 입력해주세요. +
+
@@ -115,7 +120,7 @@