emotion-chatbot-app / src /templates /static /js /diary_logic.js
hfexample's picture
Deploy clean snapshot of the repository
e221c83
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();
});