feat(site): 쇼케이스를 별도 페이지로 분리하고 본문을 존댓말로 바꾼다
All checks were successful
ci / build (push) Successful in 18s
홈에 있던 쇼케이스 섹션은 스크린샷 두 장을 카드에 넣고 각도를 준 것이었고,
눌러도 갈 곳이 없었다. 그림만 보여주고 끝나는 자리는 증거가 아니라 장식이다.
- /showcase 를 별도 페이지로 만들었다. 모자이크 그리드에 썸네일을 놓고
누르면 실제로 도는 사이트(/work/<slug>/)가 열린다.
- 사례 데이터를 src/data/work.ts 한 곳에 모았다. 항목 하나만 추가하면
그리드에 들어간다 — 페이지 코드는 건드리지 않는다.
- 첫 사례로 목요일의 화원(플로럴 스튜디오)을 넣고 실제 파일을
public/work/thursday-flowers/ 에 함께 싣는다.
- 홈에서 예전 섹션을 제거하고 내비를 /showcase 로 돌렸다.
본문 말투:
사이트 문장이 전부 반말이었고 일부는 내부 감사 로그처럼 읽혔다.
스킬 문서의 단정한 반말은 의도지만, 방문자를 향한 화면은 다르다.
제목은 단정문이 카피로서 힘을 가지므로 그대로 두고, 설명하는 문장과
명령형("~해라")을 전부 존댓말로 바꿨다. 실측 결과 홈·쇼케이스 모두 0건.
구현에서 걸린 것:
Base 레이아웃이 이미 <main class="page"> 를 제공하는데 새 페이지에서
또 감쌌다. 바깥 page 의 본문 칼럼(561px) 안에 안쪽 page 가 들어가
전 페이지가 좁아졌다 — 모자이크가 561px, 썸네일이 240px 이었다.
중첩을 걷어내자 1180px / 596px 로 돌아왔다.
BIN
apps/site/public/work/thursday-flowers/assets/flower-01.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
apps/site/public/work/thursday-flowers/assets/flower-02.webp
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
apps/site/public/work/thursday-flowers/assets/flower-03.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/site/public/work/thursday-flowers/assets/flower-04.webp
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
apps/site/public/work/thursday-flowers/assets/flower-05.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
301
apps/site/public/work/thursday-flowers/index.html
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>목요일의 화원 · 플로럴 스튜디오</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="한 주에 한 번, 목요일에만 문을 여는 플로럴 스튜디오. 그 주에 좋은 꽃만 들여 주문받은 만큼만 만듭니다. 서울 서촌."
|
||||||
|
/>
|
||||||
|
<meta property="og:title" content="목요일의 화원 · 플로럴 스튜디오" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="한 주에 한 번, 목요일에만 문을 여는 플로럴 스튜디오. 서울 서촌."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:image" content="./assets/flower-01.webp" />
|
||||||
|
<meta name="theme-color" content="#f8f6f0" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||||
|
<!-- 한글 세리프. 라틴 세리프만 지정하면 한글이 시스템 기본으로 떨어져 전부 무너진다 -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="./styles/main.css" />
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E🌿%3C/text%3E%3C/svg%3E" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a class="skip" href="#main">본문으로 건너뛰기</a>
|
||||||
|
|
||||||
|
<header class="site-head wrap">
|
||||||
|
<a class="wordmark" href="#main">
|
||||||
|
<span>목요일의</span> <span>화원</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="섹션">
|
||||||
|
<a href="#work">작업</a>
|
||||||
|
<a href="#thursday">목요일</a>
|
||||||
|
<a href="#subscribe">정기</a>
|
||||||
|
<a href="#visit">찾아오기</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<!--
|
||||||
|
히어로. 큰 사진 한 장을 깔지 않는다 — 그게 이 카테고리의 기본값이다.
|
||||||
|
크기도 비율도 다른 사진 넷을 공통 정렬선 없이 흩뿌리고,
|
||||||
|
그 사이 여백에 글자를 세로로 세운다. (R1: sarahwinward.com)
|
||||||
|
-->
|
||||||
|
<section class="hero" aria-labelledby="hero-h">
|
||||||
|
<div class="hero-inner wrap">
|
||||||
|
<div class="hero-text">
|
||||||
|
<p class="vertical hero-role reveal">플로럴 스튜디오 / 서울 서촌 / 2019</p>
|
||||||
|
|
||||||
|
<div class="hero-body">
|
||||||
|
<h1 id="hero-h" class="hero-h reveal">
|
||||||
|
그 주에 좋은 꽃만<br />들입니다
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="lead hero-lead reveal" style="--delay: 120ms">
|
||||||
|
목요일 아침에 시장에서 그 주의 꽃을 고르고, 주문받은 만큼만 만듭니다.
|
||||||
|
남는 꽃이 없어서 시들 일도 없습니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn hero-cta reveal" href="#contact" style="--delay: 200ms">
|
||||||
|
상담 신청하기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-photos">
|
||||||
|
<figure class="ph ph-a reveal" style="--delay: 80ms">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-01.webp"
|
||||||
|
alt="흰 라넌큘러스와 작약, 유칼립투스로 묶은 꽃다발"
|
||||||
|
width="1086"
|
||||||
|
height="1448"
|
||||||
|
loading="eager"
|
||||||
|
fetchpriority="high"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<figure class="ph ph-b reveal" style="--delay: 260ms">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-03.webp"
|
||||||
|
alt="꽃 한 송이의 꽃잎 결이 보이는 접사"
|
||||||
|
width="1086"
|
||||||
|
height="1448"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<figure class="ph ph-c reveal" style="--delay: 340ms">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-02.webp"
|
||||||
|
alt="작업대 위에 놓인 꽃 줄기와 가위, 노끈"
|
||||||
|
width="1448"
|
||||||
|
height="1086"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<p class="vertical hero-tags reveal" style="--delay: 420ms">
|
||||||
|
주문 제작 / 정기 구독 / 공간 장식
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 왜 목요일인가 — 이름이 곧 운영 방식이라는 것을 여기서 푼다 -->
|
||||||
|
<section id="thursday" class="section band" aria-labelledby="thursday-h">
|
||||||
|
<div class="wrap two">
|
||||||
|
<div class="two-text">
|
||||||
|
<h2 id="thursday-h">
|
||||||
|
일주일에 하루만 문을 엽니다
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
수요일 밤에 경매가 끝나고, 목요일 새벽 시장에 그 주의 꽃이 나옵니다.
|
||||||
|
그날 좋은 것만 골라 그날 만듭니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
나머지 엿새는 만들지 않습니다. 미리 사다 두면 파는 날까지 꽃이 기다려야
|
||||||
|
하고, 기다린 꽃은 이미 한 번 시든 꽃입니다.
|
||||||
|
</p>
|
||||||
|
<dl class="facts">
|
||||||
|
<div>
|
||||||
|
<dt>여는 날</dt>
|
||||||
|
<dd>매주 목요일 10:00–19:00</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>주문 마감</dt>
|
||||||
|
<dd>수요일 18:00</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>그 주 제작 수</dt>
|
||||||
|
<dd>최대 20건</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<figure class="two-photo">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-05.webp"
|
||||||
|
alt="꽃 줄기를 가위로 다듬는 손"
|
||||||
|
width="1086"
|
||||||
|
height="1448"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption class="label">목요일 아침, 손질</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 작업 — 사진이 설득의 주체다. 크기 차이가 리듬이다 -->
|
||||||
|
<section id="work" class="section" aria-labelledby="work-h">
|
||||||
|
<div class="wrap">
|
||||||
|
<p class="label">작업</p>
|
||||||
|
<h2 id="work-h">같은 꽃을 두 번 쓰지 않습니다</h2>
|
||||||
|
<p class="lead">
|
||||||
|
주에 따라 들어오는 꽃이 다르니 결과도 매번 다릅니다. 지난 작업을 그대로
|
||||||
|
다시 만들어 달라는 요청은 받지 않습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="wrap gallery">
|
||||||
|
<figure class="g g-1">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-04.webp"
|
||||||
|
alt="창가 자연광이 드는 작업실, 양철 양동이에 담긴 꽃들과 나무 선반"
|
||||||
|
width="1600"
|
||||||
|
height="900"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>작업실 · 서촌</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="g g-2">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-01.webp"
|
||||||
|
alt="흰색과 크림색 위주로 묶은 꽃다발"
|
||||||
|
width="1086"
|
||||||
|
height="1448"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>화이트 온 화이트 · 6월</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="g g-3">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-02.webp"
|
||||||
|
alt="꽃 줄기와 가위, 노끈이 놓인 작업대를 위에서 본 모습"
|
||||||
|
width="1448"
|
||||||
|
height="1086"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption>손질 전 · 목요일 오전</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="wrap">
|
||||||
|
<p class="muted work-note">
|
||||||
|
더 많은 작업은
|
||||||
|
<a class="link" href="https://instagram.com" rel="noopener">인스타그램</a>에
|
||||||
|
주 단위로 올립니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 정기구독. 주 CTA(상담)와 경쟁하지 않게 아래쪽에 조용히 둔다 -->
|
||||||
|
<section id="subscribe" class="section band" aria-labelledby="sub-h">
|
||||||
|
<div class="wrap two two-reverse">
|
||||||
|
<div class="two-text">
|
||||||
|
<h2 id="sub-h">격주 목요일, 같은 자리에</h2>
|
||||||
|
<p>
|
||||||
|
사무실 응접실이나 매장 카운터처럼 꽃이 늘 있어야 하는 자리를 위한
|
||||||
|
것입니다. 격주로 그 주의 꽃을 가져다 놓고, 지난 것을 가져옵니다.
|
||||||
|
</p>
|
||||||
|
<p class="muted">
|
||||||
|
화병은 저희 것을 두고 씁니다. 자리와 조도를 먼저 보고 정하기 때문에
|
||||||
|
첫 회는 방문 상담으로 시작합니다.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-quiet" href="#contact">정기 상담 신청</a>
|
||||||
|
</div>
|
||||||
|
<figure class="two-photo">
|
||||||
|
<img
|
||||||
|
src="./assets/flower-03.webp"
|
||||||
|
alt="꽃잎의 결이 보이는 접사"
|
||||||
|
width="1086"
|
||||||
|
height="1448"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<figcaption class="label">이번 주 · 라넌큘러스</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 문의 — 이 페이지의 유일한 주 CTA가 도착하는 곳 -->
|
||||||
|
<section id="contact" class="section contact" aria-labelledby="contact-h">
|
||||||
|
<div class="wrap contact-inner">
|
||||||
|
<p class="label">문의</p>
|
||||||
|
<h2 id="contact-h">무엇에 쓸 꽃인지부터 듣습니다</h2>
|
||||||
|
<p class="lead">
|
||||||
|
받는 분과 자리, 예산을 알려주시면 그 주에 가능한 것을 먼저 말씀드립니다.
|
||||||
|
가능하지 않은 주에는 그렇다고 말씀드립니다.
|
||||||
|
</p>
|
||||||
|
<ul class="contact-list">
|
||||||
|
<li>
|
||||||
|
<span class="label">전화</span>
|
||||||
|
<a class="link" href="tel:0212345678">02-1234-5678</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="label">메일</span>
|
||||||
|
<a class="link" href="mailto:hello@thursday.flowers"
|
||||||
|
>hello@thursday.flowers</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="label">인스타그램</span>
|
||||||
|
<a class="link" href="https://instagram.com" rel="noopener"
|
||||||
|
>@thursday.flowers</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 찾아오기. 푸터를 따로 두지 않고 마지막 섹션이 그 역할을 한다 -->
|
||||||
|
<section id="visit" class="section visit" aria-labelledby="visit-h">
|
||||||
|
<div class="wrap visit-inner">
|
||||||
|
<div>
|
||||||
|
<h2 id="visit-h">서촌, 목요일에만</h2>
|
||||||
|
<address class="visit-address">
|
||||||
|
서울 종로구 자하문로 00길 00, 2층<br />
|
||||||
|
경복궁역 3번 출구에서 도보 8분
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
<p class="visit-note muted">
|
||||||
|
목요일 외에는 상주하지 않습니다. 다른 요일 방문은 미리 연락 주세요.
|
||||||
|
</p>
|
||||||
|
<p class="vertical visit-mark">목요일의 화원</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*
|
||||||
|
등장 처리만 한다. 스크롤 이벤트는 쓰지 않는다 —
|
||||||
|
매 프레임 레이아웃을 강제하고 모바일에서 프레임이 무너진다.
|
||||||
|
한 번 보이면 유지한다: 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 아니다.
|
||||||
|
*/
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.isIntersecting) continue;
|
||||||
|
e.target.classList.add("is-in");
|
||||||
|
io.unobserve(e.target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
|
||||||
|
);
|
||||||
|
for (const el of document.querySelectorAll(".reveal")) io.observe(el);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
602
apps/site/public/work/thursday-flowers/styles/main.css
Normal file
|
|
@ -0,0 +1,602 @@
|
||||||
|
@import url("./tokens.css");
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────────────────
|
||||||
|
리셋 — 필요한 것만
|
||||||
|
──────────────────────────────────────────────────────── */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--step-body);
|
||||||
|
line-height: var(--leading-body);
|
||||||
|
font-weight: var(--weight-regular);
|
||||||
|
/* 종이 질감. 에디토리얼 프리셋의 기본 재질이고, 아주 약해야 한다.
|
||||||
|
세게 걸면 화면이 지저분해지고 사진의 디테일과 싸운다. */
|
||||||
|
background-image: url("#");
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
picture {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
/* CLS 를 막으려고 HTML 에 width/height 속성을 적어두면, height 속성이
|
||||||
|
그대로 계산된 높이가 되어 **aspect-ratio 가 무시된다.**
|
||||||
|
실측: aspect-ratio 는 "3 / 4" 로 적용돼 있는데 높이는 원본 1448px 이었다.
|
||||||
|
height:auto 를 줘야 비율이 높이를 정한다. */
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 한글 조판. 서구 레퍼런스에는 이 정보가 없어서 직접 정한다.
|
||||||
|
keep-all 이 없으면 어절 한가운데서 줄이 바뀐다. */
|
||||||
|
:where(p, li, h1, h2, h3, figcaption, dd, dt) {
|
||||||
|
word-break: keep-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 키보드 사용자를 위한 것. 어떤 이펙트보다 우선한다 */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
top: 0;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.skip:focus {
|
||||||
|
left: var(--gutter);
|
||||||
|
top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────────────────
|
||||||
|
타이포그래피 — 크기 4종, 웨이트 2종. 그 이상 늘리지 않는다
|
||||||
|
──────────────────────────────────────────────────────── */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: var(--weight-regular); /* 큰 글자일수록 가늘게 */
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: var(--track-display);
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: var(--step-display);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.3rem);
|
||||||
|
letter-spacing: var(--track-lead);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: var(--track-lead);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
font-size: var(--step-lead);
|
||||||
|
letter-spacing: var(--track-lead);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 작은 라벨만 굵고 자간이 넓다. 크기 위계의 반대쪽 끝이다 */
|
||||||
|
.label {
|
||||||
|
font-size: var(--step-label);
|
||||||
|
font-weight: var(--weight-bold);
|
||||||
|
letter-spacing: var(--track-label);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-transform: none; /* 한글에 uppercase 는 아무 효과가 없고 라틴만 튄다 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 제목 다음에 오는 것은 반드시 떨어진다.
|
||||||
|
h2 는 margin: 0 이고 p 도 margin-top: 0 이라 그냥 두면 붙는다. */
|
||||||
|
h1 + *,
|
||||||
|
h2 + *,
|
||||||
|
h3 + * {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────────────────
|
||||||
|
레이아웃
|
||||||
|
──────────────────────────────────────────────────────── */
|
||||||
|
.wrap {
|
||||||
|
max-width: var(--container);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--gutter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 세로로 세운 글자. R1 이 여백을 이렇게 쓴다 —
|
||||||
|
가로 공간을 먹지 않으면서 빈 곳에 정보를 흘려 넣는다. */
|
||||||
|
.vertical {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
font-size: var(--step-label);
|
||||||
|
font-weight: var(--weight-bold);
|
||||||
|
/* 가로쓰기 라벨의 자간(0.14em)을 그대로 쓰면 안 된다.
|
||||||
|
세로쓰기에서 자간은 **글자 사이 세로 간격**이 되어 낱글자가 흩어진다.
|
||||||
|
한글은 자소가 모여 한 글자라 특히 심하다. */
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 링크: 밑줄이 글자를 침범하지 않게 ── */
|
||||||
|
.link {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 0.25em;
|
||||||
|
transition: text-decoration-color 250ms var(--ease-out);
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
text-decoration-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 버튼: 하나뿐인 주 CTA ── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0 var(--space-5);
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
font-size: var(--step-body);
|
||||||
|
font-weight: var(--weight-regular);
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--ink);
|
||||||
|
border-radius: 0; /* 이 페이지의 모서리는 전부 각지다. 하나만 둥글면 그게 튄다 */
|
||||||
|
transition:
|
||||||
|
background 250ms var(--ease-out),
|
||||||
|
color 250ms var(--ease-out);
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.btn-quiet {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.btn-quiet:hover {
|
||||||
|
border-color: var(--ink);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────────────────
|
||||||
|
등장 — 페이드와 10px 상승뿐.
|
||||||
|
R1 의 스크롤 이동량이 0px 이었다. 하이엔드는 움직이지 않는다.
|
||||||
|
──────────────────────────────────────────────────────── */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(var(--rise));
|
||||||
|
transition:
|
||||||
|
opacity var(--dur-slow) var(--ease-out),
|
||||||
|
transform var(--dur-slow) var(--ease-out);
|
||||||
|
transition-delay: var(--delay, 0ms);
|
||||||
|
}
|
||||||
|
.reveal.is-in {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════
|
||||||
|
섹션별 레이아웃
|
||||||
|
════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── 헤더 ─────────────────────────────────────────────
|
||||||
|
워드마크 좌상단, 내비 우상단. R1 이 이렇게 놓는다.
|
||||||
|
고정하지 않는다 — 에디토리얼에서 떠다니는 바는 읽기를 방해한다. */
|
||||||
|
.site-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding-top: var(--space-5);
|
||||||
|
}
|
||||||
|
/* 넓은 화면에서는 두 줄, 좁은 화면에서는 한 줄로 쓴다.
|
||||||
|
<br> 을 display:none 으로 지우면 줄만 붙는 게 아니라 **공백까지 사라져**
|
||||||
|
"목요일의화원" 이 된다. span 두 개로 두고 display 를 바꾼다. */
|
||||||
|
.wordmark span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.wordmark {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
font-size: var(--step-label);
|
||||||
|
font-weight: var(--weight-bold);
|
||||||
|
letter-spacing: var(--track-label);
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
transition: color 250ms var(--ease-out);
|
||||||
|
}
|
||||||
|
.nav a:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 히어로 ───────────────────────────────────────────
|
||||||
|
리스크를 감수한 자리다. 큰 사진 한 장 대신 셋을 흩뿌린다.
|
||||||
|
|
||||||
|
그리드를 12칸으로 두되 각 사진이 **서로 다른 칸에서 시작해 다른 칸에서 끝난다.**
|
||||||
|
R1 을 실측했더니 이미지 6개의 좌측 정렬선이 6개 전부 달랐다(117·173·188·433·588·948).
|
||||||
|
정렬선이 하나로 모이는 순간 이 레이아웃은 그냥 어긋난 그리드가 된다. */
|
||||||
|
.hero {
|
||||||
|
padding-block: var(--space-6) var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 12칸 그리드에 grid-row 를 손으로 배치했더니 행 높이가 콘텐츠에 따라
|
||||||
|
285/195/88/266px 로 제각각 잡히고 사이에 빈 칸이 생겼다.
|
||||||
|
글과 사진을 두 덩어리로 나누고, **사진 안에서만** 흩뿌린다.
|
||||||
|
그래야 정렬선을 흩는 것과 빈 공간이 생기는 것이 분리된다. */
|
||||||
|
.hero-inner {
|
||||||
|
display: grid;
|
||||||
|
/* 텍스트 칼럼이 좁으면 h1 이 3줄로 접힌다(카운트 규칙: 헤드라인 2줄 이하).
|
||||||
|
세로 라벨이 왼쪽에서 자리를 먹으므로 그만큼 더 준다. */
|
||||||
|
grid-template-columns: minmax(0, 1.06fr) minmax(0, 1fr);
|
||||||
|
gap: var(--space-5);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.hero-text {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: var(--space-5);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.hero-body > * + * {
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
.hero-role {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
.hero-cta {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 사진 셋. 위치·크기·비율이 전부 다르고 서로 겹친다.
|
||||||
|
R1 실측에서 이미지 6개의 좌측 정렬선이 6개 전부 달랐다 —
|
||||||
|
정렬선이 하나로 모이면 이 레이아웃은 그냥 어긋난 그리드가 된다. */
|
||||||
|
.hero-photos {
|
||||||
|
position: relative;
|
||||||
|
/* 가장 아래 사진(.ph-c)이 63% 에서 시작해 폭 47%·4:3 이므로 약 0.99 에서 끝난다.
|
||||||
|
컨테이너를 그보다 길게 잡으면 히어로 아래에 빈 띠가 남는다. */
|
||||||
|
aspect-ratio: 1 / 0.995;
|
||||||
|
}
|
||||||
|
.ph {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ph img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.ph-a {
|
||||||
|
left: 16%;
|
||||||
|
top: 0;
|
||||||
|
width: 60%;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
}
|
||||||
|
.ph-b {
|
||||||
|
left: 0;
|
||||||
|
top: 52%;
|
||||||
|
width: 27%;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
}
|
||||||
|
.ph-c {
|
||||||
|
right: 0;
|
||||||
|
top: 63%;
|
||||||
|
width: 47%;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
|
.hero-tags {
|
||||||
|
position: absolute;
|
||||||
|
right: -1.4rem;
|
||||||
|
top: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 섹션 공통 ────────────────────────────────────── */
|
||||||
|
.band {
|
||||||
|
background: var(--paper-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 이미지와 글의 2단. 같은 스플릿을 세 번 이상 쓰지 않는다 —
|
||||||
|
두 번째는 좌우를 뒤집어 리듬을 준다 */
|
||||||
|
.two {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
|
||||||
|
gap: var(--space-7);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.two-reverse .two-text {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.two-text > * + * {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
.two-text p {
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
.two-photo {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.two-photo img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.two-photo figcaption {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 숫자와 사실. 표가 아니라 조판으로 둔다 */
|
||||||
|
.facts {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
.facts > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7.5rem 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.facts dt {
|
||||||
|
font-size: var(--step-label);
|
||||||
|
font-weight: var(--weight-bold);
|
||||||
|
letter-spacing: var(--track-label);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
padding-top: 0.35rem;
|
||||||
|
}
|
||||||
|
.facts dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 작업 갤러리 ──────────────────────────────────────
|
||||||
|
균일한 3열 그리드를 쓰지 않는다. 셋의 크기와 세로 위치가 전부 다르다. */
|
||||||
|
.gallery {
|
||||||
|
margin-top: var(--space-7);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
gap: var(--space-5) var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.g {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.g img {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.g figcaption {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
font-size: var(--step-label);
|
||||||
|
font-weight: var(--weight-bold);
|
||||||
|
letter-spacing: var(--track-label);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.g-1 {
|
||||||
|
grid-column: 1 / 9;
|
||||||
|
}
|
||||||
|
.g-1 img {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
.g-2 {
|
||||||
|
grid-column: 9 / 13;
|
||||||
|
margin-top: var(--space-7);
|
||||||
|
}
|
||||||
|
.g-2 img {
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
}
|
||||||
|
.g-3 {
|
||||||
|
grid-column: 3 / 10;
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
.g-3 img {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-note {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 문의 ─────────────────────────────────────────── */
|
||||||
|
.contact-inner {
|
||||||
|
max-width: 46rem;
|
||||||
|
}
|
||||||
|
.contact-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
.contact-list li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7rem 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: baseline;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 찾아오기 (푸터를 대신한다) ───────────────────── */
|
||||||
|
.visit {
|
||||||
|
padding-bottom: var(--space-7);
|
||||||
|
}
|
||||||
|
.visit-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 0.7fr) auto;
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.visit-address {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.visit-note {
|
||||||
|
max-width: 24ch;
|
||||||
|
}
|
||||||
|
.visit-mark {
|
||||||
|
color: var(--line);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════
|
||||||
|
좁은 화면 — 흩뿌림은 넓은 화면의 사치다.
|
||||||
|
320px 에서 정렬선 여섯 개를 유지하면 그냥 부서진 페이지가 된다.
|
||||||
|
════════════════════════════════════════════════════════ */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
/* 히어로를 한 칸으로 내린다. 흩뿌림은 넓은 화면의 사치다 —
|
||||||
|
390px 에서 정렬선 셋을 유지하면 사진이 서로를 가린다. */
|
||||||
|
.hero-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
/* 여기가 실제 사고 지점이었다. .hero-text 가 `auto 1fr` 2칸인데
|
||||||
|
세로 라벨을 가로로 눕히자 그 긴 문장이 auto 칸을 통째로 먹어
|
||||||
|
h1 칸이 짜부라졌다 — 390px 에서 헤드라인이 6줄, 320px 에서 11줄이 됐다. */
|
||||||
|
.hero-text {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
.hero-role {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
}
|
||||||
|
/* 히어로의 텍스트 요소는 넷을 넘지 않는다(카운트 규칙) */
|
||||||
|
.hero-tags {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hero-photos {
|
||||||
|
aspect-ratio: 1 / 0.9;
|
||||||
|
}
|
||||||
|
.visit-mark {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two,
|
||||||
|
.visit-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
.two-reverse .two-text {
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.g-1,
|
||||||
|
.g-2,
|
||||||
|
.g-3 {
|
||||||
|
grid-column: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.facts > div,
|
||||||
|
.contact-list li {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
/* 워드마크와 내비를 한 줄에 나란히 두면 320px 에서 둘 다 접힌다.
|
||||||
|
실측: 내비 링크가 두 줄(top 46/77)에 걸리고 워드마크는 3줄이 됐다.
|
||||||
|
좁은 화면에서는 위아래로 쌓는다 — 접히는 것보다 쌓는 것이 낫다. */
|
||||||
|
.site-head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
.wordmark {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
/* 세로로 쌓았으니 폭에 여유가 있다. 워드마크를 한 줄로 편다 */
|
||||||
|
.wordmark span {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
/* 버튼 라벨이 2줄로 접히면 하드 게이트 6 위반이다.
|
||||||
|
좌우 패딩을 줄이고 줄바꿈을 막는다. */
|
||||||
|
.btn {
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
/* 히어로 사진 셋을 세로로 세우면 화면이 너무 길어진다.
|
||||||
|
가장 작은 것 하나만 남기고 나머지는 갤러리에서 다시 보여준다. */
|
||||||
|
.hero-photos {
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
}
|
||||||
|
.ph-b {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ph-a {
|
||||||
|
left: 0;
|
||||||
|
width: 78%;
|
||||||
|
}
|
||||||
|
.ph-c {
|
||||||
|
top: 58%;
|
||||||
|
width: 62%;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
apps/site/public/work/thursday-flowers/styles/tokens.css
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
목요일의 화원 — 디자인 토큰
|
||||||
|
전부 3단계에서 확정한 값이고, 근거는 design.md 에 있다.
|
||||||
|
|
||||||
|
값의 출처는 취향이 아니라 실측이다.
|
||||||
|
Aesop(#FFFEF2) · Sarah Winward(#F8F7F3) · Cereal(#F7F6EF) 세 곳이
|
||||||
|
전부 순백을 피하고 따뜻한 오프화이트를 쓴다. 글자도 순검정이 아니다.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
/* ── 색 ──────────────────────────────────────────────
|
||||||
|
강조색은 하나다. 그리고 **면이 아니라 점**으로만 쓴다.
|
||||||
|
꽃 사진에는 이미 온갖 색이 들어 있어서, 화면에 색을 더 칠하면
|
||||||
|
사진과 싸운다. 링크·라벨·구분선 정도가 강조색이 쓰이는 전부다. */
|
||||||
|
--paper: #f8f6f0;
|
||||||
|
--paper-deep: #f1ede4; /* 섹션 하나만 살짝 눕힌다. 리듬용 */
|
||||||
|
--ink: #2a2723;
|
||||||
|
--ink-muted: #6b655c;
|
||||||
|
--ink-faint: #a09889;
|
||||||
|
--accent: #7c4a3f; /* 마른 장미. 꽃의 초록과 충돌하지 않는다 */
|
||||||
|
--line: #ddd7ca;
|
||||||
|
|
||||||
|
/* ── 타입 ────────────────────────────────────────────
|
||||||
|
크기를 4종으로 묶는다. Aesop 이 3종(31/14/12), Cereal 이 2종(12/11)이었다.
|
||||||
|
종류를 늘리면 위계가 아니라 소음이 된다.
|
||||||
|
|
||||||
|
자간은 크기에 따라 **방향이 뒤집힌다**(Kinfolk 실측).
|
||||||
|
50px → -0.5px(-1%) · 15px → +0.15px(+1%)
|
||||||
|
큰 글자는 조여서 덩어리로, 작은 글자는 풀어서 낱글자로 읽히게 한다. */
|
||||||
|
--font-display: "MaruBuri", "Nanum Myeongjo", Georgia, serif;
|
||||||
|
--font-body: "Pretendard", -apple-system, "Segoe UI", sans-serif;
|
||||||
|
|
||||||
|
/* 가장 긴 줄이 "그 주에 좋은 꽃만"(한글 7 + 공백 2 ≒ 7.7em)이다.
|
||||||
|
세로 라벨이 왼쪽에서 60px 을 먹으므로 텍스트 칼럼은 화면 폭의 절반보다 좁다.
|
||||||
|
1024px 에서 55.7px 로 잡히자 딱 넘쳐 3줄이 됐다(카운트 규칙 위반).
|
||||||
|
기울기를 낮춰 전 구간에서 2줄을 유지한다. */
|
||||||
|
--step-display: clamp(2.2rem, 1.3rem + 2.9vw, 4.2rem);
|
||||||
|
--step-lead: clamp(1.05rem, 0.98rem + 0.3vw, 1.25rem);
|
||||||
|
--step-body: 1rem;
|
||||||
|
--step-label: 0.75rem;
|
||||||
|
|
||||||
|
--track-display: -0.02em;
|
||||||
|
--track-lead: -0.005em;
|
||||||
|
--track-body: 0;
|
||||||
|
--track-label: 0.14em;
|
||||||
|
|
||||||
|
/* 웨이트는 둘이면 충분하다. 굵기로 소리치지 않는다.
|
||||||
|
큰 글자일수록 가늘게 — Aesop 의 31px 이 400 이었고 12px 라벨이 700 이었다. */
|
||||||
|
--weight-regular: 400;
|
||||||
|
--weight-bold: 700;
|
||||||
|
|
||||||
|
--leading-tight: 1.2;
|
||||||
|
--leading-body: 1.85; /* 한글은 라틴보다 넉넉해야 한다. 받침 때문에 줄이 붙어 보인다 */
|
||||||
|
|
||||||
|
/* 한글 본문은 25~35자가 읽기 폭이다. 라틴 기준 65자를 그대로 쓰면 너무 넓다 */
|
||||||
|
--measure: 32ch;
|
||||||
|
|
||||||
|
/* ── 간격 ────────────────────────────────────────────
|
||||||
|
여백이 콘텐츠다. 섹션 간격을 본문 간격의 5배 이상으로 벌린다. */
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-3: 1rem;
|
||||||
|
--space-4: 1.5rem;
|
||||||
|
--space-5: 2.5rem;
|
||||||
|
--space-6: 4rem;
|
||||||
|
--space-7: 6.5rem;
|
||||||
|
--space-8: clamp(6rem, 4rem + 8vw, 11rem); /* 섹션 사이 */
|
||||||
|
|
||||||
|
--gutter: clamp(1.25rem, 0.6rem + 2.6vw, 3rem);
|
||||||
|
--container: 1240px;
|
||||||
|
|
||||||
|
/* ── 모션 ────────────────────────────────────────────
|
||||||
|
R1(sarahwinward.com)의 스크롤 이동량을 쟀더니 **0px** 이었다.
|
||||||
|
하이엔드 플로럴은 움직이지 않는다. 등장만 조용히 처리한다. */
|
||||||
|
--dur-slow: 900ms;
|
||||||
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--rise: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
:root {
|
||||||
|
--ink-muted: #4a453e;
|
||||||
|
--line: #b9b0a0;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/site/src/assets/shots/work-thursday-flowers-2.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
BIN
apps/site/src/assets/shots/work-thursday-flowers.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
55
apps/site/src/data/work.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* 쇼케이스 목록.
|
||||||
|
*
|
||||||
|
* 사례를 추가할 때 **여기 한 항목만 넣으면** 모자이크 그리드에 자동으로 들어간다.
|
||||||
|
* 페이지 코드는 건드리지 않는다.
|
||||||
|
*
|
||||||
|
* 순서가 곧 표시 순서다(최근 것을 위에). `size` 는 타일 크기이고,
|
||||||
|
* 전부 같은 크기로 두지 마라 — 균일한 격자는 이 스킬이 금지하는 형태다.
|
||||||
|
*/
|
||||||
|
import type { ImageMetadata } from "astro";
|
||||||
|
import thursdayFlowers from "../assets/shots/work-thursday-flowers.png";
|
||||||
|
import thursdayFlowers2 from "../assets/shots/work-thursday-flowers-2.png";
|
||||||
|
|
||||||
|
export interface WorkItem {
|
||||||
|
slug: string;
|
||||||
|
/** 사이트 이름. 실제 클라이언트가 아니라 데모면 그렇다고 적는다 */
|
||||||
|
name: string;
|
||||||
|
/** 무엇을 만든 것인가 — 한 줄 */
|
||||||
|
kind: string;
|
||||||
|
/** 2단계에서 정한 한 문장 컨셉 */
|
||||||
|
concept: string;
|
||||||
|
/** 감수한 리스크 하나. 이 스킬의 결과물을 다른 것과 구별하는 지점이다 */
|
||||||
|
risk: string;
|
||||||
|
preset: string;
|
||||||
|
/** YYYY-MM */
|
||||||
|
date: string;
|
||||||
|
/** 자랑이 아니라 잰 값. 없으면 비워 둔다 */
|
||||||
|
metrics: string[];
|
||||||
|
/** 실제로 열리는 주소. 목업이 아니라 동작하는 사이트여야 한다 */
|
||||||
|
href: string;
|
||||||
|
shot: ImageMetadata;
|
||||||
|
/** 호버할 때 바뀌는 두 번째 컷. 없어도 된다 */
|
||||||
|
shotAlt?: ImageMetadata;
|
||||||
|
alt: string;
|
||||||
|
/** 모자이크 타일 크기 */
|
||||||
|
size: "lg" | "md" | "sm";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORK: WorkItem[] = [
|
||||||
|
{
|
||||||
|
slug: "thursday-flowers",
|
||||||
|
name: "목요일의 화원",
|
||||||
|
kind: "플로럴 스튜디오 · 한 장",
|
||||||
|
concept: "꽃을 파는 가게가 아니라, 꽃으로 작업하는 사람의 기록",
|
||||||
|
risk: "첫 화면에 큰 사진을 깔지 않았습니다. 크기가 제각각인 사진 셋을 정렬선 없이 흩뿌립니다",
|
||||||
|
preset: "editorial",
|
||||||
|
date: "2026-08",
|
||||||
|
metrics: ["이미지 603KB", "JS 0.6KB", "본문 대비 13.75:1", "하드 게이트 12/12"],
|
||||||
|
href: "/work/thursday-flowers/",
|
||||||
|
shot: thursdayFlowers,
|
||||||
|
shotAlt: thursdayFlowers2,
|
||||||
|
alt: "목요일의 화원 첫 화면 — 크림색 배경에 꽃 사진 셋이 서로 다른 위치에 놓여 있습니다",
|
||||||
|
size: "lg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -108,34 +108,34 @@ export interface Content {
|
||||||
const STAGES_KO: Stage[] = [
|
const STAGES_KO: Stage[] = [
|
||||||
{
|
{
|
||||||
name: "브리프 게이트",
|
name: "브리프 게이트",
|
||||||
does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축한다. 규모를 판정해 경로를 고른다.",
|
does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축합니다. 규모를 판정해 경로를 고릅니다.",
|
||||||
becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다",
|
becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분합니다",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "레퍼런스 조사",
|
name: "레퍼런스 조사",
|
||||||
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조, 톤, 디테일.",
|
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고릅니다. 구조, 톤, 디테일.",
|
||||||
becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다",
|
becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져옵니다",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "방향 결정",
|
name: "방향 결정",
|
||||||
does: "레퍼런스를 하나의 축으로 합성한다. 감수할 리스크를 하나 고른다.",
|
does: "레퍼런스를 하나의 축으로 합성합니다. 감수할 리스크를 하나 고릅니다.",
|
||||||
becomes: "감상은 이름에만, 화면에는 수치만",
|
becomes: "감상은 이름에만, 화면에는 수치만",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "디자인 토큰",
|
name: "디자인 토큰",
|
||||||
does: "코드를 쓰기 전에 숫자를 정한다. 성능 예산도 여기서 확정한다.",
|
does: "코드를 쓰기 전에 숫자를 정합니다. 성능 예산도 여기서 확정합니다.",
|
||||||
becomes: "--measure: 33em / --leading-normal: 1.7 / JS 예산 0KB",
|
becomes: "--measure: 33em / --leading-normal: 1.7 / JS 예산 0KB",
|
||||||
mono: true,
|
mono: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "구현",
|
name: "구현",
|
||||||
does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.",
|
does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 합니다.",
|
||||||
becomes: "<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>",
|
becomes: "<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>",
|
||||||
mono: true,
|
mono: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "프리플라이트",
|
name: "프리플라이트",
|
||||||
does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.",
|
does: "자기 결과물을 남의 것처럼 봅니다. 게이트 12개는 전부 '아니오'여야 합니다.",
|
||||||
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정",
|
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정",
|
||||||
mono: true,
|
mono: true,
|
||||||
},
|
},
|
||||||
|
|
@ -181,17 +181,17 @@ export const ko: Content = {
|
||||||
meta: {
|
meta: {
|
||||||
title: "designpaca · 웹 디자인 파이프라인 스킬",
|
title: "designpaca · 웹 디자인 파이프라인 스킬",
|
||||||
description:
|
description:
|
||||||
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치한다.",
|
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치합니다.",
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
h1: "결정하지 않으면 기본값이 나온다",
|
h1: "결정하지 않으면 기본값이 나온다",
|
||||||
lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 난다. 그 결정을 여섯 단계로 강제하는 스킬.",
|
lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 납니다. 그 결정을 여섯 단계로 강제하는 스킬.",
|
||||||
install: "npx designpaca",
|
install: "npx designpaca",
|
||||||
installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다",
|
installNote: "Node 20.11+ · 설치 대상을 감지해 골라줍니다",
|
||||||
links: [{ label: "npm", href: "https://www.npmjs.com/package/designpaca" }],
|
links: [{ label: "npm", href: "https://www.npmjs.com/package/designpaca" }],
|
||||||
stage: {
|
stage: {
|
||||||
alt: "유리판 여섯 장이 깊이 방향으로 나란히 서 있다. 스크롤하면 카메라가 판을 하나씩 통과하고, 통과한 판은 뒤로 물러난다.",
|
alt: "유리판 여섯 장이 깊이 방향으로 나란히 서 있습니다. 스크롤하면 카메라가 판을 하나씩 통과하고, 통과한 판은 뒤로 물러납니다.",
|
||||||
caption: "판 하나가 단계 하나다. 통과 조건을 채우지 못하면 다음 판으로 가지 않는다.",
|
caption: "판 하나가 단계 하나다. 통과 조건을 채우지 못하면 다음 판으로 가지 않습니다.",
|
||||||
gate: "게이트",
|
gate: "게이트",
|
||||||
pass: "통과",
|
pass: "통과",
|
||||||
},
|
},
|
||||||
|
|
@ -200,7 +200,7 @@ export const ko: Content = {
|
||||||
eyebrow: "여섯 단계",
|
eyebrow: "여섯 단계",
|
||||||
h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
|
h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
|
||||||
lead:
|
lead:
|
||||||
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.",
|
"각 단계에는 통과 조건이 있습니다. 통과하지 못하면 다음으로 가지 않습니다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.",
|
||||||
briefLabel: "들어온 브리프",
|
briefLabel: "들어온 브리프",
|
||||||
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
|
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
|
||||||
stages: STAGES_KO,
|
stages: STAGES_KO,
|
||||||
|
|
@ -208,17 +208,17 @@ export const ko: Content = {
|
||||||
showcase: {
|
showcase: {
|
||||||
h2: "결과물은 서로 닮지 않는다",
|
h2: "결과물은 서로 닮지 않는다",
|
||||||
lead:
|
lead:
|
||||||
"같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않는다. 둘 다 수치 자리가 비어 있는 것도 우연이 아니다.",
|
"같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않습니다. 둘 다 수치 자리가 비어 있는 것도 우연이 아닙니다.",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있다.",
|
alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있습니다.",
|
||||||
caption: "수제 커피 로스터리 · 구독 전환",
|
caption: "수제 커피 로스터리 · 구독 전환",
|
||||||
note: "프리셋 quiet-commerce. 사진이 없다는 제약을 수치표로 뒤집었다. 표의 값은 아직 비어 있다. 로스터에게 받기 전까지 채우지 않는다.",
|
note: "프리셋 quiet-commerce. 사진이 없습니다는 제약을 수치표로 뒤집었습니다. 표의 값은 아직 비어 있습니다. 로스터에게 받기 전까지 채우지 않습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있다.",
|
alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있습니다.",
|
||||||
caption: "API 모니터링 SaaS · 무료 체험 가입",
|
caption: "API 모니터링 SaaS · 무료 체험 가입",
|
||||||
note: "개발자 도구라고 자동으로 다크로 가지 않았다. 브리프에 근거가 없었기 때문이다. 리전 수는 아직 중괄호 자리로 남아 있다.",
|
note: "개발자 도구라고 자동으로 다크로 가지 않았습니다. 브리프에 근거가 없었기 때문입니다. 리전 수는 아직 중괄호 자리로 남아 있습니다.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -226,43 +226,43 @@ export const ko: Content = {
|
||||||
eyebrow: "SVG 필터",
|
eyebrow: "SVG 필터",
|
||||||
h2: "이미지 편집기를 열지 않고 고치는 네 가지",
|
h2: "이미지 편집기를 열지 않고 고치는 네 가지",
|
||||||
lead:
|
lead:
|
||||||
"질감을 만드는 필터 데모는 많다. 손이 실제로 줄어드는 자리는 따로 있다. 아래 넷은 왼쪽이 매번 손으로 맞추던 것, 오른쪽이 같은 소재에 선언 하나만 얹은 것이다. 아래 수치는 이 화면을 그대로 캡처해 잰 값이다.",
|
"질감을 만드는 필터 데모는 많다. 손이 실제로 줄어드는 자리는 따로 있습니다. 아래 넷은 왼쪽이 매번 손으로 맞추던 것, 오른쪽이 같은 소재에 선언 하나만 얹은 것입니다. 아래 수치는 이 화면을 그대로 캡처해 잰 값이다.",
|
||||||
demos: [
|
demos: [
|
||||||
{
|
{
|
||||||
id: "scrim",
|
id: "scrim",
|
||||||
name: "글자 모양대로 생기는 스크림",
|
name: "글자 모양대로 생기는 스크림",
|
||||||
problem: "사진 위 캡션이 대비를 못 넘긴다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞추게 된다.",
|
problem: "사진 위 캡션이 대비를 못 넘깁니다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞추게 됩니다.",
|
||||||
beforeLabel: "그냥 얹음",
|
beforeLabel: "그냥 얹음",
|
||||||
afterLabel: "filter: url(#scrim)",
|
afterLabel: "filter: url(#scrim)",
|
||||||
how: "글자 알파를 부풀려 그 모양대로만 색을 채운다. 박스가 아니라 글자를 따라가므로 사진을 덜 가린다.",
|
how: "글자 알파를 부풀려 그 모양대로만 색을 채웁니다. 박스가 아니라 글자를 따라가므로 사진을 덜 가립니다.",
|
||||||
verdict: "흰 글자 대비 1.70:1 → 4.79:1",
|
verdict: "흰 글자 대비 1.70:1 → 4.79:1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cvd",
|
id: "cvd",
|
||||||
name: "색각 이상 시뮬레이션",
|
name: "색각 이상 시뮬레이션",
|
||||||
problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 된다. 내 화면만 봐서는 못 잡는다.",
|
problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 됩니다. 내 화면만 봐서는 못 잡는다.",
|
||||||
beforeLabel: "내가 보는 화면",
|
beforeLabel: "내가 보는 화면",
|
||||||
afterLabel: "적록색약(2형)",
|
afterLabel: "적록색약(2형)",
|
||||||
how: "feColorMatrix 한 줄. 이것만은 linearRGB 로 둔다 — 색각 변환은 물리적인 빛의 혼합이다.",
|
how: "feColorMatrix 한 줄. 이것만은 linearRGB 로 둡니다 — 색각 변환은 물리적인 빛의 혼합이다.",
|
||||||
verdict: "배지 셋의 최소 색거리 82 → 2 · 주의와 실패가 같은 색",
|
verdict: "배지 셋의 최소 색거리 82 → 2 · 주의와 실패가 같은 색",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "goo",
|
id: "goo",
|
||||||
name: "떨어진 것이 하나로 뭉치기",
|
name: "떨어진 것이 하나로 뭉치기",
|
||||||
problem: "조각이 녹아 붙는 표현에 캔버스나 물리 라이브러리를 켠다. 로더와 메뉴 전환에서 반복된다.",
|
problem: "조각이 녹아 붙는 표현에 캔버스나 물리 라이브러리를 켜게 됩니다. 로더와 메뉴 전환에서 반복됩니다.",
|
||||||
beforeLabel: "필터 없음 · 원 3개",
|
beforeLabel: "필터 없음 · 원 3개",
|
||||||
afterLabel: "filter: url(#goo)",
|
afterLabel: "filter: url(#goo)",
|
||||||
how: "블러한 뒤 알파만 세워 잘라낸다. 붙을지 말지는 취향이 아니라 간격과 블러 반경의 비율로 정해진다.",
|
how: "블러한 뒤 알파만 세워 잘라냅니다. 붙을지 말지는 취향이 아니라 간격과 블러 반경의 비율로 정해진다.",
|
||||||
verdict: "덩어리 3개 → 1개 · JS 0줄",
|
verdict: "덩어리 3개 → 1개 · JS 0줄",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "glass",
|
id: "glass",
|
||||||
name: "배경을 휘게 하는 오버레이",
|
name: "배경을 휘게 하는 오버레이",
|
||||||
problem: "반투명 패널은 뒤를 흐리게만 만든다. 실제 유리는 빛을 휜다.",
|
problem: "반투명 패널은 뒤를 흐리게만 만듭니다. 실제 유리는 빛을 휜다.",
|
||||||
beforeLabel: "backdrop-filter: blur",
|
beforeLabel: "backdrop-filter: blur",
|
||||||
afterLabel: "blur + url(#lens)",
|
afterLabel: "blur + url(#lens)",
|
||||||
how: "변위 지도를 backdrop-filter 에 넘긴다. 노이즈로 만들면 값이 중립으로 수렴해 아무 일도 안 일어난다.",
|
how: "변위 지도를 backdrop-filter 에 넘깁니다. 노이즈로 만들면 값이 중립으로 수렴해 아무 일도 안 일어납니다.",
|
||||||
verdict: "가장자리에서 격자가 눌린다 · Chromium 전용",
|
verdict: "가장자리에서 격자가 눌립니다 · Chromium 전용",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -270,112 +270,112 @@ export const ko: Content = {
|
||||||
eyebrow: "이 페이지의 수치",
|
eyebrow: "이 페이지의 수치",
|
||||||
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
|
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
|
||||||
lead:
|
lead:
|
||||||
"문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.",
|
"문장을 다듬는 것으로 해결되는 것은 문장뿐입니다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남습니다. 그래서 이 페이지 자신의 형태 수치를 공개합니다. 결과가 나쁘게 나와도 그대로 싣는다.",
|
||||||
items: [
|
items: [
|
||||||
{ label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true },
|
{ label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true },
|
||||||
{ label: "하드 게이트", value: "12/12", note: "정적 검사와 320px · 1440px 실렌더 양쪽", feature: true },
|
{ label: "하드 게이트", value: "12/12", note: "정적 검사와 320px · 1440px 실렌더 양쪽", feature: true },
|
||||||
{ label: "three.js 추가분", value: "184KB", note: "검사대가 화면에 들어올 때만 받는다. 초기 번들에는 없다" },
|
{ label: "three.js 추가분", value: "184KB", note: "검사대가 화면에 들어올 때만 받습니다. 초기 번들에는 없습니다" },
|
||||||
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
|
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
|
||||||
{ label: "radius 어휘", value: "2종", note: "3px 과 14px. 알약 모양을 없앴다" },
|
{ label: "radius 어휘", value: "2종", note: "3px 과 14px. 알약 모양을 없앴습니다" },
|
||||||
{ label: "반투명 패널", value: "2개", note: "29개까지 늘렸다가 걷어냈다. 유리는 무언가를 담는 액자에만 남겼다" },
|
{ label: "반투명 패널", value: "2개", note: "29개까지 늘렸다가 걷어냈습니다. 유리는 무언가를 담는 액자에만 남겼습니다" },
|
||||||
{ label: "타입 단계", value: "6단계", note: "토큰에 정의된 수. 디스플레이는 스케일 밖이고, 최대 글자는 본문의 5.6배" },
|
{ label: "타입 단계", value: "6단계", note: "토큰에 정의된 수. 디스플레이는 스케일 밖이고, 최대 글자는 본문의 5.6배" },
|
||||||
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
|
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받습니다" },
|
||||||
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
|
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
|
||||||
{ label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" },
|
{ label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잽니다" },
|
||||||
],
|
],
|
||||||
caveat:
|
caveat:
|
||||||
"값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.",
|
"값은 프로덕션 빌드에서 측정합니다. 아직 측정하지 않은 항목은 비워둡니다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.",
|
||||||
},
|
},
|
||||||
preflight: {
|
preflight: {
|
||||||
h2: "이 페이지를 만들며 실제로 걸린 것",
|
h2: "이 페이지를 만들며 실제로 걸린 것",
|
||||||
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
|
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과입니다.",
|
||||||
items: [
|
items: [
|
||||||
|
|
||||||
{
|
{
|
||||||
caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었다",
|
caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었습니다",
|
||||||
fixed:
|
fixed:
|
||||||
"웹폰트가 오기 전에는 시스템 폰트로 그려진다. 맑은 고딕은 Pretendard 보다 13% 넓어서 교체되는 순간 줄바꿈이 다시 잡힌다. 폴백 폰트에 size-adjust 로 치수를 맞췄다. 실측 차이 0.0%. OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의했다.",
|
"웹폰트가 오기 전에는 시스템 폰트로 그려진다. 맑은 고딕은 Pretendard 보다 13% 넓어서 교체되는 순간 줄바꿈이 다시 잡힙니다. 폴백 폰트에 size-adjust 로 치수를 맞췄다. 실측 차이 0.0%. OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의했습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
caught: "유리 카드를 29개 뿌렸다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다",
|
caught: "유리 카드를 29개 뿌렸습니다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였습니다",
|
||||||
fixed:
|
fixed:
|
||||||
"우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨다. 유리는 5개만 남았다.",
|
"우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었습니다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨습니다. 유리는 5개만 남았습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
caught: "“backdrop-filter 에 SVG 를 얹으면 Chrome 이 버린다” 고 적었는데 사실이 아니었다",
|
caught: "“backdrop-filter 에 SVG 를 얹으면 Chrome 이 버린다” 고 적었는데 사실이 아니었습니다",
|
||||||
fixed:
|
fixed:
|
||||||
"한 번 실패하고 브라우저 탓으로 적어뒀다. 다시 재보니 Chrome 151 에서 computed 값이 none 이 아니라 url(#id) 로 살아 있고 배경도 실제로 굴절된다. 진짜 원인은 feImage 의 width/height 를 요소 크기와 맞추지 않은 것이었다 — backdrop-filter 는 필터 이미지를 자동으로 조정하지 않는다. 한 번 실패한 것을 ‘안 되는 것’ 으로 적으면 그 문장이 다음 사람의 상한선이 된다.",
|
"한 번 실패하고 브라우저 탓으로 적어뒀다. 다시 재보니 Chrome 151 에서 computed 값이 none 이 아니라 url(#id) 로 살아 있고 배경도 실제로 굴절됩니다. 진짜 원인은 feImage 의 width/height 를 요소 크기와 맞추지 않은 것이었습니다 — backdrop-filter 는 필터 이미지를 자동으로 조정하지 않습니다. 한 번 실패한 것을 ‘안 되는 것’ 으로 적으면 그 문장이 다음 사람의 상한선이 됩니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
caught: "가로로 444px 넘쳤다. 레이아웃 전체가 어긋나 있었다",
|
caught: "가로로 444px 넘쳤습니다. 레이아웃 전체가 어긋나 있었습니다",
|
||||||
fixed:
|
fixed:
|
||||||
"main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.",
|
"main 에서 그리드 클래스가 빠져 있었습니다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였습니다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다",
|
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였습니다",
|
||||||
fixed:
|
fixed:
|
||||||
"scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.",
|
"scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못합니다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1",
|
caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1",
|
||||||
fixed:
|
fixed:
|
||||||
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.",
|
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였습니다. 반전 대응 토큰을 하나 더 두고 함께 바꿨습니다.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못한다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.",
|
note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못합니다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.",
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
eyebrow: "한글 조판",
|
eyebrow: "한글 조판",
|
||||||
h2: "라틴 기본값은 한글에서 세 군데가 깨진다",
|
h2: "라틴 기본값은 한글에서 세 군데가 깨진다",
|
||||||
lead:
|
lead:
|
||||||
"영어권 디자인 도구에는 한글 조판 규칙이 없다. 그래서 기본값을 그대로 쓰면 아래 셋이 동시에 무너진다. 왼쪽이 기본값, 오른쪽이 이 스킬이 정한 값이다. 글자도 폭도 폰트도 같다.",
|
"영어권 디자인 도구에는 한글 조판 규칙이 없습니다. 그래서 기본값을 그대로 쓰면 아래 셋이 동시에 무너집니다. 왼쪽이 기본값, 오른쪽이 이 스킬이 정한 값이다. 글자도 폭도 폰트도 같습니다.",
|
||||||
sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 한다.",
|
sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 합니다.",
|
||||||
demos: [
|
demos: [
|
||||||
{
|
{
|
||||||
id: "break",
|
id: "break",
|
||||||
title: "줄바꿈",
|
title: "줄바꿈",
|
||||||
badLabel: "기본값",
|
badLabel: "기본값",
|
||||||
goodLabel: "word-break: keep-all",
|
goodLabel: "word-break: keep-all",
|
||||||
note: "기본값은 어절 한가운데를 끊는다. 왼쪽 마지막 줄을 보라 — 한 단어가 두 줄에 걸쳐 있다.",
|
note: "기본값은 어절 한가운데를 끊는다. 왼쪽 마지막 줄을 보라 — 한 단어가 두 줄에 걸쳐 있습니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "leading",
|
id: "leading",
|
||||||
title: "행간",
|
title: "행간",
|
||||||
badLabel: "line-height 1.45",
|
badLabel: "line-height 1.45",
|
||||||
goodLabel: "line-height 1.75",
|
goodLabel: "line-height 1.75",
|
||||||
note: "라틴 표준 1.4~1.5 는 받침이 있는 글자에 좁다. 줄이 서로 붙어 보인다.",
|
note: "라틴 표준 1.4~1.5 는 받침이 있는 글자에 좁습니다. 줄이 서로 붙어 보입니다.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "size",
|
id: "size",
|
||||||
title: "크기",
|
title: "크기",
|
||||||
badLabel: "11px",
|
badLabel: "11px",
|
||||||
goodLabel: "15px",
|
goodLabel: "15px",
|
||||||
note: "라틴에서 11px 은 라벨로 쓴다. 한글은 획이 많아 같은 크기에서 뭉갠다.",
|
note: "라틴에서 11px 은 라벨로 씁니다. 한글은 획이 많아 같은 크기에서 뭉갠다.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: [
|
rules: [
|
||||||
"word-break: keep-all 없으면 단어가 아무 데서나 잘린다",
|
"word-break: keep-all 없으면 단어가 아무 데서나 잘립니다",
|
||||||
"line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게",
|
"line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게",
|
||||||
"음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
|
"음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개집니다",
|
||||||
"한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다",
|
"한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 깁니다",
|
||||||
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다",
|
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호입니다",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
h2: "규칙마다 왜 그런지 적혀 있다",
|
h2: "규칙마다 왜 그런지 적혀 있다",
|
||||||
lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.",
|
lead: "근거 없는 금지는 지켜지지 않습니다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이깁니다.",
|
||||||
items: [
|
items: [
|
||||||
{ rule: "레퍼런스 없이 시작하지 않는다", why: "머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 된다." },
|
{ rule: "레퍼런스 없이 시작하지 않는다", why: "머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 됩니다." },
|
||||||
{ rule: "강조색은 하나다", why: "두 개가 필요하다고 느끼면 위계 설계가 실패한 것이다." },
|
{ rule: "강조색은 하나다", why: "두 개가 필요하다고 느끼면 위계 설계가 실패한 것입니다." },
|
||||||
{ rule: "대담함은 한 곳에만", why: "두 곳에서 소리치면 둘 다 죽는다." },
|
{ rule: "대담함은 한 곳에만", why: "두 곳에서 소리치면 둘 다 죽습니다." },
|
||||||
{ rule: "화려함은 4순위다", why: "심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10이다." },
|
{ rule: "화려함은 4순위다", why: "심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10입니다." },
|
||||||
{ rule: "지어낸 수치를 쓰지 않는다", why: "숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎는다." },
|
{ rule: "지어낸 수치를 쓰지 않는다", why: "숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎습니다." },
|
||||||
{ rule: "이펙트를 다 꺼도 완성돼 있어야 한다", why: "그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것이다." },
|
{ rule: "이펙트를 다 꺼도 완성돼 있어야 한다", why: "그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것입니다." },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
install: {
|
install: {
|
||||||
h2: "한 줄이면 된다",
|
h2: "한 줄이면 된다",
|
||||||
lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.",
|
lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않습니다.",
|
||||||
command: "npx designpaca",
|
command: "npx designpaca",
|
||||||
targets: [
|
targets: [
|
||||||
{ name: "Claude Code", path: "~/.claude/skills/designpaca/" },
|
{ name: "Claude Code", path: "~/.claude/skills/designpaca/" },
|
||||||
|
|
@ -386,7 +386,7 @@ export const ko: Content = {
|
||||||
{ name: "AGENTS.md", path: "블록 주입. 에이전트 무관" },
|
{ name: "AGENTS.md", path: "블록 주입. 에이전트 무관" },
|
||||||
],
|
],
|
||||||
commands: [
|
commands: [
|
||||||
{ cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" },
|
{ cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뜁니다" },
|
||||||
{ cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" },
|
{ cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" },
|
||||||
{ cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" },
|
{ cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" },
|
||||||
],
|
],
|
||||||
|
|
@ -394,7 +394,7 @@ export const ko: Content = {
|
||||||
nav: {
|
nav: {
|
||||||
items: [
|
items: [
|
||||||
{ label: "파이프라인", href: "#pipeline" },
|
{ label: "파이프라인", href: "#pipeline" },
|
||||||
{ label: "사례", href: "#showcase" },
|
{ label: "사례", href: "/showcase" },
|
||||||
{ label: "수치", href: "#metrics" },
|
{ label: "수치", href: "#metrics" },
|
||||||
{ label: "설치", href: "#install" },
|
{ label: "설치", href: "#install" },
|
||||||
],
|
],
|
||||||
|
|
@ -661,7 +661,7 @@ export const en: Content = {
|
||||||
nav: {
|
nav: {
|
||||||
items: [
|
items: [
|
||||||
{ label: "Pipeline", href: "#pipeline" },
|
{ label: "Pipeline", href: "#pipeline" },
|
||||||
{ label: "Work", href: "#showcase" },
|
{ label: "Work", href: "/showcase" },
|
||||||
{ label: "Numbers", href: "#metrics" },
|
{ label: "Numbers", href: "#metrics" },
|
||||||
{ label: "Install", href: "#install" },
|
{ label: "Install", href: "#install" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import Base from "../../layouts/Base.astro";
|
import Base from "../../layouts/Base.astro";
|
||||||
import Hero from "../../components/Hero.astro";
|
import Hero from "../../components/Hero.astro";
|
||||||
import Pipeline from "../../components/Pipeline.astro";
|
import Pipeline from "../../components/Pipeline.astro";
|
||||||
import Showcase from "../../components/Showcase.astro";
|
|
||||||
import Materials from "../../components/Materials.astro";
|
import Materials from "../../components/Materials.astro";
|
||||||
import Metrics from "../../components/Metrics.astro";
|
import Metrics from "../../components/Metrics.astro";
|
||||||
import Preflight from "../../components/Preflight.astro";
|
import Preflight from "../../components/Preflight.astro";
|
||||||
|
|
@ -20,7 +19,7 @@ import { en as c } from "../../i18n/content";
|
||||||
|
|
||||||
① 무엇인가 히어로 · 파이프라인
|
① 무엇인가 히어로 · 파이프라인
|
||||||
② 무엇을 아는가 SVG 필터 · 한글 조판
|
② 무엇을 아는가 SVG 필터 · 한글 조판
|
||||||
③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙
|
③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다)
|
||||||
④ 행동 설치
|
④ 행동 설치
|
||||||
*/}
|
*/}
|
||||||
<Hero c={c.hero} ui={c.ui} />
|
<Hero c={c.hero} ui={c.ui} />
|
||||||
|
|
@ -29,7 +28,6 @@ import { en as c } from "../../i18n/content";
|
||||||
<Materials c={c.materials} />
|
<Materials c={c.materials} />
|
||||||
<Typography c={c.typography} />
|
<Typography c={c.typography} />
|
||||||
|
|
||||||
<Showcase c={c.showcase} />
|
|
||||||
<Metrics c={c.metrics} />
|
<Metrics c={c.metrics} />
|
||||||
<Preflight c={c.preflight} />
|
<Preflight c={c.preflight} />
|
||||||
<Rules c={c.rules} />
|
<Rules c={c.rules} />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import Base from "../layouts/Base.astro";
|
import Base from "../layouts/Base.astro";
|
||||||
import Hero from "../components/Hero.astro";
|
import Hero from "../components/Hero.astro";
|
||||||
import Pipeline from "../components/Pipeline.astro";
|
import Pipeline from "../components/Pipeline.astro";
|
||||||
import Showcase from "../components/Showcase.astro";
|
|
||||||
import Materials from "../components/Materials.astro";
|
import Materials from "../components/Materials.astro";
|
||||||
import Metrics from "../components/Metrics.astro";
|
import Metrics from "../components/Metrics.astro";
|
||||||
import Preflight from "../components/Preflight.astro";
|
import Preflight from "../components/Preflight.astro";
|
||||||
|
|
@ -20,7 +19,7 @@ import { ko as c } from "../i18n/content";
|
||||||
|
|
||||||
① 무엇인가 히어로 · 파이프라인
|
① 무엇인가 히어로 · 파이프라인
|
||||||
② 무엇을 아는가 SVG 필터 · 한글 조판
|
② 무엇을 아는가 SVG 필터 · 한글 조판
|
||||||
③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙
|
③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다)
|
||||||
④ 행동 설치
|
④ 행동 설치
|
||||||
*/}
|
*/}
|
||||||
<Hero c={c.hero} ui={c.ui} />
|
<Hero c={c.hero} ui={c.ui} />
|
||||||
|
|
@ -29,7 +28,6 @@ import { ko as c } from "../i18n/content";
|
||||||
<Materials c={c.materials} />
|
<Materials c={c.materials} />
|
||||||
<Typography c={c.typography} />
|
<Typography c={c.typography} />
|
||||||
|
|
||||||
<Showcase c={c.showcase} />
|
|
||||||
<Metrics c={c.metrics} />
|
<Metrics c={c.metrics} />
|
||||||
<Preflight c={c.preflight} />
|
<Preflight c={c.preflight} />
|
||||||
<Rules c={c.rules} />
|
<Rules c={c.rules} />
|
||||||
|
|
|
||||||
256
apps/site/src/pages/showcase.astro
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
---
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import Base from "../layouts/Base.astro";
|
||||||
|
import BgPhoto from "../components/BgPhoto.astro";
|
||||||
|
import { WORK } from "../data/work.ts";
|
||||||
|
|
||||||
|
/*
|
||||||
|
쇼케이스. 소개 페이지 안의 섹션이 아니라 **별도 페이지**다.
|
||||||
|
|
||||||
|
홈에 있던 예전 섹션은 스크린샷 두 장을 카드에 넣고 각도를 살짝 준 것이었고,
|
||||||
|
들어갈 곳이 없었다 — 그림만 보여주고 끝나는 자리는 증거가 아니라 장식이다.
|
||||||
|
여기서는 타일을 누르면 **실제로 도는 사이트**로 들어간다.
|
||||||
|
|
||||||
|
타일 크기를 섞는다. 균일한 격자는 이 스킬이 금지하는 형태이고,
|
||||||
|
사례마다 중요도가 다른데 같은 크기로 두면 그 차이가 사라진다.
|
||||||
|
*/
|
||||||
|
const title = "사례 · designpaca";
|
||||||
|
const description =
|
||||||
|
"designpaca 파이프라인으로 만든 사이트들. 각 사례의 컨셉과 감수한 리스크, 실측값을 함께 적었습니다. 타일을 누르면 실제 사이트가 열립니다.";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base lang="ko" title={title} description={description}>
|
||||||
|
<section class="section intro has-bg" aria-labelledby="sc-h">
|
||||||
|
<BgPhoto variant="align" mode="fixed" opacity={0.85} />
|
||||||
|
<p class="eyebrow reveal">사례</p>
|
||||||
|
<h1 id="sc-h" class="reveal">파이프라인을 돌려서 나온 것들</h1>
|
||||||
|
<p class="lead intro-lead reveal">
|
||||||
|
모두 0단계부터 6단계까지 그대로 돌려서 나온 결과물입니다. 각 사례마다
|
||||||
|
<strong>감수한 리스크</strong>를 함께 적었습니다 — 그것이 없으면 안전하지만 잊히는 디자인이 됩니다.
|
||||||
|
</p>
|
||||||
|
<p class="muted intro-note reveal">
|
||||||
|
타일을 누르면 실제 사이트가 열립니다. 목업이 아니라 동작하는 페이지입니다.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section grid-section" aria-label="사례 목록">
|
||||||
|
<ul class="full mosaic">
|
||||||
|
{
|
||||||
|
WORK.map((w, i) => (
|
||||||
|
<li class:list={["tile", `tile-${w.size}`, "reveal"]} style={`--reveal-delay:${i * 80}ms`}>
|
||||||
|
<a class="tile-link" href={w.href}>
|
||||||
|
<span class="tile-shot">
|
||||||
|
<Image
|
||||||
|
src={w.shot}
|
||||||
|
alt={w.alt}
|
||||||
|
widths={[520, 900, 1440]}
|
||||||
|
sizes="(max-width: 820px) 92vw, 46vw"
|
||||||
|
loading={i === 0 ? "eager" : "lazy"}
|
||||||
|
/>
|
||||||
|
{w.shotAlt && (
|
||||||
|
<Image
|
||||||
|
class="tile-shot-alt"
|
||||||
|
src={w.shotAlt}
|
||||||
|
alt=""
|
||||||
|
widths={[520, 900, 1440]}
|
||||||
|
sizes="(max-width: 820px) 92vw, 46vw"
|
||||||
|
loading="lazy"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="tile-body">
|
||||||
|
<span class="tile-head">
|
||||||
|
<span class="tile-name">{w.name}</span>
|
||||||
|
<span class="tile-kind muted">{w.kind}</span>
|
||||||
|
</span>
|
||||||
|
<span class="tile-concept">{w.concept}</span>
|
||||||
|
<span class="tile-risk">
|
||||||
|
<span class="risk-label">감수한 리스크</span>
|
||||||
|
{w.risk}
|
||||||
|
</span>
|
||||||
|
<span class="tile-metrics mono">
|
||||||
|
{w.metrics.map((m) => (
|
||||||
|
<span class="metric">{m}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</Base>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* .page 그리드는 base.css 가 준다. 여기서 다시 정의하면
|
||||||
|
main 칼럼이 본문 폭(--measure)에서 벗어나 전 페이지가 어긋난다. */
|
||||||
|
.intro {
|
||||||
|
padding-block: calc(var(--header-h) + var(--space-7)) var(--space-7);
|
||||||
|
}
|
||||||
|
/* 목록 페이지의 제목이라 홈 히어로만큼 클 이유가 없다.
|
||||||
|
h1 기본값(--display)은 여기서 세 줄로 접힌다. */
|
||||||
|
#sc-h {
|
||||||
|
font-size: clamp(2rem, 1.3rem + 2.4vw, 3.4rem);
|
||||||
|
max-width: 18ch;
|
||||||
|
}
|
||||||
|
.intro-lead {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
.intro-note {
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-section {
|
||||||
|
padding-block: 0 var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 모자이크 ────────────────────────────────────────
|
||||||
|
타일 크기가 섞인다. 사례마다 무게가 다른데 같은 크기로 두면
|
||||||
|
그 차이가 사라지고, 그때부터는 격자가 내용을 지운다. */
|
||||||
|
.mosaic {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 1180px;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 0 var(--gutter);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: var(--space-6) var(--space-5);
|
||||||
|
}
|
||||||
|
.tile {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.tile-lg {
|
||||||
|
grid-column: span 6;
|
||||||
|
}
|
||||||
|
.tile-md {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
.tile-sm {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-link {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
/* 큰 타일은 그림과 글을 나란히 둔다. 세로로 쌓으면 화면을 통째로 먹는다 */
|
||||||
|
.tile-lg .tile-link {
|
||||||
|
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-shot {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--surface-2);
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
}
|
||||||
|
.tile-shot :global(img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* 두 번째 컷은 위에 겹쳐 두고 호버에서 바꾼다.
|
||||||
|
스크롤이나 자동 전환으로 바꾸지 않는다 — 읽는 사람의 속도를 뺏지 않는다 */
|
||||||
|
.tile-shot-alt {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-body {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
.tile-head {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
.tile-name {
|
||||||
|
font-size: var(--step-1);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
}
|
||||||
|
.tile-kind {
|
||||||
|
font-size: var(--step-0);
|
||||||
|
}
|
||||||
|
.tile-concept {
|
||||||
|
color: var(--ink);
|
||||||
|
max-width: 34em;
|
||||||
|
}
|
||||||
|
.tile-risk {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
max-width: 38em;
|
||||||
|
}
|
||||||
|
.risk-label {
|
||||||
|
font-size: var(--step-0);
|
||||||
|
letter-spacing: var(--tracking-label);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-metrics {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--mono-size);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.metric + .metric::before {
|
||||||
|
content: "·";
|
||||||
|
margin-right: var(--space-4);
|
||||||
|
color: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── 호버: 그림이 바뀌고 테두리가 선다 ─────────────── */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.tile-shot :global(img) {
|
||||||
|
transition: opacity var(--dur-slow) var(--ease-soft);
|
||||||
|
}
|
||||||
|
.tile-shot {
|
||||||
|
transition: border-color var(--dur-quick) var(--ease-out);
|
||||||
|
}
|
||||||
|
.tile-link:hover .tile-shot,
|
||||||
|
.tile-link:focus-visible .tile-shot {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.tile-link:hover .tile-shot-alt,
|
||||||
|
.tile-link:focus-visible .tile-shot-alt {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.mosaic {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.tile-lg,
|
||||||
|
.tile-md,
|
||||||
|
.tile-sm {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
.tile-lg .tile-link {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
apps/site/tools/polite.mjs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
/**
|
||||||
|
* 방문자에게 보이는 한국어 본문을 존댓말로 바꾼다.
|
||||||
|
*
|
||||||
|
* 제목(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}개 문자열`);
|
||||||