Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.8 KiB
HTML
108 lines
3.8 KiB
HTML
{% 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 %}
|
|
|