From 40f8352a28a6b83f8ac8523679ed979af4787ca0 Mon Sep 17 00:00:00 2001 From: 2620227 Date: Fri, 3 Jul 2026 15:43:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EB=A7=A4=EB=89=B4?= =?UTF-8?q?=EC=96=BC=20=EB=B2=84=ED=8A=BC=20=C2=B7=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EA=B2=80=EC=83=89=20=C2=B7=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=8E=81=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더 프로필 왼쪽에 '매뉴얼' 버튼 추가(MANUAL_URL 환경변수, 기본 gitea MANUAL) - 검색을 페이지 내 data-search 카드 전체 대상으로 일반화 → 프로필에서 그 사람 프로젝트도 검색(자리표시자도 페이지별로: 공유/내/○○ 님의 프로젝트) - 상세 페이지 뒤로가기: ?from= 내부경로 기반으로 프로필→상세는 프로필로 복귀 - '사내 개발 포털' +4pt, 도구 카드 이름·설명 +2pt Co-Authored-By: Claude Opus 4.8 --- public/css/components.css | 4 ++-- public/css/layout.css | 10 +++++++++- public/js/feed.js | 6 ++++-- src/config.js | 3 +++ src/helpers.js | 1 + src/server.js | 15 +++++++++++++-- views/_header.ejs | 6 +++++- views/_project_detail.ejs | 2 +- views/profile.ejs | 4 +++- views/project.ejs | 2 +- 10 files changed, 42 insertions(+), 11 deletions(-) diff --git a/public/css/components.css b/public/css/components.css index f8b964d..d5fa4a1 100644 --- a/public/css/components.css +++ b/public/css/components.css @@ -14,8 +14,8 @@ font-size: 24px; background: var(--tile); border: 1px solid var(--tile-bd); } .tool-card.admin .tool-icon { background: var(--admin-bg); border-color: var(--admin-bd); } -.tool-name { display: block; margin-top: 14px; font-size: 14.5px; font-weight: 700; color: var(--fg); } -.tool-desc { display: block; margin-top: 3px; font-size: 12.5px; color: var(--fg2); line-height: 1.45; } +.tool-name { display: block; margin-top: 14px; font-size: 16.5px; font-weight: 700; color: var(--fg); } +.tool-desc { display: block; margin-top: 3px; font-size: 14.5px; color: var(--fg2); line-height: 1.45; } .tool-admin-badge { display: inline-flex; align-items: center; gap: 5px; margin-top: 11px; font-size: 11px; font-weight: 600; color: var(--admin-fg); background: var(--admin-bg); border: 1px solid var(--admin-bd); border-radius: 999px; padding: 2px 8px; white-space: nowrap; diff --git a/public/css/layout.css b/public/css/layout.css index 5a44eb2..7f9a2a3 100644 --- a/public/css/layout.css +++ b/public/css/layout.css @@ -6,7 +6,7 @@ .brand { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; flex: 0 0 auto; } .brand-mark { display: inline-flex; color: var(--hfg); transition: transform .2s ease, filter .2s ease; } .brand-name { font-weight: 800; font-size: 15px; letter-spacing: .13em; color: var(--hfg); } -.brand-sub { font-size: 11px; color: var(--hfg2); font-weight: 500; border-left: 1px solid var(--hbd); padding-left: 10px; margin-left: 2px; white-space: nowrap; } +.brand-sub { font-size: 15px; color: var(--hfg2); font-weight: 500; border-left: 1px solid var(--hbd); padding-left: 10px; margin-left: 2px; white-space: nowrap; } .hdr-search { flex: 1; max-width: 300px; position: relative; display: flex; align-items: center; } .hdr-search-ico { position: absolute; left: 10px; display: inline-flex; color: var(--hfg2); pointer-events: none; } @@ -25,6 +25,14 @@ transition: background .14s ease, color .14s ease; } .icon-btn:hover { background: rgba(255,255,255,.08); color: var(--hfg); } +/* 헤더 텍스트 링크 버튼(매뉴얼 등) — 아이콘 + 글자 */ +.hdr-link { + display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; + border-radius: 8px; border: 1px solid var(--hbd); background: transparent; + color: var(--hfg2); font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; + transition: background .14s ease, color .14s ease; +} +.hdr-link:hover { background: rgba(255,255,255,.08); color: var(--hfg); } .theme-ico-sun { display: none; } html[data-aidev-theme="dark"] .theme-ico-moon { display: none; } html[data-aidev-theme="dark"] .theme-ico-sun { display: inline-flex; } diff --git a/public/js/feed.js b/public/js/feed.js index cc09236..a94fdbf 100644 --- a/public/js/feed.js +++ b/public/js/feed.js @@ -2,11 +2,13 @@ import { $, $$ } from "./util.js"; function applyFeed() { - const grid = $("#feedGrid"); if (!grid) return; + // 검색 대상 = 현재 페이지의 검색 가능한 카드 전체(대시보드=공유 프로젝트, 프로필=그 사람 프로젝트). + const cards = $$(".fcard[data-search]"); + if (!cards.length) return; const q = (($("#feedSearch") && $("#feedSearch").value) || "").trim().toLowerCase(); const starOnly = $("#feedStarFilter") && $("#feedStarFilter").classList.contains("on"); let visible = 0; - $$(".fcard", grid).forEach((c) => { + cards.forEach((c) => { const hit = !q || (c.getAttribute("data-search") || "").indexOf(q) >= 0; const st = !starOnly || c.getAttribute("data-starred") === "true"; const show = hit && st; diff --git a/src/config.js b/src/config.js index 91c24e1..d09440e 100644 --- a/src/config.js +++ b/src/config.js @@ -35,4 +35,7 @@ export const config = { // 포털 표시 이름 (변경 가능) brand: process.env.PORTAL_BRAND || "AI DEV", + + // 포털 사용 매뉴얼 링크(헤더 '매뉴얼' 버튼). 도메인 변경 대비 환경변수로 덮어쓰기 가능. + manualUrl: process.env.MANUAL_URL || "https://gitea.bokdev.in/playground/MANUAL", }; diff --git a/src/helpers.js b/src/helpers.js index e1ac169..b1df278 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -27,6 +27,7 @@ const OCTICONS = { check: '', ext: '', trash: '', + book: '', }; // 인라인 SVG 아이콘 문자열. fill 기본은 currentColor(부모 색 상속). diff --git a/src/server.js b/src/server.js index 35897df..d23de3c 100644 --- a/src/server.js +++ b/src/server.js @@ -31,6 +31,7 @@ app.use( // 모든 뷰 공통 노출 (브랜드·사용자·뷰 헬퍼) app.use((req, res, next) => { res.locals.brand = config.brand; + res.locals.manualUrl = config.manualUrl; res.locals.user = req.session.user || null; res.locals.icon = icon; res.locals.identicon = identicon; @@ -124,7 +125,10 @@ app.get("/u/:sabun", requireAuth, async (req, res) => { 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 }); + const searchPlaceholder = profile.sabun === me + ? "내 프로젝트 검색…" + : `${profile.name} 님의 프로젝트 검색…`; + res.render("profile", { profile, projects, isSelf: profile.sabun === me, searchPlaceholder }); }); // --- 대시보드 --- @@ -183,7 +187,14 @@ app.get("/projects/:id", requireAuth, async (req, res) => { // 비공개는 본인만 열람 if (!project.is_public && project.owner_sabun !== me) return res.status(403).send("비공개 프로젝트입니다."); const comments = await db.listComments(project.id); - res.render("project", { project, comments, starred: project.starred }); + // 뒤로가기 대상: ?from= 으로 넘어온 내부 경로(프로필 등)가 있으면 그곳, 없으면 대시보드. + // (뎁스 순서: 프로필 → 프로젝트 상세 로 들어왔으면 프로필로 돌아간다) + const from = req.query.from; + const safeFrom = typeof from === "string" && /^\/[^/]/.test(from) ? from : null; // 내부 절대경로만 허용 + const back = safeFrom && safeFrom.startsWith("/u/") + ? { href: safeFrom, label: "프로필" } + : { href: "/", label: "대시보드" }; + res.render("project", { project, comments, starred: project.starred, back }); }); // --- 반응: ❤️ 좋아요 / ⭐ 즐겨찾기 --- diff --git a/views/_header.ejs b/views/_header.ejs index 79dce95..4b18e76 100644 --- a/views/_header.ejs +++ b/views/_header.ejs @@ -32,7 +32,7 @@ <% if (user) { %>
@@ -41,6 +41,10 @@ <%- icon("sun", { size: 16 }) %> + + <%- icon("book", { size: 15 }) %>매뉴얼 + + <% if (user.isAdmin) { %> <%- icon("lock", { size: 11 }) %>관리자 <% } %> diff --git a/views/_project_detail.ejs b/views/_project_detail.ejs index 6d85df0..13dbc64 100644 --- a/views/_project_detail.ejs +++ b/views/_project_detail.ejs @@ -44,7 +44,7 @@ <% if (modal) { %> <% } else { %> - ← 대시보드 + ← <%= typeof back !== 'undefined' && back ? back.label : '대시보드' %> <% } %>
<% if (pdTags.length) { %> diff --git a/views/profile.ejs b/views/profile.ejs index 76b19c2..61ac86b 100644 --- a/views/profile.ejs +++ b/views/profile.ejs @@ -3,6 +3,7 @@ // 뷰 로컬 헬퍼 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 searchText(p) { return (p.title + " " + (p.description || "") + " " + (p.tags || "")).toLowerCase(); } %>
@@ -38,10 +39,11 @@ <% if (!projects.length) { %>
<%= isSelf ? "아직 등록한 프로젝트가 없습니다." : "아직 공개된 프로젝트가 없습니다." %>
<% } %> +