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>
This commit is contained in:
@@ -6,7 +6,7 @@ import path from "path";
|
||||
import { config } from "./config.js";
|
||||
import { toolsForUser } from "./sites.js";
|
||||
import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js";
|
||||
import { icon, identicon, langColor, langFromTags, renderMarkdown, mdPlain } from "./helpers.js";
|
||||
import { icon, identicon, identiconSvg, avatar, langColor, langFromTags, renderMarkdown, mdPlain } from "./helpers.js";
|
||||
import { fetchMainLang, fetchReadme } from "./gitea.js";
|
||||
import * as db from "./db.js";
|
||||
|
||||
@@ -34,6 +34,7 @@ app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
res.locals.icon = icon;
|
||||
res.locals.identicon = identicon;
|
||||
res.locals.avatar = avatar;
|
||||
res.locals.langColor = langColor;
|
||||
res.locals.renderMarkdown = renderMarkdown;
|
||||
res.locals.mdPlain = mdPlain;
|
||||
@@ -82,6 +83,50 @@ app.get("/logout", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- 아바타 이미지 ---
|
||||
// 업로드한 프로필 사진이 있으면 그 bytes 를, 없으면 사번 기반 identicon SVG 로 응답.
|
||||
app.get("/avatar/:sabun", requireAuth, async (req, res) => {
|
||||
const sabun = req.params.sabun;
|
||||
try {
|
||||
const row = await db.getAvatar(sabun);
|
||||
if (row && row.avatar) {
|
||||
res.set("Content-Type", row.avatar_mime || "image/webp");
|
||||
res.set("Cache-Control", "private, max-age=300");
|
||||
return res.send(row.avatar);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("avatar load error", e);
|
||||
}
|
||||
res.set("Content-Type", "image/svg+xml; charset=utf-8");
|
||||
res.set("Cache-Control", "private, max-age=300");
|
||||
res.send(identiconSvg(sabun, 128));
|
||||
});
|
||||
|
||||
// --- 프로필 사진 업로드(본인) ---
|
||||
// 브라우저에서 1:1 크롭·축소한 작은 이미지를 raw body 로 받는다(멀터/이미지 라이브러리 불필요).
|
||||
app.post(
|
||||
"/profile/avatar",
|
||||
requireAuth,
|
||||
express.raw({ type: ["image/webp", "image/jpeg", "image/png"], limit: "700kb" }),
|
||||
async (req, res) => {
|
||||
const buf = req.body;
|
||||
const mime = (req.get("Content-Type") || "").split(";")[0].trim();
|
||||
if (!Buffer.isBuffer(buf) || buf.length === 0) return res.status(400).json({ ok: false, error: "empty" });
|
||||
if (!["image/webp", "image/jpeg", "image/png"].includes(mime)) return res.status(415).json({ ok: false, error: "type" });
|
||||
await db.setAvatar({ sabun: req.session.user.sabun, buf, mime });
|
||||
res.json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
// --- 프로필 페이지 (본인/타인) — 프로필 + 해당 사용자의 프로젝트 목록 ---
|
||||
app.get("/u/:sabun", requireAuth, async (req, res) => {
|
||||
const me = req.session.user.sabun;
|
||||
const profile = await db.getUserProfile(req.params.sabun);
|
||||
if (!profile) return res.status(404).send("사용자를 찾을 수 없습니다.");
|
||||
const projects = await db.listProjectsByOwner(profile.sabun, me);
|
||||
res.render("profile", { profile, projects, isSelf: profile.sabun === me });
|
||||
});
|
||||
|
||||
// --- 대시보드 ---
|
||||
app.get("/", requireAuth, async (req, res) => {
|
||||
const me = req.session.user.sabun;
|
||||
|
||||
Reference in New Issue
Block a user