# Ditto 위젯 반응형 규격 · 1.0 제안

2026-09-28. 기존 시안의 그림과 스타일을 유지하면서, 크기에 따라 내용 배치를 바꾸는 규격이다. **신규 크기 목록은 디자인 제안이며 앱 출시 승인/구현 완료가 아니다.**

## 적용 범위와 상태

- 정본: `design/tokens/widget-responsive.json`.
- 확인 화면: `design/site/ditto-design-system.html#widget-responsive`.
- 계산 참고 구현: `design/site/widget-responsive.js`. Swift 런타임에 연결하지 않았다.
- Figma 원본: [UI_Final / widget / 1:36807](https://www.figma.com/design/XG8Cm5nLScPoNc9NAXX1DD/Ditto?node-id=1-36807). 원본 삭제·이동·수정 없음.
- Figma 음악: `1:36928`. 기존 6개 예시(정사각 LP·커버와 가로·세로 4형태)를 대조했다.
- 기존 Assets 페이지의 WidgetGrid 변수에는 390/402pt 기준이 이미 존재한다. 이번 작업으로 중복 생성하거나 변경하지 않았다.
- Figma 편집 도구에서 Pretendard 및 Pretendard Variable의 로딩 실패를 확인했다. **새 편집 가능한 텍스트/반응형 Figma 컴포넌트는 미반영.** 대체 폰트나 윤곽선으로 조용히 대체하지 않는다.
- Swift 앱·서버·AI 모듈 레지스트리는 변경하지 않았다. 기존 작업 중인 파일을 보존했다.

## 1. 원본과 현재 구현의 차이

| 대상 | 확인한 현재 상태 | 이번 규격 |
| --- | --- | --- |
| Figma 음악 | 정사각 140×140, 가로 214×140, 세로 140×212, 배너 354×80.45, 좁은 세로 80×354 | 원형 참고로 보존. 정해진 배율로 통째로 확대하지 않음 |
| Swift 그리드 | 8열×16행, 좌우16, 간격8, 화면폭320~620 | 기존 계산 유지 |
| 크기 명칭 | `3:3`→4×4, `1:6`→6×2, `6:1`→2×6 | 사용자에게 정사각/가로/세로 + 실제 가로칸×세로칸을 표시 |
| 음악 크기 분기 | 저장된 `w.w/w.h`로 배치를 정함 | 리사이즈 중 실제 전달된 너비·높이로 내용 배치 결정 |
| 음악 가로형 | 커버를 `H-24`로 잡아 390pt의 4×3에서 텍스트 폭 약36.75pt | 곡명 폭을 먼저 확보하고 이미지 크기 제한 |
| 음악 세로형 | 커버를 `W-24`로 잡아 제목+아티스트 영역이 부족 | 텍스트 높이를 뺀 나머지에 이미지 배치 |
| 위젯 크기 제한 | 추가 화면의 고정/비율 제한과 홈 리사이즈가 분리 | 추가·편집·드래그가 같은 위젯별 허용 목록을 소비 |

근거: `GridEngine.swift`, `WidgetFrame.swift`, `WidgetContent.swift`, `HomeTab.swift`, `AppState.swift`의 현재 작업 트리. 코드에 분기문이 있다는 사실은 모든 크기에서 레이아웃이 검증됐다는 뜻이 아니다.

## 2. 크기 계산

단위는 논리 pt다. iOS @2x/@3x 이미지 해상도와 구분한다.

```text
V = 플래터가 들어가는 컨테이너 폭 (앱 화면 기준, 좌우16 포함)
cell = (V - 2×16 - 7×8) / 8
pitch = cell + 8
widgetWidth  = columns × pitch - 8
widgetHeight = rows × pitch - 8
radius = min(20, 12 + (min(columns, rows) - 2) × 2)
```

| 칸 | 320pt 화면 | 375pt 화면 | 390pt 화면 | 402pt 화면 | 430pt 화면 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 2 | 66 | 79.75 | 83.5 | 86.5 | 93.5 |
| 3 | 103 | 123.625 | 129.25 | 133.75 | 144.25 |
| 4 | 140 | 167.5 | 175 | 181 | 195 |
| 6 | 214 | 255.25 | 266.5 | 275.5 | 296.5 |
| 8 | 288 | 343 | 358 | 370 | 398 |

소수점 값을 저장 단계에서 정수로 반올림하지 않는다. 칸 수가 저장 단위다. 디바이스 픽셀 정렬은 마지막 렌더 단계에서 수행하고, 공유 경계 좌표를 같은 식으로 계산한다.

그리드 전체 가로8칸과 일반 위젯 최대 가로6칸을 혼동하지 않는다. 전체 배치 상한6×8은 안전 경계이지 모든 위젯이 반드시6×8을 지원한다는 의미가 아니다. 큰 화면은 현재620pt 상한을 유지한 가운데 정렬 플래터가 기본 제안이다. iPad 전용 다열 편집은 별도 설계 대상이다.

## 3. 크기 수와 레이아웃 수를 구분

기기마다 새 위젯 그림을 만드는 방식이 아니다.

- 일반 위젯: 2~3개의 구조적 배치 규칙.
- 사진/영상: 원본의 5개 형태를 유지하는 최대5개 규칙.
- 음악: 가로 / 정사각 / 세로 / 좁은 세로 **4개 배치 규칙**, 11개 허용 크기 제안.
- LP/커버는 별도 표현 속성이다. 화면폭과 상태를 곱해 수십 개의 독립 마스터를 만들지 않는다.
- 음악 웹 예시는 커버만 사용한다. LP 원형은 원본 Figma에서 보존되며 새 LP 반응형 구현은 후속이다.

전체32종(코드30종+신규기획2종)의 개별 크기 목록, 기본 크기, 예상 배치 규칙 수는 정본JSON과 웹 표에 있다. `AppState.sizeMap`에 없는 폴라로이드·Ditto 액션의 기본 크기와 신규2종은 제안으로 표시한다.

**최소·최대는 하나의 직사각형 범위가 아니라 허용 목록의 경계다.** 음악의4×2와3×3은 각각 다른 최소 선택지다. ASK의4×6과6×4도 서로 다른 최대 선택지다. 그 사이 크기를 무조건 허용하면 동일한 문제가 반복된다.

현재 고정형의 확장안도 규격 제안에 포함하지만, 실제 추가 메뉴에서 해제하는 시점은 해당 타입의 렌더러와 경계 테스트가 통과한 뒤다. 신규 빈티지 카메라·같이 있었어는 앱 타입이 없으므로 기능 완료로 세지 않는다.

## 4. 음악 위젯의 실제 재배치

### 공통

- Pretendard Semibold14 / 제목 행높이18, Regular 또는 기존 Medium11 / 아티스트 행높이14. 원본의14/11 계층 유지.
- 짧은 변120pt 미만은 여백8, 그 외12. 내부 간격8, 제목-아티스트 간격4.
- 제목은 자동 축소하지 않는다. 기본2줄, 좁은 세로형 최대4줄. 큰 글자와 낮은 높이는1줄로 줄이고 전체 문구는 상세와 접근성 이름에 유지한다.
- 제공자 배지는 실제 데이터에 맞게 분리해야 한다. 모든 음악에 YouTube 배지를 하드코딩하지 않는다. 웹 배치 예시는 제공자 배지를 생략했으며 최종 에셋이라고 표시하지 않는다.
- 카드 전체가 상세 진입점. 재생·다음 곡 등 작은 터치 버튼을 이 안에 임의로 추가하지 않는다.

### 가로

```text
innerW = W - padding×2
innerH = H - padding×2
art = min(112, innerH, floor((innerW-8)×0.44), innerW-8-72)
textW = innerW - art - 8
```

최소 이미지44pt를 확보하지 못하면 이미지를 감추고 제목을 우선한다. 승인된 일반 크기/기본 글자에서는 이미지가 보인다. 390pt의4×3은 커버62pt/텍스트81pt로 재배치된다. 기존 커버105.25pt/텍스트36.75pt보다 내용 폭을 먼저 보장한다.

### 세로

```text
metadataH = titleLines × titleLineHeight + 4 + artistLineHeight
art = min(innerW, 240, innerH - metadataH - 8)
```

큰 글자에서 아티스트가 들어가지 않을 때는 아티스트를 상세로 넘기고 제목을 유지한다. 실제 Dynamic Type 카테고리별 승인은 네이티브 테스트 후 한다. 웹의1.35/1.7배는 스트레스 테스트이지 iOS 카테고리의 정확한 매핑이 아니다.

### 정사각 / 좁은 세로

정사각은 커버·LP 위에 원본의 상단 가독성 그라디언트와 텍스트를 겹친다. 제목은 별도로 재배치하며 워드마크처럼 통째로 스케일하지 않는다. 좁은 세로는 상단 커버와 하단 곡 정보를 분리하고 기존 퍼플 계열을 유지한다.

배치 분기는 저장된 칸 수가 아니라 **실제 콘텐츠 영역** 기준이다. `H/W≥2`: 좁은 세로, `W/H>1.15`: 가로, `W/H<0.87`: 세로, 그 사이 정사각. 이 경계는 제안값이다. 자유로운 중간 크기의 저장을 열지 말고 허용 크기로 스냅한다. 추후 연속 리사이즈를 열 때는 경계 흔들림 방지(히스테리시스)를 별도로 검증한다.

## 5. 출처가 있는 위젯

기존 원칙 **프로필 + Ditto 로고 + 사용자가 부르는 애칭에게서 가져옴**을 모두 유지한다.

- 푸터44pt, 큰 글자 비교 예시48pt를 전체 높이 안에서 먼저 예약한다.
- 콘텐츠 높이 = 위젯 전체 높이 − 출처 높이.
- 최소 가로3칸이면서 실제 폭128pt 이상, 최소 세로3칸이면서 콘텐츠 높이96pt 이상이어야 한다.
- 320pt에서는3칸이103pt밖에 안 되므로3칸 출처형은 제공하지 않는다. 사용자가4칸 이상의 대안을 선택하도록 안내한다.
- 이미 배치된 작은 위젯을 몰래 키우지 않는다. 출처를 지우거나 다른 위젯 위에 겹치게 하지 않는다.
- 매우 긴 애칭은 이름만 말줄임하고 ‘에게서 가져옴’은 유지한다. 원래 이름 전체는 접근성/상세에 제공한다.
- 큰 글자에서48pt로도 부족하면 실제 측정 높이를 예약하거나 더 큰 크기를 요청한다.48pt를 무조건적인 상한으로 사용하지 않는다.

## 6. Swift 구현 계약 · 아직 미적용

1. `WidgetSizePolicy` 한 곳에서 타입별 허용 칸 조합을 반환한다. 추가 시트와 리사이즈 핸들이 같은 정책을 사용한다.
2. `GeometryReader` 등으로 전달받은 실제 `CGSize`를 렌더러에 주입한다. 배경·전경·출처도 같은 크기 컨텍스트를 사용한다.
3. 리사이즈 중 `resizeLive`의 크기로 `musicLayout`에 해당하는 순수 계산을 실행한다. 저장 모델은 드래그 확정 시에만 갱신한다.
4. 실제 텍스트를 Pretendard로 측정하고 제목/아티스트의 높이를 예약한다. 프로토타입의 고정 줄높이 계산보다 네이티브 측정을 우선한다.
5. 커버는 제한된 정사각 프레임에서 crop하고 제목은 남은 폭을 사용한다. `scaleEffect`로 전체 위젯을 줄이는 것은 목록 썸네일에만 한정한다.
6. 같은 위젯 ID를 유지해 이미지 재요청·플레이어 초기화 없이 레이아웃만 바꾼다. LP 토글의 적용 범위는 원본의 정사각 변형과 대조해 확정한다.
7. 고정형/미지원 크기는 핸들을 숨기고 선택 메뉴에도 노출하지 않는다. 기존 데이터는 그대로 읽되 변경 시 합법적인 크기만 제안한다.
8. 충돌/영역 초과/출처 높이 부족 시 확정을 차단하고 원래 크기와 위치로 복구한다. 다른 위젯의 콘텐츠를 바꾸지 않는다.
9. Floating은 외부 래퍼, 마스크는 내부 콘텐츠에만 적용한다. 그림자·집기 확대는 그리드 점유 면적에 포함하지 않는다.

상태는 ready / empty / loading / error / restricted / editing으로 분리한다. 크기×상태마다 서버 데이터를 복사하지 않는다. 상태가 바뀌어도 외곽 슬롯을 유지한다. 제한 콘텐츠는 이미지 요청/렌더 자체를 막는다.

## 7. Figma 후속 구성

Pretendard를 도구에서 로딩할 수 있게 된 뒤 진행한다. 윤곽선 비교판을 만드는 경우 텍스트 편집·자동 줄바꿈 불가를 명시하고 별도 동의를 받는다.

- 기존 `Assets_1.0_재사용`에 `Widget / Responsive / 1.0` 섹션을 추가. 원본 `UI_Final/widget`은 보존.
- 기존 WidgetGrid 변수와390/402 모드를 재사용.320/375/430은 필요한 검수 모드로 추가 제안. 변수 모드가 화면폭에 따라 자동 전환되는 것은 아니다.
- `WidgetShell` = 크기/공용 여백/반경 + 콘텐츠 인스턴스 + 선택적 출처 + 외부Floating.
- `MusicContent` = 가로/정사각/세로/좁은 세로 배치, `Artwork` LP/커버 표현, 제목/아티스트 편집 가능한 TEXT 속성.
- 음악4개 배치 규칙과 원본5형태의 대응, 원형/개선형 비교,320/390/402/430 검수 인스턴스.
- 허용 크기에서 수동 Variant 변경 없는 내부 Auto Layout 리사이즈를 검사. 구조적 배치 간 전환은 Figma variant 속성/프로토타입과 앱 로직이 담당한다. Figma가 임의 폭에서 조건부 구조를 자동 선택한다고 문서화하지 않는다.
- 그 외 위젯은32종 매트릭스의 우선순위를 따라 실제 상태별 에셋을 채운 뒤 완료 처리.

## 8. 승인 전 체크리스트

- [ ] Pretendard를 로딩하는 Figma 편집 환경 확보 및 편집 가능한 컴포넌트 반영.
- [ ] 각 위젯 허용 크기 승인. 기존 고정형의 확장은 타입별 검수 뒤 해제.
- [ ] Swift 추가/편집/리사이즈를 한 크기 정책으로 통합.
- [ ] 음악 LP·커버·실제 제공자·긴 제목·미디어 실패·큰 글자 네이티브 검수.
- [ ] ASK: 기존 ASK 열기/없으면 만들기, 친구 측 질문하기, 꾸미기의 상태별 원형 유지.
- [ ] 파파라치/빈티지: 내 촬영 진입과 친구의 앨범 열람을 구분. 빈티지·같이 있었어는 앱 타입/데이터 계약부터 필요.
- [ ] 방명록·플레이리스트: 높이로 표시 행 수를 결정하고 상세에서 이어 보기.
- [ ] 출처 최소 크기/긴 애칭/이름 없음/친구가 부르는 이름/큰 글자 전 상태 검수.
- [ ] 실기기 세로·가로·큰 글자·VoiceOver·움직임 줄이기·다크 배경 확인.

웹 자동 검증 결과는 `output/ditto-widget-responsive-2026-09-28/`에 기록한다. 웹 계산과 DOM 경계 검사 통과는 Figma/Swift 앱 적용 완료를 뜻하지 않는다.

### 이번 검증 결과

- 정본JSON/웹 스냅샷 일치,32종165개 허용 크기 정의, 기본 크기 포함 여부 통과.
- 음악308개 조합(6개 화면폭×3개 글자 배율×출처 유무의 합법적 크기)에 대해 계산과 실제DOM의 텍스트·이미지·출처 경계 검사 통과.
- Pretendard 로딩, 긴 한국어/공백 없는 제목, Tab/Space 조작,320/390/768 브라우저 폭에서 섹션 넘침 없음, 중복ID/페이지 오류 없음 확인.
- macOS 네이티브 select 팝업의 방향키 확정은 자동화하지 못했다. select의 값 변경 핸들러와 체크박스 키보드 조작은 확인했다.
- 기존 플래터 계산 테스트10개 통과(결정적2,000회 이동/리사이즈 포함).
- 기존 DS 브라우저 회귀 검사 통과: 버튼 누름·복원, 상태별 입력, 플래터 탭/이동/리사이즈/복구, 자동스크롤, 모바일 터치 스크롤, 동작 줄이기. 콘솔 오류 없음.
- 별도 `design/widget-kit/test.mjs`는18개 중17개 통과. 기존 첫 테스트가 버튼의 Figma 상태를 `pending`으로 기대하지만 현재 토큰은 `asset-synced`라 실패했다. 이번 작업은 해당 토큰·테스트를 변경하지 않았으며 이 불일치는 범위 밖으로 기록한다.
- 실기기 iOS/SwiftUI 및 새 Figma 컴포넌트 검수는 미실시.
