.reply-banner 에 display:flex 가 걸려 [hidden] 기본 규칙을 이겨 배너가
항상 보이던 문제를 .reply-banner[hidden]{display:none} 으로 해결.
댓글/대댓글 작성자·답글 대상·프로젝트 소유자를 사번 대신 이름(author_name/
owner_name)으로 표시하고, 사번은 title 툴팁으로 남긴다.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
7.4 KiB
Plaintext
135 lines
7.4 KiB
Plaintext
<%
|
|
// 상세(모달/단독 페이지 공용) — locals: p, comments[], me, returnTo, modal(bool)
|
|
function tagList(s) { return String(s || "").split(",").map(function (t) { return t.trim(); }).filter(Boolean); }
|
|
function reltime(d) {
|
|
var s = Math.floor((Date.now() - new Date(d).getTime()) / 1000);
|
|
if (s < 60) return "방금 전";
|
|
var m = Math.floor(s / 60); if (m < 60) return m + "분 전";
|
|
var h = Math.floor(m / 60); if (h < 24) return h + "시간 전";
|
|
var dd = Math.floor(h / 24); if (dd < 7) return dd + "일 전";
|
|
var x = new Date(d); return x.getFullYear() + "." + String(x.getMonth() + 1).padStart(2, "0") + "." + String(x.getDate()).padStart(2, "0");
|
|
}
|
|
function isLgtm(b) { return /\blgtm\b/i.test(b || ""); }
|
|
var pdTags = tagList(p.tags);
|
|
var tops = comments.filter(function (c) { return !c.parent_id; });
|
|
var repliesOf = function (id) { return comments.filter(function (c) { return String(c.parent_id) === String(id); }); };
|
|
%>
|
|
<div class="modal-head">
|
|
<div class="modal-title-row">
|
|
<div class="modal-titlewrap">
|
|
<div class="modal-title-line">
|
|
<span class="modal-repoico"><%- icon("repo", { size: 18 }) %></span>
|
|
<h3 class="modal-title"><%= p.title %></h3>
|
|
<span class="vis-badge <%= p.is_public ? "public" : "private" %>"><%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %></span>
|
|
</div>
|
|
<div class="modal-meta">
|
|
<%- identicon(p.owner_sabun, 24) %>
|
|
<span class="sabun" title="<%= p.owner_sabun %>"><%= p.owner_name || p.owner_sabun %></span>
|
|
<span class="date">· <%= reltime(p.created_at) %> 공유</span>
|
|
<% if (p.lang) { %><span class="lang"><span class="lang-dot" style="background:<%= langColor(p.lang) %>"></span><%= p.lang %></span><% } %>
|
|
</div>
|
|
</div>
|
|
<% if (modal) { %>
|
|
<button type="button" class="modal-close" data-close-detail title="닫기"><%- icon("x", { size: 16 }) %></button>
|
|
<% } else { %>
|
|
<a class="back-link" href="/">← 대시보드</a>
|
|
<% } %>
|
|
</div>
|
|
<% if (pdTags.length) { %>
|
|
<div class="modal-tags"><% pdTags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
|
|
<% } %>
|
|
<p class="modal-desc"><%= p.description || "설명 없음" %></p>
|
|
<div class="modal-actions">
|
|
<button type="button" class="modal-action heart<%= p.hearted ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="heart">
|
|
<span class="ico ico-on"><%- icon("heartFill", { size: 15 }) %></span><span class="ico ico-off"><%- icon("heart", { size: 15 }) %></span>
|
|
<span>좋아요</span><span class="count" data-rcount><%= p.heart_count %></span>
|
|
</button>
|
|
<button type="button" class="modal-action star<%= p.starred ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="star">
|
|
<span class="ico ico-on"><%- icon("starFill", { size: 15 }) %></span><span class="ico ico-off"><%- icon("star", { size: 15 }) %></span>
|
|
<span>즐겨찾기</span><span class="count" data-rcount><%= p.star_count %></span>
|
|
</button>
|
|
<% if (p.app_url) { %>
|
|
<a class="modal-action primary" href="<%= p.app_url %>" target="_blank" rel="noopener">바로가기 <%- icon("ext", { size: 12, fill: "#fff" }) %></a>
|
|
<% } %>
|
|
<% if (p.repo_url) { %>
|
|
<a class="modal-action ghost" href="<%= p.repo_url %>" target="_blank" rel="noopener"><%- icon("code", { size: 14 }) %>저장소</a>
|
|
<% } %>
|
|
<% if (p.owner_sabun === me) { %>
|
|
<form method="post" action="/projects/<%= p.id %>/delete" style="margin-left:auto">
|
|
<input type="hidden" name="return_to" value="/" />
|
|
<button type="submit" class="comment-link comment-del" data-confirm="이 프로젝트를 삭제할까요? 댓글·반응도 함께 삭제됩니다." style="font-size:12.5px"><%- icon("trash", { size: 13 }) %>프로젝트 삭제</button>
|
|
</form>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="comments-title">댓글 <span class="comments-count tabnum"><%= comments.length %></span></div>
|
|
<div class="comment-list">
|
|
<% tops.forEach(function (c) { var reps = repliesOf(c.id); %>
|
|
<div class="comment-item">
|
|
<div class="comment">
|
|
<%- identicon(c.author_sabun, 28) %>
|
|
<div class="comment-main">
|
|
<div class="comment-head">
|
|
<span class="comment-author" title="<%= c.author_sabun %>"><%= c.author_name || c.author_sabun %></span>
|
|
<span class="comment-time"><%= reltime(c.created_at) %></span>
|
|
<% if (isLgtm(c.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
|
</div>
|
|
<div class="comment-text"><%= c.body %></div>
|
|
<div class="comment-actions">
|
|
<button type="button" class="comment-link" data-reply-to="<%= c.id %>" data-reply-name="<%= c.author_name || c.author_sabun %>"><%- icon("reply", { size: 13 }) %>답글</button>
|
|
<% if (c.author_sabun === me) { %>
|
|
<form method="post" action="/comments/<%= c.id %>/delete" data-stop style="display:inline">
|
|
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
|
<button type="submit" class="comment-link comment-del" data-confirm="댓글을 삭제할까요?">삭제</button>
|
|
</form>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% if (reps.length) { %>
|
|
<div class="replies">
|
|
<% reps.forEach(function (r) { %>
|
|
<div class="reply">
|
|
<%- identicon(r.author_sabun, 24) %>
|
|
<div class="comment-main">
|
|
<div class="comment-head">
|
|
<span class="comment-author" title="<%= r.author_sabun %>"><%= r.author_name || r.author_sabun %></span>
|
|
<span class="comment-time"><%= reltime(r.created_at) %></span>
|
|
<% if (isLgtm(r.body)) { %><span class="lgtm"><%- icon("check", { size: 11 }) %>LGTM</span><% } %>
|
|
</div>
|
|
<div class="comment-text"><%= r.body %></div>
|
|
<% if (r.author_sabun === me) { %>
|
|
<form method="post" action="/comments/<%= r.id %>/delete" data-stop style="display:inline">
|
|
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
|
<button type="submit" class="comment-link comment-del" data-confirm="댓글을 삭제할까요?" style="margin-top:4px">삭제</button>
|
|
</form>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<% }); %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-foot">
|
|
<form method="post" action="/projects/<%= p.id %>/comments">
|
|
<input type="hidden" name="parent_id" value="" />
|
|
<input type="hidden" name="return_to" value="<%= returnTo %>" />
|
|
<div class="reply-banner" hidden>
|
|
<span><b class="reply-banner-name"></b> 님에게 답글 작성 중</span>
|
|
<button type="button" data-cancel-reply>취소</button>
|
|
</div>
|
|
<div class="comment-form">
|
|
<%- identicon(me, 28) %>
|
|
<textarea class="comment-input" name="body" rows="1" placeholder="댓글을 입력하세요…"></textarea>
|
|
<button type="submit" class="submit-btn">등록</button>
|
|
</div>
|
|
<div class="comment-hint">Enter 등록 · Shift+Enter 줄바꿈 · 대댓글은 1뎁스까지만 가능합니다</div>
|
|
</form>
|
|
</div>
|