Files
ai-dev-portal/views/profile.ejs
2620227 6c2bb888b4 feat: 프로필 사진 업로드·프로필 페이지 + 모달/이름 개선
- 프로필 페이지 /u/:sabun 신설 (본인=비공개 포함, 타인=공개만).
  헤더 "내 프로필", 카드·모달·댓글 작성자 클릭 시 프로필로 이동.
- 프로필 사진 업로드: users.avatar(bytea) 저장, GET /avatar/:sabun
  (업로드본 없으면 identicon SVG 폴백), POST /profile/avatar(express.raw).
  브라우저 canvas 크롭(256px webp) — 서버 이미지 라이브러리/멀터 불필요.
  identicon 6개 렌더 지점을 avatar() 헬퍼로 통일.
- 상세 모달 폭 640→760px, 소유자 액션(비공개 전환·삭제)을 우측 그룹으로
  묶어 한 줄 정렬.
- 표시 이름을 한국식 성+이름(family_name+given_name)으로 교정.
- .project-env.example ADMIN_ROLE→ADMIN_GROUP 정리.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:45:47 +09:00

105 lines
4.9 KiB
Plaintext

<%- include("_header") %>
<%
// 뷰 로컬 헬퍼
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"); }
%>
<main class="main">
<!-- 프로필 히어로 -->
<section class="section">
<div class="profile-hero">
<div class="profile-avatar-wrap">
<img class="profile-avatar" id="profileAvatar" src="/avatar/<%= profile.sabun %>" alt="<%= profile.name %>" width="96" height="96" />
<% if (isSelf) { %>
<button type="button" class="profile-avatar-edit" id="avatarEditBtn" title="프로필 사진 변경" aria-label="프로필 사진 변경"><%- icon("plus", { size: 15, fill: "#fff" }) %></button>
<% } %>
</div>
<div class="profile-info">
<h1 class="profile-name"><%= profile.name %></h1>
<div class="profile-sabun"><%= profile.sabun %></div>
<div class="profile-metaline">
<span><%= profile.project_count %>개 프로젝트</span>
<span>· 가입 <%= fdate(profile.created_at) %></span>
</div>
</div>
</div>
</section>
<!-- 프로젝트 목록 -->
<section class="section">
<div class="section-head">
<div class="section-titlewrap">
<h2 class="section-title"><%= isSelf ? "내 프로젝트" : (profile.name + " 님의 프로젝트") %></h2>
<span class="section-sub"><%= isSelf ? "공개·비공개 모두 표시" : "공개된 프로젝트만 표시" %></span>
</div>
</div>
<% if (!projects.length) { %>
<div class="empty"><%= isSelf ? "아직 등록한 프로젝트가 없습니다." : "아직 공개된 프로젝트가 없습니다." %></div>
<% } %>
<div class="feed-grid">
<% projects.forEach(function (p) { var tags = tagList(p.tags); %>
<a class="fcard" href="/projects/<%= p.id %>">
<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>
<span class="vis-badge <%= p.is_public ? "public" : "private" %>">
<%- icon(p.is_public ? "globe" : "lock", { size: 11 }) %><%= p.is_public ? "공개" : "비공개" %>
</span>
</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-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-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>
<span class="date"><%= fdate(p.created_at) %></span>
</div>
</a>
<% }); %>
</div>
</section>
</main>
<% if (isSelf) { %>
<!-- 프로필 사진 크롭 모달 -->
<div class="modal-overlay" id="cropModal" hidden>
<div class="crop-modal">
<div class="crop-head">
<h3>프로필 사진</h3>
<button type="button" class="modal-close" id="cropClose" title="닫기"><%- icon("x", { size: 16 }) %></button>
</div>
<div class="crop-body">
<div class="crop-stage" id="cropStage">
<img id="cropImg" alt="" draggable="false" />
<div class="crop-veil"></div>
</div>
<label class="crop-zoom">
<span><%- icon("search", { size: 13 }) %></span>
<input type="range" id="cropZoom" min="1" max="4" step="0.01" value="1" />
</label>
<p class="crop-hint">사진을 드래그해 위치를 맞추고 슬라이더로 확대하세요. 정사각형으로 저장됩니다.</p>
</div>
<div class="modal-foot-actions">
<input type="file" id="avatarFile" accept="image/png,image/jpeg,image/webp" hidden />
<button type="button" class="btn" id="cropPick">다른 사진 선택</button>
<button type="button" class="btn btn-success" id="cropSave">저장</button>
</div>
</div>
</div>
<% } %>
<script type="module" src="/public/js/main.js"></script>
<% if (isSelf) { %><script type="module" src="/public/js/profile.js"></script><% } %>
</body>
</html>