갤러리 선택과 드래그 순서 개선
This commit is contained in:
@@ -22,11 +22,14 @@ const isApplyingExternalValue = ref(false)
|
|||||||
const uploadingBlockIds = ref([])
|
const uploadingBlockIds = ref([])
|
||||||
const mediaItems = ref([])
|
const mediaItems = ref([])
|
||||||
const mediaPickerTarget = ref(null)
|
const mediaPickerTarget = ref(null)
|
||||||
|
const selectedGalleryMediaUrls = ref([])
|
||||||
const isMediaPickerOpen = ref(false)
|
const isMediaPickerOpen = ref(false)
|
||||||
const isLoadingMedia = ref(false)
|
const isLoadingMedia = ref(false)
|
||||||
const isComposingText = ref(false)
|
const isComposingText = ref(false)
|
||||||
const isNormalizingTrailingBlock = ref(false)
|
const isNormalizingTrailingBlock = ref(false)
|
||||||
const pendingSoftLineBreakIndex = ref(-1)
|
const pendingSoftLineBreakIndex = ref(-1)
|
||||||
|
const draggingGalleryImage = ref(null)
|
||||||
|
const galleryDragTarget = ref(null)
|
||||||
let blockIdSeed = 0
|
let blockIdSeed = 0
|
||||||
|
|
||||||
const imageWidthOptions = [
|
const imageWidthOptions = [
|
||||||
@@ -419,6 +422,20 @@ const emitContent = () => {
|
|||||||
*/
|
*/
|
||||||
const isTextBlock = (block) => !['divider', 'image', 'gallery', 'toggle', 'embed'].includes(block.type)
|
const isTextBlock = (block) => !['divider', 'image', 'gallery', 'toggle', 'embed'].includes(block.type)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 열 개수 반환
|
||||||
|
* @param {Object} block - 갤러리 블록
|
||||||
|
* @returns {number} 열 개수
|
||||||
|
*/
|
||||||
|
const getGalleryColumnCount = (block) => Math.min(Math.max(block.images.length, 1), 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 미디어 선택 여부 확인
|
||||||
|
* @param {Object} mediaItem - 미디어 항목
|
||||||
|
* @returns {boolean} 선택 여부
|
||||||
|
*/
|
||||||
|
const isGalleryMediaSelected = (mediaItem) => selectedGalleryMediaUrls.value.includes(mediaItem.url)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 비어 있는 문단 블록 여부 반환
|
* 비어 있는 문단 블록 여부 반환
|
||||||
* @param {Object|undefined} block - 에디터 블록
|
* @param {Object|undefined} block - 에디터 블록
|
||||||
@@ -953,6 +970,9 @@ const openMediaPicker = async (block) => {
|
|||||||
blockId: block.id,
|
blockId: block.id,
|
||||||
type: block.type
|
type: block.type
|
||||||
}
|
}
|
||||||
|
selectedGalleryMediaUrls.value = block.type === 'gallery'
|
||||||
|
? block.images.map((image) => image.url).filter(Boolean)
|
||||||
|
: []
|
||||||
isMediaPickerOpen.value = true
|
isMediaPickerOpen.value = true
|
||||||
await fetchMediaItems()
|
await fetchMediaItems()
|
||||||
}
|
}
|
||||||
@@ -964,6 +984,44 @@ const openMediaPicker = async (block) => {
|
|||||||
const closeMediaPicker = () => {
|
const closeMediaPicker = () => {
|
||||||
isMediaPickerOpen.value = false
|
isMediaPickerOpen.value = false
|
||||||
mediaPickerTarget.value = null
|
mediaPickerTarget.value = null
|
||||||
|
selectedGalleryMediaUrls.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 미디어 선택 상태 전환
|
||||||
|
* @param {Object} mediaItem - 미디어 항목
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const toggleGalleryMediaSelection = (mediaItem) => {
|
||||||
|
if (selectedGalleryMediaUrls.value.includes(mediaItem.url)) {
|
||||||
|
selectedGalleryMediaUrls.value = selectedGalleryMediaUrls.value.filter((url) => url !== mediaItem.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGalleryMediaUrls.value = [...selectedGalleryMediaUrls.value, mediaItem.url]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 미디어 선택을 블록에 적용
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const applyGalleryMediaSelection = () => {
|
||||||
|
const block = editorBlocks.value.find((item) => item.id === mediaPickerTarget.value?.blockId)
|
||||||
|
|
||||||
|
if (!block || mediaPickerTarget.value?.type !== 'gallery') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingImages = new Map(block.images.map((image) => [image.url, image]))
|
||||||
|
block.images = selectedGalleryMediaUrls.value.map((url) => existingImages.get(url) || {
|
||||||
|
url,
|
||||||
|
alt: '',
|
||||||
|
width: 'regular'
|
||||||
|
})
|
||||||
|
|
||||||
|
normalizeTrailingTextBlock()
|
||||||
|
emitContent()
|
||||||
|
closeMediaPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -972,26 +1030,19 @@ const closeMediaPicker = () => {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const selectMediaItem = (mediaItem) => {
|
const selectMediaItem = (mediaItem) => {
|
||||||
|
if (mediaPickerTarget.value?.type === 'gallery') {
|
||||||
|
toggleGalleryMediaSelection(mediaItem)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const block = editorBlocks.value.find((item) => item.id === mediaPickerTarget.value?.blockId)
|
const block = editorBlocks.value.find((item) => item.id === mediaPickerTarget.value?.blockId)
|
||||||
|
|
||||||
if (!block) {
|
if (!block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaPickerTarget.value.type === 'gallery') {
|
block.url = mediaItem.url
|
||||||
block.images = [
|
block.alt = ''
|
||||||
...block.images,
|
|
||||||
{
|
|
||||||
url: mediaItem.url,
|
|
||||||
alt: '',
|
|
||||||
width: 'regular'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
block.url = mediaItem.url
|
|
||||||
block.alt = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeTrailingTextBlock()
|
normalizeTrailingTextBlock()
|
||||||
emitContent()
|
emitContent()
|
||||||
closeMediaPicker()
|
closeMediaPicker()
|
||||||
@@ -1084,6 +1135,88 @@ const removeGalleryImage = (block, imageIndex) => {
|
|||||||
emitContent()
|
emitContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 이미지 드래그 시작
|
||||||
|
* @param {DragEvent} event - 드래그 이벤트
|
||||||
|
* @param {Object} block - 갤러리 블록
|
||||||
|
* @param {number} imageIndex - 이미지 인덱스
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const startGalleryImageDrag = (event, block, imageIndex) => {
|
||||||
|
draggingGalleryImage.value = {
|
||||||
|
blockId: block.id,
|
||||||
|
imageIndex
|
||||||
|
}
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
event.dataTransfer.setData('text/plain', `${block.id}:${imageIndex}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 이미지 드래그 종료
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const finishGalleryImageDrag = () => {
|
||||||
|
draggingGalleryImage.value = null
|
||||||
|
galleryDragTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 이미지 삽입 위치 표시
|
||||||
|
* @param {DragEvent} event - 드래그 이벤트
|
||||||
|
* @param {Object} block - 갤러리 블록
|
||||||
|
* @param {number} imageIndex - 이미지 인덱스
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const updateGalleryImageDropTarget = (event, block, imageIndex) => {
|
||||||
|
const source = draggingGalleryImage.value
|
||||||
|
|
||||||
|
if (!source || source.blockId !== block.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
galleryDragTarget.value = {
|
||||||
|
blockId: block.id,
|
||||||
|
imageIndex,
|
||||||
|
position: event.clientX < rect.left + rect.width / 2 ? 'before' : 'after'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 갤러리 이미지 순서 변경
|
||||||
|
* @param {DragEvent} event - 드롭 이벤트
|
||||||
|
* @param {Object} block - 갤러리 블록
|
||||||
|
* @param {number} targetIndex - 대상 인덱스
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const dropGalleryImage = (event, block, targetIndex) => {
|
||||||
|
const source = draggingGalleryImage.value
|
||||||
|
const target = galleryDragTarget.value
|
||||||
|
|
||||||
|
if (!source || source.blockId !== block.id || source.imageIndex === targetIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextTargetIndex = target?.blockId === block.id && target.position === 'after'
|
||||||
|
? targetIndex + 1
|
||||||
|
: targetIndex
|
||||||
|
|
||||||
|
if (source.imageIndex < nextTargetIndex) {
|
||||||
|
nextTargetIndex -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.imageIndex === nextTargetIndex) {
|
||||||
|
finishGalleryImageDrag()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [image] = block.images.splice(source.imageIndex, 1)
|
||||||
|
block.images.splice(nextTargetIndex, 0, image)
|
||||||
|
finishGalleryImageDrag()
|
||||||
|
normalizeTrailingTextBlock()
|
||||||
|
emitContent()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 슬래시 메뉴 선택을 아래로 이동
|
* 슬래시 메뉴 선택을 아래로 이동
|
||||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||||
@@ -1607,13 +1740,30 @@ defineExpose({
|
|||||||
@keydown.enter="handleEnter($event, index)"
|
@keydown.enter="handleEnter($event, index)"
|
||||||
@keydown.backspace="handleBackspace($event, index)"
|
@keydown.backspace="handleBackspace($event, index)"
|
||||||
>
|
>
|
||||||
<div v-if="block.images.length" class="admin-block-editor__gallery-grid grid grid-cols-2 gap-2 md:grid-cols-3">
|
<div
|
||||||
|
v-if="block.images.length"
|
||||||
|
class="admin-block-editor__gallery-grid grid gap-2"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${getGalleryColumnCount(block)}, minmax(0, 1fr))` }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(image, imageIndex) in block.images"
|
v-for="(image, imageIndex) in block.images"
|
||||||
:key="`${block.id}-${image.url}`"
|
:key="`${block.id}-${image.url}`"
|
||||||
class="admin-block-editor__gallery-item group/item relative overflow-hidden rounded bg-surface"
|
class="admin-block-editor__gallery-item group/item relative overflow-hidden rounded bg-surface"
|
||||||
|
:class="{
|
||||||
|
'opacity-50': draggingGalleryImage?.blockId === block.id && draggingGalleryImage?.imageIndex === imageIndex,
|
||||||
|
'admin-block-editor__gallery-item--drop-before': galleryDragTarget?.blockId === block.id && galleryDragTarget?.imageIndex === imageIndex && galleryDragTarget?.position === 'before',
|
||||||
|
'admin-block-editor__gallery-item--drop-after': galleryDragTarget?.blockId === block.id && galleryDragTarget?.imageIndex === imageIndex && galleryDragTarget?.position === 'after'
|
||||||
|
}"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.stop="startGalleryImageDrag($event, block, imageIndex)"
|
||||||
|
@dragover.prevent.stop="updateGalleryImageDropTarget($event, block, imageIndex)"
|
||||||
|
@drop.prevent.stop="dropGalleryImage($event, block, imageIndex)"
|
||||||
|
@dragend="finishGalleryImageDrag"
|
||||||
>
|
>
|
||||||
<img class="admin-block-editor__gallery-image aspect-[4/3] w-full object-cover" :src="image.url" :alt="image.alt">
|
<img class="admin-block-editor__gallery-image aspect-[4/3] w-full object-cover" :src="image.url" :alt="image.alt">
|
||||||
|
<span class="admin-block-editor__gallery-drag-hint pointer-events-none absolute left-2 top-2 rounded bg-black/70 px-2 py-1 text-xs font-semibold text-white opacity-0 transition-opacity group-hover/item:opacity-100">
|
||||||
|
드래그
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
class="admin-block-editor__gallery-remove absolute right-2 top-2 rounded bg-white/95 px-2 py-1 text-xs font-semibold text-ink opacity-0 shadow transition-opacity group-hover/item:opacity-100"
|
class="admin-block-editor__gallery-remove absolute right-2 top-2 rounded bg-white/95 px-2 py-1 text-xs font-semibold text-ink opacity-0 shadow transition-opacity group-hover/item:opacity-100"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1752,11 +1902,19 @@ defineExpose({
|
|||||||
<button
|
<button
|
||||||
v-for="item in mediaItems"
|
v-for="item in mediaItems"
|
||||||
:key="item.url"
|
:key="item.url"
|
||||||
class="admin-block-editor__media-picker-item overflow-hidden border border-line bg-white text-left"
|
class="admin-block-editor__media-picker-item relative overflow-hidden border bg-white text-left transition-colors"
|
||||||
|
:class="mediaPickerTarget?.type === 'gallery' && isGalleryMediaSelected(item) ? 'border-[#15171a] ring-2 ring-[#15171a]/20' : 'border-line hover:border-[#8e9cac]'"
|
||||||
type="button"
|
type="button"
|
||||||
@click="selectMediaItem(item)"
|
@click="selectMediaItem(item)"
|
||||||
>
|
>
|
||||||
<img class="admin-block-editor__media-picker-image aspect-[4/3] w-full bg-surface object-cover" :src="item.url" :alt="item.title">
|
<img class="admin-block-editor__media-picker-image aspect-[4/3] w-full bg-surface object-cover" :src="item.url" :alt="item.title">
|
||||||
|
<span
|
||||||
|
v-if="mediaPickerTarget?.type === 'gallery' && isGalleryMediaSelected(item)"
|
||||||
|
class="admin-block-editor__media-picker-selected absolute right-2 top-2 grid size-6 place-items-center rounded-full bg-[#15171a] text-xs font-bold text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
<span class="admin-block-editor__media-picker-name block truncate px-3 py-2 text-xs font-semibold text-ink">{{ item.name }}</span>
|
<span class="admin-block-editor__media-picker-name block truncate px-3 py-2 text-xs font-semibold text-ink">{{ item.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1764,6 +1922,22 @@ defineExpose({
|
|||||||
선택할 미디어가 없습니다.
|
선택할 미디어가 없습니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="mediaPickerTarget?.type === 'gallery'"
|
||||||
|
class="admin-block-editor__media-picker-footer flex items-center justify-between gap-3 border-t border-line px-5 py-4"
|
||||||
|
>
|
||||||
|
<p class="admin-block-editor__media-picker-count text-sm text-muted">
|
||||||
|
{{ selectedGalleryMediaUrls.length }}개 선택됨
|
||||||
|
</p>
|
||||||
|
<div class="admin-block-editor__media-picker-actions flex gap-2">
|
||||||
|
<button class="admin-block-editor__media-picker-cancel rounded border border-line px-3 py-2 text-sm font-semibold text-ink" type="button" @click="closeMediaPicker">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button class="admin-block-editor__media-picker-apply rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white" type="button" @click="applyGalleryMediaSelection">
|
||||||
|
갤러리에 적용
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1882,4 +2056,30 @@ defineExpose({
|
|||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
caret-color: #f8fafc;
|
caret-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-block-editor__gallery-item--drop-before::before,
|
||||||
|
.admin-block-editor__gallery-item--drop-after::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #2eb6ea;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px rgba(46, 182, 234, 0.16),
|
||||||
|
0 6px 18px rgba(46, 182, 234, 0.35);
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-block-editor__gallery-item--drop-before::before {
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-block-editor__gallery-item--drop-after::before {
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-05-13 v0.0.117
|
||||||
|
|
||||||
|
### 갤러리 선택과 순서 편집 흐름 정리
|
||||||
|
|
||||||
|
갤러리는 단일 이미지 블록과 달리 여러 이미지를 한 번에 구성하는 블록이므로, 미디어 클릭 즉시 적용하면 선택을 이어갈 수 없고 실수 수정도 번거롭다. 갤러리 미디어 선택은 모달 안에서 복수 선택 상태를 유지한 뒤 확인 시점에 블록에 반영한다. 이미지 개수별 열 수를 1·2·3열로 제한해 빈 칸을 줄이고, 작성자가 시각 흐름을 직접 정할 수 있도록 갤러리 내부 이미지는 드래그로 재정렬한다. 드래그 중에는 이미지 사이 삽입 위치를 선으로 표시해 어느 위치에 들어갈지 명확히 보여준다.
|
||||||
|
|
||||||
## 2026-05-13 v0.0.116
|
## 2026-05-13 v0.0.116
|
||||||
|
|
||||||
### 게시글 제목 IME 입력과 목록 태그 표시 보정
|
### 게시글 제목 IME 입력과 목록 태그 표시 보정
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|------|-----------|
|
|------|-----------|
|
||||||
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지 hover 액션과 업로드/미디어 선택 확정, 제목 IME Enter 가드, 배지형 태그 입력(한글 유지), 로컬 자동 저장, 미저장 변경사항 이탈 확인, 예약 발행 시각 입력, 검색 노출 제외(noindex), 저장 시 제목·요약을 SEO 메타로 반영, 미리보기 요청 |
|
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지 hover 액션과 업로드/미디어 선택 확정, 제목 IME Enter 가드, 배지형 태그 입력(한글 유지), 로컬 자동 저장, 미저장 변경사항 이탈 확인, 예약 발행 시각 입력, 검색 노출 제외(noindex), 저장 시 제목·요약을 SEO 메타로 반영, 미리보기 요청 |
|
||||||
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
|
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
|
||||||
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
|
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 갤러리 복수 미디어 선택·이미지 수별 열 배치·삽입 위치 표시 드래그 순서 변경, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
|
||||||
| components/admin/AdminNavPrimaryBranch.vue | 관리자 상단 네비 트리(테이블·태그와 동일한 행 드래그 하이라이트, 하위·삭제) |
|
| components/admin/AdminNavPrimaryBranch.vue | 관리자 상단 네비 트리(테이블·태그와 동일한 행 드래그 하이라이트, 하위·삭제) |
|
||||||
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상만 편집) |
|
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상만 편집) |
|
||||||
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 미저장 변경사항 이탈 확인) |
|
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 미저장 변경사항 이탈 확인) |
|
||||||
|
|||||||
@@ -510,6 +510,9 @@ components/content/
|
|||||||
- 이미지 블록 alt 입력은 이미지 hover 또는 focus 상태에서만 표시한다.
|
- 이미지 블록 alt 입력은 이미지 hover 또는 focus 상태에서만 표시한다.
|
||||||
- 이미지 블록 표시 옵션은 `regular`, `wide`, `full` 값을 사용하며 `regular`는 width 옵션을 생략한다.
|
- 이미지 블록 표시 옵션은 `regular`, `wide`, `full` 값을 사용하며 `regular`는 width 옵션을 생략한다.
|
||||||
- 갤러리 블록은 `:::gallery` fenced block 안에 이미지 마크다운 행을 여러 개 저장한다.
|
- 갤러리 블록은 `:::gallery` fenced block 안에 이미지 마크다운 행을 여러 개 저장한다.
|
||||||
|
- 관리자 갤러리 미디어 선택은 여러 이미지를 선택한 뒤 확인 버튼으로 적용한다.
|
||||||
|
- 관리자 갤러리 블록은 이미지 1개일 때 1열, 2개일 때 2열, 3개 이상일 때 3열로 표시한다.
|
||||||
|
- 관리자 갤러리 블록의 이미지 순서는 드래그 앤 드롭으로 변경하며, 드래그 중 삽입 위치를 이미지 사이 라인으로 표시한다.
|
||||||
- 공개 갤러리는 한 줄에 2~3개 이미지 그리드로 표시하고 클릭 시 라이트박스로 크게 확인한다.
|
- 공개 갤러리는 한 줄에 2~3개 이미지 그리드로 표시하고 클릭 시 라이트박스로 크게 확인한다.
|
||||||
- 이미지와 갤러리 블록은 기존 업로드 미디어 선택 또는 새 이미지 업로드를 제공한다.
|
- 이미지와 갤러리 블록은 기존 업로드 미디어 선택 또는 새 이미지 업로드를 제공한다.
|
||||||
- 콜아웃 블록은 `:::callout` fenced block 안에 본문을 저장한다.
|
- 콜아웃 블록은 `:::callout` fenced block 안에 본문을 저장한다.
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v0.0.117
|
||||||
|
|
||||||
|
- 관리자 글쓰기 갤러리 미디어 선택을 복수 선택 후 확인 적용 방식으로 변경.
|
||||||
|
- 관리자 갤러리 블록의 이미지 수에 따라 1개는 전체 너비, 2개는 2열, 3개 이상은 3열로 표시하도록 수정.
|
||||||
|
- 관리자 갤러리 블록 이미지 드래그 순서 변경과 삽입 위치 표시 추가.
|
||||||
|
- 패키지 버전 `0.0.117`로 갱신.
|
||||||
|
|
||||||
## v0.0.116
|
## v0.0.116
|
||||||
|
|
||||||
- 관리자 게시글 제목 입력에서 한글 조합 중 Enter가 본문으로 마지막 글자를 넘기지 않도록 IME 조합 상태 가드 추가.
|
- 관리자 게시글 제목 입력에서 한글 조합 중 Enter가 본문으로 마지막 글자를 넘기지 않도록 IME 조합 상태 가드 추가.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.116",
|
"version": "0.0.117",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.116",
|
"version": "0.0.117",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.116",
|
"version": "0.0.117",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
Reference in New Issue
Block a user