add journaling feature: categories, tags, attachments, calendar, and multi-select emotions

Adds an 8th development stage that lets users keep a free-form journal alongside
habit tracking, reusing the existing Google OAuth/DB/PWA infrastructure instead
of a separate project. Users organize entries into custom categories, filter by
a month calendar with day-detail drill-down, attach photos/videos (served via an
authenticated route, never /static), tag entries, and pick multiple emotions per
entry from a curated 9-option set. Includes a global journal prompt bank for
lightweight guided journaling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 18:40:07 +09:00
co-authored by Claude Sonnet 5
parent ef2a3ea082
commit 54c971875c
24 changed files with 2485 additions and 7 deletions
+166 -1
View File
@@ -325,7 +325,10 @@ p {
/* 입력 */
input[type="text"],
input[type="time"],
input[type="password"] {
input[type="date"],
input[type="password"],
select,
textarea {
width: 100%;
font-family: inherit;
font-size: 15px;
@@ -336,6 +339,33 @@ input[type="password"] {
color: var(--color-text);
}
textarea {
resize: vertical;
min-height: 90px;
line-height: 1.5;
}
select {
cursor: pointer;
}
input[type="file"] {
width: 100%;
font-family: inherit;
font-size: 14px;
color: var(--color-text-muted);
}
input[type="color"] {
width: 56px;
height: 40px;
padding: 2px;
border: 1px solid var(--color-border);
border-radius: var(--radius-control);
background: var(--color-bg);
cursor: pointer;
}
label {
display: block;
font-size: 13px;
@@ -466,6 +496,32 @@ label {
color: #fff;
}
/* 저널 기분 선택 pill */
.mood-picker {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.mood-pill {
border-radius: 999px;
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text-muted);
font-size: 14px;
padding: 8px 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.mood-pill.selected {
background: var(--color-accent);
border-color: var(--color-accent);
color: #fff;
}
.difficulty-hint {
font-size: 12px;
color: var(--color-text-muted);
@@ -810,6 +866,115 @@ label {
color: var(--color-border);
}
/* 저널링 */
.journal-day-dots {
display: flex;
gap: 2px;
}
.journal-day-dot {
width: 5px;
height: 5px;
border-radius: 50%;
}
.journal-prompt-card,
.on-this-day-card {
margin-top: var(--space-2);
padding: var(--space-2);
border-radius: var(--radius-control);
background: var(--color-success-tint);
font-size: 14px;
}
.journal-entry-item {
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
.journal-entry-item:last-child {
border-bottom: none;
}
.journal-entry-title {
font-weight: 600;
margin-top: 6px;
}
.journal-entry-content {
white-space: pre-wrap;
margin-top: 4px;
}
.journal-entry-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.tag-chip {
font-size: 12px;
color: var(--color-text-muted);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 999px;
padding: 2px 8px;
}
.mood-icon {
font-size: 18px;
}
.attachment-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 8px;
margin-top: 8px;
}
.attachment-thumb {
position: relative;
aspect-ratio: 1;
border-radius: var(--radius-control);
overflow: hidden;
border: 1px solid var(--color-border);
}
.attachment-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.attachment-video-link {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 12px;
text-align: center;
color: var(--color-text-muted);
text-decoration: none;
padding: 4px;
}
.attachment-delete-btn {
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 14px;
line-height: 1;
cursor: pointer;
}
/* 랜딩 페이지 (/) */
.landing {
max-width: 480px;
+27
View File
@@ -0,0 +1,27 @@
(function () {
function initSortable() {
var list = document.getElementById("journal-category-list");
if (!list || typeof Sortable === "undefined") return;
Sortable.create(list, {
handle: ".drag-handle",
animation: 150,
forceFallback: true, // iOS Safari는 네이티브 HTML5 D&D 터치 지원이 불안정해서 자체 포인터 시뮬레이션을 강제한다
fallbackTolerance: 3,
onEnd: function () {
var ids = Array.from(list.children)
.map(function (el) { return el.getAttribute("data-category-id"); })
.filter(Boolean)
.map(Number);
fetch("/api/journal/categories/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ category_ids: ids }),
});
},
});
}
document.addEventListener("DOMContentLoaded", initSortable);
})();
+2 -1
View File
@@ -1,9 +1,10 @@
const CACHE_NAME = "habit-tracker-v4";
const CACHE_NAME = "habit-tracker-v5";
const APP_SHELL = [
"/static/css/style.css",
"/static/js/app.js",
"/static/js/push-register.js",
"/static/js/habit-reorder.js",
"/static/js/journal-category-reorder.js",
"/static/js/vendor/htmx.min.js",
"/static/js/vendor/alpine.min.js",
"/static/js/vendor/sortable.min.js",