관리자 목록과 이미지 설정 개선
This commit is contained in:
@@ -38,6 +38,24 @@ const emit = defineEmits([
|
||||
'update-inline-color'
|
||||
])
|
||||
|
||||
/** @type {number} 단일 이미지 직접 너비 최소값 */
|
||||
const IMAGE_WIDTH_MIN = 120
|
||||
/** @type {number} 단일 이미지 직접 너비 최대값 */
|
||||
const IMAGE_WIDTH_MAX = 1600
|
||||
/** @type {Array<{ label: string, value: string, pixelWidth: number|null }>} 단일 이미지 대표 너비 옵션 */
|
||||
const IMAGE_WIDTH_PRESET_OPTIONS = [
|
||||
{ label: '전체 너비(자동)', value: 'auto', pixelWidth: null },
|
||||
{ label: '갤러리 2장 1칸(356px)', value: '356', pixelWidth: 356 },
|
||||
{ label: '갤러리 3장 1칸(235px)', value: '235', pixelWidth: 235 },
|
||||
{ label: '갤러리 3장 2칸(477px)', value: '477', pixelWidth: 477 },
|
||||
{ label: '직접 입력', value: 'custom', pixelWidth: null }
|
||||
]
|
||||
|
||||
/** @type {import('vue').Ref<Set<string>>} 직접 입력으로 열어둔 이미지 너비 상태 */
|
||||
const customImageWidthKeys = ref(new Set())
|
||||
/** @type {import('vue').Ref<Record<string, string>>} 직접 너비 입력 중인 원본 문자열 */
|
||||
const imageWidthDraftValues = ref({})
|
||||
|
||||
const backgroundLabels = {
|
||||
gray: '회색',
|
||||
blue: '파랑',
|
||||
@@ -193,6 +211,190 @@ const panelMeta = computed(() => {
|
||||
return '커서가 위치한 이미지 줄'
|
||||
})
|
||||
|
||||
/**
|
||||
* 이미지 너비 입력 값을 화면용 문자열로 변환한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @returns {string} 입력 표시 값
|
||||
*/
|
||||
const getImagePixelWidthInputValue = (imageIndex, image) => {
|
||||
const draftKey = getImageWidthStateKey(imageIndex, image)
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(imageWidthDraftValues.value, draftKey)) {
|
||||
return imageWidthDraftValues.value[draftKey]
|
||||
}
|
||||
|
||||
const width = Math.round(Number(image?.pixelWidth) || 0)
|
||||
return width ? String(width) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 UI 상태 키를 반환한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @returns {string} 상태 키
|
||||
*/
|
||||
const getImageWidthStateKey = (imageIndex, image) => `${imageIndex}:${String(image?.url || '')}`
|
||||
|
||||
/**
|
||||
* 이미지 너비 직접 입력 UI 상태를 저장한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @param {boolean} enabled - 직접 입력 여부
|
||||
* @returns {void}
|
||||
*/
|
||||
const setImageWidthCustomState = (imageIndex, image, enabled) => {
|
||||
const nextKeys = new Set(customImageWidthKeys.value)
|
||||
const key = getImageWidthStateKey(imageIndex, image)
|
||||
|
||||
if (enabled) {
|
||||
nextKeys.add(key)
|
||||
} else {
|
||||
nextKeys.delete(key)
|
||||
}
|
||||
|
||||
customImageWidthKeys.value = nextKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 입력 임시 값을 저장한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @param {string} value - 입력 문자열
|
||||
* @returns {void}
|
||||
*/
|
||||
const setImageWidthDraftValue = (imageIndex, image, value) => {
|
||||
imageWidthDraftValues.value = {
|
||||
...imageWidthDraftValues.value,
|
||||
[getImageWidthStateKey(imageIndex, image)]: value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 입력 임시 값을 지운다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @returns {void}
|
||||
*/
|
||||
const clearImageWidthDraftValue = (imageIndex, image) => {
|
||||
const key = getImageWidthStateKey(imageIndex, image)
|
||||
const nextValues = { ...imageWidthDraftValues.value }
|
||||
delete nextValues[key]
|
||||
imageWidthDraftValues.value = nextValues
|
||||
}
|
||||
|
||||
/**
|
||||
* 직접 입력 너비를 허용 범위로 정리한다.
|
||||
* @param {unknown} value - 입력 너비
|
||||
* @returns {number|null} 정리된 너비
|
||||
*/
|
||||
const normalizeImageWidthInput = (value) => {
|
||||
const width = Math.round(Number(value) || 0)
|
||||
|
||||
if (!width) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Math.min(Math.max(width, IMAGE_WIDTH_MIN), IMAGE_WIDTH_MAX)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 대표 옵션 값을 반환한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @returns {string} 셀렉트 값
|
||||
*/
|
||||
const getImageWidthPresetValue = (imageIndex, image) => {
|
||||
const width = Math.round(Number(image?.pixelWidth) || 0)
|
||||
|
||||
if (customImageWidthKeys.value.has(getImageWidthStateKey(imageIndex, image))) {
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
if (!width) {
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
return IMAGE_WIDTH_PRESET_OPTIONS.some((option) => option.pixelWidth === width)
|
||||
? String(width)
|
||||
: 'custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 대표 옵션 변경을 전달한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @param {Event} event - 변경 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const emitImageWidthPresetChange = (imageIndex, image, event) => {
|
||||
const value = String(event.target?.value || 'auto')
|
||||
const preset = IMAGE_WIDTH_PRESET_OPTIONS.find((option) => option.value === value)
|
||||
|
||||
if (value === 'custom') {
|
||||
setImageWidthCustomState(imageIndex, image, true)
|
||||
setImageWidthDraftValue(imageIndex, image, String(Math.round(Number(image?.pixelWidth) || 0) || 356))
|
||||
emit('update-media-image', imageIndex, {
|
||||
pixelWidth: Math.round(Number(image?.pixelWidth) || 0) || 356
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setImageWidthCustomState(imageIndex, image, false)
|
||||
clearImageWidthDraftValue(imageIndex, image)
|
||||
emit('update-media-image', imageIndex, {
|
||||
pixelWidth: preset?.pixelWidth || null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 입력 변경을 전달한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @param {Event} event - 입력 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const emitImagePixelWidthChange = (imageIndex, image, event) => {
|
||||
const raw = String(event.target?.value || '').trim()
|
||||
const width = Math.round(Number(raw) || 0)
|
||||
|
||||
setImageWidthCustomState(imageIndex, image, true)
|
||||
setImageWidthDraftValue(imageIndex, image, raw)
|
||||
|
||||
if (!raw) {
|
||||
emit('update-media-image', imageIndex, {
|
||||
pixelWidth: null
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (width < IMAGE_WIDTH_MIN || width > IMAGE_WIDTH_MAX) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update-media-image', imageIndex, {
|
||||
pixelWidth: width
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 너비 입력 종료 시 허용 범위로 정리해 전달한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Object} image - 이미지 정보
|
||||
* @param {Event} event - 입력 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const commitImagePixelWidthChange = (imageIndex, image, event) => {
|
||||
const raw = String(event.target?.value || '').trim()
|
||||
const pixelWidth = normalizeImageWidthInput(raw)
|
||||
|
||||
setImageWidthCustomState(imageIndex, image, true)
|
||||
clearImageWidthDraftValue(imageIndex, image)
|
||||
emit('update-media-image', imageIndex, {
|
||||
pixelWidth
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 포커스가 패널 밖으로 나갔을 때만 이탈 이벤트를 보낸다.
|
||||
* @param {FocusEvent} event - focusout 이벤트
|
||||
@@ -505,6 +707,46 @@ const onPanelFocusOut = (event) => {
|
||||
@input="emit('update-media-image', imageIndex, { url: $event.target.value })"
|
||||
>
|
||||
</label>
|
||||
<label
|
||||
v-if="panel.kind === 'image'"
|
||||
class="grid gap-1 text-xs font-semibold text-[#394047]"
|
||||
>
|
||||
표시 너비
|
||||
<span class="relative block">
|
||||
<select
|
||||
class="w-full appearance-none rounded border border-[#d7dde2] bg-white px-3 py-2 pr-9 text-sm font-normal text-[#15171a] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
||||
:value="getImageWidthPresetValue(imageIndex, image)"
|
||||
@change="emitImageWidthPresetChange(imageIndex, image, $event)"
|
||||
>
|
||||
<option
|
||||
v-for="option in IMAGE_WIDTH_PRESET_OPTIONS"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="panel.kind === 'image' && getImageWidthPresetValue(imageIndex, image) === 'custom'"
|
||||
class="grid gap-1 text-xs font-semibold text-[#394047]"
|
||||
>
|
||||
직접 너비
|
||||
<input
|
||||
class="rounded border border-[#d7dde2] bg-white px-3 py-2 text-sm font-normal text-[#15171a] placeholder:font-normal placeholder:text-[#8e9cac]"
|
||||
:value="getImagePixelWidthInputValue(imageIndex, image)"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:min="IMAGE_WIDTH_MIN"
|
||||
:max="IMAGE_WIDTH_MAX"
|
||||
step="10"
|
||||
placeholder="자동"
|
||||
@input="emitImagePixelWidthChange(imageIndex, image, $event)"
|
||||
@blur="commitImagePixelWidthChange(imageIndex, image, $event)"
|
||||
>
|
||||
</label>
|
||||
<div v-if="panel.kind === 'gallery'" class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="rounded px-2.5 py-1.5 text-xs font-semibold text-[#394047] hover:bg-[#eff1f2] disabled:opacity-40"
|
||||
|
||||
Reference in New Issue
Block a user