Merge branch 'main' into commuters

This commit is contained in:
yoon 2025-03-28 17:05:31 +09:00
commit 5b82d3d315
4 changed files with 104 additions and 77 deletions

View File

@ -1,17 +1,8 @@
<template> <template>
<form @submit.prevent="search"> <form @submit.prevent="search">
<div class="input-group mb-3 d-flex"> <div class="input-group mb-3 d-flex">
<input <input type="text" class="form-control" placeholder="Search" v-model="searchQuery" @input="preventLeadingSpace" />
type="text" <button type="submit" class="btn btn-primary">
class="form-control"
placeholder="Search"
v-model="searchQuery"
@input="preventLeadingSpace"
/>
<button
type="submit"
class="btn btn-primary"
>
<i class="bx bx-search bx-md"></i> <i class="bx bx-search bx-md"></i>
</button> </button>
</div> </div>
@ -19,44 +10,54 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
maxlength: { maxlength: {
type: Number, type: Number,
default: 30, default: 30,
required: false, required: false,
}, },
}); initKeyword: {
type: String,
},
});
const emits = defineEmits(["update:data"]); const emits = defineEmits(['update:data']);
const searchQuery = ref(""); const searchQuery = ref('');
// ( or ) watch(
const search = () => { () => props.initKeyword,
const trimmedQuery = searchQuery.value.trimStart(); (newVal, oldVal) => {
if (trimmedQuery === "") { searchQuery.value = newVal;
emits("update:data", ""); },
return; );
}
if (trimmedQuery.length < 2 ) {
alert("검색어는 최소 2글자 이상 입력해주세요.");
searchQuery.value = '';
return;
}
// // ( or )
if (trimmedQuery.length > props.maxlength) { const search = () => {
searchQuery.value = trimmedQuery.slice(0, props.maxlength); const trimmedQuery = searchQuery.value.trimStart();
} else { if (trimmedQuery === '') {
searchQuery.value = trimmedQuery; emits('update:data', '');
} return;
}
if (trimmedQuery.length < 2) {
alert('검색어는 최소 2글자 이상 입력해주세요.');
searchQuery.value = '';
return;
}
emits("update:data", searchQuery.value); //
}; if (trimmedQuery.length > props.maxlength) {
searchQuery.value = trimmedQuery.slice(0, props.maxlength);
} else {
searchQuery.value = trimmedQuery;
}
// emits('update:data', searchQuery.value);
const preventLeadingSpace = () => { };
searchQuery.value = searchQuery.value.trimStart();
}; //
const preventLeadingSpace = () => {
searchQuery.value = searchQuery.value.trimStart();
};
</script> </script>

View File

@ -55,7 +55,7 @@
<div class="col-md-12"> <div class="col-md-12">
<QEditor <QEditor
v-if="contentLoaded" v-if="contentLoaded"
@update:data="content = $event" @update:data="handleEditorDataUpdate"
@update:imageUrls="imageUrls = $event" @update:imageUrls="imageUrls = $event"
@update:uploadedImgList="handleUpdateEditorImg" @update:uploadedImgList="handleUpdateEditorImg"
@update:deleteImgIndexList="handleDeleteEditorImg" @update:deleteImgIndexList="handleDeleteEditorImg"
@ -89,6 +89,7 @@
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useBoardAccessStore } from '@s/useBoardAccessStore'; import { useBoardAccessStore } from '@s/useBoardAccessStore';
import axios from '@api'; import axios from '@api';
import Quill from 'quill';
// //
const $common = inject('common'); const $common = inject('common');
@ -121,25 +122,60 @@
const editorDeleteImgList = ref([]); const editorDeleteImgList = ref([]);
const originalTitle = ref(''); const originalTitle = ref('');
const originalPlainText = ref(''); const originalContent = ref({});
const originalFiles = ref([]); const originalFiles = ref([]);
const contentInitialized = ref(false);
//
const isFirstContentUpdate = ref(true);
//
const handleEditorDataUpdate = (data) => {
content.value = data;
if (isFirstContentUpdate.value) {
originalContent.value = structuredClone(data);
isFirstContentUpdate.value = false;
contentInitialized.value = true;
}
};
function extractPlainText(delta) { function isDeltaChanged(current, original) {
if (!delta || !Array.isArray(delta.ops)) return ''; const Delta = Quill.import('delta');
return delta.ops const currentDelta = new Delta(current || []);
.filter(op => typeof op.insert === 'string') const originalDelta = new Delta(original || []);
.map(op => op.insert.trim())
.join(' ') const diff = originalDelta.diff(currentDelta);
.trim(); if (!diff || diff.ops.length === 0) return false;
//
const getPlainText = (delta) =>
(delta.ops || [])
.filter(op => typeof op.insert === 'string')
.map(op => op.insert)
.join('');
const getImages = (delta) =>
(delta.ops || [])
.filter(op => typeof op.insert === 'object' && op.insert.image)
.map(op => op.insert.image);
const textCurrent = getPlainText(currentDelta);
const textOriginal = getPlainText(originalDelta);
const imgsCurrent = getImages(currentDelta);
const imgsOriginal = getImages(originalDelta);
const textEqual = textCurrent === textOriginal;
const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal);
return !(textEqual && imageEqual); // false
} }
const isChanged = computed(() => { const isChanged = computed(() => {
if (!contentInitialized.value) return false;
const isTitleChanged = title.value !== originalTitle.value; const isTitleChanged = title.value !== originalTitle.value;
const currentPlainText = extractPlainText(content.value); const isContentChanged = isDeltaChanged(content.value, originalContent.value);
const isContentChanged = currentPlainText !== originalPlainText.value;
const currentAttachedFiles = attachFiles.value.filter(f => f.id);
const isFilesChanged = const isFilesChanged =
attachFiles.value.some(f => !f.id) || // id attachFiles.value.some(f => !f.id) || // id
delFileIdx.value.length > 0 || // delFileIdx.value.length > 0 || //
@ -147,15 +183,9 @@
attachFiles.value.filter(f => f.id), // (id ) attachFiles.value.filter(f => f.id), // (id )
originalFiles.value originalFiles.value
); );
console.log(isTitleChanged);
console.log(isContentChanged);
console.log(isFilesChanged);
return isTitleChanged || isContentChanged || isFilesChanged;
});
watch(isChanged, (val) => {
console.log('🔄 isChanged changed:', val);
});
return isTitleChanged || isContentChanged || isFilesChanged ;
});
// //
function isSameFiles(current, original) { function isSameFiles(current, original) {
@ -200,15 +230,11 @@
title.value = boardData.title || '제목 없음'; title.value = boardData.title || '제목 없음';
content.value = boardData.content || '내용 없음'; content.value = boardData.content || '내용 없음';
originalTitle.value = title.value; originalTitle.value = title.value;
originalContent.value = structuredClone(boardData.content);
contentInitialized.value = true;
contentLoaded.value = true; contentLoaded.value = true;
}; };
watch(content, (val) => {
if (contentLoaded.value && !originalPlainText.value) {
originalPlainText.value = extractPlainText(val);
}
}, { immediate: true });
const handleUpdateEditorImg = item => { const handleUpdateEditorImg = item => {
editorUploadedImgList.value = item; editorUploadedImgList.value = item;
}; };
@ -362,6 +388,7 @@
onMounted(async () => { onMounted(async () => {
if (currentBoardId.value) { if (currentBoardId.value) {
fetchBoardDetails(); fetchBoardDetails();
} else { } else {
console.error('잘못된 게시물 ID:', currentBoardId.value); console.error('잘못된 게시물 ID:', currentBoardId.value);
} }

View File

@ -4,7 +4,7 @@
<div class="card-header d-flex flex-column"> <div class="card-header d-flex flex-column">
<!-- 검색창 --> <!-- 검색창 -->
<div class="mb-3 w-100"> <div class="mb-3 w-100">
<search-bar @update:data="search" @keyup.enter="searchOnEnter" class="flex-grow-1" /> <search-bar @update:data="search" @keyup.enter="searchOnEnter" :initKeyword="searchText" class="flex-grow-1" />
</div> </div>
<div class="d-flex align-items-center" style="gap: 15px"> <div class="d-flex align-items-center" style="gap: 15px">
<!-- 리스트 갯수 선택 --> <!-- 리스트 갯수 선택 -->

View File

@ -78,10 +78,9 @@
style="line-height: 1.6" style="line-height: 1.6"
v-html="$common.contentToHtml(boardContent)" v-html="$common.contentToHtml(boardContent)"
></div> ></div>
<div v-if="!unknown" class="my-12 py-12 pt-12"></div>
<!-- 좋아요 버튼 --> <!-- 좋아요 버튼 -->
<div class="row justify-content-center my-10"> <div v-if="unknown || authorId" class="row justify-content-center my-10">
<BoardRecommendBtn <BoardRecommendBtn
:bigBtn="true" :bigBtn="true"
:boardId="currentBoardId" :boardId="currentBoardId"
@ -93,7 +92,7 @@
@updateReaction="handleUpdateReaction" @updateReaction="handleUpdateReaction"
/> />
</div> </div>
<div> <div v-if="unknown || authorId" >
<!-- 댓글 입력 영역 --> <!-- 댓글 입력 영역 -->
<BoardCommentArea <BoardCommentArea
:profileName="profileName" :profileName="profileName"
@ -107,7 +106,7 @@
</div> </div>
<!-- 댓글 목록 --> <!-- 댓글 목록 -->
<div class="card-footer"> <div v-if="unknown || authorId" class="card-footer">
<BoardCommentList <BoardCommentList
:unknown="unknown" :unknown="unknown"
:comments="commentsWithAuthStatus" :comments="commentsWithAuthStatus"