Files
ai-dev-portal/views/dashboard.ejs
2620227 ac545651e6 refactor(tools): Harbor 관리자 전용 · Kubero SSO 표시 · SSO 배지 제거
Harbor 를 관리자 전용(adminOnly)으로 옮기고, Kubero 도 Keycloak SSO 로
로그인 가능하므로 sso 표시를 추가한다. 일반 사용자에게 보이는 도구가
모두 SSO 라 구분 의미가 사라진 SSO 배지(마크업·CSS)를 제거한다.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 14:19:49 +09:00

265 lines
14 KiB
Plaintext

<%- include("_header") %>
<%
// 뷰 로컬 헬퍼 (빌드 없이 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">
<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="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>
<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"><%= mdPlain(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" data-rcount data-id="<%= p.id %>" data-kind="heart"><%= p.heart_count %></span></span>
<span class="stat"><span class="ico-star"><%- icon("starFill", { size: 13 }) %></span><span class="tabnum" data-rcount data-id="<%= p.id %>" data-kind="star"><%= 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>
<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"><%= mdPlain(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" title="<%= p.owner_sabun %>"><%= p.owner_name || 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-left">
<span class="footer-brand"><span><%- icon("code", { size: 14 }) %></span><%= brand %></span>
<span class="footer-bug">· 버그가 있다면 <a href="https://gitea.bokdev.in/playground/ai-dev-portal/issues" target="_blank" rel="noopener">이슈</a>에 제보바랍니다!</span>
</div>
<div class="footer-tech">Express + Postgres + Keycloak SSO · deployed on Kubero</div>
</div>
</footer>
<!-- 상세 모달 (app.js 가 detailStore 의 내용을 .modal 에 주입) -->
<div class="modal-overlay" id="detailModal" hidden><div class="modal"></div></div>
<!-- 상세 저장소: 열람 가능한 프로젝트별 모달 내용을 template 으로 보관 -->
<div id="detailStore" style="display:none">
<% allProjects.forEach(function (p) { %>
<template data-detail-id="<%= p.id %>"><%- include("_project_detail", { p: p, comments: commentsByProject[p.id] || [], me: user.sabun, returnTo: "/?open=" + p.id, modal: true }) %></template>
<% }); %>
</div>
<!-- 새 프로젝트 등록 모달 -->
<div class="modal-overlay" id="shareModal" hidden>
<div class="share-modal">
<div class="share-head">
<h3>새 프로젝트 등록</h3>
<button type="button" class="modal-close" data-close-share title="닫기"><%- icon("x", { size: 16 }) %></button>
</div>
<form method="post" action="/projects">
<div class="share-body">
<label>
<span class="share-label">제목 <span class="req">*</span></span>
<input class="share-input" name="title" required placeholder="예) 방화벽 정책 DSL 변환기" />
</label>
<label>
<span class="share-label share-label-row">
<span>설명 <span class="hint">Markdown 지원 · 제목·목록·코드·링크</span></span>
<button type="button" class="readme-btn" id="readmeImport"><%- icon("repo", { size: 12 }) %>저장소 README 가져오기</button>
</span>
<textarea class="share-input share-textarea" name="description" rows="5" placeholder="무엇을 하는 프로젝트인가요?&#10;&#10;## 예시&#10;- Markdown 문법을 그대로 쓸 수 있어요&#10;- 저장소 URL 을 넣으면 README 를 가져올 수 있어요"></textarea>
</label>
<label>
<span class="share-label">태그 <span class="hint">쉼표로 구분 · 최대 5개</span></span>
<input class="share-input" name="tags" placeholder="infra, react, poc" />
</label>
<label>
<span class="share-label">저장소 URL <span class="hint">선택</span></span>
<input class="share-input mono" name="repo_url" placeholder="https://gitea.bokdev.in/사번/repo" />
</label>
<label>
<span class="share-label">메인 언어 <span class="hint">선택 · 기본은 저장소에서 자동 감지</span></span>
<select class="share-input" name="lang">
<option value="">자동 감지</option>
<option value="Python">Python</option>
<option value="JavaScript">JavaScript</option>
<option value="TypeScript">TypeScript</option>
<option value="Go">Go</option>
<option value="Java">Java</option>
<option value="Rust">Rust</option>
<option value="Shell">Shell</option>
</select>
</label>
<label>
<span class="share-label">배포 URL <span class="hint">선택 · 없으면 바로가기 숨김</span></span>
<input class="share-input mono" name="app_url" placeholder="https://myapp.apps.bokdev.in" />
</label>
<div>
<span class="share-label">공개 범위</span>
<div class="vis-toggle">
<button type="button" class="vis-opt on-public" data-vis="public"><%- icon("globe", { size: 12 }) %>공개</button>
<button type="button" class="vis-opt" data-vis="private"><%- icon("lock", { size: 12 }) %>비공개</button>
</div>
<input type="hidden" name="is_public" value="1" />
<div class="share-vishint">피드에 공개되어 동료가 볼 수 있어요</div>
</div>
</div>
<div class="modal-foot-actions">
<button type="button" class="btn" data-close-share>취소</button>
<button type="submit" class="btn btn-success">등록</button>
</div>
</form>
</div>
</div>
<script type="module" src="/public/js/main.js"></script>
</body>
</html>