feat(dashboard): 도구·내 프로젝트·공유 피드 3개 섹션 재구성

app.js DOM 계약(data-* 속성)에 맞춘 카드/배지/반응 버튼 렌더.
검색·정렬·필터·모달 오픈을 위한 data-search/created/pop/starred/vis 부여.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyemin Lee
2026-06-29 17:12:52 +09:00
parent 5ab1e9f938
commit 6549cc5870

View File

@@ -1,79 +1,190 @@
<%- include("_header") %> <%- include("_header") %>
<div class="container"> <%
<div class="layout"> // 뷰 로컬 헬퍼 (빌드 없이 EJS 스크립틀릿으로)
<!-- 메인: 사이트 모음 + 공유 프로젝트 --> function tagList(s) { return String(s || "").split(",").map(function (t) { return t.trim(); }).filter(Boolean); }
<div> function fdate(d) { var x = new Date(d); return x.getFullYear() + "." + String(x.getMonth() + 1).padStart(2, "0") + "." + String(x.getDate()).padStart(2, "0"); }
<div class="box"> function isoDate(d) { try { return new Date(d).toISOString().slice(0, 10); } catch (e) { return ""; } }
<div class="box-header">🔗 개발 사이트 바로가기</div> function searchText(p) { return (p.title + " " + (p.description || "") + " " + (p.tags || "") + " " + p.owner_sabun + " " + (p.owner_name || "")).toLowerCase(); }
<div class="box-body"> var myTotal = myProjects.length;
<% siteGroups.forEach(function(g){ %> var mySharedCount = myProjects.filter(function (p) { return p.is_public; }).length;
<div class="group-title"><%= g.group %></div> var myPrivateCount = myTotal - mySharedCount;
<div class="site-grid"> var starredCount = feedProjects.filter(function (p) { return p.starred; }).length;
<% g.sites.forEach(function(s){ %> %>
<a class="site-card" href="<%= s.url %>" target="_blank" rel="noopener">
<span class="ico"><%= s.icon %></span> <main class="main">
<span>
<span class="name"><%= s.name %><% if (s.sso) { %> <span class="sso-badge">SSO 자동로그인</span><% } %></span><br/> <!-- ① 개발 도구 -->
<span class="desc"><%= s.desc %></span> <section class="section">
</span> <div class="section-head">
<div class="section-titlewrap">
<h2 class="section-title">개발 도구</h2>
<span class="section-sub">SSO로 한 번에 로그인 · 새 탭으로 이동</span>
</div>
<% if (user.isAdmin) { %>
<span class="tool-admin-badge"><%- icon("lock", { size: 11 }) %>관리자 도구 표시 중</span>
<% } %>
</div>
<div class="tools-grid">
<% tools.forEach(function (t) { %>
<a class="tool-card<%= t.adminOnly ? " admin" : "" %>" href="<%= t.url %>" target="_blank" rel="noopener">
<% if (t.sso) { %><span class="sso-badge">SSO</span><% } %>
<span class="tool-icon"><%= t.icon %></span>
<span class="tool-name"><%= t.name %></span>
<span class="tool-desc"><%= t.desc %></span>
<% if (t.adminOnly) { %>
<span class="tool-admin-badge"><%- icon("lock", { size: 11 }) %>관리자 전용</span>
<% } %>
</a> </a>
<% }); %> <% }); %>
</div> </div>
<% }); %> </section>
<!-- ② 내 프로젝트 -->
<section class="section myproj">
<span class="myproj-accent"></span>
<div class="myproj-head">
<div class="section-titlewrap">
<h2 class="section-title">내 프로젝트</h2>
<span class="section-sub"><%= user.sabun %> 님의 작업 공간</span>
</div>
<div class="myproj-tools">
<div class="filter-tabs">
<button type="button" class="filter-tab on" data-myfilter="all">전체 <%= myTotal %></button>
<button type="button" class="filter-tab" data-myfilter="public"><%- icon("globe", { size: 12 }) %>공개 <%= mySharedCount %></button>
<button type="button" class="filter-tab" data-myfilter="private"><%- icon("lock", { size: 12 }) %>비공개 <%= myPrivateCount %></button>
</div>
<button type="button" class="new-proj-btn" data-open-share><%- icon("plus", { size: 15, fill: "#fff" }) %>새 프로젝트</button>
</div> </div>
</div> </div>
<div class="box" style="margin-top:24px"> <div class="myproj-grid" id="myProjGrid">
<div class="box-header">📂 공유 프로젝트</div> <% myProjects.forEach(function (p) { var tags = tagList(p.tags); %>
<% if (projects.length === 0) { %> <article class="pcard<%= p.is_public ? "" : " private" %>" data-open-id="<%= p.id %>" data-vis="<%= p.is_public ? "public" : "private" %>">
<div class="box-body muted">아직 공유된 프로젝트가 없습니다. 오른쪽에서 추가해 보세요.</div> <div class="pcard-top">
<% } else { %> <div class="pcard-titlewrap">
<% projects.forEach(function(p){ %> <span class="pcard-repoico"><%- icon("repo", { size: 16 }) %></span>
<div class="proj-item"> <span class="pcard-title"><%= p.title %></span>
<a class="proj-title" href="/projects/<%= p.id %>"><%= p.title %></a>
<div class="proj-meta">
<%= p.owner_name %> · <%= new Date(p.updated_at).toLocaleDateString("ko-KR") %>
&nbsp; <span class="counter">⭐ <%= p.star_count %></span>
&nbsp; <span class="counter">💬 <%= p.comment_count %></span>
</div> </div>
<% if (p.description) { %><div class="muted" style="margin-top:6px"><%= p.description %></div><% } %> <span class="vis-badge <%= p.is_public ? "public" : "private" %>">
<% if (p.tags) { %> <%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %>
<div style="margin-top:8px"> </span>
<% p.tags.split(",").map(t=>t.trim()).filter(Boolean).forEach(function(t){ %>
<span class="tag"><%= t %></span>
<% }); %>
</div> </div>
<div class="pcard-desc"><%= p.description || "설명 없음" %></div>
<% if (tags.length) { %>
<div class="pcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
<% } %> <% } %>
<div class="pcard-foot">
<% if (p.is_public) { %>
<div class="pcard-stats">
<% if (p.lang) { %><span class="stat"><span class="lang-dot" style="background:<%= langColor(p.lang) %>"></span><%= p.lang %></span><% } %>
<span class="stat"><span class="ico-heart"><%- icon("heartFill", { size: 13 }) %></span><span class="tabnum"><%= p.heart_count %></span></span>
<span class="stat"><span class="ico-star"><%- icon("starFill", { size: 13 }) %></span><span class="tabnum"><%= p.star_count %></span></span>
<span class="stat"><span class="ico-comment"><%- icon("comment", { size: 14 }) %></span><span class="tabnum"><%= p.comment_count %></span></span>
</div> </div>
<% }); %> <form method="post" action="/projects/<%= p.id %>/visibility" data-stop style="display:inline">
<% } %> <input type="hidden" name="is_public" value="0" />
</div> <button type="submit" class="toggle-share-btn" data-stop>비공개로</button>
</div>
<!-- 사이드바: 프로젝트 공유 폼 -->
<div>
<div class="box">
<div class="box-header"> 프로젝트 공유</div>
<div class="box-body">
<form method="post" action="/projects">
<label>제목 *</label>
<input type="text" name="title" required placeholder="내 앱 이름" />
<label>설명</label>
<textarea name="description" placeholder="무엇을 만들었나요?"></textarea>
<label>Repo URL</label>
<input type="url" name="repo_url" placeholder="https://gitea.bokdev.in/..." />
<label>앱 URL</label>
<input type="url" name="app_url" placeholder="https://....apps.bokdev.in" />
<label>태그 (콤마 구분)</label>
<input type="text" name="tags" placeholder="node, ai" />
<div style="margin-top:16px">
<button class="btn btn-primary" style="width:100%" type="submit">공유하기</button>
</div>
</form> </form>
<% } else { %>
<span class="private-note">나만 볼 수 있어요</span>
<form method="post" action="/projects/<%= p.id %>/visibility" data-stop style="display:inline">
<input type="hidden" name="is_public" value="1" />
<button type="submit" class="share-mini-btn" data-stop><%- icon("globe", { size: 12, fill: "#fff" }) %>공유하기</button>
</form>
<% } %>
</div>
</article>
<% }); %>
<button type="button" class="add-card" id="addCard" data-open-share>
<span class="add-card-plus"><%- icon("plus", { size: 18, fill: "currentColor" }) %></span>
<span class="add-card-title">새 프로젝트 등록</span>
<span class="add-card-sub">공개·비공개를 정해 등록하세요</span>
</button>
<% if (myTotal === 0) { %>
<div class="empty" style="grid-column:auto">아직 등록한 프로젝트가 없습니다. 첫 프로젝트를 공유해 보세요.</div>
<% } %>
</div>
</section>
<!-- ③ 공유 프로젝트 피드 -->
<section class="section">
<div class="feed-head">
<div class="section-titlewrap">
<h2 class="section-title">공유 프로젝트</h2>
<span class="section-sub">동료들이 만든 결과물 둘러보기</span>
</div>
<div class="feed-head-right">
<button type="button" class="feed-star-btn" id="feedStarFilter" title="내가 즐겨찾기한 프로젝트">
<span class="ico"><%- icon("star", { size: 14 }) %></span>즐겨찾기 <span class="tabnum"><%= starredCount %></span>
</button>
<div class="sort-tabs">
<button type="button" class="sort-tab on" id="sortLatest">최신순</button>
<button type="button" class="sort-tab" id="sortPopular">인기순</button>
</div> </div>
</div> </div>
</div> </div>
<% if (feedProjects.length === 0) { %>
<div class="empty">아직 공유된 프로젝트가 없습니다. 위에서 첫 프로젝트를 공개해 보세요.</div>
<% } %>
<div class="empty" id="feedEmpty" hidden></div>
<div class="feed-grid" id="feedGrid">
<% feedProjects.forEach(function (p) { var tags = tagList(p.tags); %>
<article class="fcard" data-open-id="<%= p.id %>" data-search="<%= searchText(p) %>"
data-created="<%= isoDate(p.created_at) %>" data-pop="<%= p.heart_count %>"
data-starred="<%= p.starred ? "true" : "false" %>">
<div class="fcard-top">
<div class="fcard-titlewrap">
<span class="pcard-repoico"><%- icon("repo", { size: 16 }) %></span>
<span class="fcard-title"><%= p.title %></span>
</div>
<div class="react-btns">
<button type="button" class="react-btn heart<%= p.hearted ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="heart" title="좋아요">
<span class="ico ico-on"><%- icon("heartFill", { size: 14 }) %></span>
<span class="ico ico-off"><%- icon("heart", { size: 14 }) %></span>
<span class="count" data-rcount><%= p.heart_count %></span>
</button>
<button type="button" class="react-btn star<%= p.starred ? " on" : "" %>" data-react data-id="<%= p.id %>" data-kind="star" title="즐겨찾기(Star)">
<span class="ico ico-on"><%- icon("starFill", { size: 14 }) %></span>
<span class="ico ico-off"><%- icon("star", { size: 14 }) %></span>
<span class="count" data-rcount><%= p.star_count %></span>
</button>
</div> </div>
</div> </div>
<div class="fcard-desc"><%= p.description || "설명 없음" %></div>
<% if (tags.length) { %>
<div class="fcard-tags"><% tags.forEach(function (t) { %><span class="tag"><%= t %></span><% }); %></div>
<% } %>
<div class="fcard-foot">
<div class="fcard-author">
<%- identicon(p.owner_sabun, 22) %>
<span class="sabun"><%= p.owner_sabun %></span>
<span class="date">· <%= fdate(p.created_at) %></span>
</div>
<div class="fcard-meta">
<% if (p.lang) { %><span class="stat"><span class="lang-dot" style="background:<%= langColor(p.lang) %>"></span><%= p.lang %></span><% } %>
<span class="stat"><span class="ico-comment"><%- icon("comment", { size: 14 }) %></span><span class="tabnum"><%= p.comment_count %></span></span>
<% if (p.app_url) { %>
<a class="bara-link" href="<%= p.app_url %>" target="_blank" rel="noopener">바로가기 <%- icon("ext", { size: 12 }) %></a>
<% } %>
</div>
</div>
</article>
<% }); %>
</div>
</section>
</main>
<footer class="footer">
<div class="footer-inner">
<div class="footer-brand"><span><%- icon("code", { size: 14 }) %></span><%= brand %> · 사내 개발 포털</div>
<div class="footer-tech">Express + Postgres + Keycloak SSO · deployed on Kubero</div>
</div>
</footer>
<!-- 상세/공유 모달 + 상세 저장소는 Phase 5 에서 이 아래에 추가 -->
<script src="/public/app.js"></script>
</body> </body>
</html> </html>