Files
sori.studio/composables/useAdminPostPublishState.js

451 lines
12 KiB
JavaScript

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
}
}