Spaces:
Runtime error
Runtime error
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- Emotion Map --- | |
| const emotionMap = { | |
| '๊ธฐ์จ': { emoji: '๐', bgClass: 'bg-๊ธฐ์จ', itemClass: 'item-๊ธฐ์จ' }, | |
| '์ฌํ': { emoji: '๐ข', bgClass: 'bg-์ฌํ', itemClass: 'item-์ฌํ' }, | |
| '๋ถ๋ ธ': { emoji: '๐ ', bgClass: 'bg-๋ถ๋ ธ', itemClass: 'item-๋ถ๋ ธ' }, | |
| '๋ถ์': { emoji: '๐', bgClass: 'bg-๋ถ์', itemClass: 'item-๋ถ์' }, | |
| '๋นํฉ': { emoji: '๐ฎ', bgClass: 'bg-๋นํฉ', itemClass: 'item-๋นํฉ' }, | |
| '์์ฒ': { emoji: '๐', bgClass: 'bg-์์ฒ', itemClass: 'item-์์ฒ' }, | |
| 'default': { emoji: '๐ค', bgClass: 'bg-default', itemClass: 'item-default' } | |
| }; | |
| // --- DOM Elements --- | |
| const currentYearEl = document.getElementById('current-year'); | |
| const prevYearBtn = document.getElementById('prev-year'); | |
| const nextYearBtn = document.getElementById('next-year'); | |
| const monthList = document.querySelector('.month-list'); | |
| const calendarMonthTitle = document.getElementById('calendar-month-title'); | |
| const diaryListContainer = document.getElementById('diary-list-container'); | |
| console.log("diaryListContainer element:", diaryListContainer); // ์์ ํ์ธ ๋ก๊ทธ | |
| const recModalOverlay = document.getElementById('rec-modal-overlay'); | |
| const recModalTitle = document.getElementById('rec-modal-title'); | |
| const recModalBody = document.getElementById('rec-modal-body'); | |
| const recModalCloseBtn = document.getElementById('rec-modal-close'); | |
| // --- State --- | |
| let diaryDataByDate = {}; | |
| let currentYear, currentMonth; | |
| let fp; // flatpickr instance | |
| let lastFetchedYear = null; // ์๋ณ ์นด์ดํธ๋ฅผ ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์ฐ๋ | |
| // --- Functions --- | |
| async function updateMonthlyCounts(year) { | |
| if (year === lastFetchedYear) return; // ์ด๋ฏธ ํด๋น ์ฐ๋์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ๋ฉด ์คํ ์ํจ | |
| try { | |
| const response = await fetch(`/api/diaries/counts?year=${year}`); | |
| if (!response.ok) throw new Error('Failed to load diary counts.'); | |
| const counts = await response.json(); | |
| document.querySelectorAll('.month-item').forEach(item => { | |
| const month_key = (parseInt(item.dataset.month) + 1).toString(); // ์ ๋ฒํธ๋ฅผ ๋ฌธ์์ด๋ก ๋ณํ | |
| const countSpan = item.querySelector('.diary-count'); | |
| const count = counts[month_key] || 0; // ๋ฌธ์์ด ํค๋ก ์ ๊ทผ | |
| if (count > 0) { | |
| countSpan.textContent = count; | |
| } else { | |
| countSpan.textContent = ''; | |
| } | |
| }); | |
| lastFetchedYear = year; // ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์ฐ๋ ๊ธฐ๋ก | |
| } catch (error) { | |
| console.error("Error fetching diary counts:", error); | |
| } | |
| } | |
| async function fetchDiaries(year, month) { | |
| try { | |
| console.log(`Fetching diaries for year: ${year}, month: ${month}`); | |
| const response = await fetch(`/api/diaries?year=${year}&month=${month}`); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Diary data failed to load. Status: ${response.status}, Message: ${errorText}`); | |
| } | |
| const diaries = await response.json(); | |
| console.log("Received diaries:", diaries); | |
| diaryDataByDate = {}; | |
| diaries.forEach(diary => { | |
| // Ensure diary.date is valid before assignment | |
| if (diary.date) { | |
| diaryDataByDate[diary.date] = diaryDataByDate[diary.date] || []; | |
| diaryDataByDate[diary.date].push(diary); | |
| } else { | |
| console.warn("Diary item with missing date:", diary); | |
| } | |
| }); | |
| console.log("Processed diaryDataByDate:", diaryDataByDate); | |
| return diaries; | |
| } catch (error) { | |
| console.error("Error in fetchDiaries:", error); | |
| // display a user-friendly error message on the UI | |
| diaryListContainer.innerHTML = `<div class="placeholder"><p>์ผ๊ธฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p><p style="font-size: 0.8em; color: #666;">${error.message}</p></div>`; | |
| return []; | |
| } | |
| } | |
| function renderTimeline(dateStr) { | |
| const diaries = diaryDataByDate[dateStr] || []; | |
| diaryListContainer.innerHTML = ''; | |
| if (diaries.length === 0) { | |
| diaryListContainer.innerHTML = '<div class="placeholder"><p>์์ฑ๋ ์ผ๊ธฐ๊ฐ ์์ต๋๋ค.</p></div>'; | |
| return; | |
| } | |
| diaries.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); | |
| diaries.forEach(diary => { | |
| const emotionInfo = emotionMap[diary.emotion] || emotionMap.default; | |
| const item = document.createElement('div'); | |
| item.className = `timeline-item ${emotionInfo.itemClass}`; | |
| item.dataset.diary = JSON.stringify(diary); // ์ ์ฒด diary ๊ฐ์ฒด ์ ์ฅ | |
| const time = new Date(diary.createdAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }); | |
| item.innerHTML = ` | |
| <div class="item-header"> | |
| <span class="item-time">${time}</span> | |
| <div class="item-controls"> | |
| <span class="item-emotion">${emotionInfo.emoji}</span> | |
| <button class="delete-diary-btn" data-diary-id="${diary.id}">์ญ์ </button> | |
| </div> | |
| </div> | |
| <div class="item-content"> | |
| <p>${diary.content.replace(/\n/g, '<br>')}</p> | |
| </div> | |
| `; | |
| diaryListContainer.appendChild(item); | |
| }); | |
| } | |
| function updateUI(year, month) { // month is 0-indexed | |
| currentYear = year; | |
| currentMonth = month; | |
| currentYearEl.textContent = year; | |
| calendarMonthTitle.textContent = new Date(year, month).toLocaleString('en-US', { month: 'long' }); | |
| document.querySelectorAll('.month-item').forEach(item => { | |
| item.classList.toggle('active', parseInt(item.dataset.month) === month); | |
| }); | |
| } | |
| async function handleDateChange(year, month) { // month is 0-indexed | |
| updateUI(year, month); | |
| await updateMonthlyCounts(year); // ์ฐ๋๊ฐ ๋ฐ๋ ๋๋ง๋ค ์นด์ดํธ ์ ๋ฐ์ดํธ | |
| await fetchDiaries(year, month + 1); | |
| if (fp) fp.redraw(); | |
| } | |
| const parseRecs = (text) => { | |
| const contents = { ์์ฉ: '', ์ ํ: '' }; | |
| if (!text) return contents; | |
| const regex = /#+\s*\[\s*(์์ฉ|๊ณต๊ฐ|์ ํ|ํ๊ธฐ)\s*\]([\s\S]*?)(?=(?:#+\s*\[\s*(?:์์ฉ|๊ณต๊ฐ|์ ํ|ํ๊ธฐ)\s*\])|$)/gi; | |
| let match; | |
| while ((match = regex.exec(text)) !== null) { | |
| const type = match[1].trim(); | |
| let content = match[2].trim(); | |
| if (type === '์์ฉ' || type === '๊ณต๊ฐ') contents.์์ฉ = content; | |
| else if (type === '์ ํ' || type === 'ํ๊ธฐ') contents.์ ํ = content; | |
| } | |
| return contents; | |
| }; | |
| const parseAndClean = (markdown) => { | |
| if (!markdown) return '<p class="empty-msg">์ถ์ฒ ํญ๋ชฉ์ด ์์ต๋๋ค.</p>'; | |
| const rawHtml = marked.parse(markdown); | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = rawHtml; | |
| // ๋ฐฉ์ 1: "์ถ์ฒ ์ด์ "๊ฐ ์ด ํค๋์ธ ๊ฒฝ์ฐ ํด๋น ์ด ์ ์ฒด ์ ๊ฑฐ | |
| const tables = tempDiv.querySelectorAll('table'); | |
| tables.forEach(table => { | |
| let reasonColumnIndex = -1; | |
| table.querySelectorAll('th').forEach((th, index) => { | |
| if (th.textContent.trim() === '์ถ์ฒ ์ด์ ') { | |
| reasonColumnIndex = index; | |
| } | |
| }); | |
| if (reasonColumnIndex !== -1) { | |
| table.querySelectorAll('tr').forEach(row => { | |
| if (row.cells[reasonColumnIndex]) { | |
| row.deleteCell(reasonColumnIndex); | |
| } | |
| }); | |
| } | |
| }); | |
| // ๋ฐฉ์ 2: "์ถ์ฒ ์ด์ :" ํ ์คํธ๊ฐ ํฌํจ๋ ํ ์ ๊ฑฐ | |
| const rowsToRemove = []; | |
| tempDiv.querySelectorAll('td').forEach(td => { | |
| if (td.textContent.includes('์ถ์ฒ ์ด์ :')) { | |
| const row = td.closest('tr'); | |
| if (row) rowsToRemove.push(row); | |
| } | |
| }); | |
| rowsToRemove.forEach(row => row.remove()); | |
| // ์นดํ ๊ณ ๋ฆฌ ํ ์คํธ("์ํ", "์์ ", "๋์")๋ฅผ ์ด๋ชจ์ง๋ก ๋ณ๊ฒฝ (์ฒซ ๋ฒ์งธ ์ด๋ง) | |
| const categoryEmojiMap = { '์ํ': '๐ฌ', '์์ ': '๐ต', '๋์': '๐' }; | |
| tempDiv.querySelectorAll('tr').forEach(row => { | |
| // ํค๋ ํ์ด ์๋๊ณ , ์ ์ด ์กด์ฌํ ๊ฒฝ์ฐ | |
| if (row.cells.length > 0 && row.cells[0].tagName === 'TD') { | |
| const firstCell = row.cells[0]; | |
| let cellHtml = firstCell.innerHTML; | |
| for (const category in categoryEmojiMap) { | |
| const regex = new RegExp(`(<strong>)?${category}(</strong>)?`, "g"); | |
| cellHtml = cellHtml.replace(regex, categoryEmojiMap[category]); | |
| } | |
| firstCell.innerHTML = cellHtml; | |
| } | |
| }); | |
| return tempDiv.innerHTML; | |
| }; | |
| // --- Event Listeners --- | |
| const detailModalOverlay = document.getElementById('diary-detail-modal-overlay'); | |
| const detailModalCloseBtn = document.getElementById('diary-detail-modal-close'); | |
| monthList.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('month-item')) { | |
| const month = parseInt(e.target.dataset.month); | |
| if (month !== currentMonth) fp.changeMonth(month - currentMonth); | |
| } | |
| }); | |
| prevYearBtn.addEventListener('click', () => fp.changeYear(fp.currentYear - 1)); | |
| nextYearBtn.addEventListener('click', () => fp.changeYear(fp.currentYear + 1)); | |
| diaryListContainer.addEventListener('click', async (e) => { | |
| // ์ญ์ ๋ฒํผ ๋ก์ง | |
| if (e.target.classList.contains('delete-diary-btn')) { | |
| e.stopPropagation(); // ์ด๋ฒคํธ ๋ฒ๋ธ๋ง ๋ฐฉ์ง | |
| const diaryId = e.target.dataset.diaryId; | |
| if (!diaryId || !confirm('์ ๋ง๋ก ์ด ์ผ๊ธฐ๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?')) { | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/diary/delete/${diaryId}`, { | |
| method: 'DELETE', | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || '์ญ์ ์ ์คํจํ์ต๋๋ค.'); | |
| } | |
| // ์ญ์ ์ฑ๊ณต ํ UI ์ ๋ฐ์ดํธ | |
| const selectedDate = fp.selectedDates[0]; | |
| await handleDateChange(selectedDate.getFullYear(), selectedDate.getMonth()); | |
| renderTimeline(flatpickr.formatDate(selectedDate, "Y-m-d")); | |
| } catch (error) { | |
| console.error('์ญ์ ์ค ์ค๋ฅ ๋ฐ์:', error); | |
| alert(error.message); | |
| } | |
| return; | |
| } | |
| // ์์ธ ๋ชจ๋ฌ ๋ก์ง | |
| const timelineItem = e.target.closest('.timeline-item'); | |
| if (timelineItem && timelineItem.dataset.diary) { | |
| try { | |
| const diary = JSON.parse(timelineItem.dataset.diary); | |
| openDiaryDetailModal(diary); | |
| } catch (jsonError) { | |
| console.error("Failed to parse diary data from dataset:", jsonError); | |
| } | |
| } | |
| }); | |
| function openDiaryDetailModal(diary) { | |
| const modalTitle = document.getElementById('diary-detail-title'); | |
| const modalBody = document.getElementById('diary-detail-body'); | |
| modalTitle.innerHTML = ''; // ์ ๋ชฉ ์ ๊ฑฐ | |
| let bodyHtml = ` | |
| <div class="diary-content-section"> | |
| <h3>๋์ ๊ธฐ๋ก</h3> | |
| <p>${diary.content.replace(/\n/g, '<br>')}</p> | |
| </div> | |
| `; | |
| if (diary.recommendation) { | |
| const sections = parseRecs(diary.recommendation); | |
| if (sections.์์ฉ) { | |
| bodyHtml += ` | |
| <div class="diary-content-section"> | |
| <h3>์์ฉ</h3> | |
| ${parseAndClean(sections.์์ฉ)} | |
| </div> | |
| `; | |
| } | |
| if (sections.์ ํ) { | |
| bodyHtml += ` | |
| <div class="diary-content-section"> | |
| <h3>์ ํ</h3> | |
| ${parseAndClean(sections.์ ํ)} | |
| </div> | |
| `; | |
| } | |
| } | |
| modalBody.innerHTML = bodyHtml; | |
| detailModalOverlay.style.display = 'flex'; | |
| } | |
| function closeDiaryDetailModal() { | |
| detailModalOverlay.style.display = 'none'; | |
| } | |
| detailModalCloseBtn.addEventListener('click', closeDiaryDetailModal); | |
| detailModalOverlay.addEventListener('click', (e) => { | |
| if (e.target === detailModalOverlay) { | |
| closeDiaryDetailModal(); | |
| } | |
| }); | |
| function initializeCalendar() { | |
| fp = flatpickr("#calendar", { | |
| inline: true, | |
| dateFormat: "Y-m-d", | |
| locale: "en", | |
| onReady: async (selectedDates, dateStr, instance) => { | |
| const today = new Date(); | |
| await handleDateChange(today.getFullYear(), today.getMonth()); | |
| instance.setDate(today, true); | |
| }, | |
| onChange: (selectedDates, dateStr, instance) => { | |
| if (selectedDates.length > 0) renderTimeline(dateStr); | |
| }, | |
| onMonthChange: async (selectedDates, dateStr, instance) => { | |
| await handleDateChange(instance.currentYear, instance.currentMonth); | |
| }, | |
| onYearChange: async (selectedDates, dateStr, instance) => { | |
| await handleDateChange(instance.currentYear, instance.currentMonth); | |
| }, | |
| onDayCreate: (dObj, dStr, fp, dayElem) => { | |
| // ๋ ์ง ์ซ์๋ฅผ span์ผ๋ก ๊ฐ์ธ์ z-index ์ ์ด | |
| dayElem.innerHTML = `<span class="flatpickr-day-num">${dayElem.innerHTML}</span>`; | |
| const date = flatpickr.formatDate(dayElem.dateObj, "Y-m-d"); | |
| const diariesForDay = diaryDataByDate[date]; | |
| if (diariesForDay && diariesForDay.length > 0) { | |
| const latestDiary = diariesForDay[diariesForDay.length - 1]; | |
| const emotionInfo = emotionMap[latestDiary.emotion] || emotionMap.default; | |
| // ๋ ์ง ์ ์ ์ง์ ๋ฐฐ๊ฒฝ์ ํด๋์ค๋ฅผ ์ถ๊ฐ (๊ฐ์์์ ::before๊ฐ ์ด ํด๋์ค๋ฅผ ์ฌ์ฉ) | |
| dayElem.classList.add('has-diary', emotionInfo.bgClass); | |
| } | |
| } | |
| }); | |
| } | |
| // --- Initial Load --- | |
| initializeCalendar(); | |
| }); | |