Last active
May 12, 2025 01:20
-
-
Save lunamoth/0caf6f60c0fa1974de9f0172e5375263 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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"><</button> <span id="currentMonthYear"></span> <button id="nextMonthBtn" class="beos-button">></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