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