Project_WL/Assets/WL/UI/UI_UnitScale_SafeArea_Note.md

122 lines
11 KiB
Markdown
Raw Normal View History

# WL UI 기준 노트 — CanvasScaler 유닛 스케일 · Safe Area 공통 적용
> WL-802b (#802 재단 2차) · 2026-09-08 · 브랜치 `wl/ui/WL-802b-safearea-scale` (base `main` @ `abcd03865`)
> 대상 = 세로 1080×1920 모바일(#782 모바일 전제 · #803 세로 전용). **후속 UI 작업은 이 문서의 계수·규칙을 기준으로 한다.**
> 줄 번호는 **이 브랜치(WL-802b 적용 후)** 파일 기준. 실측 재현 = `unity command run_script --file AgentScripts/WL802b_Probe.cs --entry WL802b_Probe.Dump`
---
## 1. 세 프리팹의 CanvasScaler (실측)
| 프리팹 | 참조 해상도 | UI Scale Mode | Screen Match Mode | matchWidthOrHeight | 근거 |
|---|---|---|---|---|---|
| `Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab` | **1080 × 1920** | ScaleWithScreenSize(`m_UiScaleMode: 1`) | **Expand**(`m_ScreenMatchMode: 1`) | 0 (Expand 에서 미사용) | `TitleInfo.prefab:983-988` |
| `Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab` | **1920 × 1080** | ScaleWithScreenSize | Expand | 0 | `SortOrder_5.prefab:9173-9178` |
| `Assets/Res_Addr/MainUI/NewGameUI.prefab` | **1920 × 1080** | ScaleWithScreenSize | Expand | 0 | `NewGameUI.prefab:131617-131622` |
- 세 프리팹 모두 캔버스가 **프리팹 루트**다(`m_Father: {fileID: 0}`) · `m_ReferencePixelsPerUnit: 100` 동일.
- ⚠️ **matchWidthOrHeight 는 Expand 모드에서 아무 효과가 없다.** `m_MatchWidthOrHeight: 0` 은 직렬화된 잔여값일 뿐, MatchWidthOrHeight 모드로 바꾸기 전에는 무의미하다. 「match 0 이라서 폭 기준」이라는 해석은 틀렸다 — 폭 기준이 되는 이유는 아래 Expand 산식 때문이다.
## 2. Expand 산식과 계수
```
scaleFactor = min(screenW / refW, screenH / refH) // ScreenMatchMode.Expand
실화면 px = 프리팹 유닛 × scaleFactor
```
세로(폭이 좁은) 화면에서는 두 참조 해상도 모두 **폭 항이 최솟값**이 되므로 `scaleFactor = screenW / refW` 이고,
따라서 두 계열의 비는 화면 해상도와 무관하게 **항상 1920 / 1080 = 1.7778 배**로 고정된다.
| 화면(QA 5종 · #802 §3) | ref 1080×1920 (`TitleInfo`) | ref 1920×1080 (`SortOrder_5`·`NewGameUI`) | 비 |
|---|---|---|---|
| 1080 × 1920 | **1.0** | **0.5625** | 1.7778 |
| 1080 × 2340 | 1.0 | 0.5625 | 1.7778 |
| 1080 × 2400 | 1.0 | 0.5625 | 1.7778 |
| 720 × 1600 | 0.66667 | 0.375 | 1.7778 |
| 1440 × 3200 | 1.33333 | 0.75 | 1.7778 |
- 세로 화면에서 높이는 남는다(Expand = 잘림 없음). 1080×2400 이면 1920 기준 캔버스의 높이 유닛은 2400/0.5625 = 4266.7 유닛까지 늘어난다 → **화면 상·하단에 여백이 생기므로 세로 전체를 채우는 배경은 stretch 앵커로 잡아야 한다**(고정 sizeDelta 금지).
## 3. 유닛 ↔ px 환산표 (세로 · 화면 폭 1080 기준)
| 실화면 px | ref 1080 프리팹의 유닛 | ref 1920 프리팹의 유닛 (= px ÷ 0.5625 = px × 1.7778) |
|---|---|---|
| 1 px | 1 | **1.7778** |
| 44 px (최소 터치 하한 참고) | 44 | 78.2 |
| **88 px** | 88 | **156.4** |
| 130 px | 130 | 231.1 |
| 340 px | 340 | 604.4 |
- 검증 예 (#806 후속 B · `SortOrder_5.prefab``PopupUI/child` 실측):
| 오브젝트 | 유닛(1920 기준, 직렬화 값) | ÷ 1.7778 | 실화면 px (1080 폭) |
|---|---|---|---|
| `bg` | 1529 × 924 | | 860 × 520 |
| `msg` | 1387 × 498 @ (0, 124) | | 780 × 280 @ (0, 70) |
| `btn_ok` | 604 × 231 @ (0, 267) | | 340 × 130 @ (0, 150) |
| 폰트 | 48 | | 27 |
340 ÷ 0.5625 = 604.4, 130 ÷ 0.5625 = 231.1 → 반올림 일치. 산출 근거는 `AgentScripts/WL806_Popup.cs:18-20`.
- **작성 규칙**: `TitleInfo`(1080 기준)에 넣는 값은 세로 1080 화면에서 px 와 1:1 이다. `SortOrder_5`·`NewGameUI`(1920 기준)에 같은 실크기를 내려면 **반드시 ×1.7778** 한다. 두 계열의 수치를 그대로 복사하면 1.78배 어긋난다.
## 4. 인계서 §1 표기 불일치 (문서 오기)
인계서 §1 UI 행은 `NewGameUI.prefab`**1080×1920** 으로 적고 있으나, 실제 직렬화 값은 **1920×1080** 이다(`NewGameUI.prefab:131620` `m_ReferenceResolution: {x: 1920, y: 1080}`).
→ 인계서 쪽이 오기로 **「추정」**. 이 노트의 실측값을 SOT 로 쓴다. 인계서 수정은 Lead 판단(조직 레포는 워커가 커밋하지 않는다).
## 5. Safe Area 적용 규칙
### 컴포넌트
`WL.UI.SafeAreaFitter` (`Assets/WL/UI/Scripts/SafeAreaFitter.cs` · guid `4f4cdcc41055ee140ae3a9916e8563ce`)
`Screen.safeArea` 를 화면 크기로 나눠 **anchorMin/anchorMax 에 넣고 offsetMin/offsetMax 를 0 으로 덮어쓴다**(`SafeAreaFitter.cs:40-48`).
### 반드시 지킬 것
1. **전용 루트 패널에만 붙인다.** 캔버스 직속에 Graphic 없는 빈 패널(`WL_SafeArea`)을 두고 거기에만 부착한다. 안전 영역을 따라야 하는 UI 는 이 패널의 자식으로 넣는다.
2. **이미 스트레치 + offset 을 쓰는 패널에는 붙이지 않는다.** `Apply()` 가 offsetMin/Max 를 무조건 0 으로 덮어쓰므로, 여백(offset)으로 레이아웃을 잡아둔 패널에 붙이면 그 여백이 소실된다. 같은 이유로 `LayoutGroup`/`ContentSizeFitter` 가 자기 RectTransform 을 제어하는 오브젝트에도 붙이지 않는다.
3. **프리팹당 1개.** `[DisallowMultipleComponent]` 는 같은 오브젝트의 중복만 막는다. 중첩 부착(부모·자식 양쪽)은 막지 못하고 안전 영역이 이중으로 축소되니 금지.
4. **성능**: `Update()` 가 매 프레임 `Screen.safeArea` 를 폴링한다(`SafeAreaFitter.cs:25-29`). 값이 바뀔 때만 `Apply()` 하므로 비용은 비교 3회뿐이지만, 모바일 예산상 **부착 수는 화면당 1개로 유지**한다.
5. **미적용 기기 회귀 0**: `Screen.safeArea` 가 전체 화면이면 min=(0,0)·max=(1,1)·offset 0 → 프리팹 저장값과 동일해 아무 변화가 없다(WL-802b ⓒ).
### 현재 부착 현황 (WL-802b 적용 후)
| 프리팹 | 경로 | 캔버스 직속 | 비고 |
|---|---|---|---|
| `TitleInfo.prefab` | `TitleInfo/WL_SafeArea` | O | WL-802b 신규 · 빈 패널(자식 0 · Graphic 없음) · `TitleInfo.prefab:833-882` |
| `SortOrder_5.prefab` | `SortOrder_5/WL_SafeArea` | O | WL-802b 신규 · 빈 패널(자식 0 · Graphic 없음) · `SortOrder_5.prefab:12908-12957` |
| `NewGameUI.prefab` | `NewGameUI/IngameUIs/WL_HUD` | **X** | 기존(#800 계열) · 이번 태스크 무수정 · Fitter 블록 `NewGameUI.prefab:107746-107759` |
`WL802b_Probe.Dump` 실측(2026-09-08): 세 파일 모두 `SafeAreaFitter count = 1` · `applyH=True applyV=True` · 부착 RectTransform `aMin=(0,0) aMax=(1,1) offMin=(0,0) offMax=(0,0) pivot=(0.5,0.5)`.
- 신규 패널은 **비어 있다**. 기존 자식을 재부모화하면 레이아웃 회귀(WL-802b ⓓ)가 나므로 이동시키지 않았다. 이 패널은 **후속 UI 가 들어갈 안전 영역 레이어**이고, 기존 자식을 옮기는 작업은 별도 발주(레이아웃 변경 승인 필요)로 분리한다.
- `NewGameUI` 의 기존 부착은 캔버스 직속이 아니라 `IngameUIs` 아래에 있다(`NewGameUI.prefab:107739` `m_Father: {fileID: 2647977513349308266}` = `IngameUIs`). `IngameUIs` 는 프리팹 저장 상태에서 **비활성**(`m_IsActive: 0`)이므로, 런타임에 활성화되기 전에는 Fitter 도 돌지 않는다 → **「미확인」**(런타임 활성화 시점 미검증). 파일당 1개(중복 0) 기준을 지키려 이번에는 옮기거나 추가하지 않았다.
### ⚠️ 남은 공백 — `NewGameUI` 최상위 레이어 (후속 발주 후보)
`NewGameUI` 캔버스 직속 자식은 12개다(실측 · 저장 상태 활성/비활성 표기):
`New Image`(비활성) · `Camera` · `CamRot` · `Common` · `LobbyUIs` · **`IngameUIs`(비활성)** · `FarmUIs`(비활성) · `MessageInfo` · `Center` · **`PopupMgr`** · `TestMapUIMgr`
기존 `SafeAreaFitter``IngameUIs/WL_HUD` **한 갈래만** 덮는다. 즉 **`PopupMgr`·`MessageInfo`·`Common`·`LobbyUIs`·`Center` 는 안전 영역 밖에 그대로 노출된다.**
WL-802b 는 완료 기준 ⓑ(파일당 1 · 중복 0)와 ⓓ(기존 오브젝트 무변경) 때문에 이 프리팹을 수정하지 않았다.
**후속 발주 제안**: 이 5개 레이어를 덮는 방식은 (가) 각 레이어를 새 `WL_SafeArea` 패널의 자식으로 재부모화(레이아웃 승인 필요) 또는 (나) 레이어별 부착(부착 수 증가 = 매 프레임 폴링 증가) 중 **PD·Lead 결정 사항**이다. 이 노트만으로 임의 채택하지 않는다.
## 6. NGUI 잔재 3종 — 실측과 대체 계획 (W2 Systems 후보 · 이번 태스크 무수정)
`Assets/Script/Util/**` 는 Systems 소유이자 WL-802b 금지 경로다. 아래는 **읽기 전용 실측**이다.
| 파일 | 부착 수(프리팹·씬 grep) | 세로 전용(#803)에서의 실동작 | 대체 |
|---|---|---|---|
| `Assets/Script/Util/SafeArea.cs` | **0** | `switch(Screen.orientation)``LandscapeLeft`/`LandscapeRight` 만 처리(`:20-37`) → 세로에서는 **어떤 분기도 타지 않음**. 그런데 `while(true) { … yield return null; }`(`:18-38`) 은 계속 돈다 = **효과 0 · 매 프레임 코루틴 비용만 남음** | 삭제 (부착 0 → 무위험) |
| `Assets/Script/Util/UISafeAreaTopController.cs` | **0** | NGUI `UIRoot.manualHeight` 의존. `GetComponentInParent<UIRoot>()` 가 null 이면 조기 return(`:30-31`) → uGUI 캔버스 아래에서는 no-op | 삭제 |
| `Assets/Script/Util/UISafeAreaRightController.cs` | **1**`Assets/ResWork/UIPrefabs/Equip/EquipUI.prefab:52-64` (루트 `EquipUI` · `BasePosisLeft: 1` · `xMul: 1`) | `MyCoroutine.Set_Repeat_for1sec`**1초마다** 실행(`:17`). `m_cUIRoot` 를 받아만 두고 쓰지 않아 null 이어도 조기 return 이 없다. `localPosition.x`**`Screen.safeArea.y`**(= 세로 상단 인셋)만큼 민다(`:28-30`) — **가로 화면 전제의 축 오용**. `EquipUI.prefab` 루트는 `RectTransform`(`!u!224`)이 아니라 `Transform`(`!u!4`) = NGUI 시절 프리팹 | uGUI 재작성 후 `SafeAreaFitter` 로 교체 |
- 세로 전용 전환 후 이 3종은 **동작하지 않거나(0·0건) 축이 틀린 상태(1건)**다. 세로에서 노치 대응은 전부 `WL.UI.SafeAreaFitter` 로 일원화한다.
- **W2 Systems 제안**: ① `SafeArea.cs`·`UISafeAreaTopController.cs` 삭제(부착 0 실측) ② `EquipUI.prefab` 을 uGUI 로 재작성할 때 `UISafeAreaRightController` 를 제거하고 상위 `WL_SafeArea` 패널의 자식으로 넣는다 ③ NGUI `UIRoot` 를 쓰는 실사용 프리팹은 `BoxOpenUI`·`GetItemUI`·`ItemDescUI` 3종(나머지 23건은 `Assets/ThirdParty/NGUI/Examples/**`) — 재작성 범위 산정에 사용.
## 7. 미확인 · 리스크
- **「미확인」** `NewGameUI``IngameUIs` 가 런타임에 언제 활성화되는지 · 팝업/오버레이 레이어가 `WL_HUD` 밖에 따로 있는지(5.3 MB 프리팹 통째 Read 금지 · 부분 실측만 수행).
- **「미확인」** `Title.unity` 안 PopupUI 인스턴스와 `SortOrder_5.prefab` 의 관계(씬 수정·Play 금지 범위).
- **「미확인」** 실기 노치 인셋 값. QA 5종 캡처(#802 §3)는 시뮬레이터 프리셋이며 실제 기기 `Screen.safeArea`**「추정」**.
- **「미확인」** 세로 전용 자동회전이 실기 빌드에서 적용되는지(빌드 미수행).