feat(notifications): 알림 종 + 공지 기능 추가
- 헤더 프로필 왼쪽 알림 종: 드롭다운·안읽음 배지·60초 폴링(최초 폴링 도입) - 내 프로젝트 좋아요/즐겨찾기/댓글 시 소유자에게 알림(본인 제외·재토글 도배 방지) - 관리자 공지: /announcements 목록 + 작성/수정/삭제, 등록 시 전 직원에게 fan-out 알림 - notifications/announcements 테이블(멱등) + requireAdmin 가드 + bell/megaphone 아이콘 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
views/announcements.ejs
Normal file
76
views/announcements.ejs
Normal file
@@ -0,0 +1,76 @@
|
||||
<%- include("_header") %>
|
||||
<%
|
||||
// 뷰 로컬 헬퍼 (빌드 없이 EJS 스크립틀릿으로)
|
||||
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 edited(a) { try { return (+new Date(a.updated_at) - +new Date(a.created_at)) > 1000; } catch (e) { return false; } }
|
||||
%>
|
||||
|
||||
<main class="main">
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<div class="section-titlewrap">
|
||||
<h2 class="section-title">공지사항</h2>
|
||||
<span class="section-sub">관리자 공지 모아보기</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (user.isAdmin) { %>
|
||||
<!-- 관리자 전용 새 공지 작성 폼 -->
|
||||
<form class="ann-form ann-form-new" method="post" action="/announcements">
|
||||
<input class="ann-input-title" type="text" name="title" maxlength="120" placeholder="공지 제목" required />
|
||||
<textarea class="ann-input-body" name="body" rows="4" placeholder="공지 내용 (마크다운 지원)"></textarea>
|
||||
<div class="ann-form-actions">
|
||||
<button class="btn btn-primary" type="submit"><%- icon("megaphone", { size: 15 }) %>공지 등록</button>
|
||||
</div>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<% if (!announcements.length) { %>
|
||||
<div class="ann-empty">아직 등록된 공지가 없습니다.</div>
|
||||
<% } %>
|
||||
|
||||
<div class="ann-list">
|
||||
<% announcements.forEach(function (a) { %>
|
||||
<article class="ann-item" id="ann-<%= a.id %>">
|
||||
<div class="ann-view" data-ann-view="<%= a.id %>">
|
||||
<div class="ann-item-head">
|
||||
<h3 class="ann-item-title"><%= a.title %></h3>
|
||||
<% if (user.isAdmin) { %>
|
||||
<div class="ann-item-actions">
|
||||
<button type="button" class="btn btn-sm" data-ann-edit="<%= a.id %>"><%- icon("pencil", { size: 13 }) %>수정</button>
|
||||
<form method="post" action="/announcements/<%= a.id %>/delete" style="display:inline">
|
||||
<button type="submit" class="btn btn-sm ann-del" data-confirm="이 공지를 삭제할까요?"><%- icon("trash", { size: 13 }) %>삭제</button>
|
||||
</form>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="ann-meta">
|
||||
<%= a.author_name %> · <%= fdate(a.created_at) %><% if (edited(a)) { %> · 수정됨<% } %>
|
||||
</div>
|
||||
<% if (a.body && a.body.trim()) { %>
|
||||
<div class="ann-body md-body"><%- renderMarkdown(a.body) %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (user.isAdmin) { %>
|
||||
<!-- 인라인 수정 폼 (수정 버튼으로 토글) -->
|
||||
<form class="ann-form ann-edit" method="post" action="/announcements/<%= a.id %>/update" data-ann-editform="<%= a.id %>" hidden>
|
||||
<input class="ann-input-title" type="text" name="title" maxlength="120" value="<%= a.title %>" required />
|
||||
<textarea class="ann-input-body" name="body" rows="4"><%= a.body %></textarea>
|
||||
<div class="ann-form-actions">
|
||||
<button class="btn btn-primary btn-sm" type="submit"><%- icon("check", { size: 14 }) %>저장</button>
|
||||
<button class="btn btn-sm" type="button" data-ann-cancel="<%= a.id %>">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
<% } %>
|
||||
</article>
|
||||
<% }); %>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<%- include("_footer") %>
|
||||
|
||||
<script type="module" src="/public/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user