Initial commit: add FastAPI MVP (모프) and existing web app

Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-16 17:17:22 +09:00
commit 27540269b7
37 changed files with 3246 additions and 0 deletions

107
templates/detail.html Normal file
View File

@@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block content %}
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h1 class="text-2xl font-bold leading-snug">{{ prompt.title }}</h1>
<div class="mt-2 text-xs text-gray-500 flex flex-wrap items-center gap-2">
<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-700">
{% for c in categories %}
{% if c.id == prompt.category_id %}{{ c.name }}{% endif %}
{% endfor %}
</span>
<span>작성자: <span class="font-semibold text-gray-700">{{ prompt.author_nickname }}</span></span>
<span>복사 <span id="copy-count" class="font-semibold text-gray-700">{{ prompt.copy_count }}</span></span>
</div>
{% if prompt.description %}
<div class="mt-3 text-sm text-gray-700 whitespace-pre-wrap">{{ prompt.description }}</div>
{% endif %}
</div>
<div class="shrink-0 flex flex-col gap-2">
<div id="like-{{ prompt.id }}">
<button
class="px-3 py-2 rounded border text-sm {% if liked %}bg-gray-100 text-gray-500 cursor-not-allowed{% else %}bg-white hover:bg-gray-50{% endif %}"
hx-post="/like/{{ prompt.id }}"
hx-target="#like-{{ prompt.id }}"
hx-swap="outerHTML"
{% if liked %}disabled{% endif %}
>
{% if liked %}좋아요✓{% else %}좋아요{% endif %}
<span class="font-semibold">({{ like_count }})</span>
</button>
</div>
<button
id="copy-btn"
class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-gray-800"
type="button"
>
복사
</button>
<a href="/new" class="px-3 py-2 rounded border bg-white text-sm hover:bg-gray-50 text-center">
+ 새 프롬프트
</a>
</div>
</div>
<div class="mt-6">
<div class="text-sm text-gray-600 mb-2">프롬프트</div>
<pre
id="prompt-content"
class="p-4 bg-white border rounded text-sm overflow-auto whitespace-pre-wrap leading-relaxed"
>{{ prompt.content }}</pre>
<div id="copy-toast" class="mt-2 text-xs text-gray-500 hidden">클립보드에 복사했습니다.</div>
</div>
<script>
(function () {
const btn = document.getElementById("copy-btn");
const pre = document.getElementById("prompt-content");
const toast = document.getElementById("copy-toast");
const copyCountEl = document.getElementById("copy-count");
async function incCopyCount() {
try {
const res = await fetch("/copy/{{ prompt.id }}", { method: "POST" });
const data = await res.json();
if (copyCountEl && typeof data.copy_count === "number") {
copyCountEl.textContent = String(data.copy_count);
}
} catch (e) {
// 실패해도 UX는 유지(복사 기능이 핵심)
}
}
async function copyText(text) {
// 최신 브라우저는 navigator.clipboard 사용
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return;
}
// HTTP/구형 환경 fallback
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
btn.addEventListener("click", async () => {
const text = pre.textContent || "";
try {
await copyText(text);
toast.classList.remove("hidden");
setTimeout(() => toast.classList.add("hidden"), 1200);
incCopyCount();
} catch (e) {
alert("복사에 실패했습니다. 다시 시도해주세요.");
}
});
})();
</script>
{% endblock %}