Skip to content

Instantly share code, notes, and snippets.

@lunamoth
Last active May 12, 2025 01:20
Show Gist options
  • Save lunamoth/0caf6f60c0fa1974de9f0172e5375263 to your computer and use it in GitHub Desktop.
Save lunamoth/0caf6f60c0fa1974de9f0172e5375263 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌟 BeOS 스타일 나만의 일기장</title>
<style>
:root {
--beos-bg: #D4D0C8;
--beos-window-bg: #C0C0C0;
--beos-title-bar: #FFD700;
--beos-text: #000000;
--beos-border-light: #FFFFFF;
--beos-border-dark: #707070;
--beos-button-bg: #C8C8C8;
--beos-button-hover-bg: #B8B8B8;
--beos-input-bg: #FFFFFF;
--beos-tab-text: #000000;
--beos-shadow: rgba(0, 0, 0, 0.35);
--beos-scrollbar-track: #D0D0D0;
--beos-scrollbar-thumb: #A0A0A0;
--scrollbar-width: 16px;
--container-padding: 10px;
--window-content-base-padding: 8px;
}
html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; }
body {
font-family: "Tahoma", "Geneva", "Verdana", sans-serif;
background-color: var(--beos-bg); color: var(--beos-text);
display: flex; flex-direction: column;
}
.beos-menubar-placeholder {
width: 100%; height: 20px; background-color: var(--beos-window-bg);
border-bottom: 1px solid var(--beos-border-dark); flex-shrink: 0;
display: flex; align-items: center; padding: 0 8px;
font-size: 0.8em; color: var(--beos-text); box-sizing: border-box;
}
.beos-menubar-placeholder span { margin-right: 15px; cursor: default; }
.page-content-wrapper {
width: 100%; flex-grow: 1; padding: var(--container-padding);
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.beos-button {
background-color: var(--beos-button-bg); color: var(--beos-text); border: 1px solid;
border-color: var(--beos-border-light) var(--beos-border-dark) var(--beos-border-dark) var(--beos-border-light);
padding: 5px 10px; font-weight: normal; cursor: pointer; transition: background-color 0.1s, box-shadow 0.1s;
box-shadow: 1px 1px 0 var(--beos-border-dark), 1px 1px 0 1px var(--beos-shadow);
font-size: 0.85em; border-radius: 2px; text-shadow: 1px 1px 0 var(--beos-border-light);
}
.beos-button:hover { background-color: var(--beos-button-hover-bg); }
.beos-button:active {
border-color: var(--beos-border-dark) var(--beos-border-light) var(--beos-border-light) var(--beos-border-dark);
box-shadow: inset 1px 1px 0 var(--beos-border-dark); transform: translate(1px, 1px); text-shadow: none;
}
.draggable-beos-window {
position: absolute;
background-color: var(--beos-window-bg);
border: 1px solid;
border-color: var(--beos-border-light) var(--beos-border-dark) var(--beos-border-dark) var(--beos-border-light);
box-shadow: 2px 2px 3px var(--beos-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 180px;
min-height: 80px;
resize: both;
border-radius: 0; box-sizing: border-box;
}
.draggable-title-bar {
background: linear-gradient(to right, var(--beos-title-bar) 0%, #FFEE77 70%, var(--beos-title-bar) 100%);
color: var(--beos-tab-text); padding: 0 6px 0 8px; font-weight: bold;
border-bottom: 1px solid var(--beos-border-dark); display: flex; justify-content: space-between;
align-items: center; flex-shrink: 0; font-size: 0.9em; height: 22px; box-sizing: border-box;
cursor: move;
user-select: none;
}
.draggable-title-bar > .title-bar-text { flex-grow: 1; display: flex; align-items: center; }
.title-bar-icon { margin-right: 5px; font-size: 1em; line-height: 1; }
.window-controls { display: flex; }
.title-bar-button-pseudo {
width: 14px; height: 14px; border: 1px solid;
border-color: var(--beos-border-light) var(--beos-border-dark) var(--beos-border-dark) var(--beos-border-light);
background-color: var(--beos-button-bg); margin-left: 3px;
display: flex; align-items: center; justify-content: center;
box-shadow: 1px 1px 0px var(--beos-border-dark);
}
.title-bar-button-pseudo:active {
border-color: var(--beos-border-dark) var(--beos-border-light) var(--beos-border-light) var(--beos-border-dark);
box-shadow: inset 1px 1px 0px var(--beos-border-dark);
}
.close-btn-symbol::before { content: "X"; font-size: 8px; font-weight: bold; color: var(--beos-text); }
.beos-window-content-area {
padding: var(--window-content-base-padding);
flex-grow: 1;
display: flex;
flex-direction: column;
background-color: var(--beos-input-bg);
box-sizing: border-box;
overflow: hidden;
min-height: 0;
}
#calendarWindow .beos-window-content-area { padding: 5px; background-color: var(--beos-window-bg); overflow-y: auto; }
#calendarContainer { text-align: center; font-size: 0.85em; user-select: none; }
#calendarHeader { margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; }
#calendarHeader button { padding: 2px 5px; font-size: 0.9em;}
#currentMonthYear { font-weight: bold; }
#calendarTable { width: 100%; border-collapse: collapse; table-layout: fixed; }
#calendarTable th, #calendarTable td { border: 1px solid var(--beos-border-dark); padding: 3px 0; text-align: center; height: 25px; box-sizing: border-box; }
#calendarTable th { background-color: var(--beos-button-bg); font-weight: normal; }
#calendarTable td { cursor: pointer; }
#calendarTable td:hover { background-color: var(--beos-button-hover-bg); }
#calendarTable td.today { background-color: var(--beos-title-bar) !important; font-weight: bold; color: var(--beos-text) !important; }
#calendarTable td.selected-date { background-color: var(--beos-text) !important; color: var(--beos-border-light) !important; border: 1px solid var(--beos-title-bar); }
#calendarTable td.has-diary { font-weight: bold; position: relative; }
#calendarTable td.has-diary::after { content: '●'; font-size: 0.5em; color: var(--beos-title-bar); position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%); }
#calendarTable td.other-month { color: #999; }
#formWindow .beos-window-content-area { padding: 6px; overflow: hidden; }
#diaryForm { display: flex; flex-direction: column; flex-grow: 1; min-height: 0; }
.form-group { display: flex; flex-direction: column; }
.form-group label { display: block; margin-bottom: 2px; font-weight: normal; font-size: 0.8em; color: #333; flex-shrink: 0; }
.form-group-title { flex-shrink: 0; margin-bottom: 5px; }
.form-group-content { flex-grow: 1; flex-shrink: 1; flex-basis: 0; min-height: 0; display: flex; flex-direction: column; margin-bottom: 5px; }
#diaryTitle { border: 1px inset var(--beos-border-dark); padding: 5px; box-sizing: border-box; width: 100%; background-color: var(--beos-input-bg); font-size: 0.9em; }
#diaryContent { flex-grow: 1; resize: none; border: 1px inset var(--beos-border-dark); padding: 5px; font-family: "Monaco", "Courier New", monospace; font-size: 0.95em; line-height: 1.4; box-sizing: border-box; background-color: var(--beos-input-bg); width: 100%; min-height: 40px; }
.form-buttons { flex-shrink: 0; display: flex; flex-direction: row; justify-content: flex-end; gap: 8px; margin-top: auto; padding-top: 4px; }
#listWindow .beos-window-content-area { overflow-y: auto; padding-right: calc(var(--window-content-base-padding) + var(--scrollbar-width) - 2px); }
#diaryEntriesList { width: 100%; box-sizing: border-box; }
.diary-entry { background-color: var(--beos-input-bg); border: 1px solid var(--beos-border-dark); padding: 8px 10px; margin-bottom: 8px; box-shadow: 1px 1px 1px rgba(0,0,0,0.1); border-radius: 0; box-sizing: border-box; width: 100%; }
.diary-title { font-size: 1.1em; color: var(--beos-text); margin: 0 0 2px 0; font-weight: bold; }
.diary-date-display { font-size: 0.7em; color: #555; margin-bottom: 6px; }
.diary-content { line-height: 1.4; white-space: pre-wrap; font-size: 0.9em; font-family: "Monaco", "Courier New", monospace; word-break: break-word; }
.diary-actions { float: right; margin-left: 10px; }
.diary-actions .beos-button { margin-left: 5px; padding: 2px 5px; font-size: 0.75em; }
#diaryEntriesList:empty::before { content: "✨ 오늘 하루는 어땠나요? 여기에 비밀 일기를 남겨보세요! ✍️\A 아니면 달력에서 다른 날짜의 일기를 찾아보세요! 📅"; white-space: pre-line; display: block; text-align: center; padding: 15px; color: #666; font-style: italic; font-size: 0.9em; }
#backupWindow .beos-window-content-area { display: flex; align-items: center; justify-content: center; padding: 10px; }
#performBackupButton { width: 100%; padding: 8px 10px; font-size: 0.9em; }
#clockWindow .beos-window-content-area {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
background-color: var(--beos-window-bg);
}
#analogClockCanvas {
max-width: 100%;
max-height: 100%;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.popup-notification { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--beos-window-bg); color: var(--beos-text); padding: 15px 25px; border: 1px solid; border-color: var(--beos-border-light) var(--beos-border-dark) var(--beos-border-dark) var(--beos-border-light); box-shadow: 3px 3px 5px var(--beos-shadow); font-size: 0.9em; z-index: 10000; opacity: 0; visibility: hidden; transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out, top 0.3s ease-in-out; min-width: 200px; text-align: center; }
.popup-notification.show { opacity: 1; visibility: visible; top: calc(50% - 20px); }
</style>
</head>
<body>
<div class="beos-menubar-placeholder">
<span>파일(F)</span> <span>편집(E)</span> <span>보기(V)</span> <span>일기(D)</span> <span>도움말(H)</span>
</div>
<div class="page-content-wrapper">
<div id="calendarWindow" class="draggable-beos-window">
<div class="draggable-title-bar"> <span class="title-bar-text"><span class="title-bar-icon">📅</span>달력</span> <div class="window-controls"> <div class="title-bar-button-pseudo"></div> <div class="title-bar-button-pseudo"></div> <div class="title-bar-button-pseudo close-btn-symbol"></div> </div> </div>
<div class="beos-window-content-area"> <div id="calendarContainer"> <div id="calendarHeader"> <button id="prevMonthBtn" class="beos-button">&lt;</button> <span id="currentMonthYear"></span> <button id="nextMonthBtn" class="beos-button">&gt;</button> </div> <table id="calendarTable"> <thead><tr><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead> <tbody id="calendarBody"></tbody> </table> </div> </div>
</div>
<div id="formWindow" class="draggable-beos-window">
<div class="draggable-title-bar"> <span class="title-bar-text" id="formTitle"><span class="title-bar-icon">✏️</span>새 일기 쓰기</span> <div class="window-controls"> <div class="title-bar-button-pseudo close-btn-symbol"></div> </div> </div>
<div class="beos-window-content-area"> <form id="diaryForm"> <input type="hidden" id="editingDiaryIndex" value="-1"> <div class="form-group form-group-title"> <label for="diaryTitle">제목:</label> <input type="text" id="diaryTitle" name="diaryTitle" required> </div> <div class="form-group form-group-content"> <label for="diaryContent">오늘의 이야기:</label> <textarea id="diaryContent" name="diaryContent" required placeholder="오늘 어떤 특별한 일이 있었나요? ✨"></textarea> </div> <div class="form-buttons"> <button type="button" id="cancelEditBtn" class="beos-button" style="display:none;">❌ 취소</button> <button type="submit" id="submitDiaryBtn" class="beos-button">💖 기록하기!</button> </div> </form> </div>
</div>
<div id="listWindow" class="draggable-beos-window">
<div class="draggable-title-bar"> <span class="title-bar-text"><span class="title-bar-icon">📚</span><span id="diaryListTitle">오늘의 일기</span></span> <div class="window-controls"> <div class="title-bar-button-pseudo close-btn-symbol"></div> </div> </div>
<div class="beos-window-content-area"> <div id="diaryEntriesList"></div> </div>
</div>
<div id="backupWindow" class="draggable-beos-window">
<div class="draggable-title-bar"> <span class="title-bar-text"><span class="title-bar-icon">💾</span>백업 도구</span> <div class="window-controls"> <div class="title-bar-button-pseudo close-btn-symbol"></div> </div> </div>
<div class="beos-window-content-area"> <button id="performBackupButton" class="beos-button">TXT 파일로 백업 실행</button> </div>
</div>
<div id="clockWindow" class="draggable-beos-window">
<div class="draggable-title-bar">
<span class="title-bar-text"><span class="title-bar-icon">🕒</span>시계</span>
<div class="window-controls">
<div class="title-bar-button-pseudo close-btn-symbol"></div>
</div>
</div>
<div class="beos-window-content-area">
<canvas id="analogClockCanvas"></canvas>
</div>
</div>
</div>
<div id="popupNotification" class="popup-notification"></div>
<script>
const diaryForm = document.getElementById('diaryForm');
const diaryEntriesList = document.getElementById('diaryEntriesList');
const diaryTitleInput = document.getElementById('diaryTitle');
const diaryContentInput = document.getElementById('diaryContent');
const performBackupButton = document.getElementById('performBackupButton');
const calendarBody = document.getElementById('calendarBody');
const currentMonthYearSpan = document.getElementById('currentMonthYear');
const prevMonthBtn = document.getElementById('prevMonthBtn');
const nextMonthBtn = document.getElementById('nextMonthBtn');
const popupNotification = document.getElementById('popupNotification');
const editingDiaryIndexInput = document.getElementById('editingDiaryIndex');
const submitDiaryBtn = document.getElementById('submitDiaryBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const formTitle = document.getElementById('formTitle');
const diaryListTitle = document.getElementById('diaryListTitle');
const DIARY_LOCAL_STORAGE_KEY = 'beosSecretDiary_Final_v3_draggableWindows_Data';
const WINDOW_STATES_KEY = 'beosDiaryWindowStates_v6_noMediaPlayer';
let currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth();
let selectedDate = null;
let popupTimeout = null;
let highestZ = 1;
const windowElements = {};
// Clock variables
let clockCanvas, clockCtx, clockRadius;
let clockUpdateInterval;
function getWindowStates() {
const states = localStorage.getItem(WINDOW_STATES_KEY);
return states ? JSON.parse(states) : {};
}
function saveWindowState(windowId, newPartialState) {
const states = getWindowStates();
states[windowId] = { ...(states[windowId] || {}), ...newPartialState };
localStorage.setItem(WINDOW_STATES_KEY, JSON.stringify(states));
}
function loadWindowStates() {
const states = getWindowStates();
let maxZ = 0;
Object.keys(states).forEach(windowId => {
const el = windowElements[windowId];
if (!el) { console.warn(`Element with ID ${windowId} not found for loading state.`); return; }
const state = states[windowId];
if (state) {
if (state.top) el.style.top = state.top;
if (state.left) el.style.left = state.left;
if (state.width) el.style.width = state.width;
if (state.height) el.style.height = state.height;
if (state.zIndex) {
const z = parseInt(state.zIndex, 10);
el.style.zIndex = z;
if (z > maxZ) maxZ = z;
}
}
});
highestZ = maxZ || 1;
}
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const windowEl = entry.target;
if (windowEl.offsetParent === null) continue;
saveWindowState(windowEl.id, {
width: windowEl.style.width,
height: windowEl.style.height
});
if (windowEl.id === 'clockWindow') {
if (!clockCanvas) setupClockCanvas();
if (clockCanvas) drawClock();
}
}
});
function makeDraggable(elmnt) {
if (!elmnt) return;
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const titleBar = elmnt.querySelector(".draggable-title-bar");
const parentContainer = document.querySelector(".page-content-wrapper");
if (titleBar) { titleBar.onmousedown = dragMouseDown; }
elmnt.onmousedown = bringToFront;
function bringToFront() {
highestZ++;
elmnt.style.zIndex = highestZ;
saveWindowState(elmnt.id, { zIndex: highestZ.toString() });
}
function dragMouseDown(e) {
e = e || window.event; e.preventDefault(); bringToFront();
pos3 = e.clientX; pos4 = e.clientY;
document.onmouseup = closeDragElement; document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event; e.preventDefault();
pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
pos3 = e.clientX; pos4 = e.clientY;
let newTop = elmnt.offsetTop - pos2; let newLeft = elmnt.offsetLeft - pos1;
if (parentContainer) {
if (newTop < 0) newTop = 0; if (newLeft < 0) newLeft = 0;
let maxTop = parentContainer.clientHeight - elmnt.offsetHeight;
let maxLeft = parentContainer.clientWidth - elmnt.offsetWidth;
if (newTop > maxTop) newTop = maxTop > 0 ? maxTop : 0;
if (newLeft > maxLeft) newLeft = maxLeft > 0 ? maxLeft : 0;
}
elmnt.style.top = newTop + "px"; elmnt.style.left = newLeft + "px";
}
function closeDragElement() {
document.onmouseup = null; document.onmousemove = null;
saveWindowState(elmnt.id, { top: elmnt.style.top, left: elmnt.style.left });
}
resizeObserver.observe(elmnt);
}
function showPopup(message, duration = 2200) { if (popupTimeout) clearTimeout(popupTimeout); popupNotification.textContent = message; popupNotification.classList.add('show'); popupTimeout = setTimeout(() => { popupNotification.classList.remove('show'); }, duration); }
function getAllDiaryEntries() { return JSON.parse(localStorage.getItem(DIARY_LOCAL_STORAGE_KEY)) || []; }
function saveDiaryEntries(entries) { localStorage.setItem(DIARY_LOCAL_STORAGE_KEY, JSON.stringify(entries)); }
function formatDateForStorage(dateObj) { return `${dateObj.getFullYear()}-${(dateObj.getMonth() + 1).toString().padStart(2, '0')}-${dateObj.getDate().toString().padStart(2, '0')}`; }
function formatDateForDisplay(dateString) { if (!dateString) return ''; const [y, m, d] = dateString.split('-'); return new Date(y, m - 1, d).toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }); }
function renderDiaryEntries(dateFilter = null) {
diaryEntriesList.innerHTML = ''; let allEntries = getAllDiaryEntries(); let entriesToDisplay;
const listTitleIconEl = diaryListTitle.parentElement.querySelector('.title-bar-icon');
if (dateFilter) {
entriesToDisplay = allEntries.filter(entry => entry.entryDate === dateFilter);
if(listTitleIconEl) listTitleIconEl.textContent = '📖';
diaryListTitle.textContent = `${formatDateForDisplay(dateFilter)} 일기`;
} else {
entriesToDisplay = allEntries;
if(listTitleIconEl) listTitleIconEl.textContent = '📚';
diaryListTitle.textContent = `나의 모든 일기`;
}
entriesToDisplay.sort((a, b) => new Date(b.submissionTime) - new Date(a.submissionTime));
entriesToDisplay.forEach((entry) => { addDiaryEntryToDOM(entry, allEntries.indexOf(entry)); });
updateCalendarHighlights();
}
function addDiaryEntryToDOM(entry, originalIndex) {
const entryElement = document.createElement('div'); entryElement.classList.add('diary-entry');
const titleElement = document.createElement('h3'); titleElement.classList.add('diary-title'); titleElement.textContent = entry.title;
const dateElement = document.createElement('p'); dateElement.classList.add('diary-date-display');
let displayDate = formatDateForDisplay(entry.entryDate);
if (entry.submissionTime) { displayDate += ` (${new Date(entry.submissionTime).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })})`; }
dateElement.textContent = `💖 ${displayDate}`;
const contentElement = document.createElement('p'); contentElement.classList.add('diary-content'); contentElement.textContent = entry.content;
const actionsDiv = document.createElement('div'); actionsDiv.classList.add('diary-actions');
const editButton = document.createElement('button'); editButton.classList.add('beos-button'); editButton.innerHTML = '✏️ 수정';
editButton.onclick = function() { loadDiaryForEditing(originalIndex); };
const deleteButton = document.createElement('button'); deleteButton.classList.add('beos-button'); deleteButton.innerHTML = '🗑️ 삭제';
deleteButton.style.backgroundColor = "#C40000"; deleteButton.style.color = "white"; deleteButton.style.borderColor = "#FFBDBD #8B0000 #8B0000 #FFBDBD";
deleteButton.onclick = function() { deleteDiaryEntry(originalIndex); };
actionsDiv.appendChild(editButton); actionsDiv.appendChild(deleteButton);
entryElement.appendChild(actionsDiv); entryElement.appendChild(titleElement); entryElement.appendChild(dateElement); entryElement.appendChild(contentElement);
diaryEntriesList.appendChild(entryElement);
}
function loadDiaryForEditing(index) {
const entries = getAllDiaryEntries(); const entryToEdit = entries[index];
if (entryToEdit) {
diaryTitleInput.value = entryToEdit.title; diaryContentInput.value = entryToEdit.content;
editingDiaryIndexInput.value = index.toString();
formTitle.innerHTML = '<span class="title-bar-icon">✍️</span>일기 수정하기';
submitDiaryBtn.innerHTML = '💾 수정 완료!';
cancelEditBtn.style.display = 'inline-block'; diaryTitleInput.focus();
const formW = windowElements.formWindow;
if(formW && formW.onmousedown) formW.onmousedown();
}
}
function resetForm() {
diaryTitleInput.value = ''; diaryContentInput.value = ''; editingDiaryIndexInput.value = "-1";
formTitle.innerHTML = '<span class="title-bar-icon">✏️</span>새 일기 쓰기';
submitDiaryBtn.innerHTML = '💖 기록하기!';
cancelEditBtn.style.display = 'none';
}
cancelEditBtn.addEventListener('click', resetForm);
diaryForm.addEventListener('submit', function (event) {
event.preventDefault(); const title = diaryTitleInput.value.trim(); const content = diaryContentInput.value.trim();
const editingIndex = parseInt(editingDiaryIndexInput.value);
if (title === '' || content === '') { showPopup('앗! 😮 제목과 내용을 모두 채워주세요!'); return; }
let entries = getAllDiaryEntries(); const today = new Date();
const entryDateForSave = selectedDate ? selectedDate : formatDateForStorage(today);
if (editingIndex > -1) {
entries[editingIndex].title = title; entries[editingIndex].content = content;
entries[editingIndex].lastModifiedTime = today.toISOString();
showPopup('✨ 일기가 성공적으로 수정되었어요!');
} else {
const newEntry = { title, content, entryDate: entryDateForSave, submissionTime: today.toISOString() };
entries.push(newEntry);
if (!selectedDate || selectedDate !== entryDateForSave) { selectedDate = entryDateForSave; }
showPopup('🎉 일기가 비밀스럽게 기록되었어요!');
}
saveDiaryEntries(entries);
currentYear = new Date(selectedDate).getFullYear(); currentMonth = new Date(selectedDate).getMonth();
generateCalendar(currentYear, currentMonth); renderDiaryEntries(selectedDate);
resetForm(); diaryTitleInput.focus();
});
function deleteDiaryEntry(indexToDelete) {
if (!confirm('정말로 이 소중한 일기를 삭제하시겠어요? 😱 복구할 수 없답니다!')) { return; }
let entries = getAllDiaryEntries(); entries.splice(indexToDelete, 1); saveDiaryEntries(entries);
renderDiaryEntries(selectedDate); generateCalendar(currentYear, currentMonth);
showPopup('🗑️ 일기가 삭제되었어요.');
if (editingDiaryIndexInput.value === indexToDelete.toString()) { resetForm(); }
}
function generateCalendar(year, month) {
calendarBody.innerHTML = ''; currentMonthYearSpan.textContent = `${year}${month + 1}월`;
const firstDayOfMonth = new Date(year, month, 1), lastDayOfMonth = new Date(year, month + 1, 0);
const firstDayOfWeek = firstDayOfMonth.getDay(), totalDaysInMonth = lastDayOfMonth.getDate();
const todayFullDate = formatDateForStorage(new Date());
const diaryDates = new Set(getAllDiaryEntries().map(entry => entry.entryDate)); let date = 1;
for (let i = 0; i < 6; i++) {
const row = document.createElement('tr');
for (let j = 0; j < 7; j++) {
const cell = document.createElement('td');
if (i === 0 && j < firstDayOfWeek || date > totalDaysInMonth) { cell.classList.add('other-month'); }
else {
cell.textContent = date; const cellDateStr = formatDateForStorage(new Date(year, month, date)); cell.dataset.date = cellDateStr;
if (cellDateStr === todayFullDate) cell.classList.add('today');
if (diaryDates.has(cellDateStr)) cell.classList.add('has-diary');
if (cellDateStr === selectedDate) cell.classList.add('selected-date');
cell.onclick = function () {
selectedDate = this.dataset.date;
document.querySelectorAll('#calendarTable td.selected-date').forEach(td => td.classList.remove('selected-date'));
this.classList.add('selected-date');
renderDiaryEntries(selectedDate);
const newSelectedDateObj = new Date(selectedDate);
currentYear = newSelectedDateObj.getFullYear(); currentMonth = newSelectedDateObj.getMonth();
};
date++;
}
row.appendChild(cell);
}
calendarBody.appendChild(row);
if (date > totalDaysInMonth && calendarBody.children.length >= Math.ceil((firstDayOfWeek + totalDaysInMonth) / 7)) { break; }
}
}
function updateCalendarHighlights() { const diaryDates = new Set(getAllDiaryEntries().map(entry => entry.entryDate)); document.querySelectorAll('#calendarTable td[data-date]').forEach(cell => { cell.classList.toggle('has-diary', diaryDates.has(cell.dataset.date)); }); }
prevMonthBtn.onclick = () => { currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } generateCalendar(currentYear, currentMonth); };
nextMonthBtn.onclick = () => { currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } generateCalendar(currentYear, currentMonth); };
performBackupButton.addEventListener('click', function () {
const entries = getAllDiaryEntries(); if (entries.length === 0) { showPopup('텅... 🤷 백업할 일기가 없네요.'); return; }
let backupText = "💖 BeOS 스타일 비밀 일기장 백업 🤫\n\n💻 백업 생성일: " + new Date().toLocaleString('ko-KR') + "\n========================================\n\n";
entries.sort((a, b) => new Date(a.submissionTime || a.entryDate) - new Date(b.submissionTime || b.entryDate));
entries.forEach((entry, i) => {
let entryFullDate = formatDateForDisplay(entry.entryDate); if (entry.submissionTime) { entryFullDate += ` (${new Date(entry.submissionTime).toLocaleTimeString('ko-KR')})`; }
backupText += `📌 일기 #${i + 1}\nር 제목: ${entry.title}\n📅 작성일: ${entryFullDate}\n----------------------------------------\n${entry.content}\n\n========================================\n\n`;
});
const blob = new Blob([backupText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url;
const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14);
a.download = `BeOS_Diary_Backup_${timestamp}.txt`;
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
showPopup('💾 모든 소중한 일기가 TXT 파일로 안전하게 백업되었습니다! 🥳');
});
// --- Analog Clock Functions ---
function setupClockCanvas() {
clockCanvas = document.getElementById('analogClockCanvas');
if (!clockCanvas) {
console.error("Clock canvas not found!");
return false;
}
clockCtx = clockCanvas.getContext('2d');
if (!clockCtx) {
console.error("Failed to get 2D context for clock canvas!");
return false;
}
const windowContent = clockCanvas.parentElement;
if (!windowContent) {
console.error("Clock canvas parent element not found!");
return false;
}
const padding = 5;
const availableWidth = Math.max(0, windowContent.clientWidth - (padding * 2));
const availableHeight = Math.max(0, windowContent.clientHeight - (padding * 2));
const size = Math.max(50, Math.min(availableWidth, availableHeight));
clockCanvas.width = size;
clockCanvas.height = size;
clockRadius = clockCanvas.width / 2 - 5;
if (clockRadius <= 0) {
return false;
}
return true;
}
function drawHand(ctx, pos, length, width, color) {
ctx.beginPath();
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.strokeStyle = color;
ctx.moveTo(clockCanvas.width / 2, clockCanvas.height / 2);
ctx.lineTo(
clockCanvas.width / 2 + length * Math.cos(pos),
clockCanvas.height / 2 + length * Math.sin(pos)
);
ctx.stroke();
}
function drawClock() {
if (!clockCanvas || !clockCtx) {
if (!setupClockCanvas()) {
return;
}
}
if (!setupClockCanvas()) {
return;
}
const centerX = clockCanvas.width / 2;
const centerY = clockCanvas.height / 2;
const style = getComputedStyle(document.documentElement);
clockCtx.clearRect(0, 0, clockCanvas.width, clockCanvas.height);
clockCtx.beginPath();
clockCtx.arc(centerX, centerY, clockRadius, 0, 2 * Math.PI);
clockCtx.fillStyle = style.getPropertyValue('--beos-input-bg').trim();
clockCtx.fill();
clockCtx.strokeStyle = style.getPropertyValue('--beos-text').trim();
clockCtx.lineWidth = Math.max(1, clockRadius * 0.03);
clockCtx.stroke();
clockCtx.beginPath();
clockCtx.arc(centerX, centerY, Math.max(1, clockRadius * 0.05), 0, 2 * Math.PI);
clockCtx.fillStyle = style.getPropertyValue('--beos-text').trim();
clockCtx.fill();
for (let i = 0; i < 12; i++) {
const angle = (i * 30 - 90) * (Math.PI / 180);
const x1 = centerX + (clockRadius - clockRadius * 0.05) * Math.cos(angle);
const y1 = centerY + (clockRadius - clockRadius * 0.05) * Math.sin(angle);
const x2 = centerX + (clockRadius - clockRadius * 0.15) * Math.cos(angle);
const y2 = centerY + (clockRadius - clockRadius * 0.15) * Math.sin(angle);
clockCtx.beginPath();
clockCtx.lineWidth = Math.max(1, clockRadius * 0.03);
clockCtx.strokeStyle = style.getPropertyValue('--beos-text').trim();
clockCtx.moveTo(x1, y1);
clockCtx.lineTo(x2, y2);
clockCtx.stroke();
}
for (let i = 0; i < 60; i++) {
if (i % 5 === 0) continue;
const angle = (i * 6 - 90) * (Math.PI / 180);
const x1 = centerX + (clockRadius - clockRadius * 0.05) * Math.cos(angle);
const y1 = centerY + (clockRadius - clockRadius * 0.05) * Math.sin(angle);
const x2 = centerX + (clockRadius - clockRadius * 0.1) * Math.cos(angle);
const y2 = centerY + (clockRadius - clockRadius * 0.1) * Math.sin(angle);
clockCtx.beginPath();
clockCtx.lineWidth = Math.max(1, clockRadius * 0.015);
clockCtx.strokeStyle = style.getPropertyValue('--beos-border-dark').trim();
clockCtx.moveTo(x1, y1);
clockCtx.lineTo(x2, y2);
clockCtx.stroke();
}
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
let hourAngle = ((hours % 12 + minutes / 60) * (360 / 12) - 90) * (Math.PI / 180);
drawHand(clockCtx, hourAngle, clockRadius * 0.5, Math.max(2, clockRadius * 0.08), style.getPropertyValue('--beos-text').trim());
let minuteAngle = (minutes * (360 / 60) - 90) * (Math.PI / 180);
drawHand(clockCtx, minuteAngle, clockRadius * 0.75, Math.max(2, clockRadius * 0.06), style.getPropertyValue('--beos-text').trim());
}
// --- End Analog Clock Functions ---
document.addEventListener('DOMContentLoaded', () => {
console.log("🚀 BeOS 스타일 비밀 일기장 (시계) 로딩 완료! 🤫");
windowElements.calendarWindow = document.getElementById("calendarWindow");
windowElements.formWindow = document.getElementById("formWindow");
windowElements.listWindow = document.getElementById("listWindow");
windowElements.backupWindow = document.getElementById("backupWindow");
windowElements.clockWindow = document.getElementById("clockWindow");
const storedStates = getWindowStates();
loadWindowStates();
const wrapper = document.querySelector(".page-content-wrapper");
const baseTop = parseInt(getComputedStyle(wrapper).paddingTop) || 10; // Use actual padding or fallback
const spacing = parseInt(getComputedStyle(wrapper).paddingLeft) || 10; // Use actual padding or fallback. Assume symmetric padding.
// If more granular control is needed, define JS specific spacing. For this change, using wrapper's padding.
// For image-like spacing, 20px might be better. Let's use a fixed 20 for spacing for now.
const fixedSpacing = 20; // Consistent spacing between windows
// 창들의 기본 너비 설정 (이미지 기반 추정)
const formWindowWidthPx = 300;
const rightColumnWidthPx = 220;
// listWindow의 너비를 계산 (화면 전체 너비에서 다른 요소 너비와 간격 제외)
// 최소 너비 200px 보장
const listWindowWidthPx = Math.max(200, wrapper.clientWidth - formWindowWidthPx - rightColumnWidthPx - (fixedSpacing * 3) - (spacing * 2) ); // 3 fixedSpacings between columns, 2 wrapper paddings
// formWindow와 listWindow의 높이 설정 (화면 높이 대부분 사용)
// 최소 높이 450px 보장. wrapper.clientHeight already accounts for menubar.
const formListHeightPx = Math.max(450, wrapper.clientHeight - baseTop - spacing);
// 오른쪽 열 창들의 높이 설정 (이미지 기반 추정)
const calendarHeightPx = 260;
const clockHeightPx = 180;
const backupHeightPx = 100; // 작게 고정
const defaultLayouts = {
formWindow: {
left: spacing + "px",
top: baseTop + "px",
width: formWindowWidthPx + "px",
height: formListHeightPx + "px"
},
listWindow: {
left: (spacing + formWindowWidthPx + fixedSpacing) + "px",
top: baseTop + "px",
width: listWindowWidthPx + "px",
height: formListHeightPx + "px"
},
calendarWindow: {
left: (spacing + formWindowWidthPx + fixedSpacing + listWindowWidthPx + fixedSpacing) + "px",
top: baseTop + "px",
width: rightColumnWidthPx + "px",
height: calendarHeightPx + "px"
},
clockWindow: {
left: (spacing + formWindowWidthPx + fixedSpacing + listWindowWidthPx + fixedSpacing) + "px",
top: (baseTop + calendarHeightPx + fixedSpacing) + "px",
width: rightColumnWidthPx + "px",
height: clockHeightPx + "px"
},
backupWindow: {
left: (spacing + formWindowWidthPx + fixedSpacing + listWindowWidthPx + fixedSpacing) + "px",
top: (baseTop + calendarHeightPx + fixedSpacing + clockHeightPx + fixedSpacing) + "px",
width: rightColumnWidthPx + "px",
height: backupHeightPx + "px"
}
};
// 모든 창이 화면 경계를 벗어나지 않도록 최종 left, top, width, height 값 보정
Object.keys(defaultLayouts).forEach(id => {
const layout = defaultLayouts[id];
const windowEl = windowElements[id];
if (layout && windowEl) { // Check if layout and element exist
let W = parseInt(layout.width);
let H = parseInt(layout.height);
let L = parseInt(layout.left);
let T = parseInt(layout.top);
const minW = parseInt(getComputedStyle(windowEl).minWidth) || 180;
const minH = parseInt(getComputedStyle(windowEl).minHeight) || 80;
W = Math.max(minW, W);
H = Math.max(minH, H);
// 화면 너비/높이보다 크면 줄이기 (wrapper padding 고려)
if (W > wrapper.clientWidth - spacing * 2) W = wrapper.clientWidth - spacing * 2;
if (H > wrapper.clientHeight - baseTop - spacing) H = wrapper.clientHeight - baseTop - spacing;
// 화면 밖으로 나가지 않도록 위치 조정 (wrapper padding 고려)
if (L + W > wrapper.clientWidth - spacing) L = wrapper.clientWidth - W - spacing;
if (T + H > wrapper.clientHeight - spacing) T = wrapper.clientHeight - H - spacing; // baseTop은 이미 T에 포함되어 계산됨
layout.width = Math.max(minW, W) + "px";
layout.height = Math.max(minH, H) + "px";
layout.left = Math.max(spacing, L) + "px";
layout.top = Math.max(baseTop, T) + "px";
}
});
Object.keys(windowElements).forEach((id) => {
const el = windowElements[id];
if (!el) { console.warn(`Window element ${id} not found during init.`); return; }
if (!storedStates[id] && defaultLayouts[id]) {
el.style.left = defaultLayouts[id].left;
el.style.top = defaultLayouts[id].top;
el.style.width = defaultLayouts[id].width;
el.style.height = defaultLayouts[id].height;
highestZ++;
el.style.zIndex = highestZ;
saveWindowState(id, {
top: el.style.top, left: el.style.left,
width: el.style.width, height: el.style.height,
zIndex: highestZ.toString()
});
}
makeDraggable(el);
});
let maxZCurrent = 0;
Object.values(windowElements).forEach(el => {
if (el && el.style.zIndex) {
const z = parseInt(el.style.zIndex, 10);
if (z > maxZCurrent) maxZCurrent = z;
}
});
highestZ = maxZCurrent || 1;
selectedDate = formatDateForStorage(new Date());
generateCalendar(currentYear, currentMonth);
renderDiaryEntries(selectedDate);
if (windowElements.clockWindow) {
if (clockUpdateInterval) clearInterval(clockUpdateInterval);
// Initial draw
if (windowElements.clockWindow.offsetParent !== null) {
if (!clockCanvas) setupClockCanvas();
if (clockCanvas) drawClock();
}
// Update every minute
clockUpdateInterval = setInterval(() => {
if (windowElements.clockWindow && windowElements.clockWindow.offsetParent !== null) {
// setupClockCanvas might not be needed every time if size doesn't change,
// but good for robustness if window is resized.
if (setupClockCanvas()) drawClock();
}
}, 60000);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment