Build MOBIPINTECH company website

This commit is contained in:
2026-09-21 13:52:16 +09:00
commit 39fefc5834
226 changed files with 6803 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
// macOS에서 실행: node scripts/generate-capability-thumbnails.mjs
// 목록용 이미지 축소본만 생성하며 원본과 프로젝트 데이터는 변경하지 않음.
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const context = { window: {} };
vm.runInNewContext(fs.readFileSync(path.join(root, "js/capabilities-data.js"), "utf8"), context);
const images = new Set(context.window.CAPABILITY_PROJECTS.flatMap((item) =>
[item.image, item.objectImage, item.objectImageSecondary].filter(Boolean)));
let bytes = 0;
for (const source of images) {
const target = path.join(root, source.replace("assets/images/", "assets/thumbnails/"));
fs.mkdirSync(path.dirname(target), { recursive: true });
execFileSync("sips", ["-Z", "320", path.join(root, source), "--out", target], { stdio: "pipe" });
bytes += fs.statSync(target).size;
}
console.log(`Generated ${images.size} thumbnails (${Math.round(bytes / 1024)} KB total).`);

View File

@@ -0,0 +1,71 @@
// 기술 역량 원본 이미지를 웹 전송용 WebP로 변환합니다.
// 실행: node scripts/generate-capability-webp.mjs [--force]
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const force = process.argv.includes("--force");
const context = { window: {} };
vm.runInNewContext(fs.readFileSync(path.join(root, "js", "capabilities-data.js"), "utf8"), context);
const sources = new Map();
for (const item of context.window.CAPABILITY_PROJECTS) {
if (item.image) sources.set(item.image, "background");
if (item.objectImage) sources.set(item.objectImage, "object");
if (item.objectImageSecondary) sources.set(item.objectImageSecondary, "object");
}
function dimensions(source) {
const output = execFileSync("sips", ["-g", "pixelWidth", "-g", "pixelHeight", source], {
encoding: "utf8",
});
return {
width: Number(output.match(/pixelWidth: (\d+)/)?.[1]),
height: Number(output.match(/pixelHeight: (\d+)/)?.[1]),
};
}
function optimizedPath(source) {
return source
.replace("assets/images/", "assets/images-webp/")
.replace(/\.(?:png|jpe?g)$/i, ".webp");
}
let sourceBytes = 0;
let outputBytes = 0;
let generated = 0;
for (const [relativeSource, type] of sources) {
const source = path.join(root, relativeSource);
const target = path.join(root, optimizedPath(relativeSource));
const sourceStat = fs.statSync(source);
sourceBytes += sourceStat.size;
fs.mkdirSync(path.dirname(target), { recursive: true });
if (!force && fs.existsSync(target) && fs.statSync(target).mtimeMs >= sourceStat.mtimeMs) {
outputBytes += fs.statSync(target).size;
continue;
}
const { width, height } = dimensions(source);
const maxWidth = type === "background" ? 2560 : 2400;
const maxHeight = type === "background" ? 1800 : 2400;
const scale = Math.min(1, maxWidth / width, maxHeight / height);
const args = ["-quiet", "-mt", "-m", "6", "-q", type === "background" ? "80" : "86"];
if (type === "background") args.push("-sharp_yuv");
else args.push("-alpha_q", "100", "-exact");
if (scale < 1) args.push("-resize", String(Math.round(width * scale)), String(Math.round(height * scale)));
args.push(source, "-o", target);
execFileSync("cwebp", args, { stdio: "inherit" });
outputBytes += fs.statSync(target).size;
generated += 1;
}
const savedPercent = Math.round((1 - outputBytes / sourceBytes) * 100);
console.log(
`${generated ? `Generated ${generated}` : "Checked"} WebP assets: ` +
`${(sourceBytes / 1048576).toFixed(1)}MB -> ${(outputBytes / 1048576).toFixed(1)}MB (${savedPercent}% reduction)`,
);

View File

@@ -0,0 +1,102 @@
// 수동 실행: new_home v1 폴더에서 node scripts/sync-capabilities-seo.mjs
// capabilities-data.js의 내용을 capabilities.html의 정적 SEO 영역에 반영함.
// 브라우저에서는 실행되지 않으며, 프로젝트 내용을 수정한 뒤 배포 전에 실행.
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const dataPath = path.join(root, "js", "capabilities-data.js");
const htmlPath = path.join(root, "capabilities.html");
const context = { window: {} };
vm.createContext(context);
vm.runInContext(fs.readFileSync(dataPath, "utf8"), context);
const projects = context.window.CAPABILITY_PROJECTS || [];
const projectIds = new Set();
for (const item of projects) {
if (!item.id || projectIds.has(item.id)) {
throw new Error(`Missing or duplicate project ID: ${item.id}`);
}
projectIds.add(item.id);
for (const field of ["image", "objectImage", "objectImageSecondary"]) {
if (item[field] && !fs.existsSync(path.resolve(root, item[field]))) {
throw new Error(`Missing capabilities image (${item.id}): ${item[field]}`);
}
}
}
const groups = [
["automotive", "Automotive 전장 기술 역량"],
["robotics", "Robotics 로봇 기술 역량"],
["solution", "Solution 솔루션 기술 역량"],
["si", "SI 웹·앱 개발 사례"],
["certifications", "Certifications 특허 기술 역량"],
];
function escapeHtml(value = "") {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function normalizeText(value = "") {
return String(value).replace(/\s+/g, " ").trim();
}
function renderProject(item) {
const subtitle = item.subtitle
? `\n <p>${escapeHtml(item.subtitle)}</p>`
: "";
const description = item.layout === "table"
? "자동차 전장 개발 및 컨설팅 이력을 사업명과 사업기간 기준으로 정리한 기타 수행 프로젝트 목록입니다."
: normalizeText(item.description);
const tableRows = item.tableRows
? `\n <ul>${item.tableRows.map((row) => `<li>${escapeHtml(row[0])} · ${escapeHtml(row[1])}</li>`).join("")}</ul>`
: "";
return ` <article id="seo-${escapeHtml(item.id)}">
<h4>${escapeHtml(item.title)}</h4>${subtitle}
<p>${escapeHtml(description)}</p>${tableRows}
</article>`;
}
const sections = groups.map(([type, title]) => {
const items = projects.filter((item) => item.type === type);
return ` <section id="seo-${type}" class="capabilities-seo-group">
<h3>${escapeHtml(title)}</h3>
${items.map(renderProject).join("\n")}
</section>`;
}).join("\n");
const block = `<!-- CAPABILITIES_SEO_START -->
<section class="capabilities-seo" aria-labelledby="capabilities-seo-title">
<h2 id="capabilities-seo-title">모비핀테크놀러지 기술 역량</h2>
<p>
모비핀테크놀러지는 자동차 전장, 로봇, 솔루션, SI 영역에서 실제 운영 환경에 적용되는 소프트웨어를 개발합니다.
</p>
${sections}
</section>
<!-- CAPABILITIES_SEO_END -->`;
const html = fs.readFileSync(htmlPath, "utf8");
const markerPattern = /<!-- CAPABILITIES_SEO_START -->[\s\S]*?<!-- CAPABILITIES_SEO_END -->/;
if (!markerPattern.test(html)) {
throw new Error("SEO marker block was not found in capabilities.html");
}
const next = html.replace(
markerPattern,
block,
);
if (next !== html) {
fs.writeFileSync(htmlPath, next);
console.log(`Updated capabilities.html SEO content (${projects.length} projects).`);
} else {
console.log(`SEO content is already up to date (${projects.length} projects).`);
}