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

플로럴 스튜디오 / 서울 서촌 / 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 deleted file mode 100644 index cc9a178..0000000 --- a/apps/site/public/work/thursday-flowers/styles/main.css +++ /dev/null @@ -1,602 +0,0 @@ -@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 deleted file mode 100644 index f87b1ff..0000000 --- a/apps/site/public/work/thursday-flowers/styles/tokens.css +++ /dev/null @@ -1,84 +0,0 @@ -/* - 목요일의 화원 — 디자인 토큰 - 전부 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 deleted file mode 100644 index 1d98778..0000000 Binary files a/apps/site/src/assets/shots/work-thursday-flowers-2.png and /dev/null differ diff --git a/apps/site/src/assets/shots/work-thursday-flowers.png b/apps/site/src/assets/shots/work-thursday-flowers.png deleted file mode 100644 index a8a7d15..0000000 Binary files a/apps/site/src/assets/shots/work-thursday-flowers.png and /dev/null differ diff --git a/apps/site/src/data/work.ts b/apps/site/src/data/work.ts deleted file mode 100644 index c3d5032..0000000 --- a/apps/site/src/data/work.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 쇼케이스 목록. - * - * 사례를 추가할 때 **여기 한 항목만 넣으면** 모자이크 그리드에 자동으로 들어간다. - * 페이지 코드는 건드리지 않는다. - * - * 순서가 곧 표시 순서다(최근 것을 위에). `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 740e325..a9908fc 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,123 +270,122 @@ 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/" }, { name: "Codex CLI", path: "~/.codex/skills/designpaca/" }, - { name: "Antigravity", path: ".agents/skills/designpaca/" }, { name: "Cursor", path: ".cursor/rules/designpaca.mdc" }, { name: "Windsurf", path: ".windsurf/rules/designpaca.md" }, { 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 +393,7 @@ export const ko: Content = { nav: { items: [ { label: "파이프라인", href: "#pipeline" }, - { label: "사례", href: "/showcase" }, + { label: "사례", href: "#showcase" }, { label: "수치", href: "#metrics" }, { label: "설치", href: "#install" }, ], @@ -647,7 +646,6 @@ export const en: Content = { targets: [ { name: "Claude Code", path: "~/.claude/skills/designpaca/" }, { name: "Codex CLI", path: "~/.codex/skills/designpaca/" }, - { name: "Antigravity", path: ".agents/skills/designpaca/" }, { name: "Cursor", path: ".cursor/rules/designpaca.mdc" }, { name: "Windsurf", path: ".windsurf/rules/designpaca.md" }, { name: "AGENTS.md", path: "block injection, agent agnostic" }, @@ -661,7 +659,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 215c9a0..ddd60a7 100644 --- a/apps/site/src/pages/en/index.astro +++ b/apps/site/src/pages/en/index.astro @@ -2,6 +2,7 @@ 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"; @@ -19,7 +20,7 @@ import { en as c } from "../../i18n/content"; ① 무엇인가 히어로 · 파이프라인 ② 무엇을 아는가 SVG 필터 · 한글 조판 - ③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다) + ③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙 ④ 행동 설치 */} @@ -28,6 +29,7 @@ import { en as c } from "../../i18n/content"; + diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro index 11e762f..bf9bc50 100644 --- a/apps/site/src/pages/index.astro +++ b/apps/site/src/pages/index.astro @@ -2,6 +2,7 @@ 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"; @@ -19,7 +20,7 @@ import { ko as c } from "../i18n/content"; ① 무엇인가 히어로 · 파이프라인 ② 무엇을 아는가 SVG 필터 · 한글 조판 - ③ 정말 그런가 수치 · 자기 감사 · 규칙 (사례는 /showcase 로 뺐다) + ③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙 ④ 행동 설치 */} @@ -28,6 +29,7 @@ import { ko as c } from "../i18n/content"; + diff --git a/apps/site/src/pages/showcase.astro b/apps/site/src/pages/showcase.astro deleted file mode 100644 index a0708cd..0000000 --- a/apps/site/src/pages/showcase.astro +++ /dev/null @@ -1,256 +0,0 @@ ---- -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 deleted file mode 100644 index 03516e0..0000000 --- a/apps/site/tools/polite.mjs +++ /dev/null @@ -1,186 +0,0 @@ -/** - * 방문자에게 보이는 한국어 본문을 존댓말로 바꾼다. - * - * 제목(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}개 문자열`); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index c1774eb..0000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# designpaca - -## 0.5.0 - -### Minor Changes - -- 8d02820: 어떤 하네스 에이전트든 스킬을 발견하게 만든다. 중립 경로 `agents` 타깃(`~/.agents/skills` · `.agents/skills`)을 새로 추가한다 — Cursor · Antigravity · ZCode 등 Agent Skills 규약을 읽는 하네스가 함께 잡는 자리다. `gemini-cli`(`.gemini/skills`)와 `copilot`(`.github/skills` · `~/.copilot/skills`) 타깃을 추가한다. Cursor 는 구식 `.cursor/rules/*.mdc` 변환을 버리고 네이티브 `.cursor/skills` 로 옮기며 전역 설치를 지원한다. Windsurf 도 규칙 파일(12,000자 상한 때문에 실제 스킬이 아예 설치되지 못했다)에서 `.windsurf/skills` · `~/.codeium/windsurf/skills` 네이티브 경로로 옮긴다. Antigravity 전역 경로를 공식 마이그레이션 문서대로 `~/.gemini/antigravity-cli/skills` 로 바로잡는다. -- 8d02820: ZCode 타깃 추가 — `~/.zcode/skills/designpaca/` (전역) 또는 `.zcode/skills/designpaca/` (프로젝트) 에 스킬을 설치한다. SKILL.md 규약이 Claude Code 와 같아 변환 없이 그대로 복사된다. diff --git a/packages/cli/README.md b/packages/cli/README.md index 18d8cba..19ea25d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,8 +1,6 @@ # designpaca -**웹 디자인 파이프라인 스킬을 어떤 에이전트 하네스에든 한 줄로 설치한다.** - -Agent Skills 규약(SKILL.md)을 그대로 따르므로, 규약을 읽는 하네스라면 종류를 가리지 않는다. +**웹 디자인 파이프라인 스킬을 Claude Code · Codex · Cursor 에 한 줄로 설치한다.** ```bash npx designpaca @@ -55,19 +53,11 @@ npx designpaca install -t claude-code,codex -s user -y npx designpaca install -t cursor -s project ``` -모든 타깃이 같은 SKILL.md 를 쓴다. 중립 경로(`agents`)는 이 규약을 읽는 하네스라면 무엇이든 잡는다. - | 대상 | 설치 위치 | |---|---| | `claude-code` | `~/.claude/skills/designpaca/` (또는 프로젝트 `.claude/`) | -| `codex` | `~/.codex/skills/designpaca/` (또는 프로젝트 `.codex/`) | -| `cursor` | `~/.cursor/skills/designpaca/` (또는 프로젝트 `.cursor/`) | -| `zcode` | `~/.zcode/skills/designpaca/` (또는 프로젝트 `.zcode/`) | -| `agents` | `~/.agents/skills/designpaca/` (또는 프로젝트 `.agents/`) — **중립 경로** | -| `antigravity` | 전역 `~/.gemini/antigravity-cli/skills/` · 워크스페이스 `.agents/skills/` | -| `gemini-cli` | `~/.gemini/skills/designpaca/` (또는 프로젝트 `.gemini/`) | -| `windsurf` | `~/.codeium/windsurf/skills/designpaca/` (또는 프로젝트 `.windsurf/`) | -| `copilot` | `~/.copilot/skills/designpaca/` (또는 프로젝트 `.github/skills/`) | +| `codex` | `~/.codex/skills/designpaca/` | +| `cursor` | `.cursor/rules/designpaca.mdc` (프로젝트 단위) | | `agents-md` | `AGENTS.md` 에 블록 주입 | ## 사용 @@ -105,7 +95,7 @@ npx designpaca update --force | 옵션 | 설명 | |---|---| -| `-t, --target ` | `claude-code` `codex` `cursor` `zcode` `agents` `antigravity` `gemini-cli` `windsurf` `copilot` `agents-md` | +| `-t, --target ` | `claude-code` `codex` `cursor` `agents-md` | | `-s, --scope <범위>` | `user` (전역) \| `project` | | `-y, --yes` | 확인 없이 진행 | | `-f, --force` | 직접 수정한 파일도 덮어쓴다 (`.orig` 백업) | diff --git a/packages/cli/package.json b/packages/cli/package.json index 9817a4d..fd9903d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,35 +1,16 @@ { "name": "designpaca", - "version": "0.5.0", + "version": "0.1.0", "description": "웹 디자인 파이프라인 스킬 — Claude Code · Codex · Cursor 에 한 줄로 설치한다", - "keywords": [ - "design", - "web-design", - "claude-code", - "codex", - "cursor", - "agent-skill", - "svg-filter", - "threejs" - ], + "keywords": ["design", "web-design", "claude-code", "codex", "cursor", "agent-skill", "svg-filter", "threejs"], "license": "MIT", "author": "Yun Chan", "homepage": "https://designpaca.chanpaca.net", - "repository": { - "type": "git", - "url": "git+https://git.chanpaca.net/yunchan/designpaca.git" - }, + "repository": { "type": "git", "url": "git+https://git.chanpaca.net/yunchan/designpaca.git" }, "type": "module", - "bin": { - "designpaca": "./dist/index.js" - }, - "files": [ - "dist", - "README.md" - ], - "engines": { - "node": ">=20.11" - }, + "bin": { "designpaca": "./dist/index.js" }, + "files": ["dist", "README.md"], + "engines": { "node": ">=20.11" }, "scripts": { "build": "node scripts/bundle-skill.mjs && tsup", "dev": "node scripts/bundle-skill.mjs && tsup --watch", diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 6a7b7fb..66f1e0c 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -18,7 +18,7 @@ export async function runDoctor(): Promise { if (pruned > 0) console.log(info(`사라진 설치 기록 ${pruned}건을 정리했다`)); if (manifest.installs.length === 0) { - console.log(warn("아직 설치된 곳이 없습니다. `npx designpaca install` 로 설치하실 수 있습니다.")); + console.log(warn("설치된 곳이 없다. `npx designpaca install` 로 설치해라.")); return 0; } @@ -40,7 +40,7 @@ export async function runDoctor(): Promise { if (rec.version !== version) problems++; if (missing.length > 0) { problems++; - console.log(bad(`파일 ${missing.length}개가 보이지 않습니다 — \`designpaca update --force\` 로 복구하실 수 있습니다`)); + console.log(bad(`파일 ${missing.length}개가 사라졌다 — \`designpaca update --force\` 로 복구해라`)); for (const m of missing.slice(0, 5)) console.log(dim(` ${tildify(m.path)}`)); } if (modified.length > 0) { diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 34ed3b4..5d717e5 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -60,7 +60,7 @@ export function printOutcomes(outcomes: InstallOutcome[], dryRun = false): void const verb = dryRun ? "쓸 파일" : "설치됨"; console.log(ok(`${o.target} — ${verb} ${o.written}개 ${dim(tildify(o.root))}`)); if (o.skipped.length > 0) { - console.log(warn(` 직접 수정하신 파일이라 그대로 두었습니다 (--force 로 덮어쓸 수 있습니다):`)); + console.log(warn(` 직접 수정한 파일이라 건드리지 않았다 (--force 로 덮어쓴다):`)); for (const s of fold(o.skipped)) console.log(dim(` ${tildify(s)}`)); } } @@ -70,41 +70,16 @@ export function printOutcomes(outcomes: InstallOutcome[], dryRun = false): void export function printNextSteps(targets: TargetId[]): void { console.log(heading("다음 단계")); if (targets.includes("claude-code")) { - console.log(info(`Claude Code 를 새로 연 뒤 ${dim("/designpaca")} 를 실행해 주세요`)); + console.log(info(`Claude Code 를 새로 열고 ${dim("/designpaca")} 를 실행해라`)); } if (targets.includes("codex")) { console.log(info(`Codex CLI 에서 designpaca 스킬이 자동으로 잡힌다`)); } - if (targets.includes("zcode")) { - console.log(info(`ZCode 는 새 세션부터 designpaca 스킬이 자동으로 잡힌다`)); - } if (targets.includes("cursor")) { - console.log(info(`Cursor 는 새 창을 열 때 ${dim(".cursor/skills/designpaca")} 스킬을 읽는다`)); - } - if (targets.includes("agents")) { - console.log( - info( - `${dim(".agents/skills")} 는 중립 경로다 — Cursor · Antigravity · ZCode 등 규약을 읽는 하네스가 함께 잡는다`, - ), - ); - } - if (targets.includes("antigravity")) { - console.log( - info(`Antigravity 는 전역 ${dim("~/.gemini/antigravity-cli/skills")} 를 읽는다 (워크스페이스는 .agents/skills)`), - ); - } - if (targets.includes("gemini-cli")) { - console.log(info(`Gemini CLI 는 ${dim("~/.gemini/skills")} 에서 스킬을 읽는다`)); + console.log(info(`Cursor 는 ${dim(".cursor/rules/designpaca.mdc")} 를 프로젝트 열 때 읽는다`)); } if (targets.includes("windsurf")) { - console.log( - info(`Windsurf(Cascade) 는 ${dim(".windsurf/skills/designpaca")} 스킬을 읽는다`), - ); - } - if (targets.includes("copilot")) { - console.log( - info(`GitHub Copilot 은 ${dim(".github/skills")} (~/.copilot/skills) 의 SKILL.md 를 읽는다`), - ); + console.log(info(`Windsurf 는 ${dim(".windsurf/rules/designpaca.md")} 를 읽는다`)); } if (targets.includes("agents-md")) { console.log( diff --git a/packages/cli/src/commands/tools.ts b/packages/cli/src/commands/tools.ts deleted file mode 100644 index d71e996..0000000 --- a/packages/cli/src/commands/tools.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { checkTools } from "@designpaca/core"; -import { bad, dim, heading, info, ok, table, warn } from "../ui.ts"; - -/** - * 선택 도구의 상태를 보여주고, 없는 것을 설치한다. - * - * 온보딩에서 건너뛴 사람과, 나중에 필요해진 사람 모두를 위한 자리다. - * `--yes` 없이 비대화형으로 부르면 **상태만 보여주고 아무것도 설치하지 않는다** — - * 도구 설치는 수백 MB 를 받는 일이라 조용히 시작하면 안 된다. - */ -export async function runTools(opts: { install: boolean }): Promise { - const status = await checkTools(); - - console.log(heading("브라우저 도구")); - console.log( - table( - status.map(({ tool, installed }) => [ - tool.label, - installed ? "설치됨" : dim("설치되지 않음"), - ]), - ), - ); - - const missing = status.filter((s) => !s.installed); - - if (missing.length === 0) { - console.log(ok("필요한 도구가 모두 준비돼 있습니다.")); - return 0; - } - - console.log(); - for (const { tool } of missing) { - console.log(`${tool.label} ${dim(`· ${tool.size}`)}`); - console.log(` ${dim(tool.why)}`); - } - - if (!opts.install) { - console.log(); - console.log(info("설치하시려면 `designpaca tools --yes` 를 실행해 주세요.")); - return 0; - } - - console.log(); - let failed = 0; - const followUps: string[] = []; - for (const { tool } of missing) { - process.stdout.write(`${tool.label} 설치 중… `); - try { - await tool.install(); - console.log("완료"); - if (tool.followUp) followUps.push(tool.followUp); - } catch (err) { - failed++; - console.log("실패"); - const first = err instanceof Error ? (err.message.split("\n")[0] ?? err.message) : String(err); - console.log(` ${bad(first)}`); - } - } - - if (followUps.length > 0) { - console.log(); - console.log(warn("아래는 에이전트 설정을 바꾸는 것이라 직접 실행해 주세요.")); - for (const f of followUps) console.log(` ${f}`); - } - - return failed > 0 ? 1 : 0; -} diff --git a/packages/cli/src/commands/uninstall.ts b/packages/cli/src/commands/uninstall.ts index fbe20c8..26aba00 100644 --- a/packages/cli/src/commands/uninstall.ts +++ b/packages/cli/src/commands/uninstall.ts @@ -11,7 +11,7 @@ export async function runUninstall(opts: { ); if (targets.length === 0) { - console.log(warn("제거할 설치 기록이 없습니다")); + console.log(warn("제거할 설치 기록이 없다")); return 0; } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 7f5980b..aeb3014 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -12,7 +12,7 @@ export async function runUpdate(opts: { force?: boolean } = {}): Promise const manifest = await readManifest(); if (manifest.installs.length === 0) { - console.log(warn("설치 기록이 없습니다. `npx designpaca install` 을 먼저 실행해 주세요.")); + console.log(warn("설치 기록이 없다. `npx designpaca install` 을 먼저 실행해라.")); return 1; } @@ -20,7 +20,7 @@ export async function runUpdate(opts: { force?: boolean } = {}): Promise const stale = manifest.installs.filter((i) => i.version !== version); if (stale.length === 0) { - console.log(ok("설치된 스킬이 모두 최신 버전입니다")); + console.log(ok("모든 설치가 이미 최신이다")); // 버전이 같아도 파일이 사라졌거나 수정됐을 수 있다. 조용히 넘기면 사용자가 모른다. let broken = 0; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7855ce1..bd0d5a6 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,7 +3,6 @@ import { getVersion } from "./skill.ts"; import { checkForUpdate } from "./update-check.ts"; import { printNextSteps, printOutcomes, runInstall } from "./commands/install.ts"; import { runDoctor } from "./commands/doctor.ts"; -import { runTools } from "./commands/tools.ts"; import { runUpdate } from "./commands/update.ts"; import { runUninstall } from "./commands/uninstall.ts"; import { runList } from "./commands/list.ts"; @@ -71,15 +70,14 @@ ${bold("사용법")} npx designpaca uninstall 제거 npx designpaca doctor 설치 상태 진단 npx designpaca list 설치 가능 대상과 현재 설치 목록 - npx designpaca tools 브라우저 도구 상태 확인 ${dim("(--yes 로 설치)")} ${bold("옵션")} -t, --target 설치 대상: ${VALID_TARGETS.join(", ")} -s, --scope <범위> user | project ${dim("(기본: user)")} - -y, --yes 확인 없이 진행합니다 - -f, --force 직접 수정한 파일도 덮어씁니다 ${dim("(.orig 로 백업)")} - --dry-run 쓰지 않고 계획만 보여줍니다 - --no-update-check 새 버전 확인을 건너뜁니다 + -y, --yes 확인 없이 진행 + -f, --force 직접 수정한 파일도 덮어쓴다 ${dim("(.orig 로 백업)")} + --dry-run 쓰지 않고 계획만 출력 + --no-update-check 새 버전 확인을 건너뛴다 -h, --help 이 도움말 -v, --version 버전 @@ -87,7 +85,6 @@ ${bold("예시")} ${dim("npx designpaca install -t claude-code,codex -s user -y")} ${dim("npx designpaca install -t cursor -s project")} ${dim("npx designpaca update --force")} - ${dim("npx designpaca tools --yes")} `.trimStart(); } @@ -149,16 +146,12 @@ async function main(): Promise { case "doctor": code = await runDoctor(); break; - case "tools": - // 수백 MB 를 받는 일이라 --yes 없이는 상태만 보여준다 - code = await runTools({ install: args.yes }); - break; case "list": case "ls": code = await runList(); break; default: - console.error(bad(`알 수 없는 명령입니다: ${args.command}`)); + console.error(bad(`알 수 없는 명령: ${args.command}`)); console.log(usage(version)); return 2; } @@ -167,7 +160,7 @@ async function main(): Promise { const latest = await checkForUpdate(version); if (latest) { console.log( - "\n" + warn(`새 버전 v${latest} 이 나왔습니다 — ${dim("npx designpaca@latest update")}`), + "\n" + warn(`새 버전 ${bold(`v${latest}`)} 이 있다 — ${dim("npx designpaca@latest update")}`), ); } return code; diff --git a/packages/cli/src/tui/onboard.ts b/packages/cli/src/tui/onboard.ts index a9e3013..4a621be 100644 --- a/packages/cli/src/tui/onboard.ts +++ b/packages/cli/src/tui/onboard.ts @@ -2,7 +2,6 @@ import * as p from "@clack/prompts"; import path from "node:path"; import { ADAPTERS, - checkTools, detectTargets, getAdapter, planInstall, @@ -28,21 +27,19 @@ export async function onboard(): Promise { p.note( [ - "웹 디자인을 순서대로 진행하게 만드는 스킬입니다.", - "브리프 정리 → 레퍼런스 조사 → 방향 결정 → 디자인 토큰 →", - "구현 → 셀프 감사까지, 단계마다 통과 조건을 두고 진행합니다.", + "브리프에서 시작해 레퍼런스 조사 · 방향 결정 · 디자인 토큰 ·", + "구현 · 셀프 감사까지 끌고 가는 웹 디자인 파이프라인 스킬.", "", - dim("설치하면 에이전트에서 /designpaca 로 부를 수 있습니다."), - dim("스킬 파일만 복사하며, 기존 설정은 건드리지 않습니다."), + `${dim("SVG 필터 · three.js · 인터랙티브 모션을 기본 재료로 쓴다.")}`, ].join("\n"), - "designpaca 는 무엇인가요", + "무엇을 설치하나", ); // 시스템에 흔적이 있는 도구를 기본 체크해 둔다 — 사용자가 매번 고르게 하지 않는다 const detected = await detectTargets(skill); const targets = await p.multiselect({ - message: "어디에 설치할까요?", + message: "어디에 설치할까?", options: ADAPTERS.map((a) => ({ value: a.id, label: a.label + (detected.includes(a.id) ? dim(" (감지됨)") : ""), @@ -54,18 +51,10 @@ export async function onboard(): Promise { if (p.isCancel(targets)) return cancel(); const wanted = await p.select({ - message: "어느 범위에 설치할까요?", + message: "설치 범위", options: [ - { - value: "user", - label: "전역 (권장)", - hint: "홈 디렉터리에 두어 모든 프로젝트에서 쓸 수 있습니다", - }, - { - value: "project", - label: "이 프로젝트만", - hint: `${tildify(process.cwd())} · 팀과 함께 쓰려면 이쪽이 좋습니다`, - }, + { value: "user", label: "전역", hint: "홈 디렉터리 — 모든 프로젝트에서 쓴다" }, + { value: "project", label: "이 프로젝트만", hint: tildify(process.cwd()) }, ], initialValue: "user", }); @@ -78,7 +67,7 @@ export async function onboard(): Promise { downgraded .map((t) => { const a = getAdapter(t); - return `${a.label} 은(는) ${a.scopes.join("/")} 범위만 지원해서, ${effectiveScope(t, wanted)} 범위로 설치됩니다`; + return `${a.label} 은(는) ${a.scopes.join("/")} 범위만 지원한다 → ${effectiveScope(t, wanted)} 로 설치한다`; }) .join("\n"), ); @@ -102,126 +91,35 @@ export async function onboard(): Promise { ].join("\n"), ); } - p.note( - [ - previews.join("\n\n"), - "", - dim("아래 경로에 스킬 파일을 복사합니다."), - dim("직접 수정하신 파일이 있으면 덮어쓰지 않고 알려드립니다."), - ].join("\n"), - "이렇게 설치됩니다", - ); + p.note(previews.join("\n\n"), "설치 계획"); - const go = await p.confirm({ message: "이대로 진행할까요?", initialValue: true }); + const go = await p.confirm({ message: "이대로 설치할까?", initialValue: true }); if (p.isCancel(go) || !go) return cancel(); const s = p.spinner(); - s.start("설치하고 있습니다"); + s.start("설치 중"); const outcomes = []; try { for (const t of targets) { const scope = effectiveScope(t, wanted); - s.message(`${getAdapter(t).label} 에 설치하고 있습니다`); + s.message(`설치 중 — ${getAdapter(t).label}`); outcomes.push(...(await runInstall({ targets: [t], scope }))); } - s.stop("설치가 끝났습니다"); + s.stop("설치 완료"); } catch (err) { - s.stop("설치하지 못했습니다", 1); + s.stop("설치 실패", 1); p.log.error(err instanceof Error ? err.message : String(err)); return 1; } printOutcomes(outcomes); - - await offerTools(); - printNextSteps(targets); - p.outro(`${accent("designpaca")} 준비됐습니다 — 만들고 싶은 것을 말씀해 주세요`); + p.outro(`${accent("designpaca")} 준비됨 — 이제 브리프를 던져라`); return 0; } -/** - * 선택 도구를 제안한다. 스킬 설치가 끝난 뒤에 묻는다 — - * 본 작업을 이것 때문에 붙잡지 않기 위해서다. 건너뛰어도 스킬은 돈다. - */ -export async function offerTools(): Promise { - const status = await checkTools(); - const missing = status.filter((s) => !s.installed); - - if (missing.length === 0) { - p.log.success( - `브라우저 도구가 이미 준비돼 있습니다 ${dim("— 레퍼런스 조사에서 바로 활용됩니다")}`, - ); - return; - } - - p.note( - [ - "이 스킬은 디자인을 시작하기 전에 실제 사이트를 찾아 분석합니다.", - "그런데 참고할 만한 사이트일수록 자동 접근을 막아두는 경우가 많습니다.", - "", - "브라우저 도구가 있으면 그런 사이트도 직접 열어서", - "화면을 캡처하고 글자 크기·여백 같은 값을 실제로 잴 수 있습니다.", - "", - dim("설치하지 않아도 스킬은 정상 동작합니다."), - dim("나중에 `designpaca tools` 로 언제든 추가하실 수 있습니다."), - ].join("\n"), - "브라우저 도구를 함께 설치할까요?", - ); - - const picked = await p.multiselect({ - message: "설치할 도구를 골라주세요 (스페이스로 선택 / 엔터로 확인)", - options: missing.map(({ tool }) => ({ - value: tool.id, - label: tool.label, - hint: `${tool.why} · ${tool.size}`, - })), - initialValues: missing.map((m) => m.tool.id), - required: false, - }); - if (p.isCancel(picked) || picked.length === 0) { - p.log.info( - `건너뛰겠습니다. ${dim("필요해지면 `designpaca tools` 로 설치하실 수 있습니다.")}`, - ); - return; - } - - const s = p.spinner(); - const followUps: string[] = []; - for (const id of picked) { - const entry = missing.find((m) => m.tool.id === id); - if (!entry) continue; - s.start(`${entry.tool.label} 을(를) 설치하고 있습니다 ${dim(`(${entry.tool.size})`)}`); - try { - await entry.tool.install(); - s.stop(`${entry.tool.label} 설치 완료`); - if (entry.tool.followUp) followUps.push(entry.tool.followUp); - } catch (err) { - // 도구 설치 실패로 스킬 설치까지 실패시키지 않는다. 스킬은 이미 깔렸다. - s.stop(`${entry.tool.label} 을(를) 설치하지 못했습니다`, 1); - p.log.warn( - [ - err instanceof Error ? err.message.split("\n")[0] : String(err), - dim("스킬 설치는 정상적으로 끝났습니다. 이 도구만 나중에 다시 시도하셔도 됩니다."), - ].join("\n"), - ); - } - } - - if (followUps.length > 0) { - p.note( - [ - "아래 명령은 에이전트 설정을 바꾸는 것이라 직접 실행해 주세요.", - "", - ...followUps, - ].join("\n"), - "마지막 한 단계", - ); - } -} - function cancel(): number { - p.cancel("설치를 취소했습니다. 아무것도 변경하지 않았습니다."); + p.cancel("설치를 취소했다. 아무것도 바꾸지 않았다."); return 130; } diff --git a/packages/cli/test/tools.test.ts b/packages/cli/test/tools.test.ts deleted file mode 100644 index 5f3f461..0000000 --- a/packages/cli/test/tools.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const exec = promisify(execFile); -const CLI = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../dist/index.js"); - -test("tools 는 --yes 없이 아무것도 설치하지 않는다", async () => { - // 수백 MB 를 받는 명령이라 조용히 시작하면 안 된다. - // 상태만 보이거나, 이미 다 있으면 그렇다고 말해야 한다. - const { stdout } = await exec(process.execPath, [CLI, "tools", "--no-update-check"]); - assert.match(stdout, /브라우저 도구/); - assert.doesNotMatch(stdout, /설치 중/); -}); - -test("도움말에 tools 명령이 나온다", async () => { - const { stdout } = await exec(process.execPath, [CLI, "--help"]); - assert.match(stdout, /designpaca tools/); -}); - -test("사용자에게 보이는 문구가 명령조가 아니다", async () => { - // 스킬 문서는 단정한 반말이 의도지만, 사용자를 향한 CLI 문구는 다르다. - const { stdout } = await exec(process.execPath, [CLI, "--help"]); - const rude = /(해라|써라|봐라|던져라|가라)/; - assert.doesNotMatch(stdout, rude, "도움말에 명령조 표현이 남아 있다"); -}); diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md deleted file mode 100644 index 54b7b59..0000000 --- a/packages/core/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# @designpaca/core - -## 0.5.0 - -### Minor Changes - -- 8d02820: 어떤 하네스 에이전트든 스킬을 발견하게 만든다. 중립 경로 `agents` 타깃(`~/.agents/skills` · `.agents/skills`)을 새로 추가한다 — Cursor · Antigravity · ZCode 등 Agent Skills 규약을 읽는 하네스가 함께 잡는 자리다. `gemini-cli`(`.gemini/skills`)와 `copilot`(`.github/skills` · `~/.copilot/skills`) 타깃을 추가한다. Cursor 는 구식 `.cursor/rules/*.mdc` 변환을 버리고 네이티브 `.cursor/skills` 로 옮기며 전역 설치를 지원한다. Windsurf 도 규칙 파일(12,000자 상한 때문에 실제 스킬이 아예 설치되지 못했다)에서 `.windsurf/skills` · `~/.codeium/windsurf/skills` 네이티브 경로로 옮긴다. Antigravity 전역 경로를 공식 마이그레이션 문서대로 `~/.gemini/antigravity-cli/skills` 로 바로잡는다. -- 8d02820: ZCode 타깃 추가 — `~/.zcode/skills/designpaca/` (전역) 또는 `.zcode/skills/designpaca/` (프로젝트) 에 스킬을 설치한다. SKILL.md 규약이 Claude Code 와 같아 변환 없이 그대로 복사된다. diff --git a/packages/core/package.json b/packages/core/package.json index 09ddd50..28b40d3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/core", - "version": "0.5.0", + "version": "0.1.0", "private": true, "description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지", "type": "module", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 98f7958..1fe9b33 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,5 +5,4 @@ export * from "./marker.ts"; export * from "./manifest.ts"; export * from "./skill-source.ts"; export * from "./installer.ts"; -export * from "./tools.ts"; export { ADAPTERS, getAdapter, MARKER } from "./targets/index.ts"; diff --git a/packages/core/src/targets/agents.ts b/packages/core/src/targets/agents.ts deleted file mode 100644 index e964e76..0000000 --- a/packages/core/src/targets/agents.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from "node:path"; -import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; - -/** - * 중립 경로 `.agents/skills` — 특정 제품의 경로가 아니라 Agent Skills 규약 자체의 홈이다. - * - * "스펙은 통일됐는데 경로가 갈렸다"가 현재 생태계의 문제라면, 이 디렉터리가 그 답이다. - * Cursor · Antigravity · ZCode 가 프로젝트와 홈에서 이 경로를 읽고, 같은 규약을 채택하는 - * 하네스가 계속 늘고 있다. 어떤 도구를 쓰는지와 무관하게 **한 번 깔면 다 같이 잡는 자리**다. - * - * 경로: - * 워크스페이스 /.agents/skills/designpaca/ - * 전역 ~/.agents/skills/designpaca/ - */ -export const agents: TargetAdapter = { - id: "agents", - label: "중립 경로 (.agents)", - hint: "~/.agents/skills/designpaca — 이 규약을 읽는 하네스라면 무엇이든 잡는다", - scopes: ["user", "project"], - - async detect(ctx: TargetContext) { - return anyExists([path.join(ctx.home, ".agents"), path.join(ctx.cwd, ".agents")]); - }, - - async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".agents", "skills", "designpaca"], - [".agents", "skills", "designpaca"], - ); - return { - target: this.id, - scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), - }; - }, -}; diff --git a/packages/core/src/targets/antigravity.ts b/packages/core/src/targets/antigravity.ts deleted file mode 100644 index 494e46f..0000000 --- a/packages/core/src/targets/antigravity.ts +++ /dev/null @@ -1,53 +0,0 @@ -import path from "node:path"; -import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; - -/** - * Antigravity 와 `.agents/skills` 를 읽는 도구들. - * - * `.agents/skills/` 는 특정 제품의 경로가 아니라 **중립 경로**다. - * Antigravity 가 여기서 스킬을 읽고, 같은 규약을 쓰는 다른 하네스도 늘고 있다 - * (중립 경로 단독 설치는 `agents` 타깃이 담당한다). - * SKILL.md 구조(프론트매터 + references/)가 Claude Code 와 같아서 - * 파일을 그대로 복사하면 된다 — 변환이 필요 없다. - * - * 경로(공식 문서 기준): - * 워크스페이스 /.agents/skills/<이름>/ - * 전역 ~/.gemini/antigravity-cli/skills/<이름>/ - * - * 전역 경로는 Gemini CLI → Antigravity 마이그레이션 문서가 정리한 현재 위치다. - * 예전 문서의 ~/.gemini/config/skills · ~/.gemini/skills 는 레거시다. - * - * 프론트매터 요구사항도 확인했다: `description` 필수, `name` 선택(없으면 폴더명). - * 우리 SKILL.md 는 둘 다 갖고 있다. - */ -export const antigravity: TargetAdapter = { - id: "antigravity", - label: "Antigravity", - hint: "전역 ~/.gemini/antigravity-cli/skills — 워크스페이스는 중립 경로 타깃과 같다", - scopes: ["user", "project"], - - async detect(ctx: TargetContext) { - return anyExists([ - path.join(ctx.home, ".gemini"), - path.join(ctx.home, ".gemini", "antigravity-cli"), - path.join(ctx.cwd, ".agents"), - ]); - }, - - async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".gemini", "antigravity-cli", "skills", "designpaca"], - [".agents", "skills", "designpaca"], - ); - return { - target: this.id, - scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), - }; - }, -}; diff --git a/packages/core/src/targets/copilot.ts b/packages/core/src/targets/copilot.ts deleted file mode 100644 index b8057ab..0000000 --- a/packages/core/src/targets/copilot.ts +++ /dev/null @@ -1,42 +0,0 @@ -import path from "node:path"; -import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; - -/** - * GitHub Copilot — ~/.copilot/skills/designpaca/ (전역) 또는 .github/skills/designpaca/ (프로젝트). - * Copilot 코딩 에이전트는 2025-12 부터 Agent Skills 규약을 지원한다. - * - * detect 는 보수적으로 잡는다 — `.github/` 은 Copilot 을 안 쓰는 저장소에도 흔하니까. - * Copilot 사용 흔적(~/.copilot, 기존 .github/skills, copilot-instructions.md)이 있을 때만 - * 기본 선택에 올린다. 명시적으로 `-t copilot` 을 주면 언제든 설치할 수 있다. - */ -export const copilot: TargetAdapter = { - id: "copilot", - label: "GitHub Copilot", - hint: "~/.copilot/skills/designpaca — SKILL.md 그대로", - scopes: ["user", "project"], - - async detect(ctx: TargetContext) { - return anyExists([ - path.join(ctx.home, ".copilot"), - path.join(ctx.cwd, ".github", "skills"), - path.join(ctx.cwd, ".github", "copilot-instructions.md"), - ]); - }, - - async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".copilot", "skills", "designpaca"], - [".github", "skills", "designpaca"], - ); - return { - target: this.id, - scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), - }; - }, -}; diff --git a/packages/core/src/targets/cursor.ts b/packages/core/src/targets/cursor.ts index 6a4d20d..f320638 100644 --- a/packages/core/src/targets/cursor.ts +++ b/packages/core/src/targets/cursor.ts @@ -1,41 +1,57 @@ import path from "node:path"; import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; +import type { FileAction, TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, referenceActions, rewriteRefPaths } from "./common.ts"; + +/** 본문과 references 가 함께 놓이는 루트 (본문 경로 재작성 기준) */ +const REF_PREFIX = ".cursor/rules/designpaca"; /** - * Cursor — ~/.cursor/skills/designpaca/ (전역) 또는 .cursor/skills/designpaca/ (프로젝트). + * Cursor — .cursor/rules/designpaca.mdc. + * .mdc 프론트매터는 SKILL.md 와 필드가 다르다(description/globs/alwaysApply)므로 변환한다. * - * Cursor 는 Agent Skills 규약을 네이티브로 읽는다. 시작 시 자기 경로(.cursor/skills)와 - * 중립 경로(.agents/skills)를 살피고, 호환 경로(.claude/skills · .codex/skills)까지 함께 - * 읽는다 — 그래서 SKILL.md 를 변환 없이 그대로 복사하면 끝난다. - * - * 예전엔 .cursor/rules/designpaca.mdc 로 변환했지만 그 접근은 버렸다. 규칙 파일은 - * 프론트매터가 다르고 본문이 통째로 프롬프트에 들어가는 규약이라 설명만 띄우고 - * 본문·references 는 필요할 때 여는 스킬 방식이 맞다. 이제 전역 설치도 된다. + * Windsurf 는 이 파일을 읽지 않는다(.windsurf/rules/*.md 를 쓴다) — 별도 어댑터로 분리했다. */ export const cursor: TargetAdapter = { id: "cursor", label: "Cursor", - hint: "~/.cursor/skills/designpaca — SKILL.md 그대로 (전역 지원)", - scopes: ["user", "project"], + hint: ".cursor/rules/designpaca.mdc — 프로젝트 단위", + // Cursor 의 전역 규칙은 파일이 아니라 앱 설정(User Rules)이라 프로젝트 범위만 지원한다 + scopes: ["project"], async detect(ctx: TargetContext) { - return anyExists([path.join(ctx.home, ".cursor"), path.join(ctx.cwd, ".cursor")]); + return anyExists([path.join(ctx.cwd, ".cursor"), path.join(ctx.home, ".cursor")]); }, async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".cursor", "skills", "designpaca"], - [".cursor", "skills", "designpaca"], - ); + const rulesDir = path.join(ctx.cwd, ".cursor", "rules"); + const mdc = path.join(rulesDir, "designpaca.mdc"); + const refRoot = path.join(rulesDir, "designpaca"); + + const header = [ + "---", + `description: ${ctx.skill.description}`, + "globs:", + "alwaysApply: false", + "---", + "", + ].join("\n"); + + const actions: FileAction[] = [ + { + kind: "write", + path: mdc, + content: header + rewriteRefPaths(ctx.skill.body, REF_PREFIX), + }, + ...referenceActions(refRoot, ctx.skill), + ]; + return { target: this.id, scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), + root: rulesDir, + actions, + alreadyInstalled: await exists(mdc), }; }, }; diff --git a/packages/core/src/targets/gemini-cli.ts b/packages/core/src/targets/gemini-cli.ts deleted file mode 100644 index 9abaf26..0000000 --- a/packages/core/src/targets/gemini-cli.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from "node:path"; -import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; - -/** - * Gemini CLI — ~/.gemini/skills/designpaca/ (전역) 또는 .gemini/skills/designpaca/ (프로젝트). - * Agent Skills 규약(SKILL.md)을 Claude 와 같은 형태로 그대로 읽는다. - * - * Google 을 Gemini CLI 에서 Antigravity 로 옮기고 있지만 마이그레이션이 끝난 사용자만 - * Antigravity 경로(~/.gemini/antigravity-cli/skills)를 쓴다 — 아직 Gemini CLI 를 쓰는 - * 사람을 위해 이 타깃을 따로 둔다. 둘 다 쓰면 스킬이 두 번 잡히니 하나만 고른다. - */ -export const geminiCli: TargetAdapter = { - id: "gemini-cli", - label: "Gemini CLI", - hint: "~/.gemini/skills/designpaca — SKILL.md 그대로 (Antigravity 전환 전)", - scopes: ["user", "project"], - - async detect(ctx: TargetContext) { - return anyExists([ - path.join(ctx.home, ".gemini"), - path.join(ctx.cwd, ".gemini"), - ]); - }, - - async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".gemini", "skills", "designpaca"], - [".gemini", "skills", "designpaca"], - ); - return { - target: this.id, - scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), - }; - }, -}; diff --git a/packages/core/src/targets/index.ts b/packages/core/src/targets/index.ts index b41fb7a..a70adfd 100644 --- a/packages/core/src/targets/index.ts +++ b/packages/core/src/targets/index.ts @@ -2,26 +2,10 @@ import type { TargetAdapter, TargetId } from "../types.ts"; import { claudeCode } from "./claude-code.ts"; import { codex } from "./codex.ts"; import { cursor } from "./cursor.ts"; -import { zcode } from "./zcode.ts"; -import { agents } from "./agents.ts"; -import { antigravity } from "./antigravity.ts"; -import { geminiCli } from "./gemini-cli.ts"; import { windsurf } from "./windsurf.ts"; -import { copilot } from "./copilot.ts"; import { agentsMd } from "./agents-md.ts"; -export const ADAPTERS: TargetAdapter[] = [ - claudeCode, - codex, - cursor, - zcode, - agents, - antigravity, - geminiCli, - windsurf, - copilot, - agentsMd, -]; +export const ADAPTERS: TargetAdapter[] = [claudeCode, codex, cursor, windsurf, agentsMd]; export function getAdapter(id: TargetId): TargetAdapter { const a = ADAPTERS.find((x) => x.id === id); @@ -30,15 +14,4 @@ export function getAdapter(id: TargetId): TargetAdapter { } export { MARKER, rewriteRefPaths } from "./common.ts"; -export { - claudeCode, - codex, - cursor, - zcode, - agents, - antigravity, - geminiCli, - windsurf, - copilot, - agentsMd, -}; +export { claudeCode, codex, cursor, windsurf, agentsMd }; diff --git a/packages/core/src/targets/windsurf.ts b/packages/core/src/targets/windsurf.ts index e92d7ca..a4ca7e6 100644 --- a/packages/core/src/targets/windsurf.ts +++ b/packages/core/src/targets/windsurf.ts @@ -1,42 +1,66 @@ import path from "node:path"; import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; +import type { FileAction, TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, referenceActions, rewriteRefPaths } from "./common.ts"; + +const REF_PREFIX = ".windsurf/rules/designpaca"; + +/** Windsurf 규칙 파일의 하드 상한. 넘으면 잘려서 조용히 망가진다. */ +const WINDSURF_CHAR_LIMIT = 12_000; /** - * Windsurf — ~/.codeium/windsurf/skills/designpaca/ (전역) 또는 .windsurf/skills/designpaca/ (프로젝트). - * Cascade 가 Agent Skills 규약(SKILL.md)을 네이티브로 읽는다. + * Windsurf — .windsurf/rules/designpaca.md. + * Cursor 와 경로·프론트매터가 모두 다르다(trigger/globs, .mdc 아님). * - * 예전 .windsurf/rules/designpaca.md 방식은 규칙 파일에 12,000자 상한이 있어서 - * 25KB 짜리 실제 스킬이 아예 설치되지 못했다 — 네이티브 스킬 경로로 옮기며 풀렸다. - * 스킬은 name/description 만 띄우고 본문은 필요할 때 열기 때문에 크기 제약이 없다. + * trigger 는 model_decision 을 쓴다. always_on 으로 두면 디자인 스킬이 모든 메시지의 + * 시스템 프롬프트에 상주한다. */ export const windsurf: TargetAdapter = { id: "windsurf", label: "Windsurf", - hint: "~/.codeium/windsurf/skills/designpaca — SKILL.md 그대로 (전역 지원)", - scopes: ["user", "project"], + hint: ".windsurf/rules/designpaca.md — 프로젝트 단위", + scopes: ["project"], async detect(ctx: TargetContext) { return anyExists([ path.join(ctx.cwd, ".windsurf"), + path.join(ctx.cwd, ".windsurfrules"), path.join(ctx.home, ".windsurf"), - path.join(ctx.home, ".codeium"), ]); }, async plan(ctx: TargetContext) { - const root = scopeRoot( - ctx, - [".codeium", "windsurf", "skills", "designpaca"], - [".windsurf", "skills", "designpaca"], - ); + const rulesDir = path.join(ctx.cwd, ".windsurf", "rules"); + const rule = path.join(rulesDir, "designpaca.md"); + const refRoot = path.join(rulesDir, "designpaca"); + + const header = [ + "---", + "trigger: model_decision", + `description: ${ctx.skill.description}`, + "---", + "", + ].join("\n"); + + const body = header + rewriteRefPaths(ctx.skill.body, REF_PREFIX); + + const actions: FileAction[] = [ + { kind: "write", path: rule, content: body }, + ...referenceActions(refRoot, ctx.skill), + ]; + return { target: this.id, scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), + root: rulesDir, + actions, + alreadyInstalled: await exists(rule), + // 상한을 넘으면 설치는 되지만 Windsurf 가 뒷부분을 버린다. 조용히 깨지느니 막는다. + ...(body.length > WINDSURF_CHAR_LIMIT + ? { + blocked: `규칙 본문이 ${body.length}자로 Windsurf 상한(${WINDSURF_CHAR_LIMIT}자)을 넘는다. SKILL.md 를 줄여야 한다.`, + } + : {}), }; }, }; diff --git a/packages/core/src/targets/zcode.ts b/packages/core/src/targets/zcode.ts deleted file mode 100644 index d37c89d..0000000 --- a/packages/core/src/targets/zcode.ts +++ /dev/null @@ -1,37 +0,0 @@ -import path from "node:path"; -import { exists } from "../fsx.ts"; -import type { TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; - -/** - * ZCode — ~/.zcode/skills/designpaca/ (전역) 또는 .zcode/skills/designpaca/ (프로젝트). - * ZCode 도 Claude Code 와 같은 SKILL.md 규약을 그대로 읽으므로 변환이 없다. - * - * ZCode 는 중립 경로인 .agents/skills 도 함께 읽지만 그쪽은 agents 타깃이 쓴다. - * 같은 스킬을 두 곳에 깔면 하나가 다른 하나를 섀도잉할 뿐이므로 - * ZCode 전용 경로에만 쓴다. - */ -export const zcode: TargetAdapter = { - id: "zcode", - label: "ZCode", - hint: "~/.zcode/skills/designpaca — SKILL.md 그대로", - scopes: ["user", "project"], - - async detect(ctx: TargetContext) { - return anyExists([ - path.join(ctx.home, ".zcode"), - path.join(ctx.cwd, ".zcode"), - ]); - }, - - async plan(ctx: TargetContext) { - const root = scopeRoot(ctx, [".zcode", "skills", "designpaca"], [".zcode", "skills", "designpaca"]); - return { - target: this.id, - scope: ctx.scope, - root, - actions: skillDirActions(root, ctx.skill), - alreadyInstalled: await exists(path.join(root, "SKILL.md")), - }; - }, -}; diff --git a/packages/core/src/tools.ts b/packages/core/src/tools.ts deleted file mode 100644 index 5c30fd6..0000000 --- a/packages/core/src/tools.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 선택 도구 — 스킬이 있으면 더 잘 작동하는 외부 도구들. - * - * 스킬 자체는 이것들 없이도 돈다. 다만 레퍼런스 조사에서 갤러리가 봇 차단(403)을 - * 걸거나 SPA 라 본문을 못 읽을 때, 텍스트 페처만으로는 방법이 없다. - * 실측에서 톤 레퍼런스 하나를 얻으려고 네 사이트를 돌고 전부 실패했다. - * headed 브라우저가 있으면 첫 번째에 끝난다. - * - * 그래서 **필수 의존성이 아니라 선택 의존성**이다. 설치는 사용자가 고른다. - */ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; - -const exec = promisify(execFile); - -export interface OptionalTool { - id: string; - label: string; - /** 이게 없으면 무엇을 못 하는가 — 고르는 사람이 판단할 수 있게 */ - why: string; - /** 설치 크기 어림값. 수백 MB 를 말없이 받게 하지 않는다 */ - size: string; - detect(): Promise; - install(): Promise; - /** 설치 후 사람이 직접 해야 하는 것이 있으면 여기에 */ - followUp?: string; -} - -/** npm 전역 루트. Windows 경로에 공백이 있어도 안전하게 얻는다 */ -async function npmRoot(): Promise { - try { - const { stdout } = await exec("npm", ["root", "-g"], { shell: true }); - return stdout.trim() || null; - } catch { - return null; - } -} - -async function exists(p: string): Promise { - try { - await fs.access(p); - return true; - } catch { - return false; - } -} - -/** - * Playwright — 레퍼런스 조사와 프리플라이트 실렌더 검사에 쓴다. - * - * 라이브러리만 있고 브라우저 바이너리가 없으면 실행 시점에 죽는다. - * 둘 다 있어야 설치된 것으로 친다. - */ -const playwright: OptionalTool = { - id: "playwright", - label: "Playwright (headed 브라우저)", - why: "봇 차단(403)·SPA 갤러리를 직접 열어 스크린샷과 실측값을 받는다. 5단계 실렌더 검사에도 쓴다", - size: "약 180MB (크로미움 포함)", - - async detect() { - const root = await npmRoot(); - if (!root || !(await exists(path.join(root, "playwright")))) return false; - // 브라우저 바이너리까지 확인한다 — 라이브러리만 있으면 첫 실행에서 실패한다 - const cacheDir = - process.platform === "win32" - ? path.join(os.homedir(), "AppData", "Local", "ms-playwright") - : process.platform === "darwin" - ? path.join(os.homedir(), "Library", "Caches", "ms-playwright") - : path.join(os.homedir(), ".cache", "ms-playwright"); - try { - const entries = await fs.readdir(cacheDir); - return entries.some((e) => e.startsWith("chromium")); - } catch { - return false; - } - }, - - async install() { - await exec("npm", ["install", "-g", "playwright"], { shell: true, maxBuffer: 1 << 24 }); - await exec("npx", ["--yes", "playwright", "install", "chromium"], { - shell: true, - maxBuffer: 1 << 24, - }); - }, -}; - -/** - * Playwright MCP — Claude Code 가 브라우저를 직접 조작하게 한다. - * - * 설치는 해주지만 **등록은 하지 않는다.** MCP 서버 등록은 사용자의 에이전트 설정을 - * 건드리는 일이고, 설정 파일 위치와 형식이 도구마다 다르다. 명령만 알려준다. - */ -const playwrightMcp: OptionalTool = { - id: "playwright-mcp", - label: "Playwright MCP 서버", - why: "에이전트가 브라우저를 직접 조작한다. 스크립트를 매번 쓰지 않아도 된다", - size: "약 2MB", - followUp: "claude mcp add playwright -- npx @playwright/mcp@latest", - - async detect() { - const root = await npmRoot(); - if (!root) return false; - return exists(path.join(root, "@playwright", "mcp")); - }, - - async install() { - await exec("npm", ["install", "-g", "@playwright/mcp"], { shell: true, maxBuffer: 1 << 24 }); - }, -}; - -export const OPTIONAL_TOOLS: OptionalTool[] = [playwright, playwrightMcp]; - -export interface ToolStatus { - tool: OptionalTool; - installed: boolean; -} - -export async function checkTools(): Promise { - return Promise.all( - OPTIONAL_TOOLS.map(async (tool) => ({ tool, installed: await tool.detect() })), - ); -} - -export function getTool(id: string): OptionalTool | undefined { - return OPTIONAL_TOOLS.find((t) => t.id === id); -} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 38af630..f11c1c1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,15 +1,5 @@ /** 설치 대상 식별자 */ -export type TargetId = - | "claude-code" - | "codex" - | "cursor" - | "zcode" - | "agents" - | "antigravity" - | "gemini-cli" - | "windsurf" - | "copilot" - | "agents-md"; +export type TargetId = "claude-code" | "codex" | "cursor" | "windsurf" | "agents-md"; /** 설치 범위 — user: 홈 디렉터리 전역, project: 현재 프로젝트 */ export type Scope = "user" | "project"; diff --git a/packages/core/test/installer.test.ts b/packages/core/test/installer.test.ts index 7ed59e1..e070f6e 100644 --- a/packages/core/test/installer.test.ts +++ b/packages/core/test/installer.test.ts @@ -118,118 +118,46 @@ test("agents-md: 포인터만 주입하고 본문은 별도 디렉터리에 푼 assert.ok(!after2.includes("designpaca:start")); }); -test("cursor: 네이티브 스킬 경로에 SKILL.md 그대로 싣는다 (전역·프로젝트)", async () => { - const user = await planInstall("cursor", "user", skill, env); - assert.equal(user.root, path.join(home, ".cursor", "skills", "designpaca")); - const proj = await planInstall("cursor", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".cursor", "skills", "designpaca")); +test("cursor 는 user 범위를 거부한다", async () => { + const plan = await planInstall("cursor", "user", skill, env); + assert.ok(plan.blocked); + await assert.rejects(() => applyPlan(plan, skill.version, { env })); +}); - const res = await applyPlan(proj, skill.version, { env }); - const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); - assert.match(md, /^---\n/, "프론트매터가 그대로 있어야 한다"); - assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); - await fs.access(path.join(proj.root, "references", "tokens.md")); +test("cursor: .mdc 프론트매터로 변환하고 참조 경로를 재작성한다", async () => { + const plan = await planInstall("cursor", "project", skill, env); + const res = await applyPlan(plan, skill.version, { env }); + + const mdc = await fs.readFile(path.join(cwd, ".cursor", "rules", "designpaca.mdc"), "utf8"); + assert.ok(mdc.startsWith("---\ndescription: 테스트용")); + assert.ok(mdc.includes("alwaysApply: false")); + assert.ok(mdc.includes("# 본문")); + // 본문과 references 가 다른 디렉터리에 놓이므로 경로가 재작성돼야 한다 + assert.ok( + mdc.includes(".cursor/rules/designpaca/references/tokens.md"), + "참조 경로가 재작성되지 않았다", + ); + await fs.access(path.join(cwd, ".cursor", "rules", "designpaca", "references", "tokens.md")); await removeInstall(res.record, { env }); }); -test("windsurf: 네이티브 스킬 경로에 싣는다 — 본문 크기 제한이 없다", async () => { - const proj = await planInstall("windsurf", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".windsurf", "skills", "designpaca")); - const user = await planInstall("windsurf", "user", skill, env); - assert.equal(user.root, path.join(home, ".codeium", "windsurf", "skills", "designpaca")); +test("windsurf: Cursor 와 다른 경로·프론트매터를 쓴다", async () => { + const plan = await planInstall("windsurf", "project", skill, env); + const res = await applyPlan(plan, skill.version, { env }); - // 예전 규칙 파일 방식은 12,000자 상한 때문에 실제 스킬(본문 25KB)이 막혔다. - // 네이티브 스킬은 본문을 통째로 프롬프트에 넣지 않으므로 큰 본문도 설치돼야 한다. + const rule = await fs.readFile(path.join(cwd, ".windsurf", "rules", "designpaca.md"), "utf8"); + // always_on 이면 디자인 스킬이 모든 메시지의 시스템 프롬프트에 상주한다 + assert.ok(rule.includes("trigger: model_decision")); + assert.ok(!rule.includes("alwaysApply")); + assert.ok(rule.includes(".windsurf/rules/designpaca/references/tokens.md")); + + await removeInstall(res.record, { env }); +}); + +test("windsurf: 12,000자 상한을 넘으면 설치를 막는다", async () => { const huge: SkillSource = { ...skill, body: "가".repeat(13_000) }; - const hugePlan = await planInstall("windsurf", "project", huge, env); - assert.ok(!hugePlan.blocked, "본문이 크다고 막히면 안 된다"); - - const res = await applyPlan(proj, skill.version, { env }); - const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); - assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); - await fs.access(path.join(proj.root, "references", "tokens.md")); - - await removeInstall(res.record, { env }); -}); - -test("antigravity: 프로젝트는 .agents/skills, 전역은 ~/.gemini/antigravity-cli/skills", async () => { - // 두 경로가 다른 제품 규약이라 하나만 맞으면 다른 쪽에서 스킬이 안 잡힌다. - const proj = await planInstall("antigravity", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".agents", "skills", "designpaca")); - - // 전역은 공식 마이그레이션 문서가 정한 현재 경로다 (~/.gemini/config/skills 는 레거시). - const user = await planInstall("antigravity", "user", skill, env); - assert.equal(user.root, path.join(home, ".gemini", "antigravity-cli", "skills", "designpaca")); -}); - -test("antigravity: SKILL.md 를 변환 없이 그대로 싣는다", async () => { - // Claude Code 와 같은 규약이라 프론트매터도 참조 경로도 손대지 않아야 한다. - // 여기서 경로를 재작성하면 references 가 존재하지 않는 곳을 가리킨다. - const plan = await planInstall("antigravity", "project", skill, env); - const res = await applyPlan(plan, skill.version, { env }); - - const md = await fs.readFile(path.join(plan.root, "SKILL.md"), "utf8"); - assert.match(md, /^---\n/, "프론트매터가 있어야 한다"); - assert.match(md, /description:/, "Antigravity 는 description 을 필수로 요구한다"); - assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); - - const ref = await fs.readFile(path.join(plan.root, "references", "tokens.md"), "utf8"); - assert.match(ref, /# 토큰/); - - await removeInstall(res.record, { env }); -}); - -test("agents: 중립 경로 — 전역 ~/.agents/skills, 프로젝트 .agents/skills", async () => { - const user = await planInstall("agents", "user", skill, env); - assert.equal(user.root, path.join(home, ".agents", "skills", "designpaca")); - const proj = await planInstall("agents", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".agents", "skills", "designpaca")); - - const res = await applyPlan(proj, skill.version, { env }); - const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); - assert.match(md, /^---\n/, "프론트매터가 있어야 한다"); - assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); - await fs.access(path.join(proj.root, "references", "tokens.md")); - - await removeInstall(res.record, { env }); - await assert.rejects(() => fs.access(proj.root)); -}); - -test("gemini-cli: 전역 ~/.gemini/skills, 프로젝트 .gemini/skills", async () => { - const user = await planInstall("gemini-cli", "user", skill, env); - assert.equal(user.root, path.join(home, ".gemini", "skills", "designpaca")); - const proj = await planInstall("gemini-cli", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".gemini", "skills", "designpaca")); -}); - -test("copilot: 전역 ~/.copilot/skills, 프로젝트 .github/skills", async () => { - const user = await planInstall("copilot", "user", skill, env); - assert.equal(user.root, path.join(home, ".copilot", "skills", "designpaca")); - const proj = await planInstall("copilot", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".github", "skills", "designpaca")); -}); - -test("zcode: 전역은 ~/.zcode/skills, 프로젝트는 .zcode/skills", async () => { - // ZCode 는 .zcode/skills 와 .agents/skills 를 모두 읽지만 중립 경로 쪽은 - // agents 타깃이 쓰는 영역이라 zcode 타깃은 건드리지 않는다. - const user = await planInstall("zcode", "user", skill, env); - assert.equal(user.root, path.join(home, ".zcode", "skills", "designpaca")); - - const proj = await planInstall("zcode", "project", skill, env); - assert.equal(proj.root, path.join(cwd, ".zcode", "skills", "designpaca")); -}); - -test("zcode: SKILL.md 를 변환 없이 그대로 싣는다", async () => { - const plan = await planInstall("zcode", "user", skill, env); - const res = await applyPlan(plan, skill.version, { env }); - - const md = await fs.readFile(path.join(plan.root, "SKILL.md"), "utf8"); - assert.match(md, /^---\n/, "프론트매터가 있어야 한다"); - assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); - await fs.access(path.join(plan.root, "references", "tokens.md")); - await fs.access(path.join(plan.root, ".designpaca_version")); - - await removeInstall(res.record, { env }); - await assert.rejects(() => fs.access(plan.root)); + const plan = await planInstall("windsurf", "project", huge, env); + assert.ok(plan.blocked, "상한 초과인데 막지 않았다"); + assert.match(plan.blocked, /상한/); }); diff --git a/packages/skill/CHANGELOG.md b/packages/skill/CHANGELOG.md deleted file mode 100644 index f8a1c6b..0000000 --- a/packages/skill/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @designpaca/skill - -## 0.5.0 - -### Minor Changes - -- 8d02820: 어떤 하네스 에이전트든 스킬을 발견하게 만든다. 중립 경로 `agents` 타깃(`~/.agents/skills` · `.agents/skills`)을 새로 추가한다 — Cursor · Antigravity · ZCode 등 Agent Skills 규약을 읽는 하네스가 함께 잡는 자리다. `gemini-cli`(`.gemini/skills`)와 `copilot`(`.github/skills` · `~/.copilot/skills`) 타깃을 추가한다. Cursor 는 구식 `.cursor/rules/*.mdc` 변환을 버리고 네이티브 `.cursor/skills` 로 옮기며 전역 설치를 지원한다. Windsurf 도 규칙 파일(12,000자 상한 때문에 실제 스킬이 아예 설치되지 못했다)에서 `.windsurf/skills` · `~/.codeium/windsurf/skills` 네이티브 경로로 옮긴다. Antigravity 전역 경로를 공식 마이그레이션 문서대로 `~/.gemini/antigravity-cli/skills` 로 바로잡는다. diff --git a/packages/skill/SKILL.md b/packages/skill/SKILL.md index 65dc7ce..0c2fec3 100644 --- a/packages/skill/SKILL.md +++ b/packages/skill/SKILL.md @@ -88,77 +88,7 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬. **애매하면 긴 쪽으로.** 단 국소 조건에 해당하면 국소로 가라 — 버튼 하나에 갤러리 3곳을 여는 것은 사용자가 이 스킬을 끄게 만든다. **국소로 시작했다가 조건이 깨지면 멈추고 올린다.** 토큰을 새로 정의하게 됐거나, 손댄 섹션이 3개를 넘었거나, 방향을 바꿔야 하면. **올렸다고 말해라. 조용히 국소에 머무는 것이 이 스킬의 최대 실패다.** -**브리프가 비어 있으면 인터뷰해라. 가정으로 채우지 마라.** - -이 단계에서 추측한 것은 전부 기본값이고, 기본값의 총합이 슬롭이다. -실측 사례 — "꽃집 홍보 사이트 하나 만들어보자"를 받고 업종 성격·목표 행동·톤·이름을 -전부 혼자 정했다. 실제로 물어보니 **넷 중 넷이 달랐다.** - -| 내가 가정한 것 | 사용자의 실제 답 | -|---|---| -| 일상 꽃 · 정기구독 | **하이엔드 플로럴 스튜디오** | -| 문의 유도 하나 | 문의 · 구독 · 방문 · 인스타 **넷 다** | -| (묻지 않음) | **에디토리얼 · 잡지** | -| (내가 지어냄) | **목요일의 화원** | - -이 상태로 1단계에 들어갔으면 **레퍼런스 세 개를 전부 틀린 방향에서 골랐을 것이다.** - -**질문 도구(AskUserQuestion)로 한 번에 묻는다.** 하나씩 캐물으면 사용자가 지친다 — -결과를 크게 바꾸는 축을 골라 **선택지와 함께** 한 화면에 낸다. 각 선택지에는 -"이걸 고르면 무엇이 달라지는지"를 적어라. 고르는 사람이 결과를 예상할 수 있어야 한다. - -거의 모든 브리프에서 다음이 결과를 가장 크게 바꾼다. **위 넷은 거의 항상 묻는다.** - -| 축 | 무엇이 달라지는가 | 안 물으면 | -|---|---|---| -| **업종·성격** | 정보 구조 전체. 같은 "꽃집"도 구독형과 하이엔드 스튜디오는 다른 사이트다 | 카테고리 평균이 나온다 | -| **목표 행동** | CTA 의 수와 위치, 어떤 섹션이 필요하고 어떤 게 군더더기인지 | CTA 가 넷이 되고 페이지가 무너진다 | -| **톤** | 2단계 프리셋과 감수할 리스크가 여기서 결정된다 | 내 기본 미학이 나온다 | -| **고유명사·실제 값** | 이름·지역·가격·연락처 | 지어내면 하드 게이트 11 | -| **기존 브랜드 색·로고** | 있으면 3단계 팔레트가 **거기서 시작한다** | 있는 자산을 무시하고 새로 만든다 | -| **좁은 화면의 내비** | 항목 5개 이상이면 구조가 달라진다 | 넓은 화면 메뉴를 그대로 접어 두 줄이 된다 | - -### 색은 반드시 확인한다 — 없다는 답도 답이다 - -**"브랜드 색이 있나요"** 는 결과를 되돌릴 수 없게 바꾸는 질문이다. -이미 로고·간판·패키지에 색이 있는데 사이트만 다른 색이면, 잘 만들어도 **틀린 것**이다. - -- **있다** → 3단계에서 그 색이 강조색이고, 레퍼런스는 그 색과 어울리는 곳으로 고른다 -- **없다·상관없다** → 레퍼런스 실측에서 뽑는다. 이때도 **어떤 색을 피해야 하는지**는 물어라 - (경쟁사가 쓰는 색, 대표가 싫어하는 색은 사용자만 안다) - -> 실측 사례: 꽃집 작업에서 색을 묻지 않고 레퍼런스 세 곳의 배경 평균(`#F8F6F0`)으로 정했다. -> 결과는 좋았지만 **운이 좋았던 것**이다. 브랜드 색이 있었다면 그걸 무시한 작업이 된다. - -### 좁은 화면의 내비를 미리 정한다 - -내비 항목 수는 0단계에서 이미 정해진다(= 섹션 수). **구현 단계에서 발견하면 늦다.** - -| 항목 수 | 좁은 화면 처리 | 비용 | -|---|---|---| -| 2~4개 | 그대로 한 줄. 자간을 줄이고 `justify-content: space-between` | 0 | -| 5~6개 | 가로 스크롤 줄 또는 하단 고정 바 | 낮음 | -| 7개 이상 | **여는 메뉴**(햄버거) | `
` 로 JS 0바이트 | - -**햄버거를 기본값으로 삼지 마라.** 항목이 넷인데 햄버거를 쓰면 한 번의 탭을 공짜로 뺏는 것이다. -반대로 여섯 개를 한 줄에 욱여넣으면 접힌다 — 실측에서 링크가 두 줄(top 46/77)에 걸렸다. - -여는 메뉴가 필요하다고 판단되면 `references/layout.md` 의 **모바일 내비** 절을 본다. - -넷을 `multiSelect` 로 낼지 단일 선택으로 낼지 구분해라 — **목표 행동은 대개 복수**고, -톤과 성격은 하나여야 한다. 둘 다 고르면 방향이 서지 않는다. - -**물어도 되는 것과 물으면 안 되는 것** - -- 묻는다: 결과를 바꾸는 결정(위 표), 사용자만 아는 사실(실제 수치·이름·재고) -- 묻지 않는다: 검색하면 나오는 것, 코드를 읽으면 아는 것, 관례가 명확한 것 - -**답이 모순되면 그 자리에서 정리해라.** 위 사례에서 목표 행동 넷이 다 선택됐는데, -하이엔드 스튜디오에서 "정기구독"과 "문의 상담"은 성격이 다르다. -**우선순위를 제안하고 확인받는다** — 넷을 같은 무게로 놓으면 CTA 가 넷이 되고 페이지가 무너진다. - -예외: 사용자가 "알아서 해줘"라고 명시했거나, 되돌리기 쉬운 습작이면 가정하고 진행해도 된다. -**단 가정한 항목을 목록으로 말하고, 6단계 `design.md` 의 미확정 목록에 올린다.** +**막혔을 때**: 브리프가 비어 있으면 추측하지 말고 물어라. 단 **한 번에 다 묻지 마라.** 결과를 가장 크게 바꾸는 것 하나만 묻고, 나머지는 가정을 명시하고 진행한다. **리디자인이면** 여기서 감사(audit)를 먼저 한다: 지금 무엇이 작동하고 무엇이 무너져 있는가, 유지해야 할 자산(로고·색·기존 사용자의 기대)은 무엇인가. 감사 없는 리디자인은 파괴다. @@ -185,11 +115,6 @@ R1과 R2를 같은 업종에서 고르면 결과는 그 업종의 평균이 된 **갤러리 목록 페이지가 아니라 원본 사이트를 열어라.** 이미지를 볼 수 없어도 구조는 읽을 수 있다. -**403·404·타임아웃을 두 번 만나면 텍스트 페처를 버리고 브라우저를 띄워라.** -좋은 레퍼런스일수록 봇을 막는다. 대체 갤러리를 찾아 헤매는 것은 시간 낭비이고, -찾아낸 대체 소스는 애초에 보려던 것보다 나쁘다. `designpaca tools` 로 설치된 -headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 — 방법은 `galleries.md` §5. - > 통과 조건: R1/R2/R3 각각의 URL과, 그것에서 **무엇을 가져올지** 한 줄씩. 형식은 `reference-method.md` 참조. --- @@ -239,12 +164,6 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다 순서가 있다. **레이아웃 → 재질 → 모션.** 거꾸로 가면 화려한데 읽을 수 없는 페이지가 나온다. -**4-0. 이미지 조달** → `references/images.md` -자리를 만들기 전에 **무엇을 실을지** 정한다. 나중에 채우면 비율이 안 맞아 레이아웃을 다시 짠다. -사용자가 준 사진이 있으면 무조건 그것이고, 없으면 **codex 가 설치돼 있는지 확인해 생성**한다. -생성했으면 `design.md` 에 생성물이라고 적는다 — 나중에 실제 사진으로 바꿀 사람이 알아야 한다. -싣기 전에 반드시 줄인다(실측: 10.1MB → 603KB). - **4-1. 레이아웃과 타이포그래피** → `references/layout.md` (+ 폰트 적용은 `references/typography.md` §5, §6) 그리드, 여백 리듬, 시선 흐름. 3단계의 토큰을 그대로 쓴다. 이 단계가 끝나면 **아무 이펙트 없이도 완성된 페이지**여야 한다. 이것이 모든 폴백의 기반이다. @@ -342,7 +261,6 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는 | `references/presets/README.md` | 2단계 — 미학 방향을 고를 때 (고른 프리셋 하나만 추가로 읽는다) | | `references/tokens.md` | 3단계 — 토큰을 정할 때 | | `references/typography.md` | 3단계 — 폰트를 고를 때 / 4-1 — 적용할 때 | -| `references/images.md` | **4-0 — 사진을 구하고 최적화할 때** | | `references/layout.md` | 4-1 — 그리드와 타이포 | | `references/svg-filters.md` | 4-2 — 재질을 만들 때 | | `references/three.md` | 4-3 — 입체가 필요할 때 | diff --git a/packages/skill/package.json b/packages/skill/package.json index 9767eda..5b4fab1 100644 --- a/packages/skill/package.json +++ b/packages/skill/package.json @@ -1,6 +1,6 @@ { "name": "@designpaca/skill", - "version": "0.5.0", + "version": "0.1.0", "private": true, "description": "designpaca 스킬 원본 — SKILL.md 와 참조 문서", "scripts": { diff --git a/packages/skill/references/antipatterns.md b/packages/skill/references/antipatterns.md index ed1bfab..27a7e26 100644 --- a/packages/skill/references/antipatterns.md +++ b/packages/skill/references/antipatterns.md @@ -328,29 +328,7 @@ const s = await sharp(buf).stats(); // 잘라낸 버퍼를 다시 물려야 > 지나가는 프레임은 방문자도 놓치고 측정도 매번 다른 값을 준다. > 정지된 before/after 두 장이 더 정확하게 말한다. -**④ 개별 요소의 높이로는 "여러 줄에 걸친 것" 을 못 잡는다.** -"컨트롤이 2줄로 접히는가"(하드 게이트 6)를 이렇게 쟀다. - -```js -// 틀렸다 — 이 검사는 "접힘 0" 이라고 답한다 -[...document.querySelectorAll('.nav a')] - .filter(e => e.getBoundingClientRect().height > lineHeight * 1.8) -``` - -각 링크는 한 줄짜리라 높이가 정상이었다. **줄바꿈된 것은 링크가 아니라 컨테이너였다** — -링크들이 두 행(top 46 / 77)에 나뉘어 놓여 있었다. 게이트를 통과했다고 보고했고, -사용자가 화면을 보고 잡아냈다. - -```js -// 맞다 — 행이 몇 개인지는 top 값의 종류로 센다 -new Set([...document.querySelectorAll('.nav a')] - .map(e => Math.round(e.getBoundingClientRect().top))).size -``` - -같은 함정이 갤러리·태그 목록·버튼 그룹 어디에나 있다. -**"몇 줄인가" 를 물을 때는 높이가 아니라 위치를 세라.** - -**⑤ 눈으로 의심한 것이 실측에서 뒤집힐 수 있다.** +**④ 눈으로 의심한 것이 실측에서 뒤집힐 수 있다.** WebGL 판이 본문 뒤를 지나가는 화면을 보고 "대비가 죽었다" 고 판단해 캔버스를 어둡게 만들려 했다. 스크린샷에서 글자 사이 빈 띠의 배경 휘도를 재보니 최악 지점이 **9.83:1** — AAA(7:1)를 넘었다. 고칠 필요가 없었다. @@ -464,8 +442,6 @@ sticky 캔버스가 끝까지 돌려면 목록 뒤에 스크롤 구간이 필요 - [ ] (한국어) 한글 폰트가 명시되어 있고 `word-break: keep-all`이 있다 - [ ] 모든 섹션에서 제목→본문 간격이 **같은 값**이다 (0px 인 섹션이 하나도 없다) - [ ] 화면에 적은 수치는 **전부 그 화면을 캡처해 잰 값**이다 (추정치·기대치가 섞여 있지 않다) -- [ ] 이미지 높이를 실제로 쟀다 — 원본 픽셀 높이가 그대로 나오면 `aspect-ratio` 가 죽은 것이다 -- [ ] 내비·버튼 그룹의 **행 수를 `top` 값으로** 셌다 (개별 높이로는 못 잡는다) **판정** diff --git a/packages/skill/references/galleries.md b/packages/skill/references/galleries.md index f6b103f..831af26 100644 --- a/packages/skill/references/galleries.md +++ b/packages/skill/references/galleries.md @@ -170,51 +170,4 @@ Awwwards는 **타이포 완성도와 야심의 상한선**으로만 쓰고, 섹 | adfolio.design | 429 | Love The Work More | | **commercecream.com** | **HTTPS 연결 거부 (방치 상태)** | **ecomm.design로 대체. 쓰지 마라** | -### 막히면 갤러리를 갈아타지 말고 **브라우저로 직접 들어가라** - -위 표는 "대체 소스"를 적어뒀지만, 그건 차선책이다. **보려던 그 사이트를 봐야 한다.** -403·404·타임아웃을 만나면 대체 갤러리를 찾아 헤매지 말고 실제 브라우저를 띄워라. -실측에서 Aesop(403) → Kinto(404) → Hasami(DNS 실패) → MUJI(타임아웃)로 네 번 왕복하고도 -**톤 레퍼런스를 하나도 못 얻었다.** 브라우저를 열었으면 첫 번째에 끝났다. - -```js -// playwright 가 없으면: npm i -g playwright && npx playwright install chromium -const path = require('path'); -const { chromium } = require(path.join(process.env.PW_ROOT, 'playwright')); -const b = await chromium.launch({ headless: false }); // headed 여야 봇 차단을 덜 받는다 -const p = await b.newPage({ viewport: { width: 1440, height: 900 } }); -await p.goto(url, { waitUntil: 'networkidle', timeout: 45000 }); -await p.screenshot({ path: 'ref-1.png', fullPage: true }); -``` - -**스크린샷은 텍스트 요약보다 낫다.** `WebFetch` 는 마크다운으로 바꾸면서 -여백·비율·크기 대비를 전부 버린다 — 6축 중 절반이 그 정보다. -그림을 보고 나서 **재라**: 히어로가 첫 화면의 몇 %인지, 제목이 몇 px 인지, 칼럼이 몇 개인지. - -```js -// 눈으로 보지 말고 숫자로 받아라 -await p.evaluate(() => { - const h1 = document.querySelector('h1'); - return { - h1px: h1 && getComputedStyle(h1).fontSize, - h1가족: h1 && getComputedStyle(h1).fontFamily.split(',')[0], - 본문px: getComputedStyle(document.body).fontSize, - 배경: getComputedStyle(document.body).backgroundColor, - 첫화면_이미지비율: (() => { - const im = [...document.querySelectorAll('img,video,canvas')] - .filter(e => e.getBoundingClientRect().top < innerHeight); - const a = im.reduce((s, e) => { const r = e.getBoundingClientRect(); return s + r.width * r.height; }, 0); - return (a / (innerWidth * innerHeight) * 100).toFixed(0) + '%'; - })(), - }; -}); -``` - -**headed 로 띄우는 이유**: 헤드리스는 Cloudflare·Akamai 봇 판정에 자주 걸린다. -그래도 막히면 그때 대체 소스로 간다 — 순서가 반대다. - -> 규칙: **접근 실패 2회 → 브라우저를 띄운다.** 대체 갤러리 탐색은 브라우저도 막힌 뒤의 일이다. - ---- - > 근거: research/references/01-gallery-catalog.md, 03-trends-2026.md (조사일 2026-08-20) diff --git a/packages/skill/references/images.md b/packages/skill/references/images.md deleted file mode 100644 index 41dad76..0000000 --- a/packages/skill/references/images.md +++ /dev/null @@ -1,142 +0,0 @@ -# images.md — 사진을 어디서 구하고 어떻게 싣나 - -이미지는 대부분의 디자인에서 **가장 무거운 자산이고 가장 큰 인상**이다. -레이아웃이 좋아도 사진이 약하면 페이지가 약하다. 반대도 마찬가지다. - -4단계(구현)에서 **레이아웃을 잡기 전에** 무엇을 실을지 정해라. -자리를 먼저 만들고 나중에 채우면 비율이 안 맞아 다시 짠다. - ---- - -## 1. 조달 경로 — 위에서부터 확인한다 - -| 순위 | 경로 | 언제 | -|---|---|---| -| 1 | **사용자가 준 사진** | 있으면 무조건 이것. 실제 작업물을 이길 생성물은 없다 | -| 2 | **생성**(codex `image_gen`) | 콘셉트 확인용, 톤이 정확히 지정된 추상·분위기 컷 | -| 3 | **스톡** | 라이선스가 명확한 곳만. 출처를 `design.md` 에 적는다 | -| 4 | 없이 간다 | 타이포·색·여백만으로 만든다. 약한 사진보다 낫다 | - -**0단계에서 물어라**: "쓸 수 있는 사진이 있나요?" -없다고 하면 2번으로 가되, **생성물이라는 사실을 `design.md` 에 적는다.** -나중에 실제 사진으로 바꿀 때 레이아웃을 다시 짜지 않도록 비율을 미리 고정해 둔다. - ---- - -## 2. codex 가 있으면 생성한다 - -설치 여부를 먼저 확인한다. 없으면 조용히 3번으로 내려간다. - -```bash -codex --version # 있나 -node -p "JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.codex/auth.json','utf8')).auth_mode" -# → "chatgpt" 여야 구독 경로다. "api_key" 면 사용자 과금이라 먼저 물어라. -``` - -### 래퍼가 실패해도 이미지는 만들어져 있다 — 반드시 확인해라 - -`~/.codex/imagegen-headless/codex_imagegen.sh` 는 세션 rollout 에서 -`image_generation_call` 의 **base64** 를 찾는다. **codex-cli 0.147.0 은 base64 를 돌려주지 않는다.** -`exec` 도구를 거쳐 **파일로 저장**하고, 래퍼는 "image_gen 이 호출되지 않았다" 며 실패한다. - -실측에서 이 메시지를 보고 두 번 다시 만들었는데, **두 번 다 파일은 멀쩡히 있었다.** - -``` -~/.codex/generated_images//exec-.png -``` - -rollout jsonl 의 `custom_tool_call_output` 에 경로가 그대로 적혀 있다. - -``` -Generated images are saved to <디렉터리> as <디렉터리>\exec-.png by default. -``` - -**한 줄에 경로가 둘**이므로 마지막 것을 쓴다. 공백을 허용하는 정규식은 -앞 디렉터리부터 `" as "` 까지 통째로 삼킨다. - -```js -// 세션 파일에서 실제 저장 경로를 읽는다 -const all = line.match(/[A-Za-z]:\\[^\s"]*generated_images[^\s"]*\.png/g); -if (all) hit = all[all.length - 1]; -``` - -호출은 이렇게 한다. **`$imagegen` 이 프롬프트 맨 앞에 와야** 내장 도구를 쓴다 — -뒤에 붙이면 codex 가 스크립트를 짜려고 헤맨다. - -```bash -printf '%s' "\$imagegen $PROMPT" \ - | codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox - -``` - -> 실패로 판정하기 전에 `~/.codex/generated_images/` 를 봐라. -> "실패했다" 고 보고했는데 파일이 있으면 그건 도구가 아니라 판정이 틀린 것이다. - -### 프롬프트 — 톤을 공통 문자열로 고정한다 - -여러 장을 만들 때 각 프롬프트에 톤을 다시 쓰면 장마다 색이 달라진다. -**공통 접두사 하나**를 만들고 장면만 바꿔라. - -``` -[공통] Editorial magazine photograph. Warm off-white setting (#F8F6F0), - soft natural window light, low saturation, muted restrained palette. - Photographic realism, shallow depth of field, fine film grain. - No text, no logo, no watermark. -[개별] Overhead flat-lay on a worn wooden workbench: cut stems, florist shears, twine. -``` - -- **3단계에서 정한 배경색을 프롬프트에 그대로 넣어라.** 사진이 페이지 배경과 이어진다 -- 크기는 양변 16의 배수: `1152x1536`(3:4) · `1536x1152`(4:3) · `2048x1152`(16:9) -- 사람 얼굴은 피하는 쪽이 안전하다. 손·뒷모습·부분은 잘 나오고 얼굴은 어색해지기 쉽다 -- `No text` 를 넣어라. 넣지 않으면 간판·라벨에 뭉개진 글자가 생긴다 - -### 생성물은 반드시 눈으로 본다 - -`Read` 로 열어 확인한 뒤에만 쓴다. 특히 **공간 사진**(작업실·매장)이 어색해지기 쉽다. -주제가 안 맞으면 프롬프트를 **한 번에 한 가지만** 바꿔 다시 만든다. - ---- - -## 3. 싣기 전에 반드시 줄인다 - -생성물은 1~3MB PNG 로 나온다. 그대로 실으면 페이지가 10MB 가 된다. - -```js -const w = Math.min(meta.width, meta.width > meta.height ? 1600 : 1100); -await sharp(src).resize({ width: w }).webp({ quality: 82 }).toFile(out); -``` - -실측: **10.1MB → 603KB (94% 감소).** 화면에서 차이가 보이지 않는다. - -| 쓰임 | 최대 폭 | -|---|---| -| 전체폭 배경·히어로 | 1600px | -| 본문 폭 이미지 | 1100px | -| 카드·썸네일 | 640px | - -## 4. `` 에 `width`/`height` 를 적었다면 `height: auto` 를 반드시 줘라 - -CLS 를 막으려고 속성을 적는 것은 맞다. 그런데 CSS 에서 높이를 정하지 않으면 -**속성 height 가 그대로 계산된 높이가 되어 `aspect-ratio` 가 무시된다.** - -```css -img { max-width: 100%; height: auto; } /* 이 한 줄이 없으면 */ -``` - -실측 사고: `.two-photo img { aspect-ratio: 3/4 }` 를 줬는데 computed 값은 -`"3 / 4"` 로 멀쩡히 보이고 **실제 높이는 원본 1448px** 이었다. -`aspect-ratio` 는 폭·높이 중 하나가 `auto` 일 때만 높이를 정한다. - -증상이 조용하다 — 오류도 경고도 없고 **페이지만 길어진다.** -이 한 줄을 넣자 전체 높이가 10.9 화면에서 7 화면이 됐다. - -> 진단: 이미지 높이를 실제로 재라. 원본 픽셀 높이(예: 1448)가 그대로 나오면 이것이다. -> ```js -> [...document.querySelectorAll('img')] -> .filter(i => i.getBoundingClientRect().height > 1000) -> ``` - -## 5. `alt` 는 장식이 아니면 반드시 쓴다 - -- 내용을 나르는 이미지 → **무엇이 보이는지** 적는다. "이미지", "사진" 은 alt 가 아니다 -- 순수 장식(배경 텍스처·그레인) → `alt=""` + `aria-hidden="true"` -- 캡션이 이미 설명하고 있으면 alt 는 짧게. 같은 문장을 두 번 읽히지 않는다 diff --git a/packages/skill/references/layout.md b/packages/skill/references/layout.md index 42b821c..379ff7f 100644 --- a/packages/skill/references/layout.md +++ b/packages/skill/references/layout.md @@ -105,79 +105,6 @@ --- -## 4-b. 모바일 내비 — 햄버거를 기본값으로 삼지 마라 - -내비 항목 수는 0단계에서 이미 정해져 있다(= 섹션 수). 구현하다 발견하면 늦다. - -| 항목 수 | 처리 | 비용 | -|---|---|---| -| **2~4개** | 한 줄 유지. 자간을 줄이고 `space-between` 으로 편다 | 0 | -| **5~6개** | 가로 스크롤 줄, 또는 하단 고정 바 | 낮음 | -| **7개 이상** | 여는 메뉴 | `
` 로 JS 0바이트 | - -**넷인데 햄버거를 쓰면 탭 한 번을 공짜로 뺏는 것이다.** 반대로 여섯을 한 줄에 -욱여넣으면 접힌다 — 실측에서 링크가 두 줄(top 46/77)에 걸렸고, 워드마크까지 3줄이 됐다. - -### 2~4개 — 쌓는 것이 접히는 것보다 낫다 - -워드마크와 내비를 한 줄에 두면 둘 다 접힌다. 좁은 화면에서는 **위아래로 쌓아라.** - -```css -@media (max-width: 560px) { - .site-head { flex-direction: column; align-items: stretch; gap: var(--space-3); } - .nav { flex-wrap: nowrap; justify-content: space-between; letter-spacing: 0.04em; } -} -``` - -라벨 자간(`0.14em`)이 좁은 화면에서 폭을 크게 먹는다. **자간부터 줄여라** — -폰트 크기를 줄이는 것보다 읽기에 덜 해롭다. - -### 7개 이상 — `
` 면 JS 가 필요 없다 - -키보드 조작·`Esc`·포커스 이동이 브라우저 기본으로 온다. -직접 만든 토글은 그걸 전부 다시 구현해야 하고, 대개 빠뜨린다. - -```html - -``` - -```css -.nav-mobile > summary { list-style: none; cursor: pointer; } -.nav-mobile > summary::-webkit-details-marker { display: none; } -.nav-sheet { position: absolute; right: 0; } -``` - -**아이콘이 세 줄(≡)일 필요는 없다.** 에디토리얼이면 "메뉴" 라고 적는 편이 낫다 — -글자는 뜻이 분명하고, 세 줄은 학습된 관례일 뿐이다. - -### 어느 쪽이든 검사는 같다 - -**항목의 `top` 값이 한 종류여야 한다.** 높이만 봐서는 못 잡는다(→ `antipatterns.md`). - -```js -new Set([...document.querySelectorAll('.nav a')] - .map(e => Math.round(e.getBoundingClientRect().top))).size === 1 -``` - ---- - -## 4-c. 구조를 바꾸면 미디어쿼리도 같이 다시 써라 - -실측 사고. 히어로를 12칸 그리드에서 "글 + 사진 컨테이너" 2단으로 바꾸면서 -좁은 화면 규칙을 그대로 뒀다. `.hero-text` 가 `auto 1fr` 2칸인데 -세로 라벨을 가로로 눕히자 **그 긴 문장이 auto 칸을 통째로 먹어** h1 칸이 짜부라졌고, -헤드라인이 390px 에서 6줄, 320px 에서 11줄이 됐다. - -넓은 화면에서는 멀쩡했다. **구조 변경의 대가는 항상 좁은 화면에서 먼저 청구된다.** - -> 규칙: 그리드 구조를 바꿨으면 **그 자리에서** 320·390·768 을 다시 재라. -> 나중에 하면 원인이 어느 변경이었는지 못 찾는다. - ---- - ## 5. 이 단계의 통과 조건 - [ ] CSS/HTML만으로 페이지가 완성됐다. JS를 꺼도 읽힌다 diff --git a/packages/skill/references/typography.md b/packages/skill/references/typography.md index dcc71c3..fe31702 100644 --- a/packages/skill/references/typography.md +++ b/packages/skill/references/typography.md @@ -270,29 +270,6 @@ font-family: "Switzer", "Pretendard Variable", system-ui, sans-serif; --- -### 세로쓰기에 가로쓰기 자간을 그대로 쓰지 마라 - -`writing-mode: vertical-rl` 로 세운 글자에 라벨용 자간(`letter-spacing: 0.14em`)을 -그대로 물리면 **글자 사이 세로 간격**이 되어 낱글자가 흩어진다. - -한글은 자소가 모여 한 글자를 이루기 때문에 라틴보다 심하다 — -"플로럴 스튜디오" 가 낱글자 일곱 개로 읽히기 시작한다. - -```css -.vertical { - writing-mode: vertical-rl; - letter-spacing: 0.02em; /* 가로쓰기 라벨의 0.14em 을 그대로 쓰면 안 된다 */ -} -``` - -세로쓰기는 **자간이 아니라 `line-height` 로 숨을 만든다.** 세로쓰기에서 `line-height` 는 -글자 사이가 아니라 **줄(세로 기둥) 사이**를 벌린다. - -> 좁은 화면에서는 세로 글자가 자리를 못 얻는다. `writing-mode: horizontal-tb` 로 눕히되, -> **그 자리의 그리드 칼럼도 같이 손봐라** — 가로로 누운 긴 문장이 `auto` 칸을 통째로 먹는다. - ---- - ## 7. 라이선스 **배포 전에 확인한다.** 폰트 라이선스 위반은 조용히 있다가 청구서로 온다.