Merge branch 'wl/ui/WL-813c3-bossbar-visual'

This commit is contained in:
깃 관리자 2026-09-09 16:13:41 +09:00
commit 2b788eed3c
5 changed files with 667 additions and 2 deletions

View File

@ -0,0 +1,108 @@
# WL813c3 Probe 2026-09-09 16:07:48 (edit mode · Play False)
프리팹 전 = 5361935 B · 2026-09-08 17:05:53.269 UTC
── ① 프리팹에 저장된 그대로 (수리 전 · Initialize 호출 0)
BossHpBar 인스턴스 = 1벌
경로 = NewGameUI/IngameUIs/WL_HUD/WL_BossHpBar · 소속 캔버스 = NewGameUI(order=0 override=False enabled=True mode=ScreenSpaceCamera)
- WL_BossHpBar active=True/False parent=WL_HUD [CanvasGroup alpha=0.00] [BossHpBar]
- Bar active=True/False parent=WL_BossHpBar
- Bg active=True/False parent=Bar [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#0D0D0FC7 raycast=False] 메쉬폭비=1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
- Fill active=True/False parent=Bar [Image type=Filled method=Horizontal origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FC4247FF raycast=False] 메쉬폭비=1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
- Ticks active=True/False parent=Bar
- Tick_0 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Tick_1 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Name active=True/False parent=WL_BossHpBar [TMP "" fs=53.3]
[PASS] Bar/Fill Image 존재
[PASS] 🔴 D-26 원인 재현: 저장된 Fill 의 sprite = 없음 (BossHpBar.cs:499 가 AddComponent 만 했다)
[PASS] 저장된 Fill 의 type = Filled (후보 ③ 「type 이 Filled 가 아니다」는 반증됨)
[PASS] 바 노드 1벌뿐 (후보 ① 「2벌」 반증)
── ② 메쉬 실측 (수리 전) — fillAmount 를 Q7 D-26 그 값 0.368 로 두고 실제 정점을 만든다
fillAmount=0.368 → 메쉬폭비 = 1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
[PASS] 🔴 D-26 기계적 증거: sprite 가 없어 fillAmount 0.368 인데 메쉬는 폭비 1.000 = 만피 사각형
── ③ 수리 — BossHpBar.Initialize() (런타임 OnEnable 과 같은 경로)
Initialize → initialized subscribed=True · layout size=(1564.44, 117.33) unitsPerPx=1.7778 ticks=2 · fill type=Filled method=Horizontal origin=0 sprite=WL_BossBarFill_White amount=0.368 · 중복차단: 1벌 — 조치 없음 · 늦은구독: 살아 있는 보스 없음(스캔 0)
[PASS] 고침: Fill.sprite = WL_BossBarFill_White
[PASS] 고침: type=Filled · method=Horizontal · origin=Left 강제
── ④ 메쉬 실측 (수리 후) — 같은 fillAmount 0.368
fillAmount=0.368 → 메쉬폭비 = 0.368 (verts=4 x[-782.2,-206.5] rectW=1564.4)
[PASS] 화면 추종: 메쉬폭비 0.368 ≈ fillAmount 0.368 (오차 0.0000)
── ⑤ 가짜 HP 1800 → 869 (발주서 §1-2) — 화면 노드가 따라오는가
dispatched Spawned(isBoss=true) subscribers=1
스폰 직후 ratio=1.000 fillAmount=1.000
HP 869/1800 → LastRatio=0.483 · fillAmount=0.483 · 메쉬폭비=0.483 (verts=4 x[-782.2,-26.9] rectW=1564.4)
[PASS] 계기 LastRatio = 0.483
[PASS] fillAmount = 0.483
[PASS] 🔴 화면 노드 메쉬폭비 = 0.483 (발주서 §1-2 목표 0.483)
[PASS] 바가 보이는 상태(CanvasGroup alpha > 0)
눈금 2개 · Ticks 형제 순서 = 2 / Fill = 1
[PASS] 3칸 = fill 위에 얹힌 구분선 2개(Fill 뒤 형제) — 별도 세그먼트 이미지 아님
── ⑥ C8 롤백 — bossBarForceFilledFill = 0 → 813c2 상태(화면 만피)로 복귀
EnsureFillRenderable → bossBarForceFilledFill=false — 건너뜀(813c2 상태)
롤백 상태 fillAmount=0.483 → 메쉬폭비 = 1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
[PASS] 롤백 시 D-26 재현(메쉬폭비 1.000) = 스위치 하나로 되돌아간다(C8)
복원 → fill type=Filled method=Horizontal origin=0 sprite=WL_BossBarFill_White amount=0.483
── ⑦ 보스 HP 0 → 숨김 예약
HP 0 → ratio=0.000 · ZeroHpHideCount 0→1
[PASS] HP 0 → ratio 0
[PASS] HP 0 숨김 예약 1회 (bossBarHideDelaySec 1.2s 뒤)
[PASS] C8: bossBarHideOnZeroHp=0 → 예약 생략
── ⑧ 중복 노드 차단 — 같은 캔버스에 바를 하나 더 만들어 본다
중복 Initialize → initialized subscribed=True · layout size=(1564.44, 117.33) unitsPerPx=1.7778 ticks=2 · fill type=Filled method=Horizontal origin=0 sprite=WL_BossBarFill_White amount=1.000 · 중복차단: 2벌 발견 → 유지 WL_BossHpBar · 숨김 1벌 · 늦은구독: 살아 있는 보스 없음(스캔 0)
원본 BarInstanceCount=1 · 중복 숨김=True · 중복 Visible=False
[PASS] 중복 감지 2벌
[PASS] 중복 바는 숨김(alpha 0)
C8: bossBarDedupInstances=0 → 중복차단: bossBarDedupInstances=false — 생략
── ⑨ GC — 폴링 20,000 회 할당
GC 1162354688 → 1162354688 (Δ 0 B)
[PASS] 폴링 20,000회 GC 증가 0 B (Δ 0)
EnsureFillRenderable 20,000회: GC 1162354688 → 1167208448 (Δ 4853760 B · 문자열 반환 포함)
── ⑩ 수리 후 노드 전수 (HP 869/1800)
BossHpBar 인스턴스 = 2벌
경로 = NewGameUI/IngameUIs/WL_HUD/WL_BossHpBar · 소속 캔버스 = NewGameUI(order=0 override=False enabled=True mode=ScreenSpaceCamera)
- WL_BossHpBar active=True/False parent=WL_HUD [CanvasGroup alpha=1.00] [BossHpBar]
- Bar active=True/False parent=WL_BossHpBar
- Bg active=True/False parent=Bar [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#0D0D0FC7 raycast=False] 메쉬폭비=1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
- Fill active=True/False parent=Bar [Image type=Filled method=Horizontal origin=0 amount=0.483 sprite=WL_BossBarFill_White enabled=True color=#FC4247FF raycast=False] 메쉬폭비=0.483 (verts=4 x[-782.2,-26.9] rectW=1564.4)
- Ticks active=True/False parent=Bar
- Tick_0 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Tick_1 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Name active=False/False parent=WL_BossHpBar [TMP "" fs=53.3]
경로 = NewGameUI/IngameUIs/WL_HUD/WL_BossHpBar_DUP · 소속 캔버스 = NewGameUI(order=0 override=False enabled=True mode=ScreenSpaceCamera)
- WL_BossHpBar_DUP active=True/False parent=WL_HUD [CanvasGroup alpha=0.00] [BossHpBar]
- Bar active=True/False parent=WL_BossHpBar_DUP
- Bg active=True/False parent=Bar [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#0D0D0FC7 raycast=False] 메쉬폭비=1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
- Fill active=True/False parent=Bar [Image type=Filled method=Horizontal origin=0 amount=1.000 sprite=WL_BossBarFill_White enabled=True color=#FC4247FF raycast=False] 메쉬폭비=1.000 (verts=4 x[-782.2,782.2] rectW=1564.4)
- Ticks active=True/False parent=Bar
- Tick_0 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Tick_1 active=True/False parent=Ticks [Image type=Simple method=Radial360 origin=0 amount=1.000 sprite=🔴없음 enabled=True color=#FFFFFFD9 raycast=False] 메쉬폭비=1.000 (verts=4 x[-3.6,3.6] rectW=7.1)
- Name active=True/False parent=WL_BossHpBar_DUP [TMP "" fs=53.3]
[BossHpBar] subscribed=True visible=True ratio=0.483 hp=869/1800 seen(Spawned/Hit/Killed)=1/0/0 늦은구독=False(스캔 0 · BossArena.Boss=없음)
[813c2] 폴링 20006회 · MaxHP 보정 0회 · 아레나 재바인드 0회 · hp=869/1800 · BossArena.BossSpawned=False · 설정 poll=True@10Hz 아레나추종=True 사망숨김=True 재바인드조건=True
[813q2] 런 리셋 0회 · 런 보스 HP=48.3% (본적=True) · RunStarted 구독=1
CombatEvents subscribers Spawned=1 HitConfirmed=1 Killed=1 Enabled=True
root pos(0.00, -337.78) size(1564.44, 117.33) aMin(0.50, 1.00) aMax(0.50, 1.00)
bar size(0.00, 49.78) aMin(0.00, 0.00) aMax(1.00, 0.00) fill=0.483 ticks=2
[813c3] fill type=Filled method=Horizontal origin=0 amount=0.483 sprite=WL_BossBarFill_White enabled=True color=FC4247FF active=False · 바 1벌(이 인스턴스 숨김=False) · HP0숨김 1회 · 설정 force=True white=True spriteSO=없음 dedup=True hp0숨김=True
[813c3] canvas NewGameUI order=0 override=False enabled=True renderMode=ScreenSpaceCamera
Tick_0 at=0.700 w=7.1
Tick_1 at=0.400 w=7.1
설정 sizePx=(880.00, 28.00) topMarginPx=190 ticks=0.70,0.40
── ⑪ 뒷정리 — 프리팹 저장 0 · 구독 0
프리팹 후 = 5361935 B · 2026-09-08 17:05:53.269 UTC
[PASS] 프리팹 파일 무변동 = 저장 0
CombatEvents Spawned=0 HitConfirmed=0 Killed=0 · RunStarted=0
SO 원복 = force:True white:True bgTick:False dedup:True hp0:True
DebugFakeHp=-1 DebugFakeMaxHp=-1 DebugFallbackMaxHp=100
== 합계 PASS 21 / FAIL 0 ==

View File

@ -0,0 +1,376 @@
// WL-813c3 프로브 — Q7 결함 D-26 「계기는 맞고 화면은 틀리다」 실측(에디트 모드 · Play 0 · 로그인 0 · #813).
// unity command run_script --file AgentScripts/WL813c3_Probe.cs --entry WL813c3_Probe.RunAll
// 산출물: <worktree>/AgentScripts/WL813c3_PROBE.txt
//
// 🔴 프리팹 저장 0 — LoadPrefabContents 로 열고 finally 에서 UnloadPrefabContents. SaveAsPrefabAsset 호출 없음.
// 파일 크기·수정시각을 전후로 찍어 "저장 0" 을 증거로 남긴다.
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복(AssetDatabase.SaveAssets 0).
// 🔴 Gameplay 는 읽기만. Play 진입 0.
// 🔴 핵심 계측 = **fillAmount 가 아니라 실제로 생성되는 메쉬 폭**.
// uGUI Image.OnPopulateMesh(VertexHelper) 를 리플렉션으로 직접 불러 정점 x 범위를 잰다.
// sprite 가 없으면 Graphic.OnPopulateMesh(사각형 전체) 로 빠지므로 fillAmount 와 무관하게 폭 비율 1.000 이 나온다.
// = D-26 「화면 만피」 의 기계적 증거.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using WL.UI;
public static class WL813c3_Probe
{
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
const string kHudPath = "IngameUIs/WL_HUD";
const string kBarName = "WL_BossHpBar";
static StringBuilder _o;
static int _pass, _fail;
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
static void N(string s) { _o.AppendLine(" " + s); }
static bool Chk(bool ok, string what)
{
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
return ok;
}
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
static string OutPath(string n) { return Path.Combine(Path.Combine(Root, "AgentScripts"), n); }
// ── 메쉬 실측 ────────────────────────────────────────────────────────────
static MethodInfo _opm;
/// <summary>Image 가 실제로 만드는 정점의 x 폭 / rect 폭. sprite 없으면 언제나 1.000 이 나온다.</summary>
static float MeshWidthRatio(Image img, out int vertexCount, out string note)
{
vertexCount = 0; note = "";
if (img == null) { note = "Image 없음"; return -1f; }
if (_opm == null)
{
_opm = typeof(Image).GetMethod("OnPopulateMesh",
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[] { typeof(VertexHelper) }, null);
if (_opm == null)
_opm = typeof(Graphic).GetMethod("OnPopulateMesh",
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[] { typeof(VertexHelper) }, null);
}
if (_opm == null) { note = "OnPopulateMesh 리플렉션 실패"; return -1f; }
var vh = new VertexHelper();
try { _opm.Invoke(img, new object[] { vh }); }
catch (Exception e) { note = "invoke 실패: " + e.GetBaseException().Message; vh.Dispose(); return -1f; }
vertexCount = vh.currentVertCount;
if (vertexCount == 0) { vh.Dispose(); note = "정점 0"; return 0f; }
float minX = float.MaxValue, maxX = float.MinValue;
var v = new UIVertex();
for (int i = 0; i < vertexCount; i++)
{
vh.PopulateUIVertex(ref v, i);
if (v.position.x < minX) minX = v.position.x;
if (v.position.x > maxX) maxX = v.position.x;
}
vh.Dispose();
float rectW = img.rectTransform.rect.width;
if (rectW <= 0f) { note = "rect 폭 0"; return -1f; }
note = "verts=" + vertexCount + " x[" + minX.ToString("F1") + "," + maxX.ToString("F1") + "] rectW=" + rectW.ToString("F1");
return (maxX - minX) / rectW;
}
// ── 노드 전수 덤프 ───────────────────────────────────────────────────────
static void DumpTree(Transform t, int depth)
{
var sb = new StringBuilder();
sb.Append(new string(' ', 4 + depth * 2)).Append("- ").Append(t.name);
sb.Append(" active=").Append(t.gameObject.activeSelf).Append("/").Append(t.gameObject.activeInHierarchy);
sb.Append(" parent=").Append(t.parent != null ? t.parent.name : "(없음)");
var img = t.GetComponent<Image>();
if (img != null)
{
sb.Append(" [Image type=").Append(img.type)
.Append(" method=").Append(img.fillMethod)
.Append(" origin=").Append(img.fillOrigin)
.Append(" amount=").Append(img.fillAmount.ToString("F3"))
.Append(" sprite=").Append(img.sprite != null ? img.sprite.name : "🔴없음")
.Append(" enabled=").Append(img.enabled)
.Append(" color=#").Append(ColorUtility.ToHtmlStringRGBA(img.color))
.Append(" raycast=").Append(img.raycastTarget).Append("]");
int vc; string note;
float r = MeshWidthRatio(img, out vc, out note);
sb.Append(" 메쉬폭비=").Append(r < 0f ? "?" : r.ToString("F3")).Append(" (").Append(note).Append(")");
}
var cg = t.GetComponent<CanvasGroup>();
if (cg != null) sb.Append(" [CanvasGroup alpha=").Append(cg.alpha.ToString("F2")).Append("]");
var cv = t.GetComponent<Canvas>();
if (cv != null) sb.Append(" [Canvas order=").Append(cv.sortingOrder).Append(" override=").Append(cv.overrideSorting)
.Append(" enabled=").Append(cv.enabled).Append("]");
var tmp = t.GetComponent<TMPro.TextMeshProUGUI>();
if (tmp != null) sb.Append(" [TMP \"").Append(tmp.text).Append("\" fs=").Append(tmp.fontSize.ToString("F1")).Append("]");
var bhb = t.GetComponent<BossHpBar>();
if (bhb != null) sb.Append(" [BossHpBar]");
_o.AppendLine(sb.ToString());
for (int i = 0; i < t.childCount; i++) DumpTree(t.GetChild(i), depth + 1);
}
static void DumpBarNodes(GameObject root, string title)
{
H(title);
var all = root.GetComponentsInChildren<BossHpBar>(true);
N("BossHpBar 인스턴스 = " + all.Length + "벌");
foreach (var b in all)
{
// 부모 사슬 + 캔버스
var chain = new List<string>();
var p = b.transform;
Canvas nearest = null;
while (p != null)
{
chain.Add(p.name);
if (nearest == null) { var c = p.GetComponent<Canvas>(); if (c != null) nearest = c; }
p = p.parent;
}
chain.Reverse();
N("경로 = " + string.Join("/", chain.ToArray()) +
" · 소속 캔버스 = " + (nearest != null ? nearest.name + "(order=" + nearest.sortingOrder +
" override=" + nearest.overrideSorting + " enabled=" + nearest.enabled + " mode=" + nearest.renderMode + ")" : "🔴없음"));
DumpTree(b.transform, 0);
}
}
// ── 본체 ─────────────────────────────────────────────────────────────────
public static string RunAll()
{
_o = new StringBuilder();
_pass = _fail = 0;
_o.AppendLine("# WL813c3 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" (edit mode · Play " + Application.isPlaying + ")");
var boss = WLBossUiSettings.Instance;
var hudS = WLHudLayoutSettings.Instance;
if (boss == null || hudS == null)
{
_o.AppendLine("🔴 설정 에셋 없음 — boss=" + (boss != null) + " hud=" + (hudS != null));
return Flush();
}
// SO 스냅샷(원복용)
bool sForce = boss.bossBarForceFilledFill, sWhite = boss.bossBarFillWhiteFallback;
bool sBgTick = boss.bossBarSpriteOnBgAndTicks, sDedup = boss.bossBarDedupInstances, sZero = boss.bossBarHideOnZeroHp;
string abs = Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar));
var fi0 = new FileInfo(abs);
long len0 = fi0.Length; DateTime mt0 = fi0.LastWriteTimeUtc;
N("프리팹 전 = " + len0 + " B · " + mt0.ToString("yyyy-MM-dd HH:mm:ss.fff") + " UTC");
GameObject root = null;
BossHpBar bar = null;
GameObject dupGo = null;
try
{
root = PrefabUtility.LoadPrefabContents(kPrefab);
var hud = root.transform.Find(kHudPath);
if (hud == null) { _o.AppendLine("🔴 " + kHudPath + " 없음"); return Flush(); }
var barT = hud.Find(kBarName);
if (barT == null) { _o.AppendLine("🔴 " + kBarName + " 없음"); return Flush(); }
bar = barT.GetComponent<BossHpBar>();
if (bar == null) { _o.AppendLine("🔴 BossHpBar 컴포넌트 없음"); return Flush(); }
// ── ① 저장된 상태 그대로 (수리 전) ──────────────────────────────
DumpBarNodes(root, "① 프리팹에 저장된 그대로 (수리 전 · Initialize 호출 0)");
var fillT = barT.Find("Bar/Fill");
var fillImg = fillT != null ? fillT.GetComponent<Image>() : null;
Chk(fillImg != null, "Bar/Fill Image 존재");
Chk(fillImg != null && fillImg.sprite == null,
"🔴 D-26 원인 재현: 저장된 Fill 의 sprite = 없음 (BossHpBar.cs:499 가 AddComponent 만 했다)");
Chk(fillImg != null && fillImg.type == Image.Type.Filled,
"저장된 Fill 의 type = Filled (후보 ③ 「type 이 Filled 가 아니다」는 반증됨)");
Chk(root.GetComponentsInChildren<BossHpBar>(true).Length == 1,
"바 노드 1벌뿐 (후보 ① 「2벌」 반증)");
// ── ② 메쉬 실측 (수리 전 · fillAmount 0.368 = Q7 캡처 그 값) ────
H("② 메쉬 실측 (수리 전) — fillAmount 를 Q7 D-26 그 값 0.368 로 두고 실제 정점을 만든다");
if (fillImg != null)
{
fillImg.fillAmount = 0.368f;
int vc; string note;
float r = MeshWidthRatio(fillImg, out vc, out note);
N("fillAmount=0.368 → 메쉬폭비 = " + r.ToString("F3") + " (" + note + ")");
Chk(r > 0.99f,
"🔴 D-26 기계적 증거: sprite 가 없어 fillAmount 0.368 인데 메쉬는 폭비 " + r.ToString("F3") + " = 만피 사각형");
}
// ── ③ 수리 = 런타임과 같은 경로(Initialize) ─────────────────────
H("③ 수리 — BossHpBar.Initialize() (런타임 OnEnable 과 같은 경로)");
boss.bossBarForceFilledFill = true; boss.bossBarFillWhiteFallback = true;
string init = bar.Initialize();
N("Initialize → " + init);
Chk(fillImg != null && fillImg.sprite != null,
"고침: Fill.sprite = " + (fillImg != null && fillImg.sprite != null ? fillImg.sprite.name : "(없음)"));
Chk(fillImg != null && fillImg.type == Image.Type.Filled &&
fillImg.fillMethod == Image.FillMethod.Horizontal &&
fillImg.fillOrigin == (int)Image.OriginHorizontal.Left,
"고침: type=Filled · method=Horizontal · origin=Left 강제");
// ── ④ 메쉬 실측 (수리 후) ───────────────────────────────────────
H("④ 메쉬 실측 (수리 후) — 같은 fillAmount 0.368");
if (fillImg != null)
{
fillImg.fillAmount = 0.368f;
int vc; string note;
float r = MeshWidthRatio(fillImg, out vc, out note);
N("fillAmount=0.368 → 메쉬폭비 = " + r.ToString("F3") + " (" + note + ")");
Chk(Mathf.Abs(r - 0.368f) < 0.01f,
"화면 추종: 메쉬폭비 " + r.ToString("F3") + " ≈ fillAmount 0.368 (오차 " +
Mathf.Abs(r - 0.368f).ToString("F4") + ")");
}
// ── ⑤ 발주서 §1-2 프로브: 가짜 HP 1800 → 869 → 화면 노드 fill 0.483 ──
H("⑤ 가짜 HP 1800 → 869 (발주서 §1-2) — 화면 노드가 따라오는가");
BossHpBar.DebugFallbackMaxHp = 1800f;
BossHpBar.DebugFakeMaxHp = 1800f;
BossHpBar.DebugFakeHp = 1800f;
N(BossHpBar.RaiseFakeBossSpawned(1800f));
bar.PollBoss(true);
N("스폰 직후 ratio=" + bar.LastRatio.ToString("F3") + " fillAmount=" +
(fillImg != null ? fillImg.fillAmount.ToString("F3") : "-"));
BossHpBar.DebugFakeHp = 869f;
bar.PollBoss(true);
float expect = 869f / 1800f;
int vc2; string note2;
float r2 = MeshWidthRatio(fillImg, out vc2, out note2);
N("HP 869/1800 → LastRatio=" + bar.LastRatio.ToString("F3") +
" · fillAmount=" + (fillImg != null ? fillImg.fillAmount.ToString("F3") : "-") +
" · 메쉬폭비=" + r2.ToString("F3") + " (" + note2 + ")");
Chk(Mathf.Abs(bar.LastRatio - expect) < 0.002f, "계기 LastRatio = " + expect.ToString("F3"));
Chk(fillImg != null && Mathf.Abs(fillImg.fillAmount - expect) < 0.002f, "fillAmount = " + expect.ToString("F3"));
Chk(Mathf.Abs(r2 - expect) < 0.01f,
"🔴 화면 노드 메쉬폭비 = " + r2.ToString("F3") + " (발주서 §1-2 목표 0.483)");
Chk(bar.Visible, "바가 보이는 상태(CanvasGroup alpha > 0)");
// 3칸 구분선(눈금)이 fill 위에 그대로 얹혀 있는가
var ticks = barT.Find("Bar/Ticks");
int tickN = ticks != null ? ticks.childCount : 0;
N("눈금 " + tickN + "개 · Ticks 형제 순서 = " + (ticks != null ? ticks.GetSiblingIndex() : -1) +
" / Fill = " + (fillT != null ? fillT.GetSiblingIndex() : -1));
Chk(tickN == 2 && ticks != null && fillT != null && ticks.GetSiblingIndex() > fillT.GetSiblingIndex(),
"3칸 = fill 위에 얹힌 구분선 2개(Fill 뒤 형제) — 별도 세그먼트 이미지 아님");
// ── ⑥ C8 롤백 재현 ──────────────────────────────────────────────
H("⑥ C8 롤백 — bossBarForceFilledFill = 0 → 813c2 상태(화면 만피)로 복귀");
boss.bossBarForceFilledFill = false;
if (fillImg != null) fillImg.sprite = null; // 저장된 프리팹과 같은 조건
N("EnsureFillRenderable → " + bar.EnsureFillRenderable());
if (fillImg != null) fillImg.fillAmount = 0.483f;
int vc3; string note3;
float r3 = MeshWidthRatio(fillImg, out vc3, out note3);
N("롤백 상태 fillAmount=0.483 → 메쉬폭비 = " + r3.ToString("F3") + " (" + note3 + ")");
Chk(r3 > 0.99f, "롤백 시 D-26 재현(메쉬폭비 " + r3.ToString("F3") + ") = 스위치 하나로 되돌아간다(C8)");
boss.bossBarForceFilledFill = true;
N("복원 → " + bar.EnsureFillRenderable());
// ── ⑦ HP 0 숨김 ────────────────────────────────────────────────
H("⑦ 보스 HP 0 → 숨김 예약");
int zero0 = bar.ZeroHpHideCount;
BossHpBar.DebugFakeHp = 0f;
bar.PollBoss(true);
N("HP 0 → ratio=" + bar.LastRatio.ToString("F3") + " · ZeroHpHideCount " + zero0 + "→" + bar.ZeroHpHideCount);
Chk(bar.LastRatio <= 0.0001f, "HP 0 → ratio 0");
Chk(bar.ZeroHpHideCount > zero0, "HP 0 숨김 예약 1회 (bossBarHideDelaySec " + hudS.bossBarHideDelaySec + "s 뒤)");
boss.bossBarHideOnZeroHp = false;
int zero1 = bar.ZeroHpHideCount;
bar.PollBoss(true);
Chk(bar.ZeroHpHideCount == zero1, "C8: bossBarHideOnZeroHp=0 → 예약 생략");
boss.bossBarHideOnZeroHp = sZero;
// ── ⑧ 중복 노드 차단 ───────────────────────────────────────────
H("⑧ 중복 노드 차단 — 같은 캔버스에 바를 하나 더 만들어 본다");
dupGo = new GameObject(kBarName + "_DUP", typeof(RectTransform));
dupGo.layer = barT.gameObject.layer;
dupGo.hideFlags = HideFlags.HideAndDontSave;
((RectTransform)dupGo.transform).SetParent(hud, false);
var dup = dupGo.AddComponent<BossHpBar>();
string dupInit = dup.Initialize();
N("중복 Initialize → " + dupInit);
N("원본 BarInstanceCount=" + bar.BarInstanceCount + " · 중복 숨김=" + dup.DedupHidden +
" · 중복 Visible=" + dup.Visible);
Chk(dup.BarInstanceCount >= 2, "중복 감지 " + dup.BarInstanceCount + "벌");
Chk(!dup.Visible, "중복 바는 숨김(alpha 0)");
dup.Subscribe(false);
boss.bossBarDedupInstances = false;
N("C8: bossBarDedupInstances=0 → " + dup.DedupInstances());
boss.bossBarDedupInstances = sDedup;
// ── ⑨ GC ───────────────────────────────────────────────────────
H("⑨ GC — 폴링 20,000 회 할당");
BossHpBar.DebugFakeHp = 900f;
bar.PollBoss(true);
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long g0 = GC.GetTotalMemory(true);
for (int i = 0; i < 20000; i++) bar.PollBoss(true);
long g1 = GC.GetTotalMemory(false);
N("GC " + g0 + " → " + g1 + " (Δ " + (g1 - g0) + " B)");
Chk(g1 - g0 <= 0, "폴링 20,000회 GC 증가 0 B (Δ " + (g1 - g0) + ")");
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long e0 = GC.GetTotalMemory(true);
for (int i = 0; i < 20000; i++) bar.EnsureFillRenderable();
long e1 = GC.GetTotalMemory(false);
N("EnsureFillRenderable 20,000회: GC " + e0 + " → " + e1 + " (Δ " + (e1 - e0) + " B · 문자열 반환 포함)");
// ── ⑩ 수리 후 노드 전수 재덤프 ─────────────────────────────────
BossHpBar.DebugFakeHp = 869f;
bar.PollBoss(true);
DumpBarNodes(root, "⑩ 수리 후 노드 전수 (HP 869/1800)");
N(bar.Dump());
}
finally
{
try { if (dupGo != null) UnityEngine.Object.DestroyImmediate(dupGo); } catch { }
try { if (bar != null) bar.Subscribe(false); } catch { }
BossHpBar.ClearFakeHp();
BossHpBar.DebugFallbackMaxHp = 100f;
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false; BossHpBar.RunResetCount = 0;
boss.bossBarForceFilledFill = sForce; boss.bossBarFillWhiteFallback = sWhite;
boss.bossBarSpriteOnBgAndTicks = sBgTick; boss.bossBarDedupInstances = sDedup;
boss.bossBarHideOnZeroHp = sZero;
try { if (root != null) PrefabUtility.UnloadPrefabContents(root); } catch { }
}
// ── ⑪ 저장 0 · 구독 0 ───────────────────────────────────────────────
H("⑪ 뒷정리 — 프리팹 저장 0 · 구독 0");
var fi1 = new FileInfo(abs); fi1.Refresh();
N("프리팹 후 = " + fi1.Length + " B · " + fi1.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff") + " UTC");
Chk(fi1.Length == len0 && fi1.LastWriteTimeUtc == mt0, "프리팹 파일 무변동 = 저장 0");
N("CombatEvents Spawned=" + WL.Combat.Core.CombatEvents.Spawned.Count +
" HitConfirmed=" + WL.Combat.Core.CombatEvents.HitConfirmed.Count +
" Killed=" + WL.Combat.Core.CombatEvents.Killed.Count +
" · RunStarted=" + WL.Combat.Run.RunEvents.RunStarted.Count);
N("SO 원복 = force:" + boss.bossBarForceFilledFill + " white:" + boss.bossBarFillWhiteFallback +
" bgTick:" + boss.bossBarSpriteOnBgAndTicks + " dedup:" + boss.bossBarDedupInstances +
" hp0:" + boss.bossBarHideOnZeroHp);
N("DebugFakeHp=" + BossHpBar.DebugFakeHp + " DebugFakeMaxHp=" + BossHpBar.DebugFakeMaxHp +
" DebugFallbackMaxHp=" + BossHpBar.DebugFallbackMaxHp);
return Flush();
}
static string Flush()
{
_o.AppendLine();
_o.AppendLine("== 합계 PASS " + _pass + " / FAIL " + _fail + " ==");
string p = OutPath("WL813c3_PROBE.txt");
try { File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true)); } catch { }
return _o.ToString() + "\n(파일: " + p + ")";
}
}

View File

@ -86,10 +86,11 @@ namespace WL.UI
if (_rt == null) _rt = GetComponent<RectTransform>();
BuildIfNeeded();
string layout = ApplyLayout();
string dedup = DedupInstances(); // WL-813c3 — 중복 바 차단(발주서 후보 ①)
HideNow();
Subscribe(true);
string late = BindExistingBoss(); // WL-813y — 늦은 구독 보정(Q3 결함 D-3)
return "initialized subscribed=" + _subscribed + " · " + layout + " · " + late;
return "initialized subscribed=" + _subscribed + " · " + layout + " · " + dedup + " · " + late;
}
/// <summary>
@ -287,9 +288,22 @@ namespace WL.UI
SetRatio(0f);
if (_hideAt < 0f) _hideAt = now + s.bossBarHideDelaySec;
}
else
{
// WL-813c3 — HP 가 0 인데 사망 판정이 아직 안 온 프레임에도 바를 내린다(발주서 §1-2).
var b = WLBossUiSettings.Instance;
if (b != null && b.bossBarHideOnZeroHp && _maxHp > 0f && _curHp <= 0f)
{
ZeroHpHideCount++;
if (_hideAt < 0f) _hideAt = now + s.bossBarHideDelaySec;
}
}
return true;
}
/// <summary>HP 0 숨김이 걸린 프레임 수(진단 · WL-813c3).</summary>
public int ZeroHpHideCount { get; private set; }
/// <summary>합성 모드 폴링 — 프로브가 넣은 가짜 HP 로 같은 산식을 태운다(게임 경로는 1 이라 무동작).</summary>
private bool PollSynthetic()
{
@ -298,6 +312,14 @@ namespace WL.UI
if (DebugFakeMaxHp > 0f && DebugFakeMaxHp != _maxHp) { _maxHp = DebugFakeMaxHp; MaxHpFixCount++; }
if (DebugFakeHp >= 0f) _curHp = Mathf.Max(0f, DebugFakeHp);
SetRatio(_maxHp > 0f ? _curHp / _maxHp : 0f);
// WL-813c3 — 실액터 경로(PollBoss)와 같은 HP 0 숨김을 합성 경로에도 태운다(검증이 같은 코드를 봐야 한다).
var bs = WLBossUiSettings.Instance;
if (bs != null && bs.bossBarHideOnZeroHp && _maxHp > 0f && _curHp <= 0f)
{
ZeroHpHideCount++;
var hs = WLHudLayoutSettings.Instance;
if (_hideAt < 0f) _hideAt = Time.unscaledTime + (hs != null ? hs.bossBarHideDelaySec : 0f);
}
return true;
}
@ -520,9 +542,117 @@ namespace WL.UI
if (nameFont != null) nameLabel.font = nameFont;
made = true;
}
EnsureFillRenderable(); // WL-813c3 — 이미 저장돼 있던 노드도 여기서 고친다
return made;
}
// ── WL-813c3 (Q7 D-26 「계기는 맞고 화면은 틀리다」) ──────────────────────
// 실측: 프리팹에 저장된 `IngameUIs/WL_HUD/WL_BossHpBar/Bar/Fill` 의 Image 는
// `m_Type: 3(Filled) · m_FillMethod: 0(Horizontal) · m_FillAmount: 1` 인데
// **`m_Sprite: {fileID: 0}` = 비어 있다**. 위 BuildIfNeeded():499 가 AddComponent 만 하고
// 스프라이트를 한 번도 안 물렸기 때문이다.
// uGUI `Image.OnPopulateMesh` 는 맨 앞에서 `if (activeSprite == null) { base.OnPopulateMesh(toFill); return; }`
// 로 빠진다 — 즉 스프라이트가 없으면 `type`·`fillAmount` 를 **아예 읽지 않고** 사각형 전체를
// m_Color 로 채운다. 그래서 계기 `fillAmount=0.368` 이 정확해도 화면은 만피 빨강이었다
// (Q7 `PD4_03_bossbar.png` · `PD4_06_timeout_hp1.png` · 3칸은 그 위에 얹힌 Tick_0/Tick_1 선).
// → 바가 켜질 때마다 스프라이트와 Filled 설정을 강제한다. 값·스위치는 전부 WLBossUiSettings(C8/C45).
// 🔴 GC: Image 세터는 값이 같으면 되돌아가므로 재적용 비용 0. 흰 스프라이트는 **정적 1개**만 만든다.
private static Sprite s_whiteSprite;
/// <summary>내장 흰 텍스처로 만든 공유 스프라이트(할당 1회 · 에셋 저장 안 됨).</summary>
public static Sprite WhiteSprite
{
get
{
if (s_whiteSprite == null)
{
var tex = Texture2D.whiteTexture;
s_whiteSprite = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height),
new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect);
s_whiteSprite.name = "WL_BossBarFill_White";
s_whiteSprite.hideFlags = HideFlags.HideAndDontSave; // 프리팹/씬에 굳지 않는다
}
return s_whiteSprite;
}
}
/// <summary>fill 이 실제로 잘려 그려지도록 스프라이트·Filled 설정을 강제한다(D-26 본체 수리).</summary>
public string EnsureFillRenderable()
{
if (fill == null) return "fill 없음 — BuildIfNeeded 먼저";
var b = WLBossUiSettings.Instance;
if (b == null) return "WLBossUiSettings 에셋 없음 — 건너뜀";
if (!b.bossBarForceFilledFill) return "bossBarForceFilledFill=false — 건너뜀(813c2 상태)";
fill.type = Image.Type.Filled;
fill.fillMethod = Image.FillMethod.Horizontal;
fill.fillOrigin = (int)Image.OriginHorizontal.Left;
fill.fillClockwise = true;
fill.preserveAspect = false; // 바는 rect 를 꽉 채운다(스프라이트 비율 무시)
fill.raycastTarget = false;
Sprite sp = b.bossBarFillSprite;
if (sp == null && b.bossBarFillWhiteFallback) sp = WhiteSprite;
if (sp != null && fill.sprite != sp) fill.sprite = sp;
if (b.bossBarSpriteOnBgAndTicks && sp != null)
{
if (background != null && background.sprite != sp) background.sprite = sp;
if (tickRoot != null)
for (int i = 0; i < tickRoot.childCount; i++)
{
var img = tickRoot.GetChild(i).GetComponent<Image>();
if (img != null && img.sprite != sp) img.sprite = sp;
}
}
return "fill type=" + fill.type + " method=" + fill.fillMethod + " origin=" + fill.fillOrigin +
" sprite=" + (fill.sprite != null ? fill.sprite.name : "(없음)") +
" amount=" + fill.fillAmount.ToString("F3");
}
// ── WL-813c3 — 중복 노드 차단 ─────────────────────────────────────────
// 발주서 후보 ①(바가 2벌이라 안 보이는 쪽만 갱신) 검증용. 실측상 프리팹에는 1벌뿐이지만,
// 프로브·에디터 도구가 런타임에 하나 더 만들면 뒤쪽 만피 바가 그대로 보인다 → 첫 1개만 남긴다.
private static readonly System.Collections.Generic.List<BossHpBar> s_barBuf =
new System.Collections.Generic.List<BossHpBar>(4);
/// <summary>같은 캔버스(없으면 계층 루트) 아래에서 발견한 BossHpBar 개수(진단).</summary>
public int BarInstanceCount { get; private set; }
/// <summary>중복이라 이 인스턴스가 숨겨졌는가(진단).</summary>
public bool DedupHidden { get; private set; }
/// <summary>중복 BossHpBar 를 첫 1개만 남기고 숨긴다. 돌려주는 값 = 진단 문자열.</summary>
public string DedupInstances()
{
var b = WLBossUiSettings.Instance;
if (b == null) return "중복차단: WLBossUiSettings 에셋 없음 — 생략";
if (!b.bossBarDedupInstances) return "중복차단: bossBarDedupInstances=false — 생략";
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
Transform scope = _canvas != null ? _canvas.transform : transform.root;
if (scope == null) return "중복차단: 범위 없음 — 생략";
s_barBuf.Clear();
scope.GetComponentsInChildren(true, s_barBuf);
BarInstanceCount = s_barBuf.Count;
if (BarInstanceCount <= 1) { DedupHidden = false; return "중복차단: 1벌 — 조치 없음"; }
BossHpBar keep = s_barBuf[0];
int hidden = 0;
for (int i = 1; i < s_barBuf.Count; i++)
{
var extra = s_barBuf[i];
if (extra == null || ReferenceEquals(extra, keep)) continue;
extra.DedupHidden = true;
extra.HideNow();
hidden++;
}
DedupHidden = !ReferenceEquals(this, keep);
return "중복차단: " + BarInstanceCount + "벌 발견 → 유지 " + keep.name + " · 숨김 " + hidden + "벌";
}
/// <summary>설정 값(px)을 캔버스 유닛으로 환산해 바·눈금·색을 맞춘다.</summary>
public string ApplyLayout()
{
@ -564,8 +694,10 @@ namespace WL.UI
if (fill != null) fill.color = s.bossBarFillColor;
RebuildTicks(s, u);
// WL-813c3 — 눈금을 다시 만든 뒤에 렌더 설정을 강제한다(새 Tick 에도 적용되도록 순서가 중요).
string render = EnsureFillRenderable();
return "layout size=" + _rt.sizeDelta + " unitsPerPx=" + u.ToString("F4") +
" ticks=" + (s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0);
" ticks=" + (s.bossPhaseTicks != null ? s.bossPhaseTicks.Length : 0) + " · " + render;
}
private void RebuildTicks(WLHudLayoutSettings s, float u)
@ -699,6 +831,24 @@ namespace WL.UI
sb.AppendLine(" bar " + (bar != null ? "size" + bar.sizeDelta + " aMin" + bar.anchorMin + " aMax" + bar.anchorMax : "(없음)") +
" fill=" + (fill != null ? fill.fillAmount.ToString("F3") : "-") +
" ticks=" + (tickRoot != null ? tickRoot.childCount : 0));
// ── WL-813c3 — D-26 「화면」 진단: fillAmount 가 실제 메쉬에 반영되는 조건 ──
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
var bset = WLBossUiSettings.Instance;
sb.AppendLine(" [813c3] fill " + (fill == null ? "(없음)" :
"type=" + fill.type + " method=" + fill.fillMethod + " origin=" + fill.fillOrigin +
" amount=" + fill.fillAmount.ToString("F3") +
" sprite=" + (fill.sprite != null ? fill.sprite.name : "🔴(없음)=fillAmount 무효") +
" enabled=" + fill.enabled + " color=" + ColorUtility.ToHtmlStringRGBA(fill.color) +
" active=" + fill.gameObject.activeInHierarchy) +
" · 바 " + BarInstanceCount + "벌(이 인스턴스 숨김=" + DedupHidden + ")" +
" · HP0숨김 " + ZeroHpHideCount + "회" +
" · 설정 " + (bset == null ? "(에셋 없음)" :
"force=" + bset.bossBarForceFilledFill + " white=" + bset.bossBarFillWhiteFallback +
" spriteSO=" + (bset.bossBarFillSprite != null ? bset.bossBarFillSprite.name : "없음") +
" dedup=" + bset.bossBarDedupInstances + " hp0숨김=" + bset.bossBarHideOnZeroHp));
sb.AppendLine(" [813c3] canvas " + (_canvas == null ? "(없음)" :
_canvas.name + " order=" + _canvas.sortingOrder + " override=" + _canvas.overrideSorting +
" enabled=" + _canvas.enabled + " renderMode=" + _canvas.renderMode));
if (tickRoot != null)
for (int i = 0; i < tickRoot.childCount; i++)
{

View File

@ -35,4 +35,10 @@ MonoBehaviour:
warnVignettePulses: 2
warnVignetteThicknessPx: 180
warnVignetteColor: {r: 1, g: 0.165, b: 0.165, a: 0.55}
bossBarForceFilledFill: 1
bossBarFillSprite: {fileID: 0}
bossBarFillWhiteFallback: 1
bossBarSpriteOnBgAndTicks: 0
bossBarDedupInstances: 1
bossBarHideOnZeroHp: 1
verboseLog: 0

View File

@ -106,6 +106,31 @@ namespace WL.UI
[Tooltip("비네트 색(알파 = 최고 밝기)")]
public Color warnVignetteColor = new Color(1f, 0.165f, 0.165f, 0.55f); // #FF2A2A · a 0.55
// ── 보스 HP 바 렌더 (WL-813c3 · Q7 결함 D-26 「계기는 맞고 화면은 틀리다」) ──
// 813c2 로 fillAmount 는 정확해졌는데(0.541/0.368/0.01) 화면 바는 만피였다.
// 실측 원인 = 바의 `Fill` Image 에 **스프라이트가 없다**(BossHpBar.cs:499 가 AddComponent 만 했다).
// uGUI `Image.OnPopulateMesh` 는 `activeSprite == null` 이면 `Graphic.OnPopulateMesh`(사각형 전체)로
// 빠져 `type`/`fillAmount` 를 **아예 보지 않는다** → 계기 0.368 · 화면 100 %.
// 배치·색은 813c 의 WLHudLayoutSettings 가 그대로 갖는다. 여기 있는 것은 **렌더 설정**뿐이다.
[Header("보스 HP 바 렌더 (WL-813c3 · D-26)")]
[Tooltip("바가 켜질 때마다 fill 의 type=Filled · fillMethod=Horizontal · fillOrigin=Left 와 스프라이트를 강제한다. 끄면 813c2 상태(화면 만피)로 돌아간다 = C8 롤백")]
public bool bossBarForceFilledFill = true;
[Tooltip("fill 이 쓸 스프라이트. 비워 두면 bossBarFillWhiteFallback 이 만든 흰 스프라이트를 쓴다(전용 아트가 생기면 여기에 물린다)")]
public Sprite bossBarFillSprite;
[Tooltip("bossBarFillSprite 가 비었을 때 엔진 내장 흰 텍스처로 스프라이트를 **1개만** 만들어 공유한다(정적 캐시 · 프레임당 할당 0). 끄면 스프라이트를 안 물려 D-26 이 그대로 남는다")]
public bool bossBarFillWhiteFallback = true;
[Tooltip("배경(Bg)·눈금(Tick_*)에도 같은 스프라이트를 물린다. 표시는 같고 머티리얼/배치만 통일된다(기본 꺼짐 = 원래 모습 유지)")]
public bool bossBarSpriteOnBgAndTicks = false;
[Tooltip("같은 캔버스 아래에 BossHpBar 가 2개 이상이면 첫 1개만 남기고 나머지는 숨긴다(중복 노드가 뒤에서 만피로 보이는 것을 막는다)")]
public bool bossBarDedupInstances = true;
[Tooltip("HP 가 0 이 되면 사망 판정 전이라도 바를 0 으로 내리고 숨김을 예약한다(WLHudLayoutSettings.bossBarHideDelaySec 뒤)")]
public bool bossBarHideOnZeroHp = true;
// ── 진단 ──────────────────────────────────────────────────────────────
[Header("진단")]
[Tooltip("배너·비네트 전이를 콘솔에 남긴다(Play 진단용 · 기본 꺼짐)")]