정읍문화소식 시민이 직접 올리고 함께 보는 이미지 중심 문화소식 플랫폼
메인 비주얼

정읍문화소식

행사 포스터처럼 보이고, 시민이 직접 사진과 영상을 올릴 수 있는 열린 문화이야기

가까운 날짜순 문화소식

관리자공지사항 미리보기

const POSTS_KEY = 'jeongeup-culture-posts-v6'; const NOTICES_KEY = 'jeongeup-culture-notices-v1'; const VIDEO_ITEMS_KEY = 'jeongeup-video-items-v1'; const R2_UPLOAD_ENDPOINT = 'https://jeongeup-upload.nockd61.workers.dev'; const ADMIN_PASSWORD = '1234'; const MAX_VISIBLE_POSTS = 8; const demoPosts = [ { id: '1', title: '정읍 봄 전시회', date: '2026-03-20', endDate: '2026-03-20', place: '정읍문화원', author: '정읍문화원', description: '정읍의 봄 풍경과 작가들의 전시를 소개합니다.', mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1518998053901-5348d3961a04?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }, { id: '2', title: '생활문화 공연마당', date: '2026-03-21', endDate: '2026-03-21', place: '정읍예술회관', author: '생활문화동아리', description: '시민과 함께하는 열린 공연입니다.', mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1501386761578-eac5c94b800a?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }, { id: '3', title: '가족 체험 프로그램', date: '2026-03-22', endDate: '2026-03-22', place: '문화창작소', author: '시민기획팀', description: '아이와 함께 만드는 즐거운 체험 시간입니다.', mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1516321497487-e288fb19713f?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }, { id: '4', title: '청년 문화 토크', date: '2026-03-23', endDate: '2026-03-23', place: '공유문화라운지', author: '청년기획단', description: '청년 문화기획자가 함께 이야기합니다.', mediaType: 'video', mediaUrl: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', likes: 0, views: 0, comments: [] }, { id: '5', title: '갤러리 오픈데이', date: '2026-03-24', endDate: '2026-03-24', place: '정읍 시내 갤러리', author: '지역 작가회', description: '지역 갤러리와 작가를 소개합니다.', mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }, { id: '6', title: '야외 음악회', date: '2026-03-25', endDate: '2026-03-25', place: '정읍천 광장', author: '정읍음악협회', description: '야외에서 즐기는 시민 음악회입니다.', mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }, { id: '7', title: '문화 영상 아카이브', date: '2026-03-29', endDate: '2026-03-29', place: '온라인 공유', author: '시민제보', description: '정읍 문화행사 현장을 담은 짧은 기록 영상입니다.', mediaType: 'video', mediaUrl: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm', likes: 0, views: 0, comments: [] } ]; const demoNotices = [ { id: 'n1', title: '정읍문화소식 오픈 안내', content: '정읍문화소식이 새롭게 열렸습니다. 시민 누구나 문화행사 정보를 올릴 수 있습니다.', date: '2026-03-19' }, { id: 'n2', title: '가까운 날짜순 자동 노출', content: '첫 화면에는 가까운 날짜 행사 8개가 자동 정렬되어 표시됩니다.', date: '2026-03-19' } ]; const eventGrid = document.getElementById('eventGrid'); const uploadForm = document.getElementById('uploadForm'); const uploadPreview = document.getElementById('uploadPreview'); const fileInput = document.getElementById('fileInput'); const mediaTypeSelect = document.getElementById('mediaType'); const filterType = document.getElementById('filterType'); const searchInput = document.getElementById('searchInput'); const searchBtn = document.getElementById('searchBtn'); const paginationInfo = document.getElementById('paginationInfo'); const detailTitle = document.getElementById('detailTitle'); const detailMeta = document.getElementById('detailMeta'); const detailMedia = document.getElementById('detailMedia'); const detailDescription = document.getElementById('detailDescription'); const detailShareBtn = document.getElementById('detailShareBtn'); const detailLikeBtn = document.getElementById('detailLikeBtn'); const noticeForm = document.getElementById('noticeForm'); const noticeViewTab = document.getElementById('noticeViewTab'); const noticeWriteTab = document.getElementById('noticeWriteTab'); const noticeListPanel = document.getElementById('noticeListPanel'); const noticeWritePanel = document.getElementById('noticeWritePanel'); const noticeList = document.getElementById('noticeList'); const adminLoginBtn = document.getElementById('adminLoginBtn'); const noticePreviewList = document.getElementById('noticePreviewList'); const boardPostList = document.getElementById('boardPostList'); const videoList = document.getElementById('videoList'); const commentForm = document.getElementById('commentForm'); let isAdminLoggedIn = false; function structuredCloneSafe(data) { return JSON.parse(JSON.stringify(data)); } function getStored(key, fallback) { const raw = localStorage.getItem(key); if (!raw) { localStorage.setItem(key, JSON.stringify(fallback)); return structuredCloneSafe(fallback); } try { return JSON.parse(raw); } catch (e) { localStorage.setItem(key, JSON.stringify(fallback)); return structuredCloneSafe(fallback); } } function saveStored(key, value) { localStorage.setItem(key, JSON.stringify(value)); } function normalizeMediaUrl(url) { let value = String(url || '').trim(); if (!value) return ''; value = value.replace(/\\/g, '/'); if (value.startsWith('//')) { value = 'https:' + value; } if (!/^https?:\/\//i.test(value) && value.includes('r2.dev/')) { value = 'https://' + value.replace(/^\/+/, ''); } try { const parsed = new URL(value); return parsed.toString(); } catch (e) { return value; } } function normalizePost(post) { const normalized = { likes: 0, views: 0, comments: [], endDate: post.date || '', ...post }; normalized.mediaType = normalized.mediaType === 'video' ? 'video' : 'image'; normalized.mediaUrl = normalizeMediaUrl(normalized.mediaUrl || normalized.url || normalized.image || ''); return normalized; } function isValidPost(post) { if (!post) return false; if (!String(post.title || '').trim()) return false; if (!String(post.date || '').trim()) return false; if (post.mediaType === 'video') { return !!String(post.mediaUrl || '').trim() || !!String(post.youtubeId || '').trim(); } return !!String(post.mediaUrl || '').trim(); } function getPosts() { const rawPosts = getStored(POSTS_KEY, demoPosts).map(normalizePost); const cleanedPosts = rawPosts.filter(isValidPost); if (cleanedPosts.length !== rawPosts.length) { saveStored(POSTS_KEY, cleanedPosts); } return cleanedPosts; } function savePosts(posts) { saveStored(POSTS_KEY, posts.map(normalizePost)); } function getNotices() { return getStored(NOTICES_KEY, demoNotices); } function saveNotices(notices) { saveStored(NOTICES_KEY, notices); } function getVideoItems() { const raw = localStorage.getItem(VIDEO_ITEMS_KEY); if (!raw) return []; try { return JSON.parse(raw); } catch (e) { localStorage.removeItem(VIDEO_ITEMS_KEY); return []; } } function saveVideoItems(items) { localStorage.setItem(VIDEO_ITEMS_KEY, JSON.stringify(items)); } function getYoutubeId(url) { if (!url) return ''; let videoId = ''; if (url.includes('watch?v=')) { videoId = url.split('watch?v=')[1]; } else if (url.includes('shorts/')) { videoId = url.split('shorts/')[1]; } else if (url.includes('youtu.be/')) { videoId = url.split('youtu.be/')[1]; } if (videoId.includes('&')) videoId = videoId.split('&')[0]; if (videoId.includes('?')) videoId = videoId.split('?')[0]; return videoId.trim(); } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function createImagePlaceholder(text) { const div = document.createElement('div'); div.className = 'thumb-placeholder'; div.textContent = text || '이미지를 불러오지 못했습니다'; return div; } window.createImagePlaceholder = createImagePlaceholder; function buildEventVideoHtml(post) { if (post.youtubeId) { return `
`; } if (!post.mediaUrl) { return `
영상 주소가 없습니다
`; } return ``; } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function retryImageLoad(img) { const original = img.getAttribute('data-original-src') || img.getAttribute('src') || ''; const tried = Number(img.getAttribute('data-retry-count') || '0'); if (!img.getAttribute('data-original-src')) { img.setAttribute('data-original-src', original); } if (tried < 3 && original) { img.setAttribute('data-retry-count', String(tried + 1)); const joiner = original.includes('?') ? '&' : '?'; img.src = `${original}${joiner}retry=${Date.now()}`; return; } img.replaceWith(createImagePlaceholder('이미지 불러오기 실패')); } function updateVisibleCount() { const count = document.querySelectorAll('#eventGrid .card').length; paginationInfo.textContent = count > 0 ? `가까운 날짜순 ${count}개 노출` : '표시할 문화소식이 없습니다.'; } function handleCardImageError(img) { retryImageLoad(img); updateVisibleCount(); } function checkImageReady(url) { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = url + (url.includes('?') ? '&' : '?') + 'check=' + Date.now(); }); } async function waitForMediaAvailability(url, mediaType) { if (mediaType !== 'image') return true; for (let i = 0; i < 8; i++) { const ok = await checkImageReady(url); if (ok) return true; await wait(900); } return false; } async function uploadFileToR2(file) { const formData = new FormData(); formData.append('file', file); const response = await fetch(R2_UPLOAD_ENDPOINT, { method: 'POST', body: formData }); if (!response.ok) { throw new Error('업로드 서버 오류'); } const result = await response.json(); let finalUrl = ''; if (result && typeof result.url === 'string' && result.url.trim()) { finalUrl = result.url.trim(); } else if (result && typeof result.pathname === 'string' && result.pathname.trim()) { finalUrl = 'https://pub-fde151a124604f9ebb0e0fc5d36b88ea.r2.dev' + result.pathname.trim(); } else if (result && typeof result.key === 'string' && result.key.trim()) { finalUrl = 'https://pub-fde151a124604f9ebb0e0fc5d36b88ea.r2.dev/' + result.key.trim().replace(/^\/+/, ''); } if (!finalUrl) { throw new Error(result?.error || '업로드 실패'); } return finalUrl; } function formatDate(dateString) { const d = new Date(dateString + 'T00:00:00'); if (Number.isNaN(d.getTime())) return dateString; return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`; } function formatDateRange(startDate, endDate) { if (!startDate && !endDate) return ''; if (!endDate || startDate === endDate) return formatDate(startDate); return `${formatDate(startDate)} ~ ${formatDate(endDate)}`; } function getTodayString() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function sortPosts(posts) { const today = getTodayString(); return [...posts].sort((a, b) => { const aDate = a.date || '9999-12-31'; const bDate = b.date || '9999-12-31'; const aFuture = aDate >= today ? 0 : 1; const bFuture = bDate >= today ? 0 : 1; if (aFuture !== bFuture) return aFuture - bFuture; return new Date(aDate) - new Date(bDate); }); } function buildMedia(post) { if (post.mediaType === 'video') { return buildEventVideoHtml(post); } const imgUrl = normalizeMediaUrl(post.mediaUrl); if (!imgUrl) { return `
이미지 없음
`; } return `${escapeHtml(post.title)}`; } function currentFilteredPosts() { const typeValue = filterType.value; const keyword = searchInput.value.trim().toLowerCase(); let posts = sortPosts(getPosts()).filter(isValidPost); if (typeValue !== 'all') { posts = posts.filter((post) => post.mediaType === typeValue); } if (keyword) { posts = posts.filter((post) => (post.title || '').toLowerCase().includes(keyword) || (post.place || '').toLowerCase().includes(keyword) ); } return posts; } function getRenderablePosts(limit = MAX_VISIBLE_POSTS) { const sourcePosts = currentFilteredPosts(); return sourcePosts.slice(0, limit); } async function renderPosts() { eventGrid.innerHTML = '
불러오는 중...
'; const posts = getRenderablePosts(MAX_VISIBLE_POSTS); if (posts.length === 0) { eventGrid.innerHTML = '
표시할 문화소식이 없습니다.
'; paginationInfo.textContent = '표시할 문화소식이 없습니다.'; return; } paginationInfo.textContent = `가까운 날짜순 ${posts.length}개 노출`; eventGrid.innerHTML = posts.map((post) => `
${post.mediaType === 'video' ? '영상' : '이미지'}
${buildMedia(post)}
${formatDateRange(post.date, post.endDate)}
${escapeHtml(post.title)}
${escapeHtml(post.place || '')}
조회 ${post.views || 0} 좋아요 ${post.likes || 0} 댓글 ${post.comments?.length || 0}
`).join(''); } function renderNotices() { const notices = [...getNotices()].sort((a, b) => new Date(b.date) - new Date(a.date)); const makeNoticeHtml = (notice) => `
${escapeHtml(notice.title)} ${escapeHtml(notice.content)}
${formatDate(notice.date)}
`; const emptyHtml = '
등록된 공지가 없습니다.
'; noticeList.innerHTML = notices.length ? notices.map(makeNoticeHtml).join('') : emptyHtml; noticePreviewList.innerHTML = notices.length ? notices.slice(0, 3).map(makeNoticeHtml).join('') : emptyHtml; } function renderBoardList() { const posts = sortPosts(getPosts()); if (!posts.length) { boardPostList.innerHTML = '
  • 등록된 게시글이 없습니다.
  • '; return; } boardPostList.innerHTML = posts.map((post) => `
  • ${escapeHtml(post.title)} ${escapeHtml(post.place || '')} · ${escapeHtml(post.author || '')} ${formatDate(post.date)}
  • `).join(''); } function renderVideoList() { const postVideos = sortPosts(getPosts()).filter((post) => post.mediaType === 'video'); const customVideos = getVideoItems(); const merged = [ ...customVideos.map(item => ({ ...item, isCustomVideoItem: true })), ...postVideos.map(item => ({ ...item, isCustomVideoItem: false })) ]; if (merged.length === 0) { videoList.innerHTML = '
    등록된 영상이 없습니다.영상행사 등록 후 이곳에 표시됩니다.
    '; document.getElementById('videoPlayer').innerHTML = ''; return; } videoList.innerHTML = merged.map((item) => { if (item.isCustomVideoItem) { return `
    ${escapeHtml(item.title)} ${escapeHtml(item.description || '영상 설명 없음')}
    ${formatDate(item.date)}
    `; } return `
    ${escapeHtml(item.title)} ${escapeHtml(item.place || '')} · ${escapeHtml(item.author || '')}
    ${formatDate(item.date)}
    `; }).join(''); const latestCustom = customVideos[0]; if (latestCustom) { playCustomVideo(latestCustom.id, false); } } function playCustomVideo(id, switchToView = true) { const items = getVideoItems(); const target = items.find(item => item.id === id); if (!target) return; const iframe = `
    `; document.getElementById('videoPlayer').innerHTML = iframe; if (switchToView) { document.getElementById('videoViewPanel').style.display = 'block'; document.getElementById('videoWritePanel').style.display = 'none'; } } window.playCustomVideo = playCustomVideo; async function renderAll() { await renderPosts(); renderNotices(); renderBoardList(); renderVideoList(); } function renderPreview(file, mediaType) { if (!file) { uploadPreview.className = 'upload-preview'; uploadPreview.innerHTML = ''; return; } const url = URL.createObjectURL(file); uploadPreview.innerHTML = mediaType === 'video' ? `` : `업로드 미리보기`; uploadPreview.className = 'upload-preview show'; } function openModal(id) { document.getElementById(id)?.classList.add('show'); document.body.style.overflow = 'hidden'; if (id === 'noticeModal') showNoticeList(); if (id === 'boardModal') showBoardList(); } function closeModal(id) { document.getElementById(id)?.classList.remove('show'); if (!document.querySelector('.modal-backdrop.show')) { document.body.style.overflow = ''; } } function showNoticeList() { noticeListPanel.style.display = 'block'; noticeWritePanel.style.display = 'none'; noticeViewTab.className = 'submit'; noticeWriteTab.className = 'reset'; noticeForm.reset(); delete noticeForm.dataset.editId; } function showNoticeWrite() { if (!isAdminLoggedIn) { const password = prompt('관리자 비밀번호를 입력하세요'); if (password !== ADMIN_PASSWORD) { alert('관리자만 접근 가능합니다'); return; } setAdminMode(true); } noticeListPanel.style.display = 'none'; noticeWritePanel.style.display = 'block'; noticeViewTab.className = 'reset'; noticeWriteTab.className = 'submit'; } function showBoardList() { document.querySelectorAll('[data-board-tab]').forEach(btn => btn.classList.remove('active')); document.querySelector('[data-board-tab="list"]')?.classList.add('active'); document.getElementById('boardListTab')?.classList.add('active'); document.getElementById('boardWriteTab')?.classList.remove('active'); document.getElementById('boardWriteForm')?.reset(); } function showBoardWrite() { document.querySelectorAll('[data-board-tab]').forEach(btn => btn.classList.remove('active')); document.querySelector('[data-board-tab="write"]')?.classList.add('active'); document.getElementById('boardWriteTab')?.classList.add('active'); document.getElementById('boardListTab')?.classList.remove('active'); } function setAdminMode(enabled) { isAdminLoggedIn = enabled; document.body.classList.toggle('admin-mode', enabled); adminLoginBtn.textContent = enabled ? '관리자 로그아웃' : '관리자 로그인'; renderAll(); } function editNotice(id) { if (!isAdminLoggedIn) { alert('관리자만 수정 가능합니다'); return; } const notices = getNotices(); const target = notices.find((item) => item.id === id); if (!target) return; openModal('noticeModal'); showNoticeWrite(); document.getElementById('noticeTitle').value = target.title; document.getElementById('noticeContent').value = target.content; noticeForm.dataset.editId = id; } function deleteNotice(id) { if (!isAdminLoggedIn) { alert('관리자만 삭제 가능합니다'); return; } if (!confirm('이 공지를 삭제하시겠습니까?')) return; const notices = getNotices().filter((item) => item.id !== id); saveNotices(notices); renderNotices(); } function findPostById(id) { return getPosts().find((post) => String(post.id) === String(id)); } function addView(id) { const posts = getPosts(); const target = posts.find(p => String(p.id) === String(id)); if (!target) return null; target.views = (target.views || 0) + 1; savePosts(posts); return target; } function likePost(id) { const posts = getPosts(); const target = posts.find(p => String(p.id) === String(id)); if (!target) return null; target.likes = (target.likes || 0) + 1; savePosts(posts); return target; } function deletePost(id) { if (!isAdminLoggedIn) { alert('관리자만 삭제 가능합니다'); return; } if (!confirm('이 게시글을 삭제하시겠습니까?')) return; const posts = getPosts().filter(p => String(p.id) !== String(id)); savePosts(posts); renderAll(); closeModal('detailModal'); alert('삭제되었습니다.'); } function renderComments(post) { const list = document.getElementById('commentList'); if (!post.comments || post.comments.length === 0) { list.innerHTML = '
    댓글이 없습니다
    '; return; } list.innerHTML = post.comments.map(c => `
    ${escapeHtml(c.author)}
    ${escapeHtml(c.text)}
    ${formatDate(c.date)}
    `).join(''); } function openDetail(id) { const post = addView(id); if (!post) return; detailTitle.textContent = post.title; detailMeta.innerHTML = `
    행사일: ${formatDateRange(post.date, post.endDate)}
    장소: ${escapeHtml(post.place || '')}
    작성자: ${escapeHtml(post.author || '')}
    유형: ${post.mediaType === 'video' ? '영상행사' : '이미지행사'}
    조회수: ${post.views || 0} · 좋아요: ${post.likes || 0} · 댓글: ${post.comments?.length || 0}
    `; detailMedia.innerHTML = post.mediaType === 'video' ? (post.youtubeId ? `
    ` : ``) : `${escapeHtml(post.title)}`; detailDescription.textContent = post.description || ''; detailShareBtn.onclick = () => sharePost(id); detailLikeBtn.onclick = () => { const updated = likePost(id); if (!updated) return; openDetail(id); renderAll(); }; commentForm.dataset.postId = String(id); renderComments(post); openModal('detailModal'); renderAll(); } window.openDetail = openDetail; function sharePost(id) { const post = findPostById(id); if (!post) { alert('공유할 글을 찾지 못했습니다.'); return; } const shareText = `[정읍문화소식] ${post.title} | ${post.place || ''} | ${formatDate(post.date)}`; const shareUrl = `${window.location.origin}${window.location.pathname}#post-${post.id}`; const fullText = `${shareText}\n${shareUrl}`; if (navigator.share) { navigator.share({ title: post.title, text: shareText, url: shareUrl }).catch(() => { fallbackShare(fullText); }); return; } fallbackShare(fullText); } function fallbackShare(fullText) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(fullText).then(() => { alert('공유용 링크를 복사했습니다.'); }).catch(() => { window.prompt('아래 내용을 복사해 공유하세요.', fullText); }); } else { window.prompt('아래 내용을 복사해 공유하세요.', fullText); } } adminLoginBtn.addEventListener('click', () => { if (isAdminLoggedIn) { setAdminMode(false); alert('관리자 로그아웃 되었습니다.'); return; } const password = prompt('관리자 비밀번호를 입력하세요'); if (password === ADMIN_PASSWORD) { setAdminMode(true); alert('관리자 로그인 완료'); } else { alert('비밀번호가 올바르지 않습니다'); } }); noticeViewTab.addEventListener('click', showNoticeList); noticeWriteTab.addEventListener('click', showNoticeWrite); document.getElementById('boardWriteForm')?.addEventListener('submit', function(e) { e.preventDefault(); const title = document.getElementById('boardTitle').value.trim(); const content = document.getElementById('boardContent').value.trim(); if (!title || !content) { alert('제목과 내용을 입력해주세요.'); return; } const posts = getPosts(); posts.unshift({ id: String(Date.now()), title, date: new Date().toISOString().slice(0, 10), endDate: new Date().toISOString().slice(0, 10), place: '시민 문화이야기', author: '시민', description: content, mediaType: 'image', mediaUrl: 'https://images.unsplash.com/photo-1518998053901-5348d3961a04?auto=format&fit=crop&w=1200&q=80', likes: 0, views: 0, comments: [] }); savePosts(posts); this.reset(); renderAll(); showBoardList(); alert('문화이야기가 등록되었습니다.'); }); document.querySelectorAll('[data-open]').forEach((button) => { button.addEventListener('click', () => { document.querySelectorAll('.menu button').forEach((item) => item.classList.remove('active')); const openId = button.dataset.open; const linkedTop = document.querySelector(`.menu button[data-open="${openId}"]`); if (linkedTop) linkedTop.classList.add('active'); openModal(openId); }); }); document.addEventListener('click', (event) => { const tabBtn = event.target.closest('[data-board-tab]'); if (tabBtn) { const tab = tabBtn.getAttribute('data-board-tab'); if (tab === 'list') showBoardList(); if (tab === 'write') showBoardWrite(); return; } const detailBtn = event.target.closest('.detail-btn'); if (detailBtn) { openDetail(detailBtn.dataset.postId); return; } const shareBtn = event.target.closest('.share-btn'); if (shareBtn) { sharePost(shareBtn.dataset.postId); return; } const boardItem = event.target.closest('.board-item-click'); if (boardItem) { openDetail(boardItem.dataset.postId); return; } const deletePostBtn = event.target.closest('.delete-post-btn'); if (deletePostBtn) { deletePost(deletePostBtn.dataset.postId); } }); document.querySelectorAll('[data-close]').forEach((button) => { button.addEventListener('click', () => closeModal(button.dataset.close)); }); document.querySelectorAll('.modal-backdrop').forEach((backdrop) => { backdrop.addEventListener('click', (event) => { if (event.target === backdrop) { backdrop.classList.remove('show'); if (!document.querySelector('.modal-backdrop.show')) { document.body.style.overflow = ''; } } }); }); fileInput.addEventListener('change', (e) => { const file = e.target.files?.[0]; renderPreview(file, mediaTypeSelect.value); }); mediaTypeSelect.addEventListener('change', () => { const file = fileInput.files?.[0]; renderPreview(file, mediaTypeSelect.value); }); searchBtn.addEventListener('click', renderPosts); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); renderPosts(); } }); filterType.addEventListener('change', renderPosts); uploadForm.addEventListener('submit', async (e) => { e.preventDefault(); const file = fileInput.files[0]; if (!file) { alert('이미지 또는 영상을 선택해 주세요.'); return; } let imageUrl = ""; if (file) { imageUrl = await uploadToR2(file); if (!imageUrl) return; } const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const title = document.getElementById('title').value; const place = document.getElementById('place').value; const description = document.getElementById('description').value; const post = { id: Date.now(), title, place, description, startDate, endDate, mediaUrl: imageUrl, createdAt: new Date().toISOString() }; if (!startDate || !endDate) { alert('행사 기간을 입력해 주세요.'); return; } if (startDate > endDate) { alert('종료일이 시작일보다 빠를 수 없습니다.'); return; } let posts = JSON.parse(localStorage.getItem(POSTS_KEY) || '[]'); posts.unshift(post); localStorage.setItem(POSTS_KEY, JSON.stringify(posts)); renderPosts(); uploadForm.reset(); savePosts(posts); uploadForm.reset(); renderPreview(null, 'image'); mediaTypeSelect.value = 'image'; renderAll(); alert('등록되었습니다. 가까운 날짜순 문화소식에 반영됩니다.'); closeModal('uploadModal'); } catch (error) { console.error(error); alert('업로드 실패: ' + error.message); } finally { submitBtn.disabled = false; submitBtn.textContent = '게시글 등록'; } }); uploadForm.addEventListener('reset', () => { setTimeout(() => { renderPreview(null, 'image'); mediaTypeSelect.value = 'image'; }, 0); }); noticeForm.addEventListener('submit', (e) => { e.preventDefault(); if (!isAdminLoggedIn) { const password = prompt('공지 등록을 위해 관리자 비밀번호를 입력하세요'); if (password !== ADMIN_PASSWORD) { alert('관리자만 등록 가능합니다'); return; } setAdminMode(true); } const notices = getNotices(); const editId = noticeForm.dataset.editId; const payload = { id: editId || String(Date.now()), title: document.getElementById('noticeTitle').value.trim(), content: document.getElementById('noticeContent').value.trim(), date: new Date().toISOString().slice(0, 10) }; let next; if (editId) { next = notices.map((item) => item.id === editId ? payload : item); } else { next = [payload, ...notices]; } saveNotices(next); noticeForm.reset(); delete noticeForm.dataset.editId; renderNotices(); showNoticeList(); alert(editId ? '공지 수정 완료' : '공지 등록 완료'); }); commentForm.addEventListener('submit', (e) => { e.preventDefault(); const id = commentForm.dataset.postId; const posts = getPosts(); const target = posts.find(p => String(p.id) === String(id)); if (!target) return; if (!target.comments) target.comments = []; target.comments.unshift({ author: document.getElementById('commentAuthor').value.trim(), text: document.getElementById('commentText').value.trim(), date: new Date().toISOString().slice(0, 10) }); savePosts(posts); commentForm.reset(); renderComments(target); renderAll(); openDetail(id); }); function openPostFromHash() { const hash = window.location.hash || ''; if (!hash.startsWith('#post-')) return; const id = hash.replace('#post-', ''); const post = findPostById(id); if (post) openDetail(id); } function addVideo() { const titleEl = document.getElementById('videoTitle'); const urlEl = document.getElementById('youtubeUrl'); const descEl = document.getElementById('videoDesc'); const title = titleEl ? titleEl.value.trim() : ''; const url = urlEl ? urlEl.value.trim() : ''; const desc = descEl ? descEl.value.trim() : ''; if (!title || !url) { alert('영상 제목과 유튜브 링크를 입력해주세요.'); return; } const videoId = getYoutubeId(url); if (!videoId || videoId.length < 5) { alert('유튜브 링크를 다시 확인해주세요.'); return; } const items = getVideoItems(); items.unshift({ id: 'video-' + Date.now(), title, description: desc, youtubeId: videoId, date: new Date().toISOString().slice(0, 10) }); saveVideoItems(items); renderVideoList(); playCustomVideo(items[0].id); if (titleEl) titleEl.value = ''; if (urlEl) urlEl.value = ''; if (descEl) descEl.value = ''; document.getElementById('videoViewPanel').style.display = 'block'; document.getElementById('videoWritePanel').style.display = 'none'; alert('영상이 등록되었습니다.'); } window.addVideo = addVideo; window.editNotice = editNotice; window.deleteNotice = deleteNotice; const videoViewTab = document.getElementById('videoViewTab'); const videoWriteTabOnly = document.getElementById('videoWriteTab'); const videoViewPanel = document.getElementById('videoViewPanel'); const videoWritePanel = document.getElementById('videoWritePanel'); videoViewTab.addEventListener('click', () => { videoViewPanel.style.display = 'block'; videoWritePanel.style.display = 'none'; }); videoWriteTabOnly.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); videoViewPanel.style.display = 'none'; videoWritePanel.style.display = 'block'; }); renderAll(); openPostFromHash(); window.addEventListener('hashchange', openPostFromHash);