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

View File

@ -55,7 +55,7 @@
<div class="col-md-12">
<QEditor
v-if="contentLoaded"
@update:data="content = $event"
@update:data="handleEditorDataUpdate"
@update:imageUrls="imageUrls = $event"
@update:uploadedImgList="handleUpdateEditorImg"
@update:deleteImgIndexList="handleDeleteEditorImg"
@ -89,6 +89,7 @@
import { useToastStore } from '@s/toastStore';
import { useBoardAccessStore } from '@s/useBoardAccessStore';
import axios from '@api';
import Quill from 'quill';
//
const $common = inject('common');
@ -121,25 +122,60 @@
const editorDeleteImgList = ref([]);
const originalTitle = ref('');
const originalPlainText = ref('');
const originalContent = 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) {
if (!delta || !Array.isArray(delta.ops)) return '';
return delta.ops
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.trim())
.join(' ')
.trim();
.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(() => {
if (!contentInitialized.value) return false;
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 isContentChanged = isDeltaChanged(content.value, originalContent.value);
const isFilesChanged =
attachFiles.value.some(f => !f.id) || // id
delFileIdx.value.length > 0 || //
@ -147,15 +183,9 @@
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) {
@ -200,15 +230,11 @@
title.value = boardData.title || '제목 없음';
content.value = boardData.content || '내용 없음';
originalTitle.value = title.value;
originalContent.value = structuredClone(boardData.content);
contentInitialized.value = true;
contentLoaded.value = true;
};
watch(content, (val) => {
if (contentLoaded.value && !originalPlainText.value) {
originalPlainText.value = extractPlainText(val);
}
}, { immediate: true });
const handleUpdateEditorImg = item => {
editorUploadedImgList.value = item;
};
@ -362,6 +388,7 @@
onMounted(async () => {
if (currentBoardId.value) {
fetchBoardDetails();
} else {
console.error('잘못된 게시물 ID:', currentBoardId.value);
}

View File

@ -4,7 +4,7 @@
<div class="card-header d-flex flex-column">
<!-- 검색창 -->
<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 class="d-flex align-items-center" style="gap: 15px">
<!-- 리스트 갯수 선택 -->

View File

@ -78,10 +78,9 @@
style="line-height: 1.6"
v-html="$common.contentToHtml(boardContent)"
></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
:bigBtn="true"
:boardId="currentBoardId"
@ -93,7 +92,7 @@
@updateReaction="handleUpdateReaction"
/>
</div>
<div>
<div v-if="unknown || authorId" >
<!-- 댓글 입력 영역 -->
<BoardCommentArea
:profileName="profileName"
@ -107,7 +106,7 @@
</div>
<!-- 댓글 목록 -->
<div class="card-footer">
<div v-if="unknown || authorId" class="card-footer">
<BoardCommentList
:unknown="unknown"
:comments="commentsWithAuthStatus"