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 로 돌아왔다.
This commit is contained in:
Yun Chan 2026-08-21 08:36:35 +09:00
parent b92853bf2b
commit 30dc6687dd
16 changed files with 1555 additions and 75 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View 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",
},
];

View file

@ -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: "<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>",
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" },
],

View file

@ -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 로 뺐다)
④ 행동 설치
*/}
<Hero c={c.hero} ui={c.ui} />
@ -29,7 +28,6 @@ import { en as c } from "../../i18n/content";
<Materials c={c.materials} />
<Typography c={c.typography} />
<Showcase c={c.showcase} />
<Metrics c={c.metrics} />
<Preflight c={c.preflight} />
<Rules c={c.rules} />

View file

@ -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 로 뺐다)
④ 행동 설치
*/}
<Hero c={c.hero} ui={c.ui} />
@ -29,7 +28,6 @@ import { ko as c } from "../i18n/content";
<Materials c={c.materials} />
<Typography c={c.typography} />
<Showcase c={c.showcase} />
<Metrics c={c.metrics} />
<Preflight c={c.preflight} />
<Rules c={c.rules} />

View 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>