release: v0.10.0
This commit is contained in:
parent
2b7fa32b5b
commit
6930906583
61 changed files with 15165 additions and 82 deletions
|
|
@ -1,5 +1,18 @@
|
|||
# CHANGELOG
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add target-market reference boundaries; trustworthy fictional/AI showcase contracts with provenance, sensitive-data, generated-evidence, and fail-closed regulated-action rules; state-based visual E2E guidance; human-gate layout invariants; page-scoped h1 line-count gates; and page-scoped static CSS/JS ownership with non-regression hygiene budgets.
|
||||
|
||||
- 목표 시장을 R1/R2/R3의 경계 조건으로 잠그고, 같은 문화권의 다른 업종 R2를 우선해 서구 레퍼런스의 무의식적 전이를 막는다.
|
||||
- `trustworthy-showcases.md`를 추가해 가상·예시·AI·규제 쇼케이스의 진실 상태·출처 계층, 표면 다이얼, 모바일 이미지 바이블, 고위험 데이터 경계, 규제 행동의 실패 폐쇄, 상태별 시각 E2E를 표준화한다.
|
||||
- 인간 시각 게이트에서 발견한 동일 행 액션 중심, 의미별 섹션 토폴로지, 조건부 데스크톱 패널의 트랙 회수를 레이아웃·E2E 계약으로 승격한다.
|
||||
- 같은 진실 범위의 공통 부모 라벨, 공개 해석/관리 내부 증거 소유권, 보이는 입력과 판정 의존성 SSOT를 추가해 경고 반복과 맞춤 분석 모순을 막는다.
|
||||
- 범용 디자인 게이트에 페이지별 `h1MaxLines` 계약과 h1 정확히 1개 검사를 추가한다. 기본 1줄은 유지하며 마케팅 페이지의 의도적 2줄만 좁게 선언할 수 있다.
|
||||
- 범용 디자인 게이트에 페이지별 `staticContract`를 추가해 selector CSS/HTML·JS 문서 코퍼스, 토큰 권위 파일, 색·radius 비회귀 예산을 명시하고 공유 CSS·동적 클래스·지역 토큰 오탐을 막는다.
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬.
|
|||
|
||||
**쓰지 않는다**: 대시보드의 데이터 밀도 설계(→ 데이터 시각화 스킬) · 순수 백엔드 · 이미 확립된 디자인 시스템을 따라야만 하는 작업(그 시스템을 따르는 게 맞다) · "일단 돌아가게만" 요청
|
||||
|
||||
관리자 화면을 함께 만들 때 designpaca가 맡는 것은 **브랜드 표면·정보 위계·상호작용 상태·진실성/규제 게이트**다. 지표 정의, 차트 해석, 대규모 표의 열 우선순위와 밀도 최적화는 데이터 시각화·도메인 스킬의 근거를 추가로 받아야 한다. `관리자 밀도 3` 다이얼은 이 경계를 없애는 허가가 아니다.
|
||||
|
||||
## 우선순위
|
||||
|
||||
충돌하면 위가 이긴다. **이 스킬의 기본값은 맨 아래다.**
|
||||
|
|
@ -200,6 +202,8 @@ description: "웹 디자인 전 과정을 끌고 가는 파이프라인 스킬.
|
|||
|
||||
R1과 R2를 같은 업종에서 고르면 결과는 그 업종의 평균이 된다. SaaS 구조 + SaaS 톤 = 또 하나의 SaaS 슬롭이다.
|
||||
|
||||
**목표 시장이 다른 레퍼런스는 자동으로 중립이 되지 않는다.** 단위·용어·문의 방식·사진 조도·신뢰 표지는 문화권의 일부다. 시장 적합성이 중요한 브리프는 R1을 같은 목표 시장에서 우선 찾고, R2는 **같은 문화권의 다른 업종**을 우선한다. 다른 문화권의 R3는 구현 기법 하나만 가져온다. 상세 계약은 `reference-method.md`와, 가상·AI·규제 쇼케이스면 `trustworthy-showcases.md`를 따른다.
|
||||
|
||||
- 어디서 찾는가 → `references/galleries.md` (브리프별 라우팅 표)
|
||||
- 어떻게 뜯어보는가 → `references/reference-method.md` (6축 해체 프레임워크, WebFetch 템플릿)
|
||||
|
||||
|
|
@ -222,6 +226,7 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다
|
|||
- **한 문장 컨셉** — 이 사이트가 주는 인상을 한 문장으로. ("고급 잡지의 여백", "계기판처럼 정확한", "밤의 스튜디오")
|
||||
- **미학 프리셋** — 아래에서 고르거나, 브리프가 요구하면 새로 정의한다
|
||||
- **감수할 리스크 하나** — 정당화할 수 있는 과감한 선택 하나. 없으면 그 디자인은 안전하고 잊힌다
|
||||
- **표면 다이얼** — 랜딩·관리자·가맹처럼 과업이 다른 화면이 함께 있으면 각 표면의 표현성·정보 밀도·모션·증거 노출을 0~3으로 따로 적는다. 공통 토큰은 공유하되 과업 밀도까지 같게 만들지 않는다
|
||||
|
||||
| 프리셋 | 한 줄 | 언제 |
|
||||
|---|---|---|
|
||||
|
|
@ -263,6 +268,7 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다
|
|||
자리를 만들기 전에 **무엇을 실을지** 정한다. 나중에 채우면 비율이 안 맞아 레이아웃을 다시 짠다.
|
||||
사용자가 준 사진이 있으면 무조건 그것이고, 없으면 **codex 가 설치돼 있는지 확인해 생성**한다.
|
||||
생성했으면 `design.md` 에 생성물이라고 적는다 — 나중에 실제 사진으로 바꿀 사람이 알아야 한다.
|
||||
히어로·증거 컷·모바일 히어로를 각각 슬롯으로 적은 **이미지 바이블**을 먼저 만든다. 데스크톱 크롭에서 핵심 피사체가 폴드 아래로 밀리면 모바일은 별도 원본을 만든다.
|
||||
싣기 전에 반드시 줄인다(실측: 10.1MB → 603KB).
|
||||
|
||||
**4-1. 레이아웃과 타이포그래피** → `references/layout.md` (+ 폰트 적용은 `references/typography.md` §5, §6)
|
||||
|
|
@ -283,6 +289,9 @@ headed 브라우저가 있으면 스크린샷과 실측값을 바로 받는다
|
|||
- **키보드 단축키** — 콘솔의 손가락 문법. 숫자키 뷰 전환, `/` 검색 포커스. `kbd` 물리 키 칩으로 안내하고, 입력 요소에 포커스가 있을 때는 무력화한다
|
||||
- **본연의 도메인 동작** — 학적부는 기록, 시간표는 격자, 주문은 원장. 그 도메인이 종이에서 하던 핵심 동작 하나가 빠져 있으면 그게 곧 '데모 티'다
|
||||
|
||||
**4-6. 진실 계약과 표면 분리** → `references/trustworthy-showcases.md` (가상 브랜드·예시 데이터·AI·규제 주제·고객/관리자 복수 경로일 때만)
|
||||
사실·예시·추론·미정을 구현 전에 나누고, 각 값의 출처를 사용자 진술·검증 출처·시스템 상태·합성 픽스처 중 하나로 추적한다. 오해가 생기는 주장·가격·행동 가까이에 라벨을 두되 같은 진실 범위는 가장 가까운 명확한 공통 부모가 한 번 소유한다. 보이는 입력과 판정 의존성은 같은 SSOT에서 만들고, 성공 행동이 다른 청중은 URL과 IA를 나눈다. AI는 출처가 붙은 입력·검증 사실·추론·불확실·수정·사람 검토를 함께 보여 주고, 진단·추천·예약·게시·내보내기처럼 위험한 규제 행동은 행동별 검증이 없으면 실패 폐쇄형으로 막는다.
|
||||
|
||||
**실험 경로** → `references/experimental-canvas.md`
|
||||
HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는다. 기본값은 쓰지 않는 것이다.
|
||||
|
||||
|
|
@ -304,8 +313,9 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는
|
|||
3. **이펙트 전부 끄기** — CSS 필터·WebGL·애니메이션을 끈 상태에서 페이지가 여전히 읽히는가
|
||||
4. **게이트 실행(폐쇄 루프)** — `tools/design-gate.mjs --init` 으로 설정을 만들고 돌린다. 오버플로·h1·수축·대비·SEO/meta·죽은 선택자·토큰 위생·스케일×폭까지 전 뷰 × 전 폭. L1(단위)·L5(탐색)·L6(시나리오 E2E)는 프로젝트 `tools/` 에 보강해 `npm run verify` 체인으로. **게이트 통과 없이 완료 선언 금지.**
|
||||
5. **상태 완결성 스윕** — 입력·파괴·열림이 있는 화면은 실제로 조작해 본다: 수치 입력에 범위 밖 값을 넣고, 파괴적 행동을 되돌려보고, 열린 메뉴를 Esc 로 닫는다. 기준은 `references/preflight.md` §4-1. 통과 못 하면 4단계로
|
||||
6. **상태별 시각 E2E** — URL별 390·1440 기본 화면과 결과·오류·모달 같은 핵심 상태를 캡처한다. 하니스는 개수·규격·0바이트·중복 해시를 단언하고, 캡처를 실제로 열어 목표 시장 적합성·시선 위계·이미지 크롭·반복을 눈으로 판정한다. 구조 PASS를 시각 PASS로 바꾸어 말하지 않는다
|
||||
|
||||
> 게이트가 잡은 실제 사고 목록과 6계층 설계 근거는 `references/audit-gate.md` 다. 자기 검사 스크립트를 매번 새로 쓰지 마라 — 이 게이트를 심고 확장해라.
|
||||
> 게이트가 잡은 실제 사고 목록과 7계층 설계 근거는 `references/audit-gate.md` 다. 자기 검사 스크립트를 매번 새로 쓰지 마라 — 이 게이트를 심고 확장해라.
|
||||
|
||||
#### 하드 게이트 — 전부 "아니오"여야 한다
|
||||
|
||||
|
|
@ -376,6 +386,7 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는
|
|||
| `references/tokens.md` | 3단계 — 토큰을 정할 때 |
|
||||
| `references/typography.md` | 3단계 — 폰트를 고를 때 / 4-1 — 적용할 때 |
|
||||
| `references/images.md` | **4-0 — 사진을 구하고 최적화할 때** |
|
||||
| `references/trustworthy-showcases.md` | **1·2·4·5단계 — 가상 브랜드·예시 데이터·AI·규제 주제·고객/관리자 복수 경로** |
|
||||
| `references/layout.md` | 4-1 — 그리드와 타이포 |
|
||||
| `references/svg-filters.md` | 4-2 — 재질을 만들 때 |
|
||||
| `references/three.md` | 4-3 — 입체가 필요할 때 |
|
||||
|
|
@ -385,7 +396,7 @@ HTML-in-Canvas(`drawElementImage`)는 **폴백을 완성한 뒤에만** 얹는
|
|||
| `references/preflight.md` | 5단계 — 감사 |
|
||||
| `references/antipatterns.md` | 3·5단계 — 한글 조판 / 슬롭 검출 |
|
||||
| `references/audit-gate.md` | **5단계 — 게이트(폐쇄 루프) 설계·설치·하니스 규칙** |
|
||||
| `tools/design-gate.mjs` | **5단계 — 범용 게이트 실행기(SEO/meta·대비·수축·시각·WebKit 옵션)** |
|
||||
| `tools/design-gate.mjs` | **5단계 — 범용 게이트 실행기(SEO/meta·대비·수축·페이지별 h1 최대 줄 계약·시각·WebKit 옵션)** |
|
||||
|
||||
## 작업 중 지켜야 할 것
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@designpaca/skill",
|
||||
"version": "0.9.2",
|
||||
"version": "0.10.0",
|
||||
"private": true,
|
||||
"description": "designpaca 스킬 원본 — SKILL.md 와 참조 문서",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@
|
|||
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 `scrollTo(0,0)` 단언(M5) + mobile-app-ux.md |
|
||||
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + **시각 무게 평가(눈)** — mobile-app-ux.md |
|
||||
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | M6 왼쪽 인셋 스위트(텍스트 시작점 ≥12px·같은 축 ±10px) + preflight §0-E |
|
||||
| 모바일에서 절대 배치 지도 자식의 부모를 `position: static`으로 바꿈 — offsetParent가 BODY가 되어 격자·핀이 히어로 전체를 덮음. 기하 게이트 8/8 통과 | **상태별 실제 캡처를 열어 보는 시각 E2E** + absolute/fixed 자식의 containing block 단언(하니스 규칙 19) |
|
||||
| 새 관리자·가맹 서브페이지가 생겼지만 `public/work/*/index.html`만 검사 — 새 HTML 오류 8건이 검증 밖 | 정적 입력을 `public/work/**/*.html`·`**/*.css`로 재귀화 + 검사 대상 파일 수 단언(하니스 규칙 20) |
|
||||
| 마케팅 h1의 의도적 2줄 때문에 범용 1줄 규칙을 꺼 버릴 위험 | 기본 1줄 유지 + 페이지별 `h1MaxLines` 계약. 375·390 × 글꼴 확대 매트릭스도 같은 상한 적용 |
|
||||
|
||||
## 7계층
|
||||
|
||||
|
|
@ -42,7 +45,7 @@
|
|||
| L0 정적 | stylelint + html-validate | 문법·구문 위반을 브라우저 켜기 전에 |
|
||||
| L1 단위 | 순수 계산 로직 추출(calc.js) + 경계값 테스트 | 프로젝트별 작성 — 0명/만석/초과/빈배열. **입력 검증·신청 가능 같은 '판정'(가능/불가+사유)도 여기 둔다** — UI 는 판정을 문장으로 번역만 |
|
||||
| L2 불변식 | `design-gate.mjs` — 오버플로·h1·수축·대비·SEO/meta·죽은 선택자·토큰 위생·theme-color 정합 | 전 뷰 × 전 폭 |
|
||||
| L3 시각 회귀 | 기준 화면 pixelmatch diff (≥0.1% 실패) | 갱신은 검증된 배포 후 `--update-baseline` |
|
||||
| L3 시각 회귀 | 기준 화면 pixelmatch diff (≥0.1% 실패) + 핵심 상태 캡처 | 갱신은 검증된 배포 후 `--update-baseline`. 캡처 존재와 사람의 시각 판정은 별개다 |
|
||||
| L4 WebKit | Playwright webkit | 사파리 엔진 렌더 차이 |
|
||||
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
|
||||
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
|
||||
|
|
@ -89,6 +92,17 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
|
|||
15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다.
|
||||
16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
|
||||
17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
|
||||
18. **시각 E2E는 기본 화면만 찍지 않는다** — URL별 390·1440 기본 화면에 더해 결과가 바뀐 입력, 오류/검토, 모달/차단 상태를 찍는다. 하니스는 파일 개수·가로세로·0바이트·해시 중복을 단언하고, 에이전트는 파일을 실제로 열어 시장 적합성·위계·크롭·반복을 판정한다. 스크린샷을 저장했다는 사실만으로 PASS하지 않는다.
|
||||
19. **absolute/fixed 자식은 containing block을 잰다** — 모바일 미디어쿼리에서 부모의 `position`을 바꿨다면 각 절대 배치 자식의 `offsetParent` 또는 기준 rect가 의도한 컨테이너인지 단언한다. `overflow-x:hidden`으로 누수를 숨기지 않는다.
|
||||
20. **정적 검사는 재귀 입력과 대상 수를 잠근다** — 서브페이지가 생기는 저장소에서 한 단계 glob은 금지한다. 실행 전에 발견한 HTML/CSS/JS 파일 수를 출력하고 0개 또는 이전 계약보다 적으면 실패시킨다.
|
||||
21. **시나리오·단언·캡처 수를 hard-lock한다** — 대규모 E2E는 `EXPECTED_SCENARIOS`, `EXPECTED_ASSERTIONS`, `EXPECTED_CAPTURES`를 둔다. 리팩터링으로 검사가 삭제돼도 전체가 조용히 GREEN이 되지 않게 한다. 수를 채우기 위한 중복 단언은 금지하고 공통·정상·오류·부작용·접근성·시각 상태로 분류한다.
|
||||
22. **사용자 승인과 검증 결과는 다른 상태다** — 사용자가 방향 게이트를 자율 승인했어도 기계·기능·시각 게이트는 면제되지 않는다. 승인됨/검증됨을 보고서에서 별도 표기한다.
|
||||
23. **혼합 액션 행은 직접 자식 hitbox 중심을 비교한다** — 같은 액션 묶음·같은 행의 직접 자식인 채운 버튼과 텍스트 링크가 둘 다 44px 이상인지, `rect.top + rect.height / 2` 차이가 브라우저 CSS 픽셀 2px 이하인지 단언한다. 중첩 아이콘·배지나 모바일 세로 스택은 비교 대상이 아니다. `align-items:center` 선언 존재는 검사가 아니다.
|
||||
24. **연속 섹션 토폴로지를 목록화한다** — 섹션별 `역할 / DOM 골격 / 회색조 실루엣`을 적는다. 역할이 다른 연속 3개가 같은 `제목+목록` 골격이면 시각 RED다. 반복 데이터처럼 같은 골격이 맞는 경우는 이유를 기록한다.
|
||||
25. **조건부 패널의 닫힘은 트랙 회수까지 단언한다** — 데스크톱 분할 작업대는 `display:none`만 검사하지 않는다. 인스펙터가 닫히면 주 표면의 오른쪽 끝과 작업대 끝 차이가 CSS 픽셀 2px 이하인지, 다시 선택하면 분할 트랙이 복원되는지 잰다. 모바일 시트·전체 화면 패널은 트랙 대신 닫힘·배경 복귀·포커스 복원을 검증한다.
|
||||
26. **진실 라벨은 개수가 아니라 의미 범위를 단언한다** — 같은 상태·출처·행동 가능성을 공유하는 자식은 제목과 경계가 분명한 가장 가까운 공통 부모가 한 번 소유하고 프로그램적으로 연결한다. 상태가 다른 자식만 예외 라벨을 둔다. 같은 공개 문구가 한 그룹의 모든 자식에 복제되면 범위 설계를 다시 본다.
|
||||
27. **보이는 입력과 판정 의존성을 같은 SSOT로 잠근다** — 폼의 사용자 조건 이름 집합과 순수 판정 함수가 참조하는 사용자 조건 집합을 비교한다. 숨은 기본값을 사용자 선호로 세지 않고, 보이는 입력을 무시하지 않으며, 분석 전 완료 결과가 없고 복합 비용 일부로 적합을 확정하지 않는지 단언한다.
|
||||
28. **공개와 관리 콘텐츠 소유권을 테스트한다** — 공개 DOM에는 내부 ID·원문 출처 원장·규제 채널 상수·감사 로그를 복제하지 않는다. 대신 사용자 행동을 막거나 바꾸는 제한과 해석 가능한 이유는 남긴다. 관리자는 그 요약을 재현하는 출처·검증 이벤트·판정 기록을 가진다.
|
||||
|
||||
## 리포트 양식
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,22 @@ printf '%s' "\$imagegen $PROMPT" \
|
|||
`Read` 로 열어 확인한 뒤에만 쓴다. 특히 **공간 사진**(작업실·매장)이 어색해지기 쉽다.
|
||||
주제가 안 맞으면 프롬프트를 **한 번에 한 가지만** 바꿔 다시 만든다.
|
||||
|
||||
### 여러 화면이면 이미지 바이블부터 만든다
|
||||
|
||||
히어로 한 장을 만든 뒤 카드·모바일·OG에 같은 파일을 억지로 자르지 않는다. 생성 전에 슬롯 계약을 쓴다.
|
||||
|
||||
| 슬롯 | 역할 | 피사체 | 비율 | 안전영역 | 진실 라벨 | 폴백 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 데스크톱 히어로 | 첫 인상 | 주체+행동 | 16:9 | 텍스트 반대쪽 55% | 필요 시 `연출 이미지` | 단색+DOM 카피 |
|
||||
| 모바일 히어로 | 첫 화면 행동 | 핵심 주체 | 4:5 | 하단/중앙 | 동일 | 별도 정지컷 |
|
||||
| 증거 컷 | 재질·동선 | 부분·공간 | 3:2 | 중앙 | 합성/예시 여부 | DOM 도식 |
|
||||
|
||||
**비율이 맞는 것과 의미가 맞는 것은 다르다.** 데스크톱 원본을 4:5로 잘랐을 때 얼굴만 남고 제품이 첫 화면 아래로 밀리면 모바일 전용 원본을 생성한다. 모바일 캡처 390×844에서 핵심 피사체가 실제로 보이는지 확인한다.
|
||||
|
||||
이미지는 재질·분위기·행동을 맡고, **가격·주소·지도·상태·AI 근거는 DOM이 맡는다.** 생성 이미지 안에 정보를 구우면 접근성·수정 가능성·사실 검증을 동시에 잃는다.
|
||||
|
||||
가상 매물·합성 매장처럼 실제 증거로 오인할 수 있는 사진은 파일 출처를 `design.md`에 적는 것만으로 부족하다. 사진과 같은 시각 그룹에 `연출 이미지 · 실제 사진 아님` 같은 라벨을 둔다. 상세 계약은 `trustworthy-showcases.md`를 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 싣기 전에 반드시 줄인다
|
||||
|
|
|
|||
|
|
@ -55,6 +55,43 @@
|
|||
3. **시선을 한 번은 멈춰라** — 전부 같은 리듬으로 흐르면 아무것도 강조되지 않는다. 섹션 하나는 리듬을 깨야 한다
|
||||
4. **스크롤은 보상이어야 한다** — 다음 화면에 무언가 새로운 것이 있어야 한다. 같은 카드 그리드가 세 번 반복되면 거기서 이탈한다
|
||||
|
||||
### 의미가 다르면 섹션 토폴로지도 달라야 한다
|
||||
|
||||
일관성은 같은 거시 그리드를 복제하는 것이 아니다. **연속한 섹션의 역할이 다른데 모두 `왼쪽 큰 제목 + 오른쪽 목록`이면 구조 슬롭**이다. 색·배경·카드 radius를 바꿔도 읽는 동작은 같다.
|
||||
|
||||
2단계에서 섹션 역할과 토폴로지를 나란히 적는다.
|
||||
|
||||
| 역할 | 맞는 토폴로지 예 |
|
||||
|---|---|
|
||||
| 장소·관계 | 지도/도식 + 접근 가능한 목록 쌍둥이 |
|
||||
| 비용·사양 | 원장·명세서·비교표 |
|
||||
| 절차·판정 | 흐름·분기·상태 전이 |
|
||||
| 이미지·재질 | 풀블리드·필름스트립·캡션 |
|
||||
| 입력·결과 | 폼 + 결과 인스펙터 |
|
||||
|
||||
**다르게 보이기 위해 억지로 바꾸지는 마라.** 같은 데이터 묶음을 반복하는 목록은 같은 구조가 맞다. 규칙은 `의미 역할이 다르면 읽는 동작도 달라야 한다`다. 연속 3개 섹션의 DOM 골격과 회색조 실루엣이 같다면, 그중 둘의 정보 구조부터 다시 설계한다.
|
||||
|
||||
### 같은 액션 행의 직접 자식은 클릭 박스 중심을 잰다
|
||||
|
||||
한 액션 묶음의 **같은 행에 놓인 직접 자식**인 채운 버튼과 텍스트 링크는 `align-items:center`만으로 시각 중심이 맞지 않을 수 있다. 중첩된 아이콘·배지까지 한꺼번에 비교하거나, 모바일에서 세로로 쌓인 액션에 이 규칙을 적용하면 오탐이다.
|
||||
|
||||
- 두 액션 모두 `inline-flex; align-items:center; min-height:44px`의 실제 hitbox를 가진다
|
||||
- `getBoundingClientRect()`의 중심 y 차이가 브라우저 CSS 픽셀 기준 2px 이하여야 한다
|
||||
- 텍스트 baseline이 아니라 클릭 가능한 박스 전체를 비교한다
|
||||
- 보조 링크를 44px로 키우면서 빈 padding이 과해지면 액션을 세로로 쌓는다
|
||||
|
||||
### 패널을 숨기면 그리드 트랙도 회수한다
|
||||
|
||||
데스크톱 목록+인스펙터 분할 작업대에서 패널만 `display:none`으로 숨기고 2열 `grid-template-columns`를 유지하면 큰 빈 면이 남는다. 닫힌 상태는 가시성 변화가 아니라 **레이아웃 상태 변화**다. 모바일에서 인스펙터가 시트·전체 화면으로 열리는 구조에는 데스크톱 2px 트랙 계약을 그대로 적용하지 않고, 시트 닫힘·배경 복귀·포커스 복원을 별도로 잰다.
|
||||
|
||||
```css
|
||||
.workbench { grid-template-columns: minmax(0, 1fr) minmax(20rem, .55fr); }
|
||||
.workbench.is-inspector-closed { grid-template-columns: minmax(0, 1fr); }
|
||||
.workbench.is-inspector-closed .inspector { display: none; }
|
||||
```
|
||||
|
||||
닫힘 상태에서 목록 오른쪽 끝과 작업대 오른쪽 끝의 차이를 2px 이하로 재고, 행 선택 뒤 2열이 복원되는 것도 함께 단언한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 타이포그래피
|
||||
|
|
@ -183,9 +220,12 @@ new Set([...document.querySelectorAll('.nav a')]
|
|||
- [ ] CSS/HTML만으로 페이지가 완성됐다. JS를 꺼도 읽힌다
|
||||
- [ ] 모든 값이 3단계 토큰에서 나온다. 하드코딩된 px/색이 없다
|
||||
- [ ] 첫 화면에 메시지가 셋 이하다
|
||||
- [ ] 의미 역할이 다른 연속 섹션이 같은 거시 그리드를 3회 반복하지 않는다. 반복 데이터면 같은 구조를 유지한 이유가 적혀 있다
|
||||
- [ ] 본문에 `--measure` 가 적용됐다
|
||||
- [ ] 모바일 폭 **320px**(iPhone SE 세로)에서 가로 스크롤이 없다
|
||||
- [ ] 키보드 Tab 만으로 모든 인터랙티브 요소에 도달한다. 포커스 링이 보인다
|
||||
- [ ] 같은 행 액션 묶음의 직접 자식 hitbox가 44px 이상이고 중심 y 차이가 CSS 픽셀 2px 이하다
|
||||
- [ ] 데스크톱 분할 패널을 숨긴 상태에서 빈 그리드 트랙이 남지 않고 주 표면이 전체 폭을 회수한다. 모바일 시트는 닫힘·배경·포커스 복원을 따로 검증한다
|
||||
- [ ] 한글이 있다면 조판 규칙이 적용됐다
|
||||
|
||||
> 근거: research/references/02-methodology.md, 03-trends-2026.md, 04-ai-slop-signatures.md (조사일 2026-08-20)
|
||||
|
|
|
|||
|
|
@ -163,6 +163,23 @@ canvas { display: none !important; }
|
|||
|
||||
---
|
||||
|
||||
## 4-2. 진실 완결성 — 가상·예시·AI·규제 화면
|
||||
|
||||
가상 브랜드·예시 데이터·AI 분석·규제 주제를 실제 서비스처럼 보이게 만든 화면에만 돈다. 전역 데모 띠 하나로 끝내지 않는다. 상세 설계는 `trustworthy-showcases.md`.
|
||||
|
||||
- [ ] 모든 숫자·주소·상태·추천이 **사실 / 예시 / 추론 / 미정** 중 하나이고 `USER_SELF_REPORT / VERIFIED_SOURCE / SYSTEM_STATE / SYNTHETIC_FIXTURE` 출처 계층이 추적된다. 자기보고 값 자체를 검증 사실로 승격하지 않았다
|
||||
- [ ] `예시 가격`, `거래 불가`, `실제 접수 아님`, `연출 이미지` 라벨이 오해가 생기는 범위 가까이에 있다. 같은 상태·출처·행동 가능성을 공유하는 자식은 가장 가까운 공통 부모가 한 번 소유하고, 다른 자식만 예외 라벨을 가진다
|
||||
- [ ] 검증하지 않은 수익·가맹비·매장 수·등록번호·연락처를 만들지 않았다. 모르면 `미정`이다
|
||||
- [ ] AI 결과에 입력 조건·출처가 붙은 입력/검증 사실·추론·불확실/누락·수정·사람 검토가 있고, 입력을 바꾸면 결과와 근거가 함께 바뀐다. 모델 출력 내용을 `SYSTEM_STATE` 사실로 두지 않았다
|
||||
- [ ] 진단·개인화 추천·예약·전송·거래·게시·내보내기 등 실제 규제 행동은 행동별 필수 검증이 없을 때 **실패 폐쇄형**이며, 미등록 행동은 기본 거절되고 막힌 이유가 화면에 보인다
|
||||
- [ ] 민감 입력과 파생값의 영속 저장·쿠키·분석 이벤트 포함·네트워크 전송 경계를 E2E로 검증했다
|
||||
- [ ] 공개 화면은 사용자 해석·제한·다음 행동을, 관리자는 내부 원장·검증 이벤트·채널 판정·감사 로그를 소유한다. 사용자 행동에 영향을 주는 제한을 관리자에만 숨기지 않았다
|
||||
- [ ] 지도·차트·도식의 같은 정보가 DOM 목록·표·설명으로도 제공되고 데이터 원천이 하나다
|
||||
- [ ] 생성 이미지는 가격·주소·점수의 증거로 쓰이지 않으며, 모바일 390×844에서 핵심 피사체가 첫 화면의 의도와 맞는다
|
||||
- [ ] URL별 390·1440 기본 화면과 핵심 입력/오류/모달 상태 캡처를 실제로 열어 보고 시각 PASS/RED를 따로 기록했다
|
||||
|
||||
---
|
||||
|
||||
## 5. 디자인 자체
|
||||
|
||||
기계로 못 잡는 부분이다. **정직하게 답해라.**
|
||||
|
|
@ -186,6 +203,7 @@ canvas { display: none !important; }
|
|||
- [ ] 반응형 체크리스트 통과
|
||||
- [ ] 콘텐츠 체크리스트 통과
|
||||
- [ ] 상태 완결성(§4-1) — 입력 검증·되돌림·Esc 를 실제 조작으로 확인
|
||||
- [ ] 해당하면 진실 완결성(§4-2) — 예시·AI·규제·생성 이미지의 오해 경로를 실제 상태로 확인
|
||||
- [ ] 5장 여섯 질문에 정직하게 답했고, 실패 항목을 고쳤다
|
||||
- [ ] `antipatterns.md` 자가 채점표 통과
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,21 @@
|
|||
SaaS 구조 + SaaS 톤 = 또 하나의 SaaS 슬롭.
|
||||
SaaS 구조 + 독립 출판사 톤 = 차별화.
|
||||
|
||||
### 목표 시장은 네 번째 레퍼런스가 아니라 경계 조건이다
|
||||
|
||||
목표 시장을 적지 않으면 검색 결과 상위의 서구 사례가 무의식적으로 기본값이 된다. 먼저 한 줄로 잠근다.
|
||||
|
||||
```md
|
||||
시장: 국가/도시/생활권 · 주 사용 기기 · 단위/통화 · 문의/구매 관습 · 피해야 할 외래 문법
|
||||
```
|
||||
|
||||
- R1은 같은 업종·목적에 더해 **같은 목표 시장을 우선**한다. 구조에 단위·용어·연락 방식이 묻어 있기 때문이다.
|
||||
- R2의 `다른 업종` 규칙은 유지한다. 목표 시장 감각이 중요하면 **같은 문화권의 다른 업종**을 먼저 찾는다.
|
||||
- 다른 문화권 R3는 기법만 가져온다. 카피·색·상징·사진 연출까지 번지면 경계 조건 위반이다.
|
||||
- 같은 시장 자료가 부족하면 빈칸을 숨기지 말고, 어떤 관습을 공식 문서·현지 서비스로 별도 검증했는지 적는다.
|
||||
|
||||
목표 시장 적합성은 국기·전통 문양 같은 표식이 아니다. 통화 단위, 면적 표기, 동네 탐색 축, 정보 순서, 촬영 조도, CTA 동사처럼 **사용자가 실제로 행동하는 문법**이다.
|
||||
|
||||
**충돌 해결 순위**
|
||||
1. 가독성·접근성이 언제나 이긴다. R2의 톤이 대비를 깨면 톤을 조정한다.
|
||||
2. R1 구조가 R3 모션을 이긴다. 모션 때문에 구조를 바꾸지 않는다.
|
||||
|
|
@ -82,6 +97,7 @@ SaaS 구조 + 독립 출판사 톤 = 차별화.
|
|||
## 3. 실전 절차 체크리스트
|
||||
|
||||
- [ ] **1. 브리프를 한 문장으로 압축한다.** `[대상]이 [행동]하게. 느낌은 [형용사 2개].` 이 문장 없이 수집을 시작하지 않는다.
|
||||
- [ ] **1-b. 목표 시장 경계 조건을 쓴다.** 국가/생활권·기기·단위/통화·행동 관습·금지할 외래 문법. R1이 같은 시장인지 표시한다.
|
||||
- [ ] **2. galleries.md 라우팅 표에서 R1/R2/R3 갤러리를 정한다.** R2가 R1과 다른 업종인지 확인한다.
|
||||
- [ ] **3. 각 갤러리에서 원본 사이트 URL을 1개씩 확보한다.** 하위 페이지가 필요하면 **인덱스에서 `href`를 뽑아라. slug를 추측하지 마라** — 추측한 URL은 404가 난다.
|
||||
- [ ] **3-b. 그 URL이 정본인지 검증한다.** 사용자가 특정 제품·브랜드를 지목했고 후보 도메인이 여럿이면 필수다. **HTTP 200은 살아있다는 뜻이 아니다.**
|
||||
|
|
@ -214,6 +230,12 @@ WebFetch는 마크다운으로 변환하면서 CSS를 버린다. **그리드·
|
|||
## 브리프
|
||||
[대상]이 [행동]하게. 느낌은 [형용사 2개].
|
||||
|
||||
## 목표 시장
|
||||
- 지역/생활권:
|
||||
- 기기/단위/통화:
|
||||
- 행동 관습:
|
||||
- 금지할 외래 문법:
|
||||
|
||||
## R1 · 구조 — <이름> <원본 URL>
|
||||
- 가져올 축: 그리드, 여백 리듬
|
||||
- 그리드: 최대폭 1200 / 비대칭 7:5 / 거터 32
|
||||
|
|
@ -244,6 +266,7 @@ WebFetch는 마크다운으로 변환하면서 CSS를 버린다. **그리드·
|
|||
|
||||
**검증 조건** — 하나라도 빠지면 되돌아간다.
|
||||
- R1/R2/R3 각각 **원본 사이트 URL**이 있다 (갤러리 URL 아님)
|
||||
- 목표 시장 경계 조건이 있고 R1의 시장 일치 여부가 적혀 있다
|
||||
- R2의 업종이 R1과 다르다
|
||||
- 각 슬롯에 **가져올 축**이 명시되어 있다
|
||||
- 각 슬롯에 **변형 1개와 그 이유**가 있다
|
||||
|
|
|
|||
355
packages/skill/references/trustworthy-showcases.md
Normal file
355
packages/skill/references/trustworthy-showcases.md
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
# trustworthy-showcases — 실제처럼 보이되 거짓말하지 않는 쇼케이스
|
||||
|
||||
다음 중 하나라도 해당하면 **1·2·4·5단계**에서 읽는다. 1단계에서 시장과 위험 경계를 잠그고, 2단계에서 정보 구조를 나누고, 4단계에서 구현하며, 5단계에서 실제 상태를 검증한다.
|
||||
|
||||
- 가상 브랜드·가상 매물·예시 가격·예시 매장을 실제 서비스처럼 설계한다
|
||||
- AI 분석·추천·점수·예측을 화면에 싣는다
|
||||
- 부동산·금융·의료처럼 규제되거나 오해 비용이 큰 주제를 다룬다
|
||||
- 고객용 랜딩과 관리자·가맹·운영 화면을 한 브랜드로 함께 만든다
|
||||
- 생성 이미지를 실제 상품·공간·사람의 증거처럼 오인할 수 있다
|
||||
|
||||
목표는 경고문을 많이 붙이는 것이 아니다. **주장의 구조가 정직하고, 그 정직함이 디자인을 약하게 만들지 않게 하는 것**이다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 근본 규칙과의 충돌 해결
|
||||
|
||||
이 문서는 기존 규칙을 느슨하게 만들지 않는다. 충돌하면 다음 순서로 해석한다.
|
||||
|
||||
1. 접근성·사실성·사용자 안전이 언제나 이긴다.
|
||||
2. 브리프와 목표 시장이 레퍼런스의 멋을 이긴다.
|
||||
3. 하나의 브랜드 방향은 유지하되, 서로 다른 과업의 표면 밀도까지 같게 만들지는 않는다.
|
||||
4. 게이트의 기본값은 유지한다. 예외가 필요하면 페이지 계약으로 좁게 선언하고 더 넓은 매트릭스로 검증한다.
|
||||
5. 생성 이미지는 분위기와 재질을 맡고, 가격·상태·근거·판정은 DOM이 맡는다.
|
||||
|
||||
예: 마케팅 h1에 2줄이 필요하면 범용 1줄 규칙을 삭제하지 않는다. 그 페이지만 `h1MaxLines: 2`로 선언하고 375·390px × 글꼴 확대에서도 2줄 이하인지 잰다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 목표 시장을 먼저 잠근다
|
||||
|
||||
`R1/R2/R3`보다 먼저 **시장 계약** 한 줄을 쓴다.
|
||||
|
||||
```md
|
||||
시장: 한국 / 서울 생활권 / 모바일 우선
|
||||
행동 관습: 원·만원·억, ㎡와 평 병기, 동네·역·도보 기준, 국내형 문의 순서
|
||||
금지: 서구 럭셔리 부동산 문법, 미국 다이너 문법, 번역투 카피, 이국화 장식
|
||||
```
|
||||
|
||||
- R1은 같은 업종·목적이면서 가능하면 **같은 목표 시장**에서 고른다. 구조에는 단위·용어·연락 방식·신뢰 표지가 들어 있기 때문이다.
|
||||
- R2는 다른 업종이어야 한다는 근본 규칙을 유지한다. 다만 목표 시장의 미감이 중요하면 **같은 문화권의 다른 업종**을 우선한다.
|
||||
- R3가 다른 문화권에서 왔다면 가져올 것은 구현 기법 하나뿐이다. 색·카피·상징까지 따라오면 시장 계약을 어긴다.
|
||||
- 국내화는 태극 문양이나 한옥 장식을 붙이는 일이 아니다. **단위·정보 순서·검색 축·사진의 조도·문의 단계**를 현지 관습에 맞추는 일이다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 한 방향, 여러 표면
|
||||
|
||||
고객용 랜딩·관리자·가맹 페이지가 함께 있을 때 전부 같은 표현 강도로 만들면 둘 중 하나가 망가진다. 공통 토큰을 공유하고 표면 다이얼을 따로 적는다.
|
||||
|
||||
| 다이얼 | 0 | 1 | 2 | 3 |
|
||||
|---|---|---|---|---|
|
||||
| 표현성 | 문서 | 조용한 제품 | 캠페인 | 실험적 |
|
||||
| 정보 밀도 | 넉넉함 | 표준 | 조밀함 | 운영 콘솔 |
|
||||
| 모션 | 없음 | 피드백만 | 절제된 전환 | 브랜드 연출 |
|
||||
| 증거 노출 | 요약 | 근거 링크 | 근거 병기 | 감사 기록 |
|
||||
|
||||
예시:
|
||||
|
||||
```md
|
||||
공통: Pretendard, 8px 리듬, 먹색, 강조색 1개, 직설형 카피
|
||||
고객 랜딩: 표현 2 / 밀도 1 / 모션 1 / 증거 2
|
||||
관리자: 표현 0 / 밀도 3 / 모션 0 / 증거 3
|
||||
```
|
||||
|
||||
이렇게 해야 랜딩은 설득하고 관리자는 판정한다. 프리셋을 두 개 쓰는 것이 아니라 **같은 브랜드가 과업에 맞춰 목소리 크기를 바꾸는 것**이다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 먼저 진실 계약을 만든다
|
||||
|
||||
화면을 그리기 전에 모든 주장과 행동을 네 종류로 분류한다. 이 **진실 상태**와 값이 어디서 왔는지 나타내는 **출처 계층**을 섞지 않는다.
|
||||
|
||||
| 종류 | 예 | 화면 규칙 |
|
||||
|---|---|---|
|
||||
| 사실 | 검증 가능한 문장: `사용자가 40억을 입력했다`, `공식 원문에 전용 118㎡로 적혔다` | 무엇이 사실인지 주어·출처·기준일을 가까이에 둔다 |
|
||||
| 예시 | 가상 매물, 예시 가격, 합성 매장 | 주장·가격·카드·행동 근처에 `예시`를 붙인다 |
|
||||
| 추론 | AI 추천, 위험 신호, 적합도 | 근거·불확실성·수정 경로를 함께 둔다 |
|
||||
| 미정 | 가맹비, 수익, 실제 주소처럼 검증 안 된 값 | `미정`으로 남기고 숫자를 만들지 않는다 |
|
||||
|
||||
같은 값이라도 출처가 다르면 화면의 말투와 가능한 행동이 달라진다.
|
||||
|
||||
| 출처 계층 | 뜻 | 허용 표현 |
|
||||
|---|---|---|
|
||||
| `USER_SELF_REPORT` | 사용자가 직접 입력·진술했지만 외부 검증은 안 됨 | `입력한 조건`, `사용자 진술` |
|
||||
| `VERIFIED_SOURCE` | 기준일과 원문을 확인한 외부·공식 출처 | `확인된 정보` + 출처·기준일 |
|
||||
| `SYSTEM_STATE` | 현재 제품이 직접 계산·관측한 상태 | 계산 규칙·측정 시점 병기 |
|
||||
| `SYNTHETIC_FIXTURE` | 쇼케이스 동작 확인용 합성 값 | `예시`, 실제 행동 차단 |
|
||||
|
||||
사용자 진술의 값 자체를 곧바로 도메인 사실로 승격하지 않는다. `사용자가 붉음을 선택했다`는 사실일 수 있지만 `피부가 실제로 붉다`는 임상 상태는 검증 전 미정이다. 합성 픽스처도 시스템 관측처럼 표현하지 않는다. 승격에는 출처·기준일·검증 이벤트가 필요하다. AI·모델 출력은 제품이 생성했다는 의미에서 시스템 상태일 수 있어도, 내용의 진실 상태는 여전히 **추론**이다.
|
||||
|
||||
### 4-a. 같은 진실 범위는 가장 가까운 공통 부모가 소유한다
|
||||
|
||||
`오해 지점 가까이에 라벨`은 같은 경고를 모든 자식에 복제하라는 뜻이 아니다.
|
||||
|
||||
- 그룹의 모든 자식이 같은 진실 상태·출처·행동 가능성을 공유하면, 제목과 시각 경계가 분명한 **가장 가까운 공통 부모**에 라벨을 한 번 둔다.
|
||||
- 자식 하나라도 상태·출처·행동 가능성이 다르면 그 자식에 예외 라벨을 둔다.
|
||||
- 공통 부모의 범위가 화면과 보조기술에서 명확하지 않으면 묶지 않는다. 폼은 `fieldset/legend`, 표는 `caption`, 카드 집합은 제목·설명과 `aria-describedby` 같은 프로그램적 연결을 쓴다.
|
||||
- 전역 띠는 사이트 전체의 가상 브랜드 상태를 맡을 수 있지만, 특정 가격·이미지·제출 행동의 서로 다른 경계까지 대신하지 않는다.
|
||||
|
||||
구현 전에 진실 라벨의 `문구 / 소유 컨테이너 / 예외 자식`을 표로 만들고, 같은 문구의 반복 횟수가 아니라 의미 범위가 닫혔는지 감사한다.
|
||||
|
||||
전역 데모 띠 하나만으로는 부족하다. 사용자는 스크롤 중 그 띠를 보지 못한다. **라벨은 오해가 발생하는 지점에 붙인다.**
|
||||
|
||||
- 후보 전체가 같은 상태인 카드 집합: 부모 설명 `아래 세 후보는 예시 · 거래 불가`; 실제·예시가 섞이면 각 카드에 예외 라벨
|
||||
- 생성 사진: 각 이미지 캡션 `연출 이미지 · 실제 증빙 아님`
|
||||
- 가격 집합이 모두 같은 상태면 표 `caption`이나 열 제목 `예시 가격`; 상태가 다른 값만 개별 표시
|
||||
- 매장 상태: `화면 동작 확인용 예시`
|
||||
- 폼: `실제 접수되지 않음 · 개인정보 입력 금지`
|
||||
|
||||
라벨은 작은 회색 각주로 숨기지 않는다. 본문 대비를 지키고 해당 주장과 같은 시각 그룹에 둔다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 성공 행동이 다르면 경로를 나눈다
|
||||
|
||||
한 페이지에서 소비자와 운영자, 구매자와 중개 관리자를 동시에 설득하지 않는다.
|
||||
|
||||
- 소비자 성공: 메뉴·가격·가까운 매장·조합 확인
|
||||
- 가맹 성공: 운영 조건·상권·점포·문의 검토
|
||||
- 고객 성공: 조건 입력·근거 확인·사람 상담
|
||||
- 관리자 성공: 검수·보류 사유·게시 채널 판정
|
||||
|
||||
성공 행동이 다르면 URL과 IA를 나눈다. 공통 헤더에서 경로는 연결하되, 각 페이지의 첫 화면 메시지와 CTA는 하나의 성공 행동만 말한다. 별도 경로는 중복이 아니라 **우선순위 충돌을 제거하는 구조**다.
|
||||
|
||||
### 5-a. 공개 해석과 내부 검수의 소유권을 나눈다
|
||||
|
||||
URL을 나눠도 관리자 언어를 공개 페이지에 복제하면 경로 분리가 끝난 것이 아니다.
|
||||
|
||||
| 공개 화면이 소유 | 관리자 화면이 소유 |
|
||||
|---|---|
|
||||
| 사용자가 결정을 해석하는 데 필요한 근거·한계·다음 행동 | 내부 ID·원문 출처 원장·검증 이벤트·모델/규칙 버전 |
|
||||
| 사용자 행동을 막거나 바꾸는 제한과 그 이유 | 규제 채널 판정 상세·역할 권한·감사 로그 |
|
||||
| 정정·사람 확인 경로 | 승인·보류·변경 이력과 운영 실패 로그 |
|
||||
|
||||
내부 상수와 감사 원장은 관리자에 두되, 제한이 사용자의 행동을 막는다는 사실과 사용자가 이해해야 할 이유까지 숨기지 않는다. 공개는 **해석 가능한 요약**, 관리자는 **재현 가능한 증거**를 소유한다. 구현 전에 각 콘텐츠 블록에 `public / admin / both-summary` 소유자를 표시한다.
|
||||
|
||||
### 5-b. 고위험 데모의 데이터 경계
|
||||
|
||||
의료·금융·법률·주거 상담처럼 입력 자체가 민감할 수 있는 쇼케이스는 폼이 예뻐 보이는 것보다 **수집하지 않는 증거**가 먼저다.
|
||||
|
||||
- 과업 시연에 필요한 최소 필드만 둔다. 이름·연락처·정확한 주소·증상 원문처럼 없어도 되는 자유 입력은 빼거나 로컬 예시 선택지로 바꾼다.
|
||||
- 합성 픽스처가 아닌 **민감 입력과 그 파생값**은 기본적으로 새로고침 후 남기지 않는다. 해당 값의 영속 저장·쿠키 기록·분석 이벤트 포함·네트워크 전송이 0회인지 E2E로 단언한다. 정적 이미지·폰트 요청과 일반 페이지 조회 자체는 이 0회 계약의 대상이 아니다.
|
||||
- `실제 접수되지 않음` 문구만으로는 부족하다. 제출 성공 UI가 있어도 실제 요청·저장 부작용이 없어야 GREEN이다.
|
||||
- 실제 운영 연결이 생기면 동의·보관 기간·삭제·전송 대상을 별도 제품 계약으로 설계한다. 쇼케이스 계약을 조용히 확장하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 이미지 바이블은 슬롯 단위로 만든다
|
||||
|
||||
이미지 한 장을 만든 뒤 여기저기 크롭하지 않는다. 생성 전에 슬롯 표를 만든다.
|
||||
|
||||
| 슬롯 | 역할 | 피사체 | 비율 | 구도 안전영역 | 진실 라벨 | 폴백 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 데스크톱 히어로 | 첫 인상 | 손+제품 | 16:9 | 우측 55% | 연출 이미지 | 단색+제품명 |
|
||||
| 모바일 히어로 | 첫 화면 행동 | 제품+손 | 4:5 | 하단 중심 | 연출 이미지 | 같은 톤 정지컷 |
|
||||
| 증거 컷 | 재질·동선 | 공간 일부 | 3:2 | 중앙 | 합성 예시 | DOM 도식 |
|
||||
|
||||
**모바일 전용 생성은 데스크톱 크롭의 사치 버전이 아니다.** 첫 화면에서 보여야 할 주체가 달라지면 별도 원본이 필요하다. 얼굴이 먼저 나오고 제품이 폴드 아래로 밀리면 비율이 맞아도 실패다.
|
||||
|
||||
- 같은 톤 접두사를 공유해 색온도·광원·재질을 잠근다.
|
||||
- 장면별로 피사체·행동·카메라 높이만 바꾼다.
|
||||
- 음식은 손가락·포장 접힘·재료 층, 공간은 문·난간·가구 접합부를 확대해 본다.
|
||||
- 이미지 안에 가격·주소·지도·점수·차트를 굽지 않는다. 그 정보는 DOM으로 만든다.
|
||||
- 이미지 파일이 존재한다는 것과 채택 가능하다는 것은 다르다. 원본을 눈으로 보고 조건부 채택 사유를 기록한다.
|
||||
|
||||
고위험 업종은 공통 이미지 규칙 위에 **도메인 금지 오버레이**를 한 장 더 만든다.
|
||||
|
||||
| 도메인 | 생성 이미지로 만들지 않는 증거 |
|
||||
|---|---|
|
||||
| 의료·미용 | 전후 비교, 병변·치료 효과, 가짜 의료진·자격·기관 로고 |
|
||||
| 금융·투자 | 수익 인증, 잔고·체결 내역, 보장 수익 그래프 |
|
||||
| 부동산 | 실제 매물 인증, 권리 서류, 조망·주소·중개 자격 증명 |
|
||||
| 음식·가맹 | 실제 매출표, 실재 점포 운영 인증, 검증하지 않은 원산지 표식 |
|
||||
|
||||
생성 이미지는 분위기·재질·사용 장면까지만 맡긴다. 자격·효과·소유·수익을 증명하는 장면은 DOM 라벨로도 합법화되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 7. AI는 답이 아니라 검토 가능한 기록이다
|
||||
|
||||
AI 화면에는 최소 다섯 블록이 필요하다.
|
||||
|
||||
1. **입력 조건** — 사용자가 무엇을 말했는지
|
||||
2. **출처가 붙은 입력·검증 사실** — 사용자가 무엇을 진술했는지와 외부·공식 자료로 무엇을 확인했는지 분리
|
||||
3. **추론** — 사실에서 어떻게 결론으로 갔는지
|
||||
4. **불확실·누락** — 모르는 것과 데이터 기준일
|
||||
5. **사람 검토·수정** — 틀린 조건을 고치고 담당자에게 넘기는 경로
|
||||
|
||||
단일 점수와 반짝이는 링만 보여 주지 않는다. 점수는 정밀해 보이지만 근거가 없으면 장식이다. 추천 카드마다 `왜`, `무엇이 부족`, `다음 확인`을 붙인다.
|
||||
|
||||
모델 출력은 제품 내부에서 생성됐다는 이유만으로 `SYSTEM_STATE`의 관측 사실이 되지 않는다. 출력 내용은 `추론`, 실행·모델 버전·생성 시각만 `SYSTEM_STATE`다.
|
||||
|
||||
AI 결과를 바꿀 수 있는 입력을 하나 이상 제공하고, E2E에서 입력을 바꿨을 때 근거·결과·불확실성도 함께 바뀌는지 단언한다.
|
||||
|
||||
### 7-a. 보이는 입력과 판정 의존성은 하나의 계약이다
|
||||
|
||||
맞춤 분석이 참조하는 조건을 UI와 코드가 따로 관리하지 않는다. 구현 전에 `입력 필드 / 출처 / 판정 사용처 / 출력 주장` 표를 만들고 하나의 스키마·데이터 객체에서 폼과 판정을 함께 만든다.
|
||||
|
||||
- 사용자가 입력하지 않은 숨은 기본값을 `희망 조건`처럼 결과에 쓰지 않는다.
|
||||
- 화면에 받은 조건을 판정에서 무시한 채 맞춤 결과라고 부르지 않는다.
|
||||
- 분석 실행 전에는 완료 수치·추천·충돌을 보여 주지 않는다. `아직 분석하지 않음` 상태가 정직한 초기값이다.
|
||||
- 보증금+월 납부액처럼 단일 축으로 비교할 수 없는 복합 비용은 일부 값만으로 `예산 안쪽`이라 판정하지 않고 `별도 확인`으로 남긴다.
|
||||
- E2E에서 보이는 입력 이름 집합과 판정 함수의 사용자 조건 의존성 집합이 일치하는지, 입력 변경 뒤 근거·결과·불확실성이 함께 변하는지 단언한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 규제된 행동은 실패 폐쇄형으로
|
||||
|
||||
게시만 규제되는 것이 아니다. 진단·개인화 추천·예약·상담 전송·거래·문서 내보내기처럼 실제 사람이나 시장에 영향을 주는 행동을 쇼케이스 성공 상태와 같은 버튼으로 다루지 않는다. 행동·채널별 판정 함수를 둔다.
|
||||
|
||||
```js
|
||||
// 예시 구조다. 실제 필수 요건은 목표 시장·업종의 공식 기준으로 다시 잠근다.
|
||||
const requiredByAction = {
|
||||
DIAGNOSE: ["requiredConsent", "verifiedClinicalData", "licensedReviewer", "humanReview"],
|
||||
BOOK_APPOINTMENT: ["requiredConsent", "verifiedProvider", "availableSlot"],
|
||||
EXPORT_RECORD: ["requiredConsent", "verifiedRecord", "accessControl", "auditLog"],
|
||||
PUBLISH_LISTING: ["verifiedBroker", "verifiedAddress", "sourceFiles", "humanReview"],
|
||||
};
|
||||
|
||||
function regulatedActionDecision(record, action) {
|
||||
if (action === "SHOWCASE_PREVIEW") {
|
||||
return record.syntheticLabelled
|
||||
? { allowed: true, reason: "예시 범위가 명확한 미리보기" }
|
||||
: { allowed: false, reason: "예시 범위 표지 누락" };
|
||||
}
|
||||
|
||||
const required = requiredByAction[action];
|
||||
if (!required) return { allowed: false, reason: "등록되지 않은 행동" };
|
||||
if (record.syntheticFixture) return { allowed: false, reason: "합성 자료로 실제 행동 불가" };
|
||||
|
||||
const missing = required.filter((field) => !record[field]);
|
||||
if (missing.length) return { allowed: false, reason: `필수 검증 누락: ${missing.join(", ")}` };
|
||||
return { allowed: true, reason: "필수 검증 완료" };
|
||||
}
|
||||
```
|
||||
|
||||
- 화면에서 비활성 버튼만 두지 말고 **왜 막혔는지**를 말한다.
|
||||
- 알 수 없는 행동은 기본 거절한다. 행동마다 필수 검증 목록이 다르며, 한 묶음의 불리언으로 모든 규제 행동을 열지 않는다.
|
||||
- 관리자 검수 큐에는 `사실 / 추론 / 미정 / 위험` 상태와 출처 계층을 분리한다.
|
||||
- 보류·거절은 사유를 필수로 받고, 닫기·Esc·포커스 복귀를 검증한다.
|
||||
- 가상 데이터만 있는 쇼케이스라면 실제 진단·추천·예약·전송·거래·게시·내보내기 경로가 끝까지 막힌 상태가 정상 GREEN이다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 시각화에는 읽을 수 있는 쌍둥이를 둔다
|
||||
|
||||
지도·차트·도식은 시선을 모으지만 사실을 완전히 전달하지 못한다. 같은 정보를 DOM 목록·표·설명으로도 제공한다.
|
||||
|
||||
- 지도 핀 ↔ 매장 목록
|
||||
- 적합도 그래프 ↔ 조건별 근거 목록
|
||||
- 재료 도식 ↔ 알레르기·구성 텍스트
|
||||
- 운영 흐름 그림 ↔ 단계별 제목·설명
|
||||
|
||||
이미지나 SVG가 실패해도 과업을 끝낼 수 있어야 한다. 두 표현의 값이 어긋나지 않도록 데이터 원천은 하나로 둔다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 시각 E2E는 캡처가 아니라 계약이다
|
||||
|
||||
정적·기하 게이트와 눈 검사는 역할이 다르다.
|
||||
|
||||
1. HTML/CSS/JS 정적 검사
|
||||
2. 320·390·768·1024·1440 기하·대비·터치·h1 계약
|
||||
3. 사용자 여정 E2E와 기대값 단언
|
||||
4. 기본 화면 + 중요한 상호작용 상태 캡처
|
||||
5. 캡처를 실제로 열어 목표 시장 적합성·시선 위계·크롭·반복을 판정
|
||||
|
||||
최소 캡처 계약:
|
||||
|
||||
- 각 URL의 390px·1440px 기본 화면
|
||||
- 결과가 바뀌는 입력 상태 1개
|
||||
- 관리자 보류·게시 차단 상태 1개
|
||||
- 폼 검토 또는 오류 상태 1개
|
||||
|
||||
폭을 숫자 목록으로만 쓰지 말고 각 폭이 잡아야 하는 실패를 적는다.
|
||||
|
||||
| 폭 | 검증 목적 |
|
||||
|---|---|
|
||||
| 320 | 긴 한국어 라벨·최소 터치 영역·긴급 수축 스트레스 |
|
||||
| 375 + 글꼴 확대 | 페이지별 `h1MaxLines`·한국어 어절 줄바꿈·보조 행동 수축 |
|
||||
| 390 | 실제 모바일 첫 화면의 주체·CTA·고정 요소 |
|
||||
| 768 | 카드 재배치와 중간 폭 공백·절대 배치 기준 |
|
||||
| 1024 | 태블릿·관리자 분할면의 트랙 회수와 표 오버플로 |
|
||||
| 1440 | 데스크톱 시선 위계·콘텐츠 최대폭·과도한 빈 면 |
|
||||
|
||||
하니스는 캡처의 **개수·가로세로·0바이트 여부·해시 중복**을 단언한다. 모델은 그 뒤에 `국내 감각인가`, `제품이 첫 화면에 보이는가`, `운영 화면이 과장됐는가` 같은 지점을 평가한다.
|
||||
|
||||
실측 사고: 기하 게이트 8/8을 통과한 모바일 페이지에서 `position: static`으로 바뀐 지도 부모 때문에 절대 배치 자식의 기준이 `BODY`가 됐다. 격자와 핀이 히어로 전체를 덮고 실제 지도는 빈 판이 됐다. **시각 캡처를 열지 않았다면 GREEN으로 오판했다.**
|
||||
|
||||
---
|
||||
|
||||
## 11. 시나리오·단언 수를 잠근다
|
||||
|
||||
대규모 E2E가 중간 리팩터링에서 조용히 줄어드는 것을 막는다.
|
||||
|
||||
```js
|
||||
const EXPECTED_SCENARIOS = 90;
|
||||
const EXPECTED_ASSERTIONS = 350;
|
||||
|
||||
if (scenarios.length !== EXPECTED_SCENARIOS) throw new Error("scenario count drift");
|
||||
if (assertionCount < EXPECTED_ASSERTIONS) throw new Error("assertion count drift");
|
||||
```
|
||||
|
||||
숫자를 채우려고 같은 검사를 복제하지 않는다. 공통·고객·관리자·오류·접근성·시각 상태로 분류하고, 각 시나리오는 `행동 → 기대 DOM/상태 → 부작용 없음`을 단언한다.
|
||||
|
||||
정적 파일도 재귀 glob으로 잡는다. 새 서브페이지가 생겼는데 `public/work/*/index.html`만 검사하면 관리자·가맹 화면이 검증 밖으로 빠진다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 구현과 사용자 게이트에서 검증된 교훈 21개
|
||||
|
||||
| # | 교훈 | 스킬에 반영하는 규칙 |
|
||||
|---|---|---|
|
||||
| 1 | 서구 레퍼런스는 국내 행동 관습을 자동으로 주지 않는다 | R1을 목표 시장에서 우선 탐색 |
|
||||
| 2 | 국내 감각은 장식보다 단위·용어·정보 순서에서 생긴다 | 시장 계약을 R1/R2/R3보다 먼저 작성 |
|
||||
| 3 | 다른 업종 R2는 유지해야 카테고리 평균을 피한다 | 같은 문화권의 다른 업종을 우선 |
|
||||
| 4 | 랜딩과 관리자는 같은 표현 밀도를 쓰면 둘 다 약해진다 | 표면 다이얼 4종을 별도 선언 |
|
||||
| 5 | 강조색은 비율만으로 통제되지 않는다 | 허용 역할과 최대 면적을 함께 적음 |
|
||||
| 6 | 생성 이미지는 데이터 증거가 아니다 | 이미지=재질, DOM=가격·상태·근거 |
|
||||
| 7 | 데스크톱 크롭은 모바일 핵심 피사체를 보장하지 않는다 | 의미가 바뀌면 모바일 원본 별도 생성 |
|
||||
| 8 | 성공 행동이 다른 두 청중은 한 히어로에서 경쟁한다 | URL·IA를 분리하고 공통 브랜드만 공유 |
|
||||
| 9 | AI 점수만 있으면 신뢰가 아니라 정밀함의 연출이다 | 사실·추론·불확실·수정·사람 검토를 병기 |
|
||||
| 10 | 가상 데이터로 실제 규제 행동이 성공하면 버그다 | 행동별 요구 조건·미등록 기본 거절·합성 자료 차단 |
|
||||
| 11 | 지도·도식만으로는 접근성과 사실 확인이 닫히지 않는다 | DOM 목록·표 쌍둥이를 의무화 |
|
||||
| 12 | 전역 데모 띠는 스크롤 후 오해를 막지 못한다 | 주장·가격·행동 가까이에 진실 라벨 |
|
||||
| 13 | 자동 승인은 검증 면제가 아니다 | 사용자 게이트 상태와 기계/시각 게이트를 분리 |
|
||||
| 14 | 구조 PASS는 시각 PASS가 아니다 | 상태 캡처를 열어 직접 판정하고 재촬영 |
|
||||
| 15 | 테스트 수는 리팩터링 중 조용히 줄어든다 | 시나리오·단언·캡처 수를 hard-lock |
|
||||
| 16 | 채운 버튼과 텍스트 링크는 `align-items:center`여도 시각 중심이 어긋난다 | 실제 hitbox 44px·중심 y 차이 ≤2px를 단언 |
|
||||
| 17 | 역할이 다른 섹션도 같은 5:7 제목+목록을 반복하면 구조가 복제돼 보인다 | 장소=도식, 비용=원장, 판정=흐름처럼 의미에 맞는 토폴로지를 배정 |
|
||||
| 18 | 인스펙터만 숨기고 그리드 트랙을 남기면 빈 면이 고장처럼 보인다 | 닫힘 상태에서 주 표면이 전체 폭을 회수하고 재선택 때 분할을 복원 |
|
||||
| 19 | 같은 `예시` 경고를 모든 자식에 반복하면 가치와 경고가 함께 묻힌다 | 같은 진실 범위는 가장 가까운 명확한 공통 부모가 한 번 소유 |
|
||||
| 20 | 공개 페이지에 내부 ID·게시 채널·감사 원장을 복제하면 제품 설명서가 된다 | 공개는 해석·제한·다음 행동, 관리자는 재현 가능한 내부 증거를 소유 |
|
||||
| 21 | 보이는 입력과 계산 조건이 다르면 맞춤 분석은 임의 결과가 된다 | 입력 필드·판정 의존성·출력 주장을 한 SSOT 표로 잠그고 초기 미실행 상태를 검증 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 통과 조건
|
||||
|
||||
- [ ] 시장 계약과 금지 문법이 적혀 있다
|
||||
- [ ] R1은 목표 시장의 정보 관습을 설명하고, R2는 다른 업종이다
|
||||
- [ ] 각 표면의 표현·밀도·모션·증거 다이얼이 적혀 있다
|
||||
- [ ] 모든 숫자·주소·상태·AI 결과가 사실/예시/추론/미정 중 하나이고 출처 계층이 추적된다
|
||||
- [ ] 진실 라벨이 전역 공지뿐 아니라 오해 지점 가까이에 있고, 같은 범위는 명확한 공통 부모가 한 번 소유하며 다른 자식만 예외 표시된다
|
||||
- [ ] 데스크톱·모바일 이미지 슬롯과 폴백이 따로 정의됐다
|
||||
- [ ] 고위험 업종의 생성 이미지 금지 증거와 민감 입력·저장·전송 경계가 검증됐다
|
||||
- [ ] 보이는 입력과 판정 의존성이 한 SSOT이고, 분석 전 완료 결과가 없으며 복합 비용 일부로 적합을 확정하지 않는다
|
||||
- [ ] AI 결과에 출처가 붙은 입력/검증 사실·추론·불확실성과 수정·사람 검토 경로가 있다
|
||||
- [ ] 규제 행동은 필수 검증이 없을 때 실패 폐쇄형이다
|
||||
- [ ] 공개는 사용자 해석·제한·다음 행동을, 관리자는 재현 가능한 내부 증거를 소유한다
|
||||
- [ ] 지도·차트·도식의 DOM 쌍둥이가 있다
|
||||
- [ ] URL별 390·1440 기본 캡처와 핵심 상태 캡처를 실제로 열어 봤다
|
||||
- [ ] E2E 시나리오·단언·캡처 수가 잠겨 있고 재귀 정적 검사가 새 서브페이지를 포함한다
|
||||
|
||||
하나라도 빠지면 4단계로 돌아간다. 정직함을 각주로 미루지 말고 구조로 해결한다.
|
||||
|
|
@ -26,6 +26,7 @@ const DEFAULT_CFG = {
|
|||
viewAttribute: "data-view",
|
||||
views: ["main"],
|
||||
h1: "h1",
|
||||
h1MaxLines: 1,
|
||||
brand: ".brand",
|
||||
},
|
||||
],
|
||||
|
|
@ -103,49 +104,105 @@ const cssFilesOf = (p) => {
|
|||
return out;
|
||||
};
|
||||
|
||||
// 페이지가 공유 CSS·동적 JS·지역 토큰을 함께 쓰면 HTML 한 장과 파일명 규칙만으로
|
||||
// 소유권을 추측할 수 없다. staticContract 는 검사 자체를 끄지 않고, 어떤 CSS를
|
||||
// 어떤 문서 코퍼스로 대조하며 어느 파일이 토큰 권위인지 명시한다.
|
||||
const contractFilesOf = (p, key, fallback) => {
|
||||
const specs = p.staticContract?.[key];
|
||||
if (!Array.isArray(specs)) return fallback;
|
||||
return specs.map((file) => path.resolve(process.cwd(), file));
|
||||
};
|
||||
|
||||
const scriptFilesOf = (p) => {
|
||||
const out = [];
|
||||
let html = "";
|
||||
try { html = fs.readFileSync(path.resolve(process.cwd(), p.path), "utf8"); } catch { return out; }
|
||||
for (const tag of html.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*>/g)) {
|
||||
const src = tag[1];
|
||||
if (/^(?:https?:|data:)/.test(src)) continue;
|
||||
const file = path.resolve(pageDir(p), src.split("?")[0]);
|
||||
if (fs.existsSync(file) && !out.includes(file)) out.push(file);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const selectorDocumentsOf = (p) => contractFilesOf(
|
||||
p,
|
||||
"selectorDocuments",
|
||||
[path.resolve(process.cwd(), p.path), ...scriptFilesOf(p)],
|
||||
);
|
||||
|
||||
const missingFiles = (files) => files.filter((file) => !fs.existsSync(file));
|
||||
|
||||
if (CFG.checks.deadCss) {
|
||||
const ignore = CFG.deadCssIgnore || [];
|
||||
for (const p of CFG.pages) {
|
||||
let html = "";
|
||||
try { html = fs.readFileSync(path.resolve(process.cwd(), p.path), "utf8"); }
|
||||
catch { skip(`${p.name} 죽은 선택자`, "HTML 읽기 실패"); continue; }
|
||||
const files = cssFilesOf(p);
|
||||
const files = contractFilesOf(p, "selectorFiles", cssFilesOf(p));
|
||||
const documents = selectorDocumentsOf(p);
|
||||
const missing = missingFiles([...files, ...documents]);
|
||||
if (missing.length) {
|
||||
fail(`${p.name} 죽은 선택자 계약`, `없는 파일 ${missing.map((file) => path.relative(process.cwd(), file)).join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
if (!files.length) { skip(`${p.name} 죽은 선택자`, "로컬 CSS 없음"); continue; }
|
||||
const corpus = documents.map((file) => fs.readFileSync(file, "utf8")).join("\n");
|
||||
const dead = new Set();
|
||||
for (const f of files) {
|
||||
const src = fs.readFileSync(f, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
for (const m of src.matchAll(/\.([A-Za-z_][A-Za-z0-9_-]*)/g)) {
|
||||
const cls = m[1];
|
||||
if (ignore.some((pfx) => cls.startsWith(pfx))) continue;
|
||||
if (!html.includes(cls)) dead.add(cls);
|
||||
if (!corpus.includes(cls)) dead.add(cls);
|
||||
}
|
||||
}
|
||||
dead.size
|
||||
? fail(`${p.name} 죽은 선택자 ${dead.size}종`, [...dead].slice(0, 8).map((c) => "." + c).join(" ") + (dead.size > 8 ? " …" : "") + " — 제거는 HTML·CSS·JS 삼위일체")
|
||||
: pass(`${p.name} 죽은 선택자`, `${files.length}개 CSS 전 클래스 사용 중(무시 ${ignore.length}접두사)`);
|
||||
: pass(`${p.name} 죽은 선택자`, `${files.length}개 CSS × ${documents.length}개 HTML·JS 문서(무시 ${ignore.length}접두사)`);
|
||||
}
|
||||
} else skip("죽은 선택자", "checks.deadCss=false");
|
||||
|
||||
if (CFG.checks.cssHygiene) {
|
||||
for (const p of CFG.pages) {
|
||||
const files = cssFilesOf(p);
|
||||
const files = contractFilesOf(p, "hygieneFiles", cssFilesOf(p));
|
||||
if (!files.length) { skip(`${p.name} 토큰 위생`, "로컬 CSS 없음"); continue; }
|
||||
// 토큰 파일 = 파일명에 tokens 가 있는 것. 이 안의 값이 프로젝트의 단일 원본이다
|
||||
const tokenFiles = files.filter((f) => /tokens/i.test(path.basename(f)));
|
||||
const others = files.filter((f) => !tokenFiles.includes(f));
|
||||
const hexes = []; const radii = new Set();
|
||||
for (const f of others) {
|
||||
const inferredTokens = cssFilesOf(p).filter((f) => /tokens/i.test(path.basename(f)));
|
||||
const tokenFiles = contractFilesOf(p, "tokenFiles", inferredTokens);
|
||||
const missing = missingFiles([...files, ...tokenFiles]);
|
||||
if (missing.length) {
|
||||
fail(`${p.name} 토큰 위생 계약`, `없는 파일 ${missing.map((file) => path.relative(process.cwd(), file)).join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
const tokenNames = new Set();
|
||||
for (const f of new Set([...tokenFiles, ...files])) {
|
||||
const src = fs.readFileSync(f, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
for (const m of src.matchAll(/#[0-9a-fA-F]{3,8}\b/g)) hexes.push(`${path.basename(f)}:${m[0]}`);
|
||||
for (const m of src.matchAll(/(?<![\w.-])rgba?\(/g)) hexes.push(`${path.basename(f)}:${m[0]}`);
|
||||
for (const m of src.matchAll(/(--[A-Za-z0-9_-]+)\s*:/g)) tokenNames.add(m[1]);
|
||||
}
|
||||
const colors = []; const colorKinds = new Set(); const radii = new Set();
|
||||
for (const f of files) {
|
||||
// 어느 파일에 있든 custom property 선언은 지역 토큰이다. 선언값을 다시
|
||||
// 하드코딩으로 세면 :root 토큰을 만든 페이지가 오히려 실패한다.
|
||||
const src = fs.readFileSync(f, "utf8")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/--[A-Za-z0-9_-]+\s*:\s*([^;{}]+);/g, "");
|
||||
for (const m of src.matchAll(/#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|lab|lch)\([^)]*\)/g)) {
|
||||
colors.push(`${path.basename(f)}:${m[0]}`);
|
||||
colorKinds.add(m[0].toLowerCase().replace(/\s+/g, " "));
|
||||
}
|
||||
for (const m of src.matchAll(/border-radius:\s*([^;]+);/g)) if (!m[1].includes("var(")) radii.add(m[1].trim());
|
||||
}
|
||||
const budgets = p.staticContract?.budgets || {};
|
||||
const hasColorBudget = Number.isFinite(budgets.colorLiterals);
|
||||
const hasRadiusBudget = Number.isFinite(budgets.radiusLiterals);
|
||||
const colorLimit = hasColorBudget ? budgets.colorLiterals : (tokenFiles.length ? 0 : null);
|
||||
const radiusLimit = hasRadiusBudget ? budgets.radiusLiterals : null;
|
||||
const bad = [];
|
||||
if (tokenFiles.length && hexes.length) bad.push(`토큰 밖 색 리터럴 ${hexes.length}건(토큰 파일이 있는데 상수색)`);
|
||||
if (radii.size >= CFG.thresholds.radiusLiterals) bad.push(`radius 리터럴 ${radii.size}종([${[...radii].join(", ")}])`);
|
||||
if (colorLimit !== null && colors.length > colorLimit) bad.push(`색 리터럴 ${colors.length}건 > 계약 ${colorLimit}건`);
|
||||
if (radiusLimit !== null ? radii.size > radiusLimit : radii.size >= CFG.thresholds.radiusLiterals) {
|
||||
bad.push(`radius 리터럴 ${radii.size}종 > 계약 ${radiusLimit ?? `<${CFG.thresholds.radiusLiterals}`}종([${[...radii].join(", ")}])`);
|
||||
}
|
||||
bad.length
|
||||
? fail(`${p.name} 토큰 위생`, bad.join(" · ") + (hexes.length ? " — 예: " + hexes.slice(0, 4).join(" ") : ""))
|
||||
: pass(`${p.name} 토큰 위생`, `색 ${hexes.length}·radius ${radii.size} 리터럴(토큰 ${tokenFiles.length}파일)`);
|
||||
? fail(`${p.name} 토큰 위생`, bad.join(" · ") + (colors.length ? " — 예: " + colors.slice(0, 4).join(" ") : ""))
|
||||
: pass(`${p.name} 토큰 위생`, `색 ${colors.length}${colorLimit === null ? "" : `/${colorLimit}`}건(${colorKinds.size}종)·radius ${radii.size}${radiusLimit === null ? "" : `/${radiusLimit}`}종·토큰 ${tokenFiles.length}파일/${tokenNames.size}개`);
|
||||
}
|
||||
} else skip("토큰 위생", "checks.cssHygiene=false");
|
||||
|
||||
|
|
@ -170,6 +227,12 @@ const UTILS = `
|
|||
for (const t of tops) if (!ls.length || t - ls[ls.length - 1] > 5) ls.push(t);
|
||||
return ls.length;
|
||||
};
|
||||
window.__textLines = (sel) => {
|
||||
const el = typeof sel === "string" ? document.querySelector(sel) : sel;
|
||||
if (!el) return -1;
|
||||
const text = [...el.childNodes].find((node) => node.nodeType === 3 && node.textContent.trim());
|
||||
return window.__lines(text || el);
|
||||
};
|
||||
`;
|
||||
|
||||
const browser = await puppeteer.default.launch({ executablePath: CHROME, headless: "new", args: ["--force-device-scale-factor=1"] });
|
||||
|
|
@ -234,7 +297,7 @@ for (const p of CFG.pages) {
|
|||
if (meta.themeColor.replace(/\s+/g, "").toLowerCase() !== hexOf(meta.bodyBg))
|
||||
m.push(`theme-color ${meta.themeColor} ≠ 실제 배경 ${meta.bodyBg}(낡은 토큰 의심)`);
|
||||
}
|
||||
if (meta.h1Count > 1) m.push(`h1 ${meta.h1Count}개(유일해야)`);
|
||||
if (meta.h1Count !== 1) m.push(`h1 ${meta.h1Count}개(정확히 하나여야)`);
|
||||
if (meta.imgNoAlt) m.push(`img alt 없음 ${meta.imgNoAlt}개`);
|
||||
if (meta.jsonldCount && !meta.jsonld) m.push("JSON-LD 파싱 실패");
|
||||
// 데모·로컬 페이지는 canonical/robots 미명시 허용 — 있으면 오히려 검증
|
||||
|
|
@ -263,10 +326,14 @@ for (const p of CFG.pages) {
|
|||
}
|
||||
overs.length ? fail(`${p.name}/${v} 오버플로`, overs.join(" ")) : pass(`${p.name}/${v} 오버플로`, CFG.widths.join("/"));
|
||||
|
||||
// h1 1줄
|
||||
// h1 줄 수 — 앱 화면은 기본 1줄, 마케팅 헤드라인은 페이지별로 최대 2줄까지 명시할 수 있다.
|
||||
await page.setViewport({ width: 1440, height: 900 });
|
||||
const h1l = await page.evaluate(`__lines(${JSON.stringify(p.h1 || "h1")})`);
|
||||
h1l === 1 ? pass(`${p.name}/${v} h1 1줄`) : (h1l > 1 ? fail(`${p.name}/${v} h1 ${h1l}줄`) : skip(`${p.name}/${v} h1`, "요소 없음"));
|
||||
const h1MaxLines = Number.isInteger(p.h1MaxLines) && p.h1MaxLines > 0 ? p.h1MaxLines : 1;
|
||||
const h1Rule = h1MaxLines === 1 ? "h1 1줄" : `h1 최대 ${h1MaxLines}줄`;
|
||||
h1l >= 1 && h1l <= h1MaxLines
|
||||
? pass(`${p.name}/${v} ${h1Rule}`)
|
||||
: (h1l > h1MaxLines ? fail(`${p.name}/${v} h1 ${h1l}줄(최대 ${h1MaxLines}줄)`) : skip(`${p.name}/${v} h1`, "요소 없음"));
|
||||
|
||||
// 세로 쌓임(수축) — 짧은 라벨 3줄 이상
|
||||
if (CFG.checks.stack) {
|
||||
|
|
@ -366,6 +433,7 @@ for (const p of CFG.pages) {
|
|||
// ── 스케일×폭 h1 행렬 (안드로이드 글꼴 확대 재현) ──
|
||||
if (CFG.checks.scaleMatrix) {
|
||||
const bad = [];
|
||||
const h1MaxLines = Number.isInteger(p.h1MaxLines) && p.h1MaxLines > 0 ? p.h1MaxLines : 1;
|
||||
for (const w of [375, 390]) {
|
||||
for (const s of CFG.fontScales) {
|
||||
await page.setViewport({ width: w, height: 800, isMobile: true, hasTouch: true });
|
||||
|
|
@ -373,10 +441,10 @@ for (const p of CFG.pages) {
|
|||
await page.evaluate(UTILS);
|
||||
await page.addStyleTag({ content: `html { font-size: ${16 * s}px !important; }` });
|
||||
const l = await page.evaluate(`__lines(${JSON.stringify(p.h1 || "h1")})`);
|
||||
if (l > 1) bad.push(`${w}@${s}x:${l}줄`);
|
||||
if (l > h1MaxLines) bad.push(`${w}@${s}x:${l}줄(최대 ${h1MaxLines}줄)`);
|
||||
}
|
||||
}
|
||||
bad.length ? fail(`${p.name} 스케일×폭 h1`, bad.join(",")) : pass(`${p.name} 스케일×폭 h1`, `scales ${CFG.fontScales.join("/")}`);
|
||||
bad.length ? fail(`${p.name} 스케일×폭 h1`, bad.join(",")) : pass(`${p.name} 스케일×폭 h1`, `최대 ${h1MaxLines}줄 · scales ${CFG.fontScales.join("/")}`);
|
||||
// 브랜드 1줄
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
|
||||
await page.goto(href, { waitUntil: "networkidle0" });
|
||||
|
|
@ -386,7 +454,7 @@ for (const p of CFG.pages) {
|
|||
const bl = await page.evaluate((sel) => {
|
||||
const els = [...document.querySelectorAll(sel)];
|
||||
const el = els.find((e) => e.offsetParent !== null || getComputedStyle(e).position === "fixed") || els[0];
|
||||
return window.__lines(el);
|
||||
return window.__textLines(el);
|
||||
}, p.brand);
|
||||
bl === 1 ? pass(`${p.name} 브랜드 1줄`) : fail(`${p.name} 브랜드 ${bl}줄(수축 의심)`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue