diff --git a/public/styles.css b/public/styles.css index 54a71dc..edeb79b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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 { diff --git a/server.js b/server.js index 4f67b9b..a34e429 100644 --- a/server.js +++ b/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) => { diff --git a/views/partials/success-story-card.ejs b/views/partials/success-story-card.ejs index b491d6c..9a8d051 100644 --- a/views/partials/success-story-card.ejs +++ b/views/partials/success-story-card.ejs @@ -1,43 +1,64 @@ <% var detailAllowed = typeof successStoryDetailAllowed !== 'undefined' ? successStoryDetailAllowed : true; %> +<% var _cover = story.coverImageUrl && String(story.coverImageUrl).trim(); %>
<% if (detailAllowed) { %> -