designpaca/packages/skill/references/images.md
Yun Chan 6805fb2be7 feat(skill): absorb external design skills, restore interview gate, add review route
- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
2026-09-24 13:26:03 +09:00

222 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# images.md — 사진을 어디서 구하고 어떻게 싣나
이미지는 대부분의 디자인에서 **가장 무거운 자산이고 가장 큰 인상**이다.
레이아웃이 좋아도 사진이 약하면 페이지가 약하다. 반대도 마찬가지다.
4단계(구현)에서 **레이아웃을 잡기 전에** 무엇을 실을지 정해라.
자리를 먼저 만들고 나중에 채우면 비율이 안 맞아 다시 짠다.
---
## 1. 조달 경로 — 기본 탐색 순서로 확인한다
| 순위 | 경로 | 언제 |
|---|---|---|
| 1 | **사용자가 준 사진** | 자산 용도·품질·권리·진실성에 맞을 때. 실제 작업물은 그 근거가 필요한 화면에서 강하다 |
| 2 | **생성**(codex `image_gen`) | 필요한 슬롯에 콘셉트 확인용 또는 톤이 정확히 지정된 추상·분위기 컷이 맞을 때 |
| 3 | **스톡** | 라이선스가 명확한 곳만. 출처를 `design.md` 에 적는다 |
| 4 | 없이 간다 | 타이포·색·여백만으로 만든다. 약한 사진보다 낫다 |
이 순위는 기본 탐색 순서일 뿐이다. 자산 용도·품질·권리·진실성 판단보다 앞서지 않는다.
사용자가 이미 자산을 제공했거나 사용 가능한 사진이 없다고 명시했거나, `design.md`·저장소 파일로 확인할 수 있으면 다시 묻지 않는다. 그 밖의 경우에는 **0단계 브리프 슬롯(기존 브랜드 자산)** 으로 반드시 묻는다: "쓸 수 있는 사진이 있나요?" — 자산 유무가 이번 화면 구성을 얼마나 바꿀지를 스스로 판단해 질문을 생략하지 않는다. 슬롯 확인·질문 카드는 [brief-interview.md](brief-interview.md)를 따른다.
없다는 답만으로 생성 경로를 자동 선택하지 않는다. 슬롯별 자산 용도·품질·권리·진실성을 비교해 경로를 고르고, 생성물을 쓰면 **생성물이라는 사실을 `design.md` 에 적는다.**
나중에 실제 사진으로 바꿀 가능성이 있으면 레이아웃을 다시 짜지 않도록 비율을 미리 고정해 둔다. 구도·크롭·타이포 검토는 [art-direction.md](art-direction.md)를 따른다.
### 제품을 고르는 화면의 추가 원칙
사용자가 제품 자체를 비교·선택하는 카드라면 CSS 도형이나 레이어 쌓기로 제품을 대신 그리지 않는다. 실제·라이선스 사진이 없을 때만 비율을 정한 생성 사진을 쓰고, 사진과 같은 시각 그룹에 `생성 이미지`처럼 가까운 출처 라벨을 둔다. DOM 도식은 재료 비교, 과정 설명, 수치처럼 **그림보다 읽히는 정보**에만 쓴다.
---
## 2. 생성이 적합할 때 사용할 경로
설치 여부를 먼저 확인한다. 없으면 조용히 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/<session-id>/exec-<call-id>.png
```
rollout jsonl 의 `custom_tool_call_output` 에 경로가 그대로 적혀 있다.
```
Generated images are saved to <디렉터리> as <디렉터리>\exec-<id>.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단계 배경색·재질을 프롬프트에 넣고, 자산의 실제 맥락이 더 중요하면 그 맥락을 우선한다
- 크기·비율은 슬롯의 배포 크롭과 생성 모델의 지원 범위를 근거로 정한다. `1152x1536`(3:4) · `1536x1152`(4:3) · `2048x1152`(16:9)는 지원될 때의 예시다
- 사람 얼굴은 피하는 쪽이 안전하다. 손·뒷모습·부분은 잘 나오고 얼굴은 어색해지기 쉽다
- `No text` 를 넣어라. 넣지 않으면 간판·라벨에 뭉개진 글자가 생긴다
### 생성물은 반드시 눈으로 본다
`Read` 로 열어 확인한 뒤에만 쓴다. 특히 **공간 사진**(작업실·매장)이 어색해지기 쉽다.
원인을 분리하려는 비교에서는 프롬프트를 한 번에 한 가지씩 바꾼다. 전체 방향이 맞지 않으면 관련 요소를 함께 고칠 수 있지만, 그 결과로 각 변경의 인과를 분리해 주장하지 않는다.
### 여러 화면이면 이미지 바이블부터 만든다
히어로 한 장을 만든 뒤 카드·모바일·OG에 같은 파일을 억지로 자르지 않는다. 생성 전에 슬롯 계약을 쓴다.
| 슬롯 | 역할 | 피사체 | 비율 | 안전영역 | 진실 라벨 | 폴백 |
|---|---|---|---|---|---|---|
| 데스크톱 히어로 | 첫 인상 | 주체+행동 | 16:9 | 텍스트 반대쪽 55% | 필요 시 `연출 이미지` | 단색+DOM 카피 |
| 모바일 히어로 | 첫 화면 행동 | 핵심 주체 | 4:5 | 하단/중앙 | 동일 | 별도 정지컷 |
| 증거 컷 | 재질·동선 | 부분·공간 | 3:2 | 중앙 | 합성/예시 여부 | DOM 도식 |
**비율이 맞는 것과 의미가 맞는 것은 다르다.** 데스크톱 원본을 4:5로 잘랐을 때 얼굴만 남고 제품이 첫 화면 아래로 밀리면 모바일 전용 원본을 생성한다. 모바일 캡처 390×844에서 핵심 피사체가 실제로 보이는지 확인한다.
이미지는 재질·분위기·행동을 맡고, **가격·주소·지도·상태·AI 근거는 DOM이 맡는다.** 생성 이미지 안에 정보를 구우면 접근성·수정 가능성·사실 검증을 동시에 잃는다.
가상 매물·합성 매장처럼 실제 증거로 오인할 수 있는 사진은 파일 출처를 `design.md`에 적는 것만으로 부족하다. 사진과 같은 시각 그룹에 `연출 이미지 · 실제 사진 아님` 같은 라벨을 둔다. 상세 계약은 `trustworthy-showcases.md`를 따른다.
---
## 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 |
## 3-b. 화면별로 다른 크기를 보낸다 — srcset·sizes·picture
**프로젝트 계약 — 히어로처럼 화면 전체에 걸리는 이미지는 `srcset`으로 화면별 실제 크기를 보낸다.** §3에서 만든 여러 해상도 파일을 하나의 `<img>`에 폭 서술자(`w`)로 나열하고, `sizes`로 뷰포트별 실제 표시 폭을 알려준다. 브라우저가 다운로드 시점에 화면 폭·픽셀 밀도에 맞는 파일을 고른다.
```html
<img
srcset="hero-640.webp 640w, hero-1100.webp 1100w, hero-1600.webp 1600w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 90vw, 1600px"
src="hero-1100.webp"
width="1600" height="900" alt="..." />
```
`sizes`는 CSS 레이아웃이 실제로 이미지에 주는 폭과 일치해야 한다. 전체폭 배경은 `100vw`에 가깝고, 그리드 안의 카드 이미지는 그리드 열 폭을 반영한다 — 대충 적으면 브라우저가 필요 이상으로 큰 파일을 내려받는다 [SKILL-IMPECCABLE].
**모바일과 데스크톱에서 피사체 자체가 다르게 잘려야 하면(art direction) `<picture>`로 분기한다.** `srcset`은 같은 그림의 해상도만 바꾼다. §2의 이미지 바이블에서 모바일 히어로를 별도 원본으로 정했다면 `<source>`로 명시적으로 분기한다. 각 `<source>`에도 `sizes`를 함께 준다 — 없으면 브라우저가 `100vw`로 가정해 필요 이상으로 큰 파일을 고를 수 있다.
```html
<picture>
<source media="(max-width: 640px)" srcset="hero-mobile-750.webp 750w, hero-mobile-1080.webp 1080w" sizes="100vw">
<source media="(min-width: 641px)" srcset="hero-1100.webp 1100w, hero-1600.webp 1600w" sizes="(max-width: 1024px) 90vw, 1600px">
<img src="hero-1100.webp" width="1600" height="900" alt="..." />
</picture>
```
`<picture>`는 항상 대체용 `<img>`로 닫는다 — 어떤 `<source>`도 조건에 맞지 않을 때의 폴백이자, `alt`·`width`·`height`가 실제로 적용되는 자리다 [SKILL-IMPECCABLE].
---
## 4. `<img>` 에 `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` 는 장식이 아니면 반드시 쓴다
**하드 게이트 — 이미지의 용도를 다섯 가지로 나누고 각각 다르게 쓴다.** 장식과 비장식을 한 덩어리로 다루면 정보성·기능성·텍스트 이미지가 각각 요구하는 다른 문장을 놓친다 [SKILL-BETTER-A11Y].
| 분류 | 무엇인가 | `alt`에 적는 것 | 예시 |
|---|---|---|---|
| 장식 | 배경 텍스처·그레인처럼 정보가 없는 순수 시각 장식 | `alt=""` + `aria-hidden="true"` | 헤더 뒤 그라디언트 패턴 |
| 정보성 | 내용을 전달하는 사진·일러스트 | 무엇이 보이는지 서술 | `alt="작업대 위에 놓인 마른 꽃 다발과 가위"` |
| 기능성 | 클릭·이동 같은 동작을 일으키는 이미지(아이콘 버튼, 링크가 된 로고) | 이미지 묘사가 아니라 **행동**을 서술 | `alt="장바구니로 이동"` |
| 텍스트 이미지 | 이미지 안에 글자가 박혀 있다(로고, 스캔한 포스터) | 이미지 속 **텍스트를 그대로** 옮긴다 | `alt="2026 가을 컬렉션"` |
| 복합 이미지 | 차트·인포그래픽·스크린샷처럼 요약이 필요하다 | 짧은 요약을 `alt`에 쓰고, 상세 값은 본문 표·목록으로 따로 둔다 | `alt="최근 3개월 방문자 추이, 9월에 40% 증가"` + 본문 수치 표 |
- 내용을 나르는 이미지 → **무엇이 보이는지** 적는다. "이미지", "사진" 은 alt 가 아니다
- 순수 장식(배경 텍스처·그레인) → `alt=""` + `aria-hidden="true"`
- 캡션이 이미 설명하고 있으면 alt 는 짧게. 같은 문장을 두 번 읽히지 않는다
**하드 게이트 — `alt` 속성 자체를 빼먹는 것은 빈 `alt=""` 보다 나쁘다.** 스크린리더는 `alt` 속성이 없는 `<img>`를 만나면 파일 경로나 파일명을 대신 읽는다. 빈 `alt=""`는 "건너뛰어도 된다"는 의도된 신호지만, 속성 누락에는 그 의도가 없어 스크린리더가 의미 없는 문자열을 그대로 읽는다 — 아무것도 전달하지 않는 것보다 나쁘다 [SKILL-BETTER-A11Y].
---
## 6. 이미지 1px 중립 외곽선 — 관찰 후보
밝은 배경 위의 흰 계열 사진, 어두운 배경 위의 검정 계열 사진은 경계가 배경에 녹아 사라진다. 색조 없는 중립 1px 외곽선을 후보로 검토한다.
```css
img {
outline: 1px solid oklch(0 0 0 / 0.1);
outline-offset: -1px;
}
@media (prefers-color-scheme: dark) {
img { outline-color: oklch(1 0 0 / 0.1); }
}
```
`border` 대신 `outline`을 쓰는 이유는 레이아웃 폭에 영향을 주지 않기 때문이다 — 이미지 표시 크기가 그대로 유지된다. slate·zinc 계열처럼 색조가 있는 중성색은 피한다. 사진 표면 위에서 "때"처럼 보이기 쉽다. 이 값은 시작값이며, 사진이 이미 카드 테두리·그림자 같은 뚜렷한 경계를 갖고 있으면 불필요하다 [SKILL-BETTER-UI].