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:
@@ -1,79 +1,190 @@
|
||||
<%- include("_header") %>
|
||||
<div class="container">
|
||||
<div class="layout">
|
||||
<!-- 메인: 사이트 모음 + 공유 프로젝트 -->
|
||||
<div>
|
||||
<div class="box">
|
||||
<div class="box-header">🔗 개발 사이트 바로가기</div>
|
||||
<div class="box-body">
|
||||
<% siteGroups.forEach(function(g){ %>
|
||||
<div class="group-title"><%= g.group %></div>
|
||||
<div class="site-grid">
|
||||
<% g.sites.forEach(function(s){ %>
|
||||
<a class="site-card" href="<%= s.url %>" target="_blank" rel="noopener">
|
||||
<span class="ico"><%= s.icon %></span>
|
||||
<span>
|
||||
<span class="name"><%= s.name %><% if (s.sso) { %> <span class="sso-badge">SSO 자동로그인</span><% } %></span><br/>
|
||||
<span class="desc"><%= s.desc %></span>
|
||||
</span>
|
||||
<%
|
||||
// 뷰 로컬 헬퍼 (빌드 없이 EJS 스크립틀릿으로)
|
||||
function tagList(s) { return String(s || "").split(",").map(function (t) { return t.trim(); }).filter(Boolean); }
|
||||
function fdate(d) { var x = new Date(d); return x.getFullYear() + "." + String(x.getMonth() + 1).padStart(2, "0") + "." + String(x.getDate()).padStart(2, "0"); }
|
||||
function isoDate(d) { try { return new Date(d).toISOString().slice(0, 10); } catch (e) { return ""; } }
|
||||
function searchText(p) { return (p.title + " " + (p.description || "") + " " + (p.tags || "") + " " + p.owner_sabun + " " + (p.owner_name || "")).toLowerCase(); }
|
||||
var myTotal = myProjects.length;
|
||||
var mySharedCount = myProjects.filter(function (p) { return p.is_public; }).length;
|
||||
var myPrivateCount = myTotal - mySharedCount;
|
||||
var starredCount = feedProjects.filter(function (p) { return p.starred; }).length;
|
||||
%>
|
||||
|
||||
<main class="main">
|
||||
|
||||
<!-- ① 개발 도구 -->
|
||||
<section class="section">
|
||||
<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>
|
||||
<% }); %>
|
||||
</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 class="box" style="margin-top:24px">
|
||||
<div class="box-header">📂 공유 프로젝트</div>
|
||||
<% if (projects.length === 0) { %>
|
||||
<div class="box-body muted">아직 공유된 프로젝트가 없습니다. 오른쪽에서 추가해 보세요.</div>
|
||||
<% } else { %>
|
||||
<% projects.forEach(function(p){ %>
|
||||
<div class="proj-item">
|
||||
<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") %>
|
||||
<span class="counter">⭐ <%= p.star_count %></span>
|
||||
<span class="counter">💬 <%= p.comment_count %></span>
|
||||
<div class="myproj-grid" id="myProjGrid">
|
||||
<% myProjects.forEach(function (p) { var tags = tagList(p.tags); %>
|
||||
<article class="pcard<%= p.is_public ? "" : " private" %>" data-open-id="<%= p.id %>" data-vis="<%= p.is_public ? "public" : "private" %>">
|
||||
<div class="pcard-top">
|
||||
<div class="pcard-titlewrap">
|
||||
<span class="pcard-repoico"><%- icon("repo", { size: 16 }) %></span>
|
||||
<span class="pcard-title"><%= p.title %></span>
|
||||
</div>
|
||||
<% if (p.description) { %><div class="muted" style="margin-top:6px"><%= p.description %></div><% } %>
|
||||
<% if (p.tags) { %>
|
||||
<div style="margin-top:8px">
|
||||
<% p.tags.split(",").map(t=>t.trim()).filter(Boolean).forEach(function(t){ %>
|
||||
<span class="tag"><%= t %></span>
|
||||
<% }); %>
|
||||
<span class="vis-badge <%= p.is_public ? "public" : "private" %>">
|
||||
<%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %>
|
||||
</span>
|
||||
</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>
|
||||
</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 method="post" action="/projects/<%= p.id %>/visibility" data-stop style="display:inline">
|
||||
<input type="hidden" name="is_public" value="0" />
|
||||
<button type="submit" class="toggle-share-btn" data-stop>비공개로</button>
|
||||
</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>
|
||||
|
||||
<% 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 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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user