diff --git a/app/templates/partials/journal_day_detail.html b/app/templates/partials/journal_day_detail.html
index 5eac0b2..ac4e13d 100644
--- a/app/templates/partials/journal_day_detail.html
+++ b/app/templates/partials/journal_day_detail.html
@@ -14,6 +14,11 @@
editing: {{ 'true' if editing_entry_id == item.id else 'false' }},
moods: [{% for m in item.moods %}'{{ m.value }}'{{ ',' if not loop.last }}{% endfor %}],
toggleMood(v) { this.moods.includes(v) ? this.moods = this.moods.filter(m => m !== v) : this.moods.push(v) },
+ previewMode: false,
+ togglePreview() {
+ this.previewMode = !this.previewMode;
+ if (this.previewMode) { this.$refs.contentField.dispatchEvent(new Event('input')); }
+ },
}"
>
기분 (여러 개 선택 가능)
diff --git a/app/templates/partials/journal_editor_toolbar.html b/app/templates/partials/journal_editor_toolbar.html
new file mode 100644
index 0000000..9628b5c
--- /dev/null
+++ b/app/templates/partials/journal_editor_toolbar.html
@@ -0,0 +1,20 @@
+
+
+ B
+ I
+ S
+ H
+ ❝
+ •
+ 1.
+ </>
+ 🔗
+
+
+
diff --git a/migrations/versions/0014_fix_template_bullets.py b/migrations/versions/0014_fix_template_bullets.py
new file mode 100644
index 0000000..601e6cf
--- /dev/null
+++ b/migrations/versions/0014_fix_template_bullets.py
@@ -0,0 +1,81 @@
+"""fix default 일상 template: bare "-" bullets don't render as list items
+
+Revision ID: 0014_fix_template_bullets
+Revises: 0013_journal_category_template
+Create Date: 2026-08-05
+
+0013에서 넣은 기본 틀의 "-" 줄들이 뒤에 공백이 없어서, markdown 렌더러가 목록으로 인식하지 못하고
+그냥 문단 텍스트("-")로 렌더링됐다(제목 줄과 "-" 사이의 빈 줄 자체는 맞게 넣어서 제목이 밑줄로
+오인되는 문제는 없었음). "- "(대시+공백)로 통일해야 실제 빈 목록 항목(
)이 된다.
+아직 이 기본값을 커스터마이징하지 않은(즉 0013이 넣어준 원래 문구 그대로인) "일상" 카테고리만
+갱신한다 — 이미 사용자가 직접 수정한 틀은 덮어쓰지 않는다.
+
+(참고: revision id를 "0014_fix_category_template_bullets"로 처음 만들었다가 alembic_version.
+version_num이 VARCHAR(32)라 34자짜리 id가 안 들어가서 DataError가 났다 — 25자로 줄여 다시 만든
+파일이다. 실제 데이터 UPDATE 자체는 그때 이미 반영됐고 버전 기록만 실패한 상태였다.)
+"""
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "0014_fix_template_bullets"
+down_revision: Union[str, None] = "0013_journal_category_template"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+_BULLET = "- "
+
+_OLD_TEMPLATE = "\n\n".join(
+ [
+ "**1. Story : 오늘 무슨 일이 있었나요?**",
+ "-",
+ "**2. Feelings : 오늘 들었던 생각과 나의 감정은?**",
+ "-",
+ "**3. Decisions : 오늘 내가 내린 결정이 있나요?**",
+ "-",
+ "**4. Insights : 나에 대해 새롭게 알게된 사실이 있나요?**",
+ "-",
+ "→ 나에 대한 인사이트는 메타인지를 키워주는 **소중한 기록**입니다. 꼭 보관하세요.",
+ "**5. Actions : 내가 다음에 할 행동은 무엇인가요?**",
+ _BULLET,
+ ]
+)
+
+_NEW_TEMPLATE = "\n\n".join(
+ [
+ "**1. Story : 오늘 무슨 일이 있었나요?**",
+ _BULLET,
+ "**2. Feelings : 오늘 들었던 생각과 나의 감정은?**",
+ _BULLET,
+ "**3. Decisions : 오늘 내가 내린 결정이 있나요?**",
+ _BULLET,
+ "**4. Insights : 나에 대해 새롭게 알게된 사실이 있나요?**",
+ _BULLET,
+ "→ 나에 대한 인사이트는 메타인지를 키워주는 **소중한 기록**입니다. 꼭 보관하세요.",
+ "**5. Actions : 내가 다음에 할 행동은 무엇인가요?**",
+ _BULLET,
+ ]
+)
+
+
+def upgrade() -> None:
+ journal_category = sa.table(
+ "journal_category", sa.column("name", sa.String), sa.column("content_template", sa.Text)
+ )
+ op.execute(
+ journal_category.update()
+ .where(journal_category.c.name == "일상", journal_category.c.content_template == _OLD_TEMPLATE)
+ .values(content_template=_NEW_TEMPLATE)
+ )
+
+
+def downgrade() -> None:
+ journal_category = sa.table(
+ "journal_category", sa.column("name", sa.String), sa.column("content_template", sa.Text)
+ )
+ op.execute(
+ journal_category.update()
+ .where(journal_category.c.name == "일상", journal_category.c.content_template == _NEW_TEMPLATE)
+ .values(content_template=_OLD_TEMPLATE)
+ )
diff --git a/pyproject.toml b/pyproject.toml
index 2c3f00c..80b8086 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,8 @@ dependencies = [
"authlib>=1.3",
"httpx>=0.27",
"pillow>=10.0", # scripts/generate_icons.py 아이콘 재생성 + 저널 첨부 이미지 썸네일 생성(런타임)
+ "markdown>=3.7", # 저널 기록 내용을 마크다운으로 렌더링
+ "bleach>=6.0", # 렌더링된 마크다운 HTML을 허용 태그만 남기고 sanitize (XSS 방지)
]
[project.optional-dependencies]
diff --git a/tests/test_journal_service.py b/tests/test_journal_service.py
index cfe64dc..5ed230a 100644
--- a/tests/test_journal_service.py
+++ b/tests/test_journal_service.py
@@ -41,13 +41,30 @@ def _upload(filename: str, content_type: str, data: bytes) -> UploadFile:
def test_ensure_default_category_creates_once(db_session, test_user):
first = journal_service.ensure_default_category(db_session, test_user.id)
assert first.name == journal_service.DEFAULT_CATEGORY_NAME
- assert first.content_template == journal_service.DEFAULT_CATEGORY_TEMPLATE.strip()
+ assert first.content_template == journal_service.DEFAULT_CATEGORY_TEMPLATE
second = journal_service.ensure_default_category(db_session, test_user.id)
assert second.id == first.id
assert len(journal_service.list_categories(db_session, test_user.id)) == 1
+def test_default_category_template_renders_as_headers_and_lists_not_bare_dashes(db_session, test_user):
+ from app.markdown_utils import render_markdown
+
+ category = journal_service.ensure_default_category(db_session, test_user.id)
+ html = render_markdown(category.content_template)
+
+ # "-"에 뒤 공백이 없으면 markdown이 목록으로 안 잡고 그냥 "
-
"로 렌더링해버리는
+ # 회귀가 있었다 — 다섯 항목 전부 실제
목록이어야 한다.
+ assert html.count("") == 5
+ assert html.count("") == 5
+ # 제목 줄 바로 다음에 "-"가 오면(빈 줄 없이) markdown이 그걸 제목 밑줄로 오인해서
+ # 굵은 글씨가 아니라 / 제목으로 바뀌어버리는 회귀도 있었다.
+ assert "") == 6
+
+
def test_category_content_template_persists_and_updates(db_session, test_user):
category = journal_service.create_category(
db_session, test_user.id, JournalCategoryCreate(name="회고", content_template="질문 1\n\n질문 2")
diff --git a/tests/test_markdown_utils.py b/tests/test_markdown_utils.py
new file mode 100644
index 0000000..3866e37
--- /dev/null
+++ b/tests/test_markdown_utils.py
@@ -0,0 +1,46 @@
+from app.markdown_utils import render_markdown
+
+
+def test_bold_and_heading_render_as_html():
+ html = render_markdown("# 제목\n\n**굵게** 쓴 문장입니다")
+ assert "제목 " in html
+ assert "굵게 " in html
+
+
+def test_single_newline_becomes_line_break():
+ html = render_markdown("첫째 줄\n둘째 줄")
+ assert " " in html
+ assert " 하나 " in html
+
+
+def test_dash_and_star_bullets_render_identically():
+ # 마크다운 글머리 기호는 -/*/+ 전부 동일하게 처리돼야 한다 — 에디터 쪽 기본 기호(unorderedListStyle)만
+ # "-"로 바뀌었을 뿐, 렌더링 결과는 어떤 기호를 써도 같아야 한다.
+ assert render_markdown("- 하나\n- 둘") == render_markdown("* 하나\n* 둘") == render_markdown("+ 하나\n+ 둘")
+
+
+def test_script_tag_is_stripped_not_executed():
+ html = render_markdown('본문')
+ assert "본문'
+ )
+
+ response = auth_client.get(f"/journal/day/{today.isoformat()}")
+ assert response.status_code == 200
+ assert "본문'})
+ assert response.status_code == 200
+ assert "