- 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.
222 lines
14 KiB
Markdown
222 lines
14 KiB
Markdown
# 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].
|