designpaca/apps/site/tools/polite.mjs
Yun Chan 30dc6687dd
All checks were successful
ci / build (push) Successful in 18s
feat(site): 쇼케이스를 별도 페이지로 분리하고 본문을 존댓말로 바꾼다
홈에 있던 쇼케이스 섹션은 스크린샷 두 장을 카드에 넣고 각도를 준 것이었고,
눌러도 갈 곳이 없었다. 그림만 보여주고 끝나는 자리는 증거가 아니라 장식이다.

- /showcase 를 별도 페이지로 만들었다. 모자이크 그리드에 썸네일을 놓고
  누르면 실제로 도는 사이트(/work/<slug>/)가 열린다.
- 사례 데이터를 src/data/work.ts 한 곳에 모았다. 항목 하나만 추가하면
  그리드에 들어간다 — 페이지 코드는 건드리지 않는다.
- 첫 사례로 목요일의 화원(플로럴 스튜디오)을 넣고 실제 파일을
  public/work/thursday-flowers/ 에 함께 싣는다.
- 홈에서 예전 섹션을 제거하고 내비를 /showcase 로 돌렸다.

본문 말투:

  사이트 문장이 전부 반말이었고 일부는 내부 감사 로그처럼 읽혔다.
  스킬 문서의 단정한 반말은 의도지만, 방문자를 향한 화면은 다르다.
  제목은 단정문이 카피로서 힘을 가지므로 그대로 두고, 설명하는 문장과
  명령형("~해라")을 전부 존댓말로 바꿨다. 실측 결과 홈·쇼케이스 모두 0건.

구현에서 걸린 것:

  Base 레이아웃이 이미 <main class="page"> 를 제공하는데 새 페이지에서
  또 감쌌다. 바깥 page 의 본문 칼럼(561px) 안에 안쪽 page 가 들어가
  전 페이지가 좁아졌다 — 모자이크가 561px, 썸네일이 240px 이었다.
  중첩을 걷어내자 1180px / 596px 로 돌아왔다.
2026-08-21 08:36:35 +09:00

186 lines
6.6 KiB
JavaScript

/**
* 방문자에게 보이는 한국어 본문을 존댓말로 바꾼다.
*
* 제목(h1·h2·name·rule·caught)은 건드리지 않는다 — 단정문이 카피로서 힘을 갖는 자리다.
* 바꾸는 것은 **설명하는 문장**과 **명령형**이다.
* 방문자에게 "~해라" 라고 말하는 화면은 무례하다.
*/
import fs from "node:fs";
const FILE = "src/i18n/content.ts";
/** 이 키의 값만 바꾼다. 나머지는 그대로 둔다 */
const TARGET_KEYS = new Set([
"lead", "note", "how", "problem", "use", "what", "does", "becomes", "made", "alt", "sample",
"why", "fixed", "caught", "caveat", "installNote", "description", "verdict", "caption",
]);
/** 순서가 중요하다. 긴 패턴을 먼저 잡지 않으면 짧은 것이 먼저 먹는다 */
const RULES = [
// ── 명령형 — 방문자에게 명령하지 않는다 ──
[/해라\./g, "하세요."], [/해라$/g, "하세요"],
[/봐라\./g, "보세요."], [/봐라$/g, "보세요"],
[/마라\./g, "마세요."], [/마라$/g, "마세요"],
[/써라\./g, "쓰세요."], [/써라$/g, "쓰세요"],
[/가라\./g, "가세요."], [/골라라/g, "고르세요"],
// ── 서술 ──
[/하지 않는다/g, "하지 않습니다"],
[/되지 않는다/g, "되지 않습니다"],
[/지 않는다/g, "지 않습니다"],
[/않는다/g, "않습니다"],
[/것이 아니다/g, "것이 아닙니다"],
[/아니다/g, "아닙니다"],
[/것이다/g, "것입니다"],
[/뿐이다/g, "뿐입니다"],
[/때문이다/g, "때문입니다"],
[/없어진다/g, "없어집니다"],
[/사라진다/g, "사라집니다"],
[/달라진다/g, "달라집니다"],
[/남는다/g, "남습니다"],
[/걸린다/g, "걸립니다"],
[/나온다/g, "나옵니다"],
[/들어간다/g, "들어갑니다"],
[/올라간다/g, "올라갑니다"],
[/떨어진다/g, "떨어집니다"],
[/무너진다/g, "무너집니다"],
[/바뀐다/g, "바뀝니다"],
[/이어진다/g, "이어집니다"],
[/생긴다/g, "생깁니다"],
[/보인다/g, "보입니다"],
[/읽힌다/g, "읽힙니다"],
[/잡힌다/g, "잡힙니다"],
[/쓰인다/g, "쓰입니다"],
[/맡긴다/g, "맡깁니다"],
[/만든다/g, "만듭니다"],
[/막는다/g, "막습니다"],
[/줄인다/g, "줄입니다"],
[/센다/g, "셉니다"],
[/짠다/g, "짭니다"],
[/뺀다/g, "뺍니다"],
[/둔다/g, "둡니다"],
[/한다/g, "합니다"],
[/된다/g, "됩니다"],
[/있다/g, "있습니다"],
[/없다/g, "없습니다"],
[/같다/g, "같습니다"],
[/좁다/g, "좁습니다"],
[/넓다/g, "넓습니다"],
[/맞다/g, "맞습니다"],
[/낫다/g, "낫습니다"],
[/멈춘다/g, "멈춥니다"],
[/이긴다/g, "이깁니다"],
[/모른다/g, "모릅니다"],
[/받는다/g, "받습니다"],
[/걸었다/g, "걸었습니다"],
[/바꿨다/g, "바꿨습니다"],
[/했다/g, "했습니다"],
[/였다/g, "였습니다"],
[/었다/g, "었습니다"],
[/았다/g, "았습니다"],
[/린다/g, "립니다"],
[/난다/g, "납니다"],
[/쓴다/g, "씁니다"],
[/본다/g, "봅니다"],
[/간다/g, "갑니다"],
[/온다/g, "옵니다"],
[/골라준다/g, "골라줍니다"],
[/고른다/g, "고릅니다"],
[/넘긴다/g, "넘깁니다"],
[/채운다/g, "채웁니다"],
[/압축한다/g, "압축합니다"],
[/판정한다/g, "판정합니다"],
[/정한다/g, "정합니다"],
[/확정한다/g, "확정합니다"],
[/구분한다/g, "구분합니다"],
[/합성한다/g, "합성합니다"],
[/내린다/g, "내립니다"],
[/열린다/g, "열립니다"],
[/붙는다/g, "붙습니다"],
[/넣는다/g, "넣습니다"],
[/놓는다/g, "놓습니다"],
[/찾는다/g, "찾습니다"],
[/맞춘다/g, "맞춥니다"],
[/갖춘다/g, "갖춥니다"],
[/낮춘다/g, "낮춥니다"],
[/높인다/g, "높입니다"],
[/모은다/g, "모읍니다"],
[/버린다/g, "버립니다"],
[/살린다/g, "살립니다"],
[/시킨다/g, "시킵니다"],
[/지킨다/g, "지킵니다"],
[/적는다/g, "적습니다"],
[/삼는다/g, "삼습니다"],
[/남긴다/g, "남깁니다"],
[/옮긴다/g, "옮깁니다"],
[/뒤집는다/g, "뒤집습니다"],
[/따른다/g, "따릅니다"],
[/부른다/g, "부릅니다"],
[/누른다/g, "누릅니다"],
[/자른다/g, "자릅니다"],
[/오른다/g, "오릅니다"],
[/미룬다/g, "미룹니다"],
[/켠다/g, "켭니다"],
[/편다/g, "폅니다"],
[/센다/g, "셉니다"],
[/건다/g, "겁니다"],
[/푼다/g, "풉니다"],
[/꾼다/g, "꿉니다"],
[/띈다/g, "띕니다"],
[/진다/g, "집니다"],
[/킨다/g, "킵니다"],
[/힌다/g, "힙니다"],
[/빈다/g, "빕니다"],
[/친다/g, "칩니다"],
[/잘라낸다/g, "잘라냅니다"],
[/잘린다/g, "잘립니다"],
[/뭉개진다/g, "뭉개집니다"],
[/건너뛴다/g, "건너뜁니다"],
[/깎는다/g, "깎습니다"],
[/죽는다/g, "죽습니다"],
[/잰다/g, "잽니다"],
[/낸다/g, "냅니다"],
[/긴다/g, "깁니다"],
[/없앴다/g, "없앴습니다"],
[/걷어냈다/g, "걷어냈습니다"],
[/남겼다/g, "남겼습니다"],
[/뿌렸다/g, "뿌렸습니다"],
[/넘쳤다/g, "넘쳤습니다"],
[/늘렸다/g, "늘렸습니다"],
[/신호다/g, "신호입니다"],
[/결과다/g, "결과입니다"],
[/길다/g, "깁니다"],
[/그대로였다/g, "그대로였습니다"],
[/보였다/g, "보였습니다"],
[/튀고 있었다/g, "튀고 있었습니다"],
[/어긋나 있었다/g, "어긋나 있었습니다"],
[/적었는데/g, "적었는데"],
];
function polite(text) {
let out = text;
for (const [re, to] of RULES) out = out.replace(re, to);
return out;
}
const src = fs.readFileSync(FILE, "utf8");
// STAGES_KO 는 `export const ko` 밖에 따로 선언돼 있다. 범위를 거기서 시작하지 않으면
// 파이프라인 여섯 단계의 설명이 통째로 빠진다 — 실측에서 37개가 반말로 남아 있었다.
const koStart = src.indexOf("const STAGES_KO");
const koEnd = src.indexOf("const STAGES_EN");
const koStart2 = src.indexOf("export const ko");
const koEnd2 = src.indexOf("export const en");
let ko = src.slice(koStart, koEnd);
let ko2 = src.slice(koStart2, koEnd2);
let changed = 0;
// `key: "..."` 와 `key:\n "..."` 를 모두 잡는다
ko = ko.replace(/(\b[a-zA-Z]+):(\s*)"((?:[^"\\]|\\.)*)"/g, (m, key, gap, val) => {
if (!TARGET_KEYS.has(key)) return m;
const next = polite(val);
if (next !== val) changed++;
return `${key}:${gap}"${next}"`;
});
fs.writeFileSync(FILE, src.slice(0, koStart) + ko + src.slice(koEnd), "utf8");
console.log(`존댓말 변환: ${changed}개 문자열`);