게시물 발행 상태 로직 분리
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
toAdminPostFormTitle,
|
toAdminPostFormTitle,
|
||||||
toAdminPostStoredTitle
|
toAdminPostStoredTitle
|
||||||
} from '../../lib/admin-post-title.js'
|
} from '../../lib/admin-post-title.js'
|
||||||
|
import { toAdminPostDateTimeLocalValue } from '../../composables/useAdminPostPublishState.js'
|
||||||
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
|
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
|
||||||
import { DEFAULT_POST_TAG_LIMIT } from '../../lib/post-tag-limit.js'
|
import { DEFAULT_POST_TAG_LIMIT } from '../../lib/post-tag-limit.js'
|
||||||
|
|
||||||
@@ -64,12 +65,6 @@ const isTitleInputComposing = ref(false)
|
|||||||
const savedPostSnapshot = ref('')
|
const savedPostSnapshot = ref('')
|
||||||
const failedDraftAutosaveSnapshot = ref('')
|
const failedDraftAutosaveSnapshot = ref('')
|
||||||
const slugFieldError = ref('')
|
const slugFieldError = ref('')
|
||||||
const isPublishModalOpen = ref(false)
|
|
||||||
const isUnpublishModalOpen = ref(false)
|
|
||||||
const publishStatus = ref('draft')
|
|
||||||
const publishTiming = ref('now')
|
|
||||||
const scheduledPublishAt = ref('')
|
|
||||||
const publishModalExpandedSection = ref(null)
|
|
||||||
|
|
||||||
const { data: adminTags } = useFetch('/admin/api/tags', {
|
const { data: adminTags } = useFetch('/admin/api/tags', {
|
||||||
default: () => []
|
default: () => []
|
||||||
@@ -81,72 +76,6 @@ const { data: siteSettings } = useFetch('/admin/api/settings', {
|
|||||||
/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
|
/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
|
||||||
const READING_CHARS_PER_MINUTE = 600
|
const READING_CHARS_PER_MINUTE = 600
|
||||||
|
|
||||||
/**
|
|
||||||
* ISO 날짜를 datetime-local 입력값으로 변환
|
|
||||||
* @param {string} value - ISO 날짜 문자열
|
|
||||||
* @returns {string} datetime-local 입력값
|
|
||||||
*/
|
|
||||||
function toDateTimeLocalValue(value) {
|
|
||||||
if (!value) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date(value)
|
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
|
||||||
|
|
||||||
return offsetDate.toISOString().slice(0, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* datetime-local 입력값을 ISO 문자열로 변환
|
|
||||||
* @param {string} value - datetime-local 입력값
|
|
||||||
* @returns {string | null} ISO 날짜 문자열
|
|
||||||
*/
|
|
||||||
function toIsoDateTime(value) {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date(value)
|
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* datetime-local 값을 날짜·시간 필드로 분리
|
|
||||||
* @param {string} value - datetime-local 문자열
|
|
||||||
* @returns {{ date: string, time: string }}
|
|
||||||
*/
|
|
||||||
function splitDateTimeLocal(value) {
|
|
||||||
if (!value || !value.includes('T')) {
|
|
||||||
return { date: '', time: '' }
|
|
||||||
}
|
|
||||||
const [date, time] = value.split('T')
|
|
||||||
return { date: date || '', time: (time || '').slice(0, 5) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 날짜·시간 필드를 datetime-local 문자열로 합침
|
|
||||||
* @param {string} date - YYYY-MM-DD
|
|
||||||
* @param {string} time - HH:mm
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function joinDateTimeLocal(date, time) {
|
|
||||||
if (!date) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return `${date}T${time || '00:00'}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
title: toAdminPostFormTitle(props.initialPost.title),
|
title: toAdminPostFormTitle(props.initialPost.title),
|
||||||
slug: props.initialPost.slug || '',
|
slug: props.initialPost.slug || '',
|
||||||
@@ -157,10 +86,49 @@ const form = reactive({
|
|||||||
isFeatured: Boolean(props.initialPost.isFeatured),
|
isFeatured: Boolean(props.initialPost.isFeatured),
|
||||||
noindex: props.initialPost.noindex === true,
|
noindex: props.initialPost.noindex === true,
|
||||||
status: props.initialPost.status || 'draft',
|
status: props.initialPost.status || 'draft',
|
||||||
publishedAt: toDateTimeLocalValue(props.initialPost.publishedAt),
|
publishedAt: toAdminPostDateTimeLocalValue(props.initialPost.publishedAt),
|
||||||
tagsText: props.initialPost.tags?.join(', ') || ''
|
tagsText: props.initialPost.tags?.join(', ') || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initialPostRef = computed(() => props.initialPost)
|
||||||
|
|
||||||
|
const {
|
||||||
|
applyPublishSelectionToForm,
|
||||||
|
closePublishModal,
|
||||||
|
closeUnpublishModal,
|
||||||
|
displayMembersOnly,
|
||||||
|
displayPrivatePost,
|
||||||
|
displayPublishedLive,
|
||||||
|
displayScheduled,
|
||||||
|
isDraftLike,
|
||||||
|
isPublishModalOpen,
|
||||||
|
isScheduledPost,
|
||||||
|
isUnpublishModalOpen,
|
||||||
|
markPublishStateSaved,
|
||||||
|
modalScheduleDate,
|
||||||
|
modalScheduleTime,
|
||||||
|
openPublishModal: openPublishModalState,
|
||||||
|
openUnpublishModal,
|
||||||
|
persistedPublishKind,
|
||||||
|
publishModalExpandedSection,
|
||||||
|
publishStatus,
|
||||||
|
publishStatusSummaryLabel,
|
||||||
|
publishTiming,
|
||||||
|
publishTimingSummaryLabel,
|
||||||
|
resolvePayloadPublishedAt,
|
||||||
|
scheduledStatusTooltip,
|
||||||
|
selectPublishStatus,
|
||||||
|
selectPublishTiming,
|
||||||
|
settingsPublishDate,
|
||||||
|
settingsPublishTime,
|
||||||
|
togglePublishModalSection,
|
||||||
|
unpublishModalKind,
|
||||||
|
unpublishPersistedPublishedLabel
|
||||||
|
} = useAdminPostPublishState({
|
||||||
|
form,
|
||||||
|
initialPost: initialPostRef
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 숫자를 한국어 로케일 문자열로 변환한다.
|
* 숫자를 한국어 로케일 문자열로 변환한다.
|
||||||
* @param {number} value - 숫자 값
|
* @param {number} value - 숫자 값
|
||||||
@@ -239,46 +207,6 @@ const postWritingStats = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 서버에 반영된 게시 형태(툴바·자동 저장·슬러그 자동 연동 분기)
|
|
||||||
* @param {Object} post - 게시물
|
|
||||||
* @returns {'draft' | 'publishedLive' | 'scheduled' | 'members' | 'private'}
|
|
||||||
*/
|
|
||||||
const getPersistedPublishKind = (post) => {
|
|
||||||
if (!post?.id) {
|
|
||||||
return 'draft'
|
|
||||||
}
|
|
||||||
const st = post.status || 'draft'
|
|
||||||
if (st === 'members' || st === 'private') {
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
if (st !== 'published') {
|
|
||||||
return 'draft'
|
|
||||||
}
|
|
||||||
const pa = post.publishedAt
|
|
||||||
if (pa && new Date(pa) > new Date()) {
|
|
||||||
return 'scheduled'
|
|
||||||
}
|
|
||||||
return 'publishedLive'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 마지막으로 서버와 맞춘 게시 형태 */
|
|
||||||
const persistedPublishKind = ref(getPersistedPublishKind(props.initialPost))
|
|
||||||
|
|
||||||
/** 서버에 반영된 발행 시각(본문 Update 시 유지) */
|
|
||||||
const persistedPublishedAtIso = ref(props.initialPost.publishedAt || null)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.initialPost,
|
|
||||||
(post) => {
|
|
||||||
persistedPublishKind.value = getPersistedPublishKind(post)
|
|
||||||
if (post?.publishedAt) {
|
|
||||||
persistedPublishedAtIso.value = post.publishedAt
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const autosaveKey = computed(() => `${autosaveStoragePrefix}:${props.initialPost.id || 'new'}`)
|
const autosaveKey = computed(() => `${autosaveStoragePrefix}:${props.initialPost.id || 'new'}`)
|
||||||
const savedPostSlug = computed(() => toSlug(props.initialPost?.slug || ''))
|
const savedPostSlug = computed(() => toSlug(props.initialPost?.slug || ''))
|
||||||
|
|
||||||
@@ -460,112 +388,6 @@ const {
|
|||||||
form
|
form
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 예약 발행 여부 확인
|
|
||||||
* @returns {boolean} 예약 발행 여부
|
|
||||||
*/
|
|
||||||
const isScheduledPost = () => {
|
|
||||||
const publishedAt = toIsoDateTime(form.publishedAt)
|
|
||||||
|
|
||||||
return form.status === 'published' && Boolean(publishedAt) && new Date(publishedAt) > new Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 초안 편집 UI(예약 발행 제외) */
|
|
||||||
const isDraftLike = computed(() => form.status === 'draft' && !isScheduledPost())
|
|
||||||
|
|
||||||
/** 발행됨(즉시 공개) */
|
|
||||||
const isPublishedLive = computed(() => form.status === 'published' && !isScheduledPost())
|
|
||||||
|
|
||||||
/** 툴바·상태줄에 즉시 발행으로 표시할지(서버 기준이거나 초안 글에서 폼만 발행으로 바뀐 경우) */
|
|
||||||
const displayPublishedLive = computed(() =>
|
|
||||||
persistedPublishKind.value === 'publishedLive'
|
|
||||||
|| (persistedPublishKind.value === 'draft' && isPublishedLive.value))
|
|
||||||
|
|
||||||
/** 툴바·상태줄에 예약으로 표시할지 */
|
|
||||||
const displayScheduled = computed(() =>
|
|
||||||
persistedPublishKind.value === 'scheduled'
|
|
||||||
|| (persistedPublishKind.value === 'draft' && isScheduledPost()))
|
|
||||||
|
|
||||||
/** 툴바·상태줄에 멤버십 글로 표시할지 */
|
|
||||||
const displayMembersOnly = computed(() =>
|
|
||||||
persistedPublishKind.value === 'members'
|
|
||||||
|| (persistedPublishKind.value === 'draft' && form.status === 'members'))
|
|
||||||
|
|
||||||
/** 툴바·상태줄에 비공개 글로 표시할지 */
|
|
||||||
const displayPrivatePost = computed(() =>
|
|
||||||
persistedPublishKind.value === 'private'
|
|
||||||
|| (persistedPublishKind.value === 'draft' && form.status === 'private'))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에 표시할 게시 상태 요약 문구
|
|
||||||
* @returns {string} 요약 문구
|
|
||||||
*/
|
|
||||||
const publishStatusSummaryLabel = computed(() => {
|
|
||||||
if (publishStatus.value === 'published') {
|
|
||||||
return '발행'
|
|
||||||
}
|
|
||||||
|
|
||||||
return '초안'
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에 표시할 발행 시점 요약 문구
|
|
||||||
* @returns {string} 요약 문구
|
|
||||||
*/
|
|
||||||
const publishTimingSummaryLabel = computed(() => {
|
|
||||||
if (publishTiming.value === 'now') {
|
|
||||||
return '지금 바로'
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = scheduledPublishAt.value
|
|
||||||
|
|
||||||
if (!raw) {
|
|
||||||
return '예약'
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date(raw)
|
|
||||||
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
|
||||||
return '예약'
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('ko-KR', {
|
|
||||||
dateStyle: 'medium',
|
|
||||||
timeStyle: 'short'
|
|
||||||
}).format(date)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 저장 요청용 발행 시각을 결정한다.
|
|
||||||
* @param {{ allowNewPublishTimestamp?: boolean }} [options] - 신규 발행 시각 허용 여부
|
|
||||||
* @returns {string|null} ISO 발행 시각 또는 null
|
|
||||||
*/
|
|
||||||
const resolvePayloadPublishedAt = (options = {}) => {
|
|
||||||
if (form.status !== 'published') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromForm = toIsoDateTime(form.publishedAt)
|
|
||||||
|
|
||||||
if (fromForm) {
|
|
||||||
return fromForm
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persistedPublishedAtIso.value) {
|
|
||||||
return persistedPublishedAtIso.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.initialPost.publishedAt) {
|
|
||||||
return props.initialPost.publishedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.allowNewPublishTimestamp) {
|
|
||||||
return new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 게시물 입력값 생성
|
* 게시물 입력값 생성
|
||||||
* @param {{ allowNewPublishTimestamp?: boolean }} [options] - 신규 발행 시각 허용 여부
|
* @param {{ allowNewPublishTimestamp?: boolean }} [options] - 신규 발행 시각 허용 여부
|
||||||
@@ -673,26 +495,6 @@ const headerDraftStatusText = computed(() => {
|
|||||||
return props.initialPost?.id ? 'Draft - Saved' : 'Draft'
|
return props.initialPost?.id ? 'Draft - Saved' : 'Draft'
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 예약 발행일 툴팁(영문 한 줄)
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
const scheduledStatusTooltip = computed(() => {
|
|
||||||
const iso = toIsoDateTime(form.publishedAt)
|
|
||||||
if (!iso) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const d = new Date(iso)
|
|
||||||
if (Number.isNaN(d.getTime())) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const line = d.toLocaleString('en-GB', {
|
|
||||||
dateStyle: 'long',
|
|
||||||
timeStyle: 'short'
|
|
||||||
})
|
|
||||||
return `Scheduled to be published on ${line}`
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 레거시 로컬 자동 저장 키 제거
|
* 레거시 로컬 자동 저장 키 제거
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
@@ -910,22 +712,6 @@ const requestToolbarUpdate = () => {
|
|||||||
emit('submit', createPostPayload())
|
emit('submit', createPostPayload())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 취소·예약 취소 확인 화면을 연다.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const openUnpublishModal = () => {
|
|
||||||
isUnpublishModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 취소·예약 취소 확인 화면을 닫는다.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const closeUnpublishModal = () => {
|
|
||||||
isUnpublishModalOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 발행 취소 또는 예약 취소(초안으로 되돌림)를 확정한다.
|
* 발행 취소 또는 예약 취소(초안으로 되돌림)를 확정한다.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
@@ -960,62 +746,12 @@ const toggleSettingsPanel = () => {
|
|||||||
isSettingsOpen.value = !isSettingsOpen.value
|
isSettingsOpen.value = !isSettingsOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 발행 모달을 연다. */
|
||||||
* 발행 모달을 현재 폼 상태로 초기화한다.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const syncPublishModalStateFromForm = () => {
|
|
||||||
if (persistedPublishKind.value === 'draft') {
|
|
||||||
publishStatus.value = 'published'
|
|
||||||
publishTiming.value = 'now'
|
|
||||||
scheduledPublishAt.value = toDateTimeLocalValue(new Date().toISOString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
publishStatus.value = form.status === 'published' ? 'published' : 'draft'
|
|
||||||
scheduledPublishAt.value = form.publishedAt || toDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
|
||||||
publishTiming.value = isScheduledPost() ? 'schedule' : 'now'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달 열기
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const openPublishModal = () => {
|
const openPublishModal = () => {
|
||||||
if (hasBlockingSlugError.value) {
|
if (hasBlockingSlugError.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
syncPublishModalStateFromForm()
|
openPublishModalState()
|
||||||
publishModalExpandedSection.value = null
|
|
||||||
isPublishModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달 닫기
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const closePublishModal = () => {
|
|
||||||
isPublishModalOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에서 선택한 값을 폼에 반영
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const applyPublishSelectionToForm = () => {
|
|
||||||
form.status = publishStatus.value
|
|
||||||
|
|
||||||
if (publishStatus.value !== 'published') {
|
|
||||||
form.publishedAt = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publishTiming.value === 'schedule') {
|
|
||||||
form.publishedAt = scheduledPublishAt.value || toDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form.publishedAt = toDateTimeLocalValue(new Date().toISOString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1030,104 +766,6 @@ const submitFromPublishModal = () => {
|
|||||||
submitPost()
|
submitPost()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에서 설정 행 펼침을 토글한다.
|
|
||||||
* @param {'status' | 'timing'} section - 펼칠 행
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const togglePublishModalSection = (section) => {
|
|
||||||
publishModalExpandedSection.value =
|
|
||||||
publishModalExpandedSection.value === section ? null : section
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에서 게시 상태를 선택한다.
|
|
||||||
* @param {'published' | 'draft'} status - 선택 상태
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const selectPublishStatus = (status) => {
|
|
||||||
publishStatus.value = status
|
|
||||||
publishModalExpandedSection.value = null
|
|
||||||
|
|
||||||
if (status !== 'published') {
|
|
||||||
publishTiming.value = 'now'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 발행 모달에서 발행 시점을 선택한다.
|
|
||||||
* @param {'now' | 'schedule'} timing - 즉시 또는 예약
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const selectPublishTiming = (timing) => {
|
|
||||||
publishTiming.value = timing
|
|
||||||
|
|
||||||
if (timing === 'now') {
|
|
||||||
publishModalExpandedSection.value = null
|
|
||||||
} else if (!scheduledPublishAt.value) {
|
|
||||||
scheduledPublishAt.value = toDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 설정 패널 발행 날짜(YYYY-MM-DD) */
|
|
||||||
const settingsPublishDate = computed({
|
|
||||||
get: () => splitDateTimeLocal(form.publishedAt).date,
|
|
||||||
set: (date) => {
|
|
||||||
const { time } = splitDateTimeLocal(form.publishedAt)
|
|
||||||
form.publishedAt = joinDateTimeLocal(date, time || '12:00')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 설정 패널 발행 시각(HH:mm) */
|
|
||||||
const settingsPublishTime = computed({
|
|
||||||
get: () => splitDateTimeLocal(form.publishedAt).time,
|
|
||||||
set: (time) => {
|
|
||||||
const { date } = splitDateTimeLocal(form.publishedAt)
|
|
||||||
const baseDate = date || new Date().toISOString().slice(0, 10)
|
|
||||||
form.publishedAt = joinDateTimeLocal(baseDate, time || '12:00')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 발행 모달 예약 날짜 */
|
|
||||||
const modalScheduleDate = computed({
|
|
||||||
get: () => splitDateTimeLocal(scheduledPublishAt.value).date,
|
|
||||||
set: (date) => {
|
|
||||||
const { time } = splitDateTimeLocal(scheduledPublishAt.value)
|
|
||||||
scheduledPublishAt.value = joinDateTimeLocal(date, time || '12:00')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 발행 모달 예약 시각 */
|
|
||||||
const modalScheduleTime = computed({
|
|
||||||
get: () => splitDateTimeLocal(scheduledPublishAt.value).time,
|
|
||||||
set: (time) => {
|
|
||||||
const { date } = splitDateTimeLocal(scheduledPublishAt.value)
|
|
||||||
const baseDate = date || new Date().toISOString().slice(0, 10)
|
|
||||||
scheduledPublishAt.value = joinDateTimeLocal(baseDate, time || '12:00')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 발행 취소 화면에 표시할 서버 반영 발행 시각 문구 */
|
|
||||||
const unpublishPersistedPublishedLabel = computed(() => {
|
|
||||||
const iso = props.initialPost?.publishedAt
|
|
||||||
if (!iso) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const d = new Date(iso)
|
|
||||||
if (Number.isNaN(d.getTime())) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return new Intl.DateTimeFormat('ko-KR', {
|
|
||||||
dateStyle: 'long',
|
|
||||||
timeStyle: 'short'
|
|
||||||
}).format(d)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 발행 취소 화면 제목(즉시 발행 / 예약) */
|
|
||||||
const unpublishModalKind = computed(() => (
|
|
||||||
persistedPublishKind.value === 'scheduled' ? 'scheduled' : 'published'
|
|
||||||
))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 입력값을 저장 완료 기준점으로 표시한다.
|
* 현재 입력값을 저장 완료 기준점으로 표시한다.
|
||||||
* @param {Object|null} [savedPayload] - 실제 서버에 저장된 입력값
|
* @param {Object|null} [savedPayload] - 실제 서버에 저장된 입력값
|
||||||
@@ -1138,10 +776,7 @@ const markSaved = (savedPayload = null) => {
|
|||||||
failedDraftAutosaveSnapshot.value = ''
|
failedDraftAutosaveSnapshot.value = ''
|
||||||
slugFieldError.value = ''
|
slugFieldError.value = ''
|
||||||
const publishedAt = resolvePayloadPublishedAt()
|
const publishedAt = resolvePayloadPublishedAt()
|
||||||
|
markPublishStateSaved(publishedAt)
|
||||||
if (publishedAt) {
|
|
||||||
persistedPublishedAtIso.value = publishedAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1190,22 +825,6 @@ const takePendingAutosavePayload = () => {
|
|||||||
return createPostPayload()
|
return createPostPayload()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(publishStatus, (next) => {
|
|
||||||
if (next !== 'published') {
|
|
||||||
publishTiming.value = 'now'
|
|
||||||
|
|
||||||
if (publishModalExpandedSection.value === 'timing') {
|
|
||||||
publishModalExpandedSection.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => form.status, (next) => {
|
|
||||||
if (next === 'published' && !toIsoDateTime(form.publishedAt)) {
|
|
||||||
form.publishedAt = toDateTimeLocalValue(new Date().toISOString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(serializedPostFingerprint, () => {
|
watch(serializedPostFingerprint, () => {
|
||||||
if (!import.meta.client) {
|
if (!import.meta.client) {
|
||||||
return
|
return
|
||||||
|
|||||||
450
composables/useAdminPostPublishState.js
Normal file
450
composables/useAdminPostPublishState.js
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 날짜를 datetime-local 입력값으로 변환한다.
|
||||||
|
* @param {string} value - ISO 날짜 문자열
|
||||||
|
* @returns {string} datetime-local 입력값
|
||||||
|
*/
|
||||||
|
export const toAdminPostDateTimeLocalValue = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value)
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
return offsetDate.toISOString().slice(0, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* datetime-local 입력값을 ISO 문자열로 변환한다.
|
||||||
|
* @param {string} value - datetime-local 입력값
|
||||||
|
* @returns {string | null} ISO 날짜 문자열
|
||||||
|
*/
|
||||||
|
const toIsoDateTime = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value)
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* datetime-local 값을 날짜·시간 필드로 분리한다.
|
||||||
|
* @param {string} value - datetime-local 문자열
|
||||||
|
* @returns {{ date: string, time: string }}
|
||||||
|
*/
|
||||||
|
const splitDateTimeLocal = (value) => {
|
||||||
|
if (!value || !value.includes('T')) {
|
||||||
|
return { date: '', time: '' }
|
||||||
|
}
|
||||||
|
const [date, time] = value.split('T')
|
||||||
|
return { date: date || '', time: (time || '').slice(0, 5) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜·시간 필드를 datetime-local 문자열로 합친다.
|
||||||
|
* @param {string} date - YYYY-MM-DD
|
||||||
|
* @param {string} time - HH:mm
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const joinDateTimeLocal = (date, time) => {
|
||||||
|
if (!date) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${date}T${time || '00:00'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버에 반영된 게시 형태를 계산한다.
|
||||||
|
* @param {Object} post - 게시물
|
||||||
|
* @returns {'draft' | 'publishedLive' | 'scheduled' | 'members' | 'private'} 게시 형태
|
||||||
|
*/
|
||||||
|
const getPersistedPublishKind = (post) => {
|
||||||
|
if (!post?.id) {
|
||||||
|
return 'draft'
|
||||||
|
}
|
||||||
|
const st = post.status || 'draft'
|
||||||
|
if (st === 'members' || st === 'private') {
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
if (st !== 'published') {
|
||||||
|
return 'draft'
|
||||||
|
}
|
||||||
|
const pa = post.publishedAt
|
||||||
|
if (pa && new Date(pa) > new Date()) {
|
||||||
|
return 'scheduled'
|
||||||
|
}
|
||||||
|
return 'publishedLive'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 게시물 폼의 발행·예약·발행 취소 상태를 만든다.
|
||||||
|
* @param {Object} options - 발행 상태 옵션
|
||||||
|
* @param {Object} options.form - 게시물 폼 reactive 객체
|
||||||
|
* @param {import('vue').ComputedRef<Object>} options.initialPost - 서버 게시물 입력값
|
||||||
|
* @returns {Object} 발행 상태와 이벤트 처리기
|
||||||
|
*/
|
||||||
|
export const useAdminPostPublishState = ({
|
||||||
|
form,
|
||||||
|
initialPost
|
||||||
|
}) => {
|
||||||
|
const getInitialPost = () => initialPost.value || {}
|
||||||
|
const persistedPublishKind = ref(getPersistedPublishKind(getInitialPost()))
|
||||||
|
const persistedPublishedAtIso = ref(getInitialPost().publishedAt || null)
|
||||||
|
const isPublishModalOpen = ref(false)
|
||||||
|
const isUnpublishModalOpen = ref(false)
|
||||||
|
const publishStatus = ref('draft')
|
||||||
|
const publishTiming = ref('now')
|
||||||
|
const scheduledPublishAt = ref('')
|
||||||
|
const publishModalExpandedSection = ref(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
initialPost,
|
||||||
|
(post) => {
|
||||||
|
persistedPublishKind.value = getPersistedPublishKind(post)
|
||||||
|
if (post?.publishedAt) {
|
||||||
|
persistedPublishedAtIso.value = post.publishedAt
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예약 발행 여부를 확인한다.
|
||||||
|
* @returns {boolean} 예약 발행 여부
|
||||||
|
*/
|
||||||
|
const isScheduledPost = () => {
|
||||||
|
const publishedAt = toIsoDateTime(form.publishedAt)
|
||||||
|
|
||||||
|
return form.status === 'published' && Boolean(publishedAt) && new Date(publishedAt) > new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDraftLike = computed(() => form.status === 'draft' && !isScheduledPost())
|
||||||
|
const isPublishedLive = computed(() => form.status === 'published' && !isScheduledPost())
|
||||||
|
const displayPublishedLive = computed(() =>
|
||||||
|
persistedPublishKind.value === 'publishedLive'
|
||||||
|
|| (persistedPublishKind.value === 'draft' && isPublishedLive.value))
|
||||||
|
const displayScheduled = computed(() =>
|
||||||
|
persistedPublishKind.value === 'scheduled'
|
||||||
|
|| (persistedPublishKind.value === 'draft' && isScheduledPost()))
|
||||||
|
const displayMembersOnly = computed(() =>
|
||||||
|
persistedPublishKind.value === 'members'
|
||||||
|
|| (persistedPublishKind.value === 'draft' && form.status === 'members'))
|
||||||
|
const displayPrivatePost = computed(() =>
|
||||||
|
persistedPublishKind.value === 'private'
|
||||||
|
|| (persistedPublishKind.value === 'draft' && form.status === 'private'))
|
||||||
|
|
||||||
|
const publishStatusSummaryLabel = computed(() => {
|
||||||
|
if (publishStatus.value === 'published') {
|
||||||
|
return '발행'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '초안'
|
||||||
|
})
|
||||||
|
|
||||||
|
const publishTimingSummaryLabel = computed(() => {
|
||||||
|
if (publishTiming.value === 'now') {
|
||||||
|
return '지금 바로'
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = scheduledPublishAt.value
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return '예약'
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(raw)
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return '예약'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('ko-KR', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short'
|
||||||
|
}).format(date)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 저장 요청용 발행 시각을 결정한다.
|
||||||
|
* @param {{ allowNewPublishTimestamp?: boolean }} [options] - 신규 발행 시각 허용 여부
|
||||||
|
* @returns {string|null} ISO 발행 시각 또는 null
|
||||||
|
*/
|
||||||
|
const resolvePayloadPublishedAt = (options = {}) => {
|
||||||
|
if (form.status !== 'published') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromForm = toIsoDateTime(form.publishedAt)
|
||||||
|
|
||||||
|
if (fromForm) {
|
||||||
|
return fromForm
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistedPublishedAtIso.value) {
|
||||||
|
return persistedPublishedAtIso.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getInitialPost().publishedAt) {
|
||||||
|
return getInitialPost().publishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.allowNewPublishTimestamp) {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledStatusTooltip = computed(() => {
|
||||||
|
const iso = toIsoDateTime(form.publishedAt)
|
||||||
|
if (!iso) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const d = new Date(iso)
|
||||||
|
if (Number.isNaN(d.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const line = d.toLocaleString('en-GB', {
|
||||||
|
dateStyle: 'long',
|
||||||
|
timeStyle: 'short'
|
||||||
|
})
|
||||||
|
return `Scheduled to be published on ${line}`
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달을 현재 폼 상태로 초기화한다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const syncPublishModalStateFromForm = () => {
|
||||||
|
if (persistedPublishKind.value === 'draft') {
|
||||||
|
publishStatus.value = 'published'
|
||||||
|
publishTiming.value = 'now'
|
||||||
|
scheduledPublishAt.value = toAdminPostDateTimeLocalValue(new Date().toISOString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishStatus.value = form.status === 'published' ? 'published' : 'draft'
|
||||||
|
scheduledPublishAt.value = form.publishedAt || toAdminPostDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
||||||
|
publishTiming.value = isScheduledPost() ? 'schedule' : 'now'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달을 연다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const openPublishModal = () => {
|
||||||
|
syncPublishModalStateFromForm()
|
||||||
|
publishModalExpandedSection.value = null
|
||||||
|
isPublishModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달을 닫는다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const closePublishModal = () => {
|
||||||
|
isPublishModalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 취소·예약 취소 확인 화면을 연다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const openUnpublishModal = () => {
|
||||||
|
isUnpublishModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 취소·예약 취소 확인 화면을 닫는다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const closeUnpublishModal = () => {
|
||||||
|
isUnpublishModalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달에서 선택한 값을 폼에 반영한다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const applyPublishSelectionToForm = () => {
|
||||||
|
form.status = publishStatus.value
|
||||||
|
|
||||||
|
if (publishStatus.value !== 'published') {
|
||||||
|
form.publishedAt = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publishTiming.value === 'schedule') {
|
||||||
|
form.publishedAt = scheduledPublishAt.value || toAdminPostDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.publishedAt = toAdminPostDateTimeLocalValue(new Date().toISOString())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달에서 설정 행 펼침을 토글한다.
|
||||||
|
* @param {'status' | 'timing'} section - 펼칠 행
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const togglePublishModalSection = (section) => {
|
||||||
|
publishModalExpandedSection.value =
|
||||||
|
publishModalExpandedSection.value === section ? null : section
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달에서 게시 상태를 선택한다.
|
||||||
|
* @param {'published' | 'draft'} status - 선택 상태
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const selectPublishStatus = (status) => {
|
||||||
|
publishStatus.value = status
|
||||||
|
publishModalExpandedSection.value = null
|
||||||
|
|
||||||
|
if (status !== 'published') {
|
||||||
|
publishTiming.value = 'now'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 모달에서 발행 시점을 선택한다.
|
||||||
|
* @param {'now' | 'schedule'} timing - 즉시 또는 예약
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const selectPublishTiming = (timing) => {
|
||||||
|
publishTiming.value = timing
|
||||||
|
|
||||||
|
if (timing === 'now') {
|
||||||
|
publishModalExpandedSection.value = null
|
||||||
|
} else if (!scheduledPublishAt.value) {
|
||||||
|
scheduledPublishAt.value = toAdminPostDateTimeLocalValue(new Date(Date.now() + 3600000).toISOString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPublishDate = computed({
|
||||||
|
get: () => splitDateTimeLocal(form.publishedAt).date,
|
||||||
|
set: (date) => {
|
||||||
|
const { time } = splitDateTimeLocal(form.publishedAt)
|
||||||
|
form.publishedAt = joinDateTimeLocal(date, time || '12:00')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const settingsPublishTime = computed({
|
||||||
|
get: () => splitDateTimeLocal(form.publishedAt).time,
|
||||||
|
set: (time) => {
|
||||||
|
const { date } = splitDateTimeLocal(form.publishedAt)
|
||||||
|
const baseDate = date || new Date().toISOString().slice(0, 10)
|
||||||
|
form.publishedAt = joinDateTimeLocal(baseDate, time || '12:00')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalScheduleDate = computed({
|
||||||
|
get: () => splitDateTimeLocal(scheduledPublishAt.value).date,
|
||||||
|
set: (date) => {
|
||||||
|
const { time } = splitDateTimeLocal(scheduledPublishAt.value)
|
||||||
|
scheduledPublishAt.value = joinDateTimeLocal(date, time || '12:00')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalScheduleTime = computed({
|
||||||
|
get: () => splitDateTimeLocal(scheduledPublishAt.value).time,
|
||||||
|
set: (time) => {
|
||||||
|
const { date } = splitDateTimeLocal(scheduledPublishAt.value)
|
||||||
|
const baseDate = date || new Date().toISOString().slice(0, 10)
|
||||||
|
scheduledPublishAt.value = joinDateTimeLocal(baseDate, time || '12:00')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const unpublishPersistedPublishedLabel = computed(() => {
|
||||||
|
const iso = getInitialPost().publishedAt
|
||||||
|
if (!iso) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const d = new Date(iso)
|
||||||
|
if (Number.isNaN(d.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('ko-KR', {
|
||||||
|
dateStyle: 'long',
|
||||||
|
timeStyle: 'short'
|
||||||
|
}).format(d)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unpublishModalKind = computed(() => (
|
||||||
|
persistedPublishKind.value === 'scheduled' ? 'scheduled' : 'published'
|
||||||
|
))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 저장 성공 후 발행 기준 시각을 갱신한다.
|
||||||
|
* @param {string|null} publishedAt - 저장된 발행 시각
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const markPublishStateSaved = (publishedAt) => {
|
||||||
|
if (publishedAt) {
|
||||||
|
persistedPublishedAtIso.value = publishedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(publishStatus, (next) => {
|
||||||
|
if (next !== 'published') {
|
||||||
|
publishTiming.value = 'now'
|
||||||
|
|
||||||
|
if (publishModalExpandedSection.value === 'timing') {
|
||||||
|
publishModalExpandedSection.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => form.status, (next) => {
|
||||||
|
if (next === 'published' && !toIsoDateTime(form.publishedAt)) {
|
||||||
|
form.publishedAt = toAdminPostDateTimeLocalValue(new Date().toISOString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyPublishSelectionToForm,
|
||||||
|
closePublishModal,
|
||||||
|
closeUnpublishModal,
|
||||||
|
displayMembersOnly,
|
||||||
|
displayPrivatePost,
|
||||||
|
displayPublishedLive,
|
||||||
|
displayScheduled,
|
||||||
|
isDraftLike,
|
||||||
|
isPublishModalOpen,
|
||||||
|
isScheduledPost,
|
||||||
|
isUnpublishModalOpen,
|
||||||
|
markPublishStateSaved,
|
||||||
|
modalScheduleDate,
|
||||||
|
modalScheduleTime,
|
||||||
|
openPublishModal,
|
||||||
|
openUnpublishModal,
|
||||||
|
persistedPublishKind,
|
||||||
|
publishModalExpandedSection,
|
||||||
|
publishStatus,
|
||||||
|
publishStatusSummaryLabel,
|
||||||
|
publishTiming,
|
||||||
|
publishTimingSummaryLabel,
|
||||||
|
resolvePayloadPublishedAt,
|
||||||
|
scheduledStatusTooltip,
|
||||||
|
selectPublishStatus,
|
||||||
|
selectPublishTiming,
|
||||||
|
settingsPublishDate,
|
||||||
|
settingsPublishTime,
|
||||||
|
togglePublishModalSection,
|
||||||
|
unpublishModalKind,
|
||||||
|
unpublishPersistedPublishedLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
| composables/useAdminSettingsNavigation.js | 관리자 설정 화면 내비게이션 검색·필터·활성 구역·스크롤 이동·스크롤 스파이 상태 |
|
| composables/useAdminSettingsNavigation.js | 관리자 설정 화면 내비게이션 검색·필터·활성 구역·스크롤 이동·스크롤 스파이 상태 |
|
||||||
| composables/useAdminPostTags.js | 관리자 게시물 폼 태그 파싱·추천·선택·삭제·키보드 처리 상태 |
|
| composables/useAdminPostTags.js | 관리자 게시물 폼 태그 파싱·추천·선택·삭제·키보드 처리 상태 |
|
||||||
| composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 |
|
| composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 |
|
||||||
|
| composables/useAdminPostPublishState.js | 관리자 게시물 폼 발행·예약·발행취소 상태와 날짜 입력 상태 |
|
||||||
|
|
||||||
## 공유 라이브러리(서버·클라이언트 공통)
|
## 공유 라이브러리(서버·클라이언트 공통)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- 거대 컴포넌트 책임 분리 진행.
|
- 거대 컴포넌트 책임 분리 진행.
|
||||||
- `components/admin/AdminMarkdownEditor.vue`: 미디어 모달, 슬래시 명령, 히스토리·선택 복원, 라이브/소스 전환 로직을 단계적으로 composable 또는 하위 컴포넌트로 분리.
|
- `components/admin/AdminMarkdownEditor.vue`: 미디어 모달, 슬래시 명령, 히스토리·선택 복원, 라이브/소스 전환 로직을 단계적으로 composable 또는 하위 컴포넌트로 분리.
|
||||||
- `components/content/ContentMarkdownRenderer.vue`: 파싱·블록 렌더링·라이브 편집 중개·라이트박스·광고 삽입 책임을 분리.
|
- `components/content/ContentMarkdownRenderer.vue`: 파싱·블록 렌더링·라이브 편집 중개·라이트박스·광고 삽입 책임을 분리.
|
||||||
- `components/admin/AdminPostForm.vue`: 발행/초안 저장 상태, 우측 설정 패널 책임을 분리.
|
- `components/admin/AdminPostForm.vue`: 우측 설정 패널 책임을 분리.
|
||||||
- `pages/admin/media/index.vue`: 미디어 목록, 폴더 트리, 상세 모달, 재생성 액션을 작은 단위로 분리.
|
- `pages/admin/media/index.vue`: 미디어 목록, 폴더 트리, 상세 모달, 재생성 액션을 작은 단위로 분리.
|
||||||
- 미디어 카탈로그 전환 코드 수명 정리.
|
- 미디어 카탈로그 전환 코드 수명 정리.
|
||||||
- 운영 `MEDIA_CATALOG_MODE=database` 안정화 후 `shadow`/`filesystem` 런타임 분기 축소 가능 여부 검토.
|
- 운영 `MEDIA_CATALOG_MODE=database` 안정화 후 `shadow`/`filesystem` 런타임 분기 축소 가능 여부 검토.
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.42
|
||||||
|
|
||||||
|
- 관리자 게시물 폼의 발행·예약·발행취소 상태 판별과 모달 상태를 `useAdminPostPublishState` composable로 분리.
|
||||||
|
- 발행 시각 datetime-local 변환·예약 날짜 입력·발행취소 표시 문구 계산을 한 경로로 정리.
|
||||||
|
- 게시물 저장 payload 생성은 폼에 유지하면서 발행 상태 관리 책임을 축소.
|
||||||
|
|
||||||
## v1.11.41
|
## v1.11.41
|
||||||
|
|
||||||
- 관리자 게시물 폼의 대표 이미지 선택·검색·페이지네이션·업로드 로직을 `useAdminFeaturedImagePicker` composable로 분리.
|
- 관리자 게시물 폼의 대표 이미지 선택·검색·페이지네이션·업로드 로직을 `useAdminFeaturedImagePicker` composable로 분리.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.41",
|
"version": "1.11.42",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.41",
|
"version": "1.11.42",
|
||||||
"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": "1.11.41",
|
"version": "1.11.42",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
Reference in New Issue
Block a user