diff --git a/apps/site/public/work/thursday-flowers/assets/flower-01.webp b/apps/site/public/work/thursday-flowers/assets/flower-01.webp new file mode 100644 index 0000000..8c08048 Binary files /dev/null and b/apps/site/public/work/thursday-flowers/assets/flower-01.webp differ diff --git a/apps/site/public/work/thursday-flowers/assets/flower-02.webp b/apps/site/public/work/thursday-flowers/assets/flower-02.webp new file mode 100644 index 0000000..7f9a906 Binary files /dev/null and b/apps/site/public/work/thursday-flowers/assets/flower-02.webp differ diff --git a/apps/site/public/work/thursday-flowers/assets/flower-03.webp b/apps/site/public/work/thursday-flowers/assets/flower-03.webp new file mode 100644 index 0000000..f63bf20 Binary files /dev/null and b/apps/site/public/work/thursday-flowers/assets/flower-03.webp differ diff --git a/apps/site/public/work/thursday-flowers/assets/flower-04.webp b/apps/site/public/work/thursday-flowers/assets/flower-04.webp new file mode 100644 index 0000000..b418c45 Binary files /dev/null and b/apps/site/public/work/thursday-flowers/assets/flower-04.webp differ diff --git a/apps/site/public/work/thursday-flowers/assets/flower-05.webp b/apps/site/public/work/thursday-flowers/assets/flower-05.webp new file mode 100644 index 0000000..fb333e8 Binary files /dev/null and b/apps/site/public/work/thursday-flowers/assets/flower-05.webp differ diff --git a/apps/site/public/work/thursday-flowers/index.html b/apps/site/public/work/thursday-flowers/index.html new file mode 100644 index 0000000..ca54211 --- /dev/null +++ b/apps/site/public/work/thursday-flowers/index.html @@ -0,0 +1,301 @@ + + + + + + 목요일의 화원 · 플로럴 스튜디오 + + + + + + + + + + + + + + + + + + +
+ + 목요일의 화원 + + +
+ +
+ +
+
+
+

플로럴 스튜디오 / 서울 서촌 / 2019

+ +
+

+ 그 주에 좋은 꽃만
들입니다 +

+ +

+ 목요일 아침에 시장에서 그 주의 꽃을 고르고, 주문받은 만큼만 만듭니다. + 남는 꽃이 없어서 시들 일도 없습니다. +

+ + + 상담 신청하기 + +
+
+ +
+
+ 흰 라넌큘러스와 작약, 유칼립투스로 묶은 꽃다발 +
+
+ 꽃 한 송이의 꽃잎 결이 보이는 접사 +
+
+ 작업대 위에 놓인 꽃 줄기와 가위, 노끈 +
+

+ 주문 제작 / 정기 구독 / 공간 장식 +

+
+
+
+ + +
+
+
+

+ 일주일에 하루만 문을 엽니다 +

+

+ 수요일 밤에 경매가 끝나고, 목요일 새벽 시장에 그 주의 꽃이 나옵니다. + 그날 좋은 것만 골라 그날 만듭니다. +

+

+ 나머지 엿새는 만들지 않습니다. 미리 사다 두면 파는 날까지 꽃이 기다려야 + 하고, 기다린 꽃은 이미 한 번 시든 꽃입니다. +

+
+
+
여는 날
+
매주 목요일 10:00–19:00
+
+
+
주문 마감
+
수요일 18:00
+
+
+
그 주 제작 수
+
최대 20건
+
+
+
+
+ 꽃 줄기를 가위로 다듬는 손 +
목요일 아침, 손질
+
+
+
+ + +
+
+

작업

+

같은 꽃을 두 번 쓰지 않습니다

+

+ 주에 따라 들어오는 꽃이 다르니 결과도 매번 다릅니다. 지난 작업을 그대로 + 다시 만들어 달라는 요청은 받지 않습니다. +

+
+ +
+

+ 더 많은 작업은 + 인스타그램에 + 주 단위로 올립니다. +

+
+
+ + +
+
+
+

격주 목요일, 같은 자리에

+

+ 사무실 응접실이나 매장 카운터처럼 꽃이 늘 있어야 하는 자리를 위한 + 것입니다. 격주로 그 주의 꽃을 가져다 놓고, 지난 것을 가져옵니다. +

+

+ 화병은 저희 것을 두고 씁니다. 자리와 조도를 먼저 보고 정하기 때문에 + 첫 회는 방문 상담으로 시작합니다. +

+ 정기 상담 신청 +
+
+ 꽃잎의 결이 보이는 접사 +
이번 주 · 라넌큘러스
+
+
+
+ + +
+
+

문의

+

무엇에 쓸 꽃인지부터 듣습니다

+

+ 받는 분과 자리, 예산을 알려주시면 그 주에 가능한 것을 먼저 말씀드립니다. + 가능하지 않은 주에는 그렇다고 말씀드립니다. +

+ +
+
+ + +
+
+
+

서촌, 목요일에만

+
+ 서울 종로구 자하문로 00길 00, 2층
+ 경복궁역 3번 출구에서 도보 8분 +
+
+

+ 목요일 외에는 상주하지 않습니다. 다른 요일 방문은 미리 연락 주세요. +

+

목요일의 화원

+
+
+
+ + + + diff --git a/apps/site/public/work/thursday-flowers/styles/main.css b/apps/site/public/work/thursday-flowers/styles/main.css new file mode 100644 index 0000000..cc9a178 --- /dev/null +++ b/apps/site/public/work/thursday-flowers/styles/main.css @@ -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); +} +/* 넓은 화면에서는 두 줄, 좁은 화면에서는 한 줄로 쓴다. +
을 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%; + } +} diff --git a/apps/site/public/work/thursday-flowers/styles/tokens.css b/apps/site/public/work/thursday-flowers/styles/tokens.css new file mode 100644 index 0000000..f87b1ff --- /dev/null +++ b/apps/site/public/work/thursday-flowers/styles/tokens.css @@ -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; + } +} diff --git a/apps/site/src/assets/shots/work-thursday-flowers-2.png b/apps/site/src/assets/shots/work-thursday-flowers-2.png new file mode 100644 index 0000000..1d98778 Binary files /dev/null and b/apps/site/src/assets/shots/work-thursday-flowers-2.png differ diff --git a/apps/site/src/assets/shots/work-thursday-flowers.png b/apps/site/src/assets/shots/work-thursday-flowers.png new file mode 100644 index 0000000..a8a7d15 Binary files /dev/null and b/apps/site/src/assets/shots/work-thursday-flowers.png differ diff --git a/apps/site/src/data/work.ts b/apps/site/src/data/work.ts new file mode 100644 index 0000000..c3d5032 --- /dev/null +++ b/apps/site/src/data/work.ts @@ -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", + }, +]; diff --git a/apps/site/src/i18n/content.ts b/apps/site/src/i18n/content.ts index 2466100..740e325 100644 --- a/apps/site/src/i18n/content.ts +++ b/apps/site/src/i18n/content.ts @@ -108,34 +108,34 @@ export interface Content { const STAGES_KO: Stage[] = [ { name: "브리프 게이트", - does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축한다. 규모를 판정해 경로를 고른다.", - becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다", + does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축합니다. 규모를 판정해 경로를 고릅니다.", + becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분합니다", }, { name: "레퍼런스 조사", - does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조, 톤, 디테일.", - becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다", + does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고릅니다. 구조, 톤, 디테일.", + becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져옵니다", }, { name: "방향 결정", - does: "레퍼런스를 하나의 축으로 합성한다. 감수할 리스크를 하나 고른다.", + does: "레퍼런스를 하나의 축으로 합성합니다. 감수할 리스크를 하나 고릅니다.", becomes: "감상은 이름에만, 화면에는 수치만", }, { name: "디자인 토큰", - does: "코드를 쓰기 전에 숫자를 정한다. 성능 예산도 여기서 확정한다.", + does: "코드를 쓰기 전에 숫자를 정합니다. 성능 예산도 여기서 확정합니다.", becomes: "--measure: 33em / --leading-normal: 1.7 / JS 예산 0KB", mono: true, }, { name: "구현", - does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.", + does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 합니다.", becomes: "

산미 8.5 · 바디 6.0 · 로스팅 라이트

", mono: true, }, { name: "프리플라이트", - does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.", + does: "자기 결과물을 남의 것처럼 봅니다. 게이트 12개는 전부 '아니오'여야 합니다.", becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정", mono: true, }, @@ -181,17 +181,17 @@ export const ko: Content = { meta: { title: "designpaca · 웹 디자인 파이프라인 스킬", description: - "AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치한다.", + "AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치합니다.", }, hero: { h1: "결정하지 않으면 기본값이 나온다", - lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 난다. 그 결정을 여섯 단계로 강제하는 스킬.", + lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 납니다. 그 결정을 여섯 단계로 강제하는 스킬.", install: "npx designpaca", - installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다", + installNote: "Node 20.11+ · 설치 대상을 감지해 골라줍니다", links: [{ label: "npm", href: "https://www.npmjs.com/package/designpaca" }], stage: { - alt: "유리판 여섯 장이 깊이 방향으로 나란히 서 있다. 스크롤하면 카메라가 판을 하나씩 통과하고, 통과한 판은 뒤로 물러난다.", - caption: "판 하나가 단계 하나다. 통과 조건을 채우지 못하면 다음 판으로 가지 않는다.", + alt: "유리판 여섯 장이 깊이 방향으로 나란히 서 있습니다. 스크롤하면 카메라가 판을 하나씩 통과하고, 통과한 판은 뒤로 물러납니다.", + caption: "판 하나가 단계 하나다. 통과 조건을 채우지 못하면 다음 판으로 가지 않습니다.", gate: "게이트", pass: "통과", }, @@ -200,7 +200,7 @@ export const ko: Content = { eyebrow: "여섯 단계", h2: "브리프 한 줄이 검증된 페이지가 될 때까지", lead: - "각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.", + "각 단계에는 통과 조건이 있습니다. 통과하지 못하면 다음으로 가지 않습니다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.", briefLabel: "들어온 브리프", brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘", stages: STAGES_KO, @@ -208,17 +208,17 @@ export const ko: Content = { showcase: { h2: "결과물은 서로 닮지 않는다", lead: - "같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않는다. 둘 다 수치 자리가 비어 있는 것도 우연이 아니다.", + "같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않습니다. 둘 다 수치 자리가 비어 있는 것도 우연이 아닙니다.", items: [ { - alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있다.", + alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있습니다.", caption: "수제 커피 로스터리 · 구독 전환", - note: "프리셋 quiet-commerce. 사진이 없다는 제약을 수치표로 뒤집었다. 표의 값은 아직 비어 있다. 로스터에게 받기 전까지 채우지 않는다.", + note: "프리셋 quiet-commerce. 사진이 없습니다는 제약을 수치표로 뒤집었습니다. 표의 값은 아직 비어 있습니다. 로스터에게 받기 전까지 채우지 않습니다.", }, { - alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있다.", + alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있습니다.", caption: "API 모니터링 SaaS · 무료 체험 가입", - note: "개발자 도구라고 자동으로 다크로 가지 않았다. 브리프에 근거가 없었기 때문이다. 리전 수는 아직 중괄호 자리로 남아 있다.", + note: "개발자 도구라고 자동으로 다크로 가지 않았습니다. 브리프에 근거가 없었기 때문입니다. 리전 수는 아직 중괄호 자리로 남아 있습니다.", }, ], }, @@ -226,43 +226,43 @@ export const ko: Content = { eyebrow: "SVG 필터", h2: "이미지 편집기를 열지 않고 고치는 네 가지", lead: - "질감을 만드는 필터 데모는 많다. 손이 실제로 줄어드는 자리는 따로 있다. 아래 넷은 왼쪽이 매번 손으로 맞추던 것, 오른쪽이 같은 소재에 선언 하나만 얹은 것이다. 아래 수치는 이 화면을 그대로 캡처해 잰 값이다.", + "질감을 만드는 필터 데모는 많다. 손이 실제로 줄어드는 자리는 따로 있습니다. 아래 넷은 왼쪽이 매번 손으로 맞추던 것, 오른쪽이 같은 소재에 선언 하나만 얹은 것입니다. 아래 수치는 이 화면을 그대로 캡처해 잰 값이다.", demos: [ { id: "scrim", name: "글자 모양대로 생기는 스크림", - problem: "사진 위 캡션이 대비를 못 넘긴다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞추게 된다.", + problem: "사진 위 캡션이 대비를 못 넘깁니다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞추게 됩니다.", beforeLabel: "그냥 얹음", afterLabel: "filter: url(#scrim)", - how: "글자 알파를 부풀려 그 모양대로만 색을 채운다. 박스가 아니라 글자를 따라가므로 사진을 덜 가린다.", + how: "글자 알파를 부풀려 그 모양대로만 색을 채웁니다. 박스가 아니라 글자를 따라가므로 사진을 덜 가립니다.", verdict: "흰 글자 대비 1.70:1 → 4.79:1", }, { id: "cvd", name: "색각 이상 시뮬레이션", - problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 된다. 내 화면만 봐서는 못 잡는다.", + problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 됩니다. 내 화면만 봐서는 못 잡는다.", beforeLabel: "내가 보는 화면", afterLabel: "적록색약(2형)", - how: "feColorMatrix 한 줄. 이것만은 linearRGB 로 둔다 — 색각 변환은 물리적인 빛의 혼합이다.", + how: "feColorMatrix 한 줄. 이것만은 linearRGB 로 둡니다 — 색각 변환은 물리적인 빛의 혼합이다.", verdict: "배지 셋의 최소 색거리 82 → 2 · 주의와 실패가 같은 색", }, { id: "goo", name: "떨어진 것이 하나로 뭉치기", - problem: "조각이 녹아 붙는 표현에 캔버스나 물리 라이브러리를 켠다. 로더와 메뉴 전환에서 반복된다.", + problem: "조각이 녹아 붙는 표현에 캔버스나 물리 라이브러리를 켜게 됩니다. 로더와 메뉴 전환에서 반복됩니다.", beforeLabel: "필터 없음 · 원 3개", afterLabel: "filter: url(#goo)", - how: "블러한 뒤 알파만 세워 잘라낸다. 붙을지 말지는 취향이 아니라 간격과 블러 반경의 비율로 정해진다.", + how: "블러한 뒤 알파만 세워 잘라냅니다. 붙을지 말지는 취향이 아니라 간격과 블러 반경의 비율로 정해진다.", verdict: "덩어리 3개 → 1개 · JS 0줄", }, { id: "glass", name: "배경을 휘게 하는 오버레이", - problem: "반투명 패널은 뒤를 흐리게만 만든다. 실제 유리는 빛을 휜다.", + problem: "반투명 패널은 뒤를 흐리게만 만듭니다. 실제 유리는 빛을 휜다.", beforeLabel: "backdrop-filter: blur", afterLabel: "blur + url(#lens)", - how: "변위 지도를 backdrop-filter 에 넘긴다. 노이즈로 만들면 값이 중립으로 수렴해 아무 일도 안 일어난다.", - verdict: "가장자리에서 격자가 눌린다 · Chromium 전용", + how: "변위 지도를 backdrop-filter 에 넘깁니다. 노이즈로 만들면 값이 중립으로 수렴해 아무 일도 안 일어납니다.", + verdict: "가장자리에서 격자가 눌립니다 · Chromium 전용", }, ], }, @@ -270,112 +270,112 @@ export const ko: Content = { eyebrow: "이 페이지의 수치", h2: "카피만 고쳐서는 슬롭이 없어지지 않는다", lead: - "문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.", + "문장을 다듬는 것으로 해결되는 것은 문장뿐입니다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남습니다. 그래서 이 페이지 자신의 형태 수치를 공개합니다. 결과가 나쁘게 나와도 그대로 싣는다.", items: [ { label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", 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: "radius 어휘", value: "2종", note: "3px 과 14px. 알약 모양을 없앴다" }, - { label: "반투명 패널", value: "2개", note: "29개까지 늘렸다가 걷어냈다. 유리는 무언가를 담는 액자에만 남겼다" }, + { label: "radius 어휘", value: "2종", note: "3px 과 14px. 알약 모양을 없앴습니다" }, + { label: "반투명 패널", value: "2개", note: "29개까지 늘렸다가 걷어냈습니다. 유리는 무언가를 담는 액자에만 남겼습니다" }, { label: "타입 단계", value: "6단계", note: "토큰에 정의된 수. 디스플레이는 스케일 밖이고, 최대 글자는 본문의 5.6배" }, - { label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" }, + { label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받습니다" }, { label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" }, - { label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" }, + { label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잽니다" }, ], caveat: - "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.", + "값은 프로덕션 빌드에서 측정합니다. 아직 측정하지 않은 항목은 비워둡니다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.", }, preflight: { h2: "이 페이지를 만들며 실제로 걸린 것", - lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.", + lead: "남을 검사한 결과가 아니라 자신을 검사한 결과입니다.", items: [ { - caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었다", + caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었습니다", fixed: - "웹폰트가 오기 전에는 시스템 폰트로 그려진다. 맑은 고딕은 Pretendard 보다 13% 넓어서 교체되는 순간 줄바꿈이 다시 잡힌다. 폴백 폰트에 size-adjust 로 치수를 맞췄다. 실측 차이 0.0%. OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의했다.", + "웹폰트가 오기 전에는 시스템 폰트로 그려진다. 맑은 고딕은 Pretendard 보다 13% 넓어서 교체되는 순간 줄바꿈이 다시 잡힙니다. 폴백 폰트에 size-adjust 로 치수를 맞췄다. 실측 차이 0.0%. OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의했습니다.", }, { - caught: "유리 카드를 29개 뿌렸다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다", + caught: "유리 카드를 29개 뿌렸습니다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였습니다", fixed: - "우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨다. 유리는 5개만 남았다.", + "우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었습니다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨습니다. 유리는 5개만 남았습니다.", }, { - caught: "“backdrop-filter 에 SVG 를 얹으면 Chrome 이 버린다” 고 적었는데 사실이 아니었다", + caught: "“backdrop-filter 에 SVG 를 얹으면 Chrome 이 버린다” 고 적었는데 사실이 아니었습니다", 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: - "main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.", + "main 에서 그리드 클래스가 빠져 있었습니다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였습니다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.", }, { - caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다", + caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였습니다", fixed: - "scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.", + "scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못합니다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨습니다.", }, { caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1", fixed: - "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.", + "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였습니다. 반전 대응 토큰을 하나 더 두고 함께 바꿨습니다.", }, ], - note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못한다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.", + note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못합니다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.", }, typography: { eyebrow: "한글 조판", h2: "라틴 기본값은 한글에서 세 군데가 깨진다", lead: - "영어권 디자인 도구에는 한글 조판 규칙이 없다. 그래서 기본값을 그대로 쓰면 아래 셋이 동시에 무너진다. 왼쪽이 기본값, 오른쪽이 이 스킬이 정한 값이다. 글자도 폭도 폰트도 같다.", - sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 한다.", + "영어권 디자인 도구에는 한글 조판 규칙이 없습니다. 그래서 기본값을 그대로 쓰면 아래 셋이 동시에 무너집니다. 왼쪽이 기본값, 오른쪽이 이 스킬이 정한 값이다. 글자도 폭도 폰트도 같습니다.", + sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 합니다.", demos: [ { id: "break", title: "줄바꿈", badLabel: "기본값", goodLabel: "word-break: keep-all", - note: "기본값은 어절 한가운데를 끊는다. 왼쪽 마지막 줄을 보라 — 한 단어가 두 줄에 걸쳐 있다.", + note: "기본값은 어절 한가운데를 끊는다. 왼쪽 마지막 줄을 보라 — 한 단어가 두 줄에 걸쳐 있습니다.", }, { id: "leading", title: "행간", badLabel: "line-height 1.45", goodLabel: "line-height 1.75", - note: "라틴 표준 1.4~1.5 는 받침이 있는 글자에 좁다. 줄이 서로 붙어 보인다.", + note: "라틴 표준 1.4~1.5 는 받침이 있는 글자에 좁습니다. 줄이 서로 붙어 보입니다.", }, { id: "size", title: "크기", badLabel: "11px", goodLabel: "15px", - note: "라틴에서 11px 은 라벨로 쓴다. 한글은 획이 많아 같은 크기에서 뭉갠다.", + note: "라틴에서 11px 은 라벨로 씁니다. 한글은 획이 많아 같은 크기에서 뭉갠다.", }, ], rules: [ - "word-break: keep-all 없으면 단어가 아무 데서나 잘린다", + "word-break: keep-all 없으면 단어가 아무 데서나 잘립니다", "line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게", - "음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다", - "한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다", - "한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다", + "음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개집니다", + "한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 깁니다", + "한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호입니다", ], }, rules: { h2: "규칙마다 왜 그런지 적혀 있다", - lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.", + lead: "근거 없는 금지는 지켜지지 않습니다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이깁니다.", items: [ - { rule: "레퍼런스 없이 시작하지 않는다", why: "머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 된다." }, - { rule: "강조색은 하나다", why: "두 개가 필요하다고 느끼면 위계 설계가 실패한 것이다." }, - { rule: "대담함은 한 곳에만", why: "두 곳에서 소리치면 둘 다 죽는다." }, - { rule: "화려함은 4순위다", why: "심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10이다." }, - { rule: "지어낸 수치를 쓰지 않는다", why: "숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎는다." }, - { rule: "이펙트를 다 꺼도 완성돼 있어야 한다", why: "그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것이다." }, + { rule: "레퍼런스 없이 시작하지 않는다", why: "머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 됩니다." }, + { rule: "강조색은 하나다", why: "두 개가 필요하다고 느끼면 위계 설계가 실패한 것입니다." }, + { rule: "대담함은 한 곳에만", why: "두 곳에서 소리치면 둘 다 죽습니다." }, + { rule: "화려함은 4순위다", why: "심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10입니다." }, + { rule: "지어낸 수치를 쓰지 않는다", why: "숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎습니다." }, + { rule: "이펙트를 다 꺼도 완성돼 있어야 한다", why: "그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것입니다." }, ], }, install: { h2: "한 줄이면 된다", - lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.", + lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않습니다.", command: "npx designpaca", targets: [ { name: "Claude Code", path: "~/.claude/skills/designpaca/" }, @@ -386,7 +386,7 @@ export const ko: Content = { { name: "AGENTS.md", path: "블록 주입. 에이전트 무관" }, ], commands: [ - { cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" }, + { cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뜁니다" }, { cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" }, { cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" }, ], @@ -394,7 +394,7 @@ export const ko: Content = { nav: { items: [ { label: "파이프라인", href: "#pipeline" }, - { label: "사례", href: "#showcase" }, + { label: "사례", href: "/showcase" }, { label: "수치", href: "#metrics" }, { label: "설치", href: "#install" }, ], @@ -661,7 +661,7 @@ export const en: Content = { nav: { items: [ { label: "Pipeline", href: "#pipeline" }, - { label: "Work", href: "#showcase" }, + { label: "Work", href: "/showcase" }, { label: "Numbers", href: "#metrics" }, { label: "Install", href: "#install" }, ], diff --git a/apps/site/src/pages/en/index.astro b/apps/site/src/pages/en/index.astro index ddd60a7..215c9a0 100644 --- a/apps/site/src/pages/en/index.astro +++ b/apps/site/src/pages/en/index.astro @@ -2,7 +2,6 @@ import Base from "../../layouts/Base.astro"; import Hero from "../../components/Hero.astro"; import Pipeline from "../../components/Pipeline.astro"; -import Showcase from "../../components/Showcase.astro"; import Materials from "../../components/Materials.astro"; import Metrics from "../../components/Metrics.astro"; import Preflight from "../../components/Preflight.astro"; @@ -20,7 +19,7 @@ import { en as c } from "../../i18n/content"; ① 무엇인가 히어로 · 파이프라인 ② 무엇을 아는가 SVG 필터 · 한글 조판 - ③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙 + ③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다) ④ 행동 설치 */} @@ -29,7 +28,6 @@ import { en as c } from "../../i18n/content"; - diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro index bf9bc50..11e762f 100644 --- a/apps/site/src/pages/index.astro +++ b/apps/site/src/pages/index.astro @@ -2,7 +2,6 @@ import Base from "../layouts/Base.astro"; import Hero from "../components/Hero.astro"; import Pipeline from "../components/Pipeline.astro"; -import Showcase from "../components/Showcase.astro"; import Materials from "../components/Materials.astro"; import Metrics from "../components/Metrics.astro"; import Preflight from "../components/Preflight.astro"; @@ -20,7 +19,7 @@ import { ko as c } from "../i18n/content"; ① 무엇인가 히어로 · 파이프라인 ② 무엇을 아는가 SVG 필터 · 한글 조판 - ③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙 + ③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다) ④ 행동 설치 */} @@ -29,7 +28,6 @@ import { ko as c } from "../i18n/content"; - diff --git a/apps/site/src/pages/showcase.astro b/apps/site/src/pages/showcase.astro new file mode 100644 index 0000000..a0708cd --- /dev/null +++ b/apps/site/src/pages/showcase.astro @@ -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 파이프라인으로 만든 사이트들. 각 사례의 컨셉과 감수한 리스크, 실측값을 함께 적었습니다. 타일을 누르면 실제 사이트가 열립니다."; +--- + + +
+ +

사례

+

파이프라인을 돌려서 나온 것들

+

+ 모두 0단계부터 6단계까지 그대로 돌려서 나온 결과물입니다. 각 사례마다 + 감수한 리스크를 함께 적었습니다 — 그것이 없으면 안전하지만 잊히는 디자인이 됩니다. +

+

+ 타일을 누르면 실제 사이트가 열립니다. 목업이 아니라 동작하는 페이지입니다. +

+
+ +
+ + +
+ + + diff --git a/apps/site/tools/polite.mjs b/apps/site/tools/polite.mjs new file mode 100644 index 0000000..03516e0 --- /dev/null +++ b/apps/site/tools/polite.mjs @@ -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}개 문자열`);