게시물 발행 상태 로직 분리
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user