- DevLog(블로그) 인프라: build-blog.js (MD→HTML), devlog.css, devlog.js - DevLog 목록/포스트 페이지 4개 언어 (ko/en/ja/zh) - 글 2편 작성 + 번역: 관성식vs광학식, 광학식 파이프라인 - 전체 네비게이션에 DevLog 탭 추가 (37+ HTML) - 메인 팝업(요금제 변경 안내) 제거 (ko/en/ja/zh) - i18n.js: 언어별 페이지에서 번역 JSON 항상 로드하도록 수정 - 방문자 싸인 이미지 3장 추가 (webp 변환) - sitemap, i18n JSON, package.json 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
132 lines
5.6 KiB
JavaScript
132 lines
5.6 KiB
JavaScript
/**
|
|
* 밍글 스튜디오 블로그 목록 페이지
|
|
* blog/index.json에서 글 목록을 불러와 렌더링
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
var grid = document.getElementById('blogGrid');
|
|
var filtersWrap = document.getElementById('blogFilters');
|
|
var loadingEl = document.getElementById('blogLoading');
|
|
if (!grid) return;
|
|
|
|
var lang = document.documentElement.lang || 'ko';
|
|
var langPrefix = { ko: '', en: '/en', ja: '/ja', zh: '/zh' };
|
|
var prefix = langPrefix[lang] || '';
|
|
|
|
var readMore = { ko: '자세히 보기', en: 'Read more', ja: '続きを読む', zh: '阅读更多' };
|
|
var emptyText = { ko: '아직 작성된 글이 없습니다.', en: 'No posts yet.', ja: 'まだ記事がありません。', zh: '暂无文章。' };
|
|
var allText = { ko: '전체', en: 'All', ja: 'すべて', zh: '全部' };
|
|
|
|
var allPosts = [];
|
|
var currentFilter = 'all';
|
|
|
|
function init() {
|
|
fetch('/devlog/index.json?t=' + Date.now())
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(posts) {
|
|
allPosts = posts;
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
buildFilters();
|
|
renderPosts();
|
|
})
|
|
.catch(function() {
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
grid.innerHTML = '<div class="blog-empty"><i class="fas fa-pen-fancy"></i><p>' + emptyText[lang] + '</p></div>';
|
|
});
|
|
}
|
|
|
|
function buildFilters() {
|
|
if (!filtersWrap || allPosts.length === 0) return;
|
|
|
|
var categories = {};
|
|
allPosts.forEach(function(p) {
|
|
var cat = (p.categories && p.categories[lang]) || (p.categories && p.categories.ko) || p.category || '';
|
|
if (cat) categories[cat] = true;
|
|
});
|
|
|
|
var cats = Object.keys(categories).sort();
|
|
if (cats.length < 2) { filtersWrap.style.display = 'none'; return; }
|
|
|
|
var html = '<button class="blog-filter-btn active" data-cat="all">' + allText[lang] + '</button>';
|
|
cats.forEach(function(c) {
|
|
html += '<button class="blog-filter-btn" data-cat="' + c + '">' + c + '</button>';
|
|
});
|
|
filtersWrap.innerHTML = html;
|
|
|
|
filtersWrap.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.blog-filter-btn');
|
|
if (!btn) return;
|
|
filtersWrap.querySelectorAll('.blog-filter-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
currentFilter = btn.getAttribute('data-cat');
|
|
renderPosts();
|
|
});
|
|
}
|
|
|
|
function getCategory(post) {
|
|
return (post.categories && post.categories[lang]) || (post.categories && post.categories.ko) || post.category || '';
|
|
}
|
|
|
|
function renderPosts() {
|
|
var filtered = currentFilter === 'all' ? allPosts : allPosts.filter(function(p) { return getCategory(p) === currentFilter; });
|
|
|
|
if (filtered.length === 0) {
|
|
grid.innerHTML = '<div class="blog-empty"><i class="fas fa-pen-fancy"></i><p>' + emptyText[lang] + '</p></div>';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
filtered.forEach(function(post) {
|
|
var title = (post.titles && post.titles[lang]) || (post.titles && post.titles.ko) || post.slug;
|
|
var desc = (post.descriptions && post.descriptions[lang]) || (post.descriptions && post.descriptions.ko) || '';
|
|
var cat = getCategory(post);
|
|
var thumb = post.thumbnail ? '/blog/posts/' + post.slug + '/' + post.thumbnail : '';
|
|
var url = prefix + '/devlog/' + post.slug;
|
|
var date = formatDate(post.date);
|
|
|
|
html += '<article class="blog-card">';
|
|
html += '<a href="' + url + '">';
|
|
if (thumb) {
|
|
html += '<img class="blog-card-thumb" src="' + thumb + '" alt="' + escapeHtml(title) + '" loading="lazy">';
|
|
} else {
|
|
html += '<div class="blog-card-thumb-placeholder"><i class="fas fa-pen-fancy"></i></div>';
|
|
}
|
|
html += '</a>';
|
|
html += '<div class="blog-card-body">';
|
|
if (cat) html += '<span class="blog-card-category">' + escapeHtml(cat) + '</span>';
|
|
html += '<h2 class="blog-card-title"><a href="' + url + '" style="color:inherit;text-decoration:none">' + escapeHtml(title) + '</a></h2>';
|
|
html += '<p class="blog-card-desc">' + escapeHtml(desc) + '</p>';
|
|
html += '<div class="blog-card-footer">';
|
|
html += '<span class="blog-card-date"><i class="far fa-calendar-alt"></i> ' + date + '</span>';
|
|
html += '<a href="' + url + '" class="blog-card-link">' + readMore[lang] + ' →</a>';
|
|
html += '</div></div></article>';
|
|
});
|
|
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
var d = new Date(dateStr + 'T00:00:00');
|
|
if (isNaN(d)) return dateStr;
|
|
var y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
|
|
if (lang === 'ko') return y + '.' + m + '.' + day;
|
|
if (lang === 'ja' || lang === 'zh') return y + '/' + m + '/' + day;
|
|
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
return months[m - 1] + ' ' + day + ', ' + y;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|