feat(ai-cases): 성공 사례 카드에 PDF 첫 슬라이드 썸네일·그라데이션 개선
- 목록에서 슬라이드 워밍(최대 24건) 후 coverImageUrl 전달 - 카드 상단 풀너비 이미지+스크림, 무이미지 시 다층 그라데이션 Made-with: Cursor
This commit is contained in:
@@ -428,33 +428,93 @@ button {
|
||||
|
||||
.success-story-link {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.success-story-link-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.success-thumb {
|
||||
border-radius: 10px;
|
||||
min-height: 72px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(135deg, #0f766e, #14b8a6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px 12px 0 0;
|
||||
min-height: 100px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* PDF 첫 장(또는 슬라이드) 썸네일 */
|
||||
.success-thumb--cover {
|
||||
min-height: 120px;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.success-thumb-media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.success-thumb-media img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-thumb-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(15, 23, 42, 0.88) 0%,
|
||||
rgba(15, 23, 42, 0.45) 45%,
|
||||
rgba(15, 23, 42, 0.2) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.success-thumb-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 슬라이드 없음: 은은한 그라데이션 + 하이라이트 */
|
||||
.success-thumb--gradient {
|
||||
background:
|
||||
radial-gradient(120% 80% at 100% 0%, rgba(45, 212, 191, 0.45), transparent 55%),
|
||||
radial-gradient(90% 70% at 0% 100%, rgba(14, 116, 144, 0.5), transparent 50%),
|
||||
linear-gradient(135deg, #0c4a6e 0%, #0e7490 42%, #0d9488 78%, #14b8a6 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.success-thumb--gradient .success-thumb-inner {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-thumb-icon {
|
||||
font-size: 22px;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.success-thumb-kicker {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.success-badge {
|
||||
|
||||
52
server.js
52
server.js
@@ -1189,21 +1189,43 @@ pageRouter.get("/ai-cases/write", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
pageRouter.get("/ai-cases", (req, res) => {
|
||||
const q = (req.query.q || "").trim();
|
||||
const tag = (req.query.tag || "").trim();
|
||||
const meta = loadAiSuccessStoriesMeta();
|
||||
const filtered = filterAiSuccessStories(meta, q, tag);
|
||||
const tags = allAiSuccessStoryTags(meta);
|
||||
res.render("ai-cases", {
|
||||
activeMenu: "ai-cases",
|
||||
adminMode: res.locals.adminMode,
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
successStoryDetailAllowed: isAiSuccessStoryDetailAllowed(req, res),
|
||||
stories: filtered,
|
||||
filters: { q, tag },
|
||||
availableTags: tags,
|
||||
});
|
||||
pageRouter.get("/ai-cases", async (req, res, next) => {
|
||||
try {
|
||||
const q = (req.query.q || "").trim();
|
||||
const tag = (req.query.tag || "").trim();
|
||||
const meta = loadAiSuccessStoriesMeta();
|
||||
const filtered = filterAiSuccessStories(meta, q, tag);
|
||||
const tags = allAiSuccessStoryTags(meta);
|
||||
/** 목록에서도 카드 썸네일을 쓰기 위해 PDF→슬라이드가 없으면 생성 시도(상세와 동일 소스) */
|
||||
await Promise.all(
|
||||
filtered.slice(0, 24).map(async (m) => {
|
||||
const pdfUrl = (m.pdfUrl || "").trim();
|
||||
if (!pdfUrl) return;
|
||||
if (getAiSuccessSlideImageUrls(m.slug).length > 0) return;
|
||||
try {
|
||||
await ensureAiSuccessStorySlides(m.slug, pdfUrl);
|
||||
} catch (err) {
|
||||
console.warn("[ai-cases] 슬라이드 워밍 실패:", m.slug, err?.message || err);
|
||||
}
|
||||
})
|
||||
);
|
||||
const stories = filtered.map((m) => {
|
||||
const slideUrls = getAiSuccessSlideImageUrls(m.slug);
|
||||
const coverImageUrl = slideUrls.length > 0 ? slideUrls[0] : "";
|
||||
return { ...m, coverImageUrl };
|
||||
});
|
||||
res.render("ai-cases", {
|
||||
activeMenu: "ai-cases",
|
||||
adminMode: res.locals.adminMode,
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
successStoryDetailAllowed: isAiSuccessStoryDetailAllowed(req, res),
|
||||
stories,
|
||||
filters: { q, tag },
|
||||
availableTags: tags,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
pageRouter.get("/ai-cases/:slug", async (req, res, next) => {
|
||||
|
||||
@@ -1,43 +1,64 @@
|
||||
<% var detailAllowed = typeof successStoryDetailAllowed !== 'undefined' ? successStoryDetailAllowed : true; %>
|
||||
<% var _cover = story.coverImageUrl && String(story.coverImageUrl).trim(); %>
|
||||
<article class="success-story-card<%= detailAllowed ? '' : ' success-story-card--locked' %>">
|
||||
<% if (detailAllowed) { %>
|
||||
<a class="success-story-link" href="/ai-cases/<%= story.slug %>">
|
||||
<div class="success-thumb" aria-hidden="true">
|
||||
<span class="success-thumb-icon">✦</span>
|
||||
<span class="success-thumb-kicker"><%= story.department || "사내 사례" %></span>
|
||||
<div class="success-thumb<%= _cover ? ' success-thumb--cover' : ' success-thumb--gradient' %>" aria-hidden="true">
|
||||
<% if (_cover) { %>
|
||||
<div class="success-thumb-media">
|
||||
<img src="<%= _cover %>" alt="" loading="lazy" decoding="async" width="560" height="315" />
|
||||
</div>
|
||||
<div class="success-thumb-scrim"></div>
|
||||
<% } %>
|
||||
<div class="success-thumb-inner">
|
||||
<span class="success-thumb-icon">✦</span>
|
||||
<span class="success-thumb-kicker"><%= story.department || "사내 사례" %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-badge"><%= story.department || "사내" %> · <%= story.author || "" %></div>
|
||||
<h3><%= story.title %></h3>
|
||||
<p class="success-excerpt"><%= story.excerpt || "" %></p>
|
||||
<div class="tag-row">
|
||||
<% (story.tags || []).forEach((oneTag) => { %>
|
||||
<span class="tag-chip">#<%= oneTag %></span>
|
||||
<% }) %>
|
||||
<div class="success-story-link-body">
|
||||
<div class="success-badge"><%= story.department || "사내" %> · <%= story.author || "" %></div>
|
||||
<h3><%= story.title %></h3>
|
||||
<p class="success-excerpt"><%= story.excerpt || "" %></p>
|
||||
<div class="tag-row">
|
||||
<% (story.tags || []).forEach((oneTag) => { %>
|
||||
<span class="tag-chip">#<%= oneTag %></span>
|
||||
<% }) %>
|
||||
</div>
|
||||
<small class="success-meta">
|
||||
<% if (story.publishedAt) { %><%= story.publishedAt %><% } %>
|
||||
</small>
|
||||
</div>
|
||||
<small class="success-meta">
|
||||
<% if (story.publishedAt) { %><%= story.publishedAt %><% } %>
|
||||
</small>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<div
|
||||
class="success-story-link"
|
||||
title="로그인 후 이용 가능합니다."
|
||||
>
|
||||
<div class="success-thumb" aria-hidden="true">
|
||||
<span class="success-thumb-icon">✦</span>
|
||||
<span class="success-thumb-kicker"><%= story.department || "사내 사례" %></span>
|
||||
<div class="success-thumb<%= _cover ? ' success-thumb--cover' : ' success-thumb--gradient' %>" aria-hidden="true">
|
||||
<% if (_cover) { %>
|
||||
<div class="success-thumb-media">
|
||||
<img src="<%= _cover %>" alt="" loading="lazy" decoding="async" width="560" height="315" />
|
||||
</div>
|
||||
<div class="success-thumb-scrim"></div>
|
||||
<% } %>
|
||||
<div class="success-thumb-inner">
|
||||
<span class="success-thumb-icon">✦</span>
|
||||
<span class="success-thumb-kicker"><%= story.department || "사내 사례" %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-badge"><%= story.department || "사내" %> · <%= story.author || "" %></div>
|
||||
<h3><%= story.title %></h3>
|
||||
<p class="success-excerpt"><%= story.excerpt || "" %></p>
|
||||
<div class="tag-row">
|
||||
<% (story.tags || []).forEach((oneTag) => { %>
|
||||
<span class="tag-chip">#<%= oneTag %></span>
|
||||
<% }) %>
|
||||
<div class="success-story-link-body">
|
||||
<div class="success-badge"><%= story.department || "사내" %> · <%= story.author || "" %></div>
|
||||
<h3><%= story.title %></h3>
|
||||
<p class="success-excerpt"><%= story.excerpt || "" %></p>
|
||||
<div class="tag-row">
|
||||
<% (story.tags || []).forEach((oneTag) => { %>
|
||||
<span class="tag-chip">#<%= oneTag %></span>
|
||||
<% }) %>
|
||||
</div>
|
||||
<small class="success-meta">
|
||||
<% if (story.publishedAt) { %><%= story.publishedAt %><% } %>
|
||||
</small>
|
||||
</div>
|
||||
<small class="success-meta">
|
||||
<% if (story.publishedAt) { %><%= story.publishedAt %><% } %>
|
||||
</small>
|
||||
</div>
|
||||
<% } %>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user