이미지 최적화 및 변환 작업 개선
메인: 홈 배경 WebP 추가 및 CSS 이미지 경로 전환 기술 역량: 솔루션 04 오브젝트 이미지 교체, 배치 설정 정리 및 썸네일 재생성 도구: WebP 누락 파일 우선 처리, 진행 표시, 파일명 필터와 강제 덮어쓰기 옵션 추가
BIN
assets/images-webp/home-background.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -376,7 +376,7 @@ body.mobile-navigation-open {
|
|||||||
background-image:
|
background-image:
|
||||||
linear-gradient(90deg, rgba(7, 9, 13, .94) 0%, rgba(7, 9, 13, .66) 38%, rgba(7, 9, 13, .18) 72%),
|
linear-gradient(90deg, rgba(7, 9, 13, .94) 0%, rgba(7, 9, 13, .66) 38%, rgba(7, 9, 13, .18) 72%),
|
||||||
linear-gradient(180deg, rgba(7, 9, 13, .1), rgba(7, 9, 13, .9)),
|
linear-gradient(180deg, rgba(7, 9, 13, .1), rgba(7, 9, 13, .9)),
|
||||||
url("../assets/images/home-background.png");
|
url("../assets/images-webp/home-background.webp");
|
||||||
animation: slow-zoom 18s ease-in-out infinite alternate;
|
animation: slow-zoom 18s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,14 +407,6 @@ GPS 기반의 위치 정보를 활용하여 대중교통의 이동 상황을 실
|
|||||||
output: "Web platform",
|
output: "Web platform",
|
||||||
image: "assets/images/solution/solution_04_background.png",
|
image: "assets/images/solution/solution_04_background.png",
|
||||||
objectImage: "assets/images/solution/solution_04_object.png",
|
objectImage: "assets/images/solution/solution_04_object.png",
|
||||||
objectLayout: {
|
|
||||||
x: "100%",
|
|
||||||
y: "calc(100% - 86px)",
|
|
||||||
anchorX: "-98%",
|
|
||||||
anchorY: "-100%",
|
|
||||||
height: "auto",
|
|
||||||
maxHeight: "none",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// P.32
|
// P.32
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 기술 역량 원본 이미지를 웹 전송용 WebP로 변환합니다.
|
// 기술 역량 원본 이미지를 웹 전송용 WebP로 변환합니다.
|
||||||
// 실행: node scripts/generate-capability-webp.mjs [--force]
|
// 실행: node scripts/generate-capability-webp.mjs [파일명 일부] [--force]
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import vm from "node:vm";
|
import vm from "node:vm";
|
||||||
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const force = process.argv.includes("--force");
|
const force = process.argv.includes("--force");
|
||||||
|
const filters = process.argv.slice(2).filter((argument) => !argument.startsWith("--"));
|
||||||
const context = { window: {} };
|
const context = { window: {} };
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(root, "js", "capabilities-data.js"), "utf8"), context);
|
vm.runInNewContext(fs.readFileSync(path.join(root, "js", "capabilities-data.js"), "utf8"), context);
|
||||||
|
|
||||||
@@ -34,21 +35,31 @@ function optimizedPath(source) {
|
|||||||
.replace(/\.(?:png|jpe?g)$/i, ".webp");
|
.replace(/\.(?:png|jpe?g)$/i, ".webp");
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourceBytes = 0;
|
const assets = Array.from(sources, ([relativeSource, type]) => {
|
||||||
let outputBytes = 0;
|
|
||||||
let generated = 0;
|
|
||||||
|
|
||||||
for (const [relativeSource, type] of sources) {
|
|
||||||
const source = path.join(root, relativeSource);
|
const source = path.join(root, relativeSource);
|
||||||
const target = path.join(root, optimizedPath(relativeSource));
|
const target = path.join(root, optimizedPath(relativeSource));
|
||||||
const sourceStat = fs.statSync(source);
|
const sourceStat = fs.statSync(source);
|
||||||
sourceBytes += sourceStat.size;
|
const targetStat = fs.existsSync(target) ? fs.statSync(target) : null;
|
||||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
return { relativeSource, type, source, target, sourceStat, targetStat };
|
||||||
|
}).filter(({ relativeSource }) =>
|
||||||
|
!filters.length || filters.some((filter) => relativeSource.includes(filter)));
|
||||||
|
|
||||||
if (!force && fs.existsSync(target) && fs.statSync(target).mtimeMs >= sourceStat.mtimeMs) {
|
if (!assets.length) {
|
||||||
outputBytes += fs.statSync(target).size;
|
throw new Error(`일치하는 기술 역량 이미지가 없습니다: ${filters.join(", ")}`);
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
// 삭제된 결과물을 먼저 복구하고, 나머지는 데이터에 정의된 순서대로 처리합니다.
|
||||||
|
assets.sort((a, b) => Number(Boolean(a.targetStat)) - Number(Boolean(b.targetStat)));
|
||||||
|
|
||||||
|
const pending = assets.filter(({ sourceStat, targetStat }) =>
|
||||||
|
force || !targetStat || targetStat.mtimeMs < sourceStat.mtimeMs);
|
||||||
|
let sourceBytes = assets.reduce((total, { sourceStat }) => total + sourceStat.size, 0);
|
||||||
|
let outputBytes = assets.reduce((total, asset) =>
|
||||||
|
pending.includes(asset) ? total : total + asset.targetStat.size, 0);
|
||||||
|
|
||||||
|
for (const [index, { relativeSource, type, source, target }] of pending.entries()) {
|
||||||
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
console.log(`[${index + 1}/${pending.length}] ${relativeSource}`);
|
||||||
|
|
||||||
const { width, height } = dimensions(source);
|
const { width, height } = dimensions(source);
|
||||||
const maxWidth = type === "background" ? 2560 : 2400;
|
const maxWidth = type === "background" ? 2560 : 2400;
|
||||||
@@ -61,11 +72,10 @@ for (const [relativeSource, type] of sources) {
|
|||||||
args.push(source, "-o", target);
|
args.push(source, "-o", target);
|
||||||
execFileSync("cwebp", args, { stdio: "inherit" });
|
execFileSync("cwebp", args, { stdio: "inherit" });
|
||||||
outputBytes += fs.statSync(target).size;
|
outputBytes += fs.statSync(target).size;
|
||||||
generated += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedPercent = Math.round((1 - outputBytes / sourceBytes) * 100);
|
const savedPercent = Math.round((1 - outputBytes / sourceBytes) * 100);
|
||||||
console.log(
|
console.log(
|
||||||
`${generated ? `Generated ${generated}` : "Checked"} WebP assets: ` +
|
`${pending.length ? `Generated ${pending.length}` : "Checked"} WebP assets: ` +
|
||||||
`${(sourceBytes / 1048576).toFixed(1)}MB -> ${(outputBytes / 1048576).toFixed(1)}MB (${savedPercent}% reduction)`,
|
`${(sourceBytes / 1048576).toFixed(1)}MB -> ${(outputBytes / 1048576).toFixed(1)}MB (${savedPercent}% reduction)`,
|
||||||
);
|
);
|
||||||
|
|||||||