TIL - UI 재설계 및 트러블 슈팅

Kyu_·2026년 7월 1일

32주차

목록 보기
3/5

1. 파츠 NPC UI 재설계

  • 참고하던 UI

  • 확정된 UI (2.png)

배경

파츠 NPC 상호작용 UI(NSPartEquipWidget)가 원래 "항목마다 개별 버튼(장착/구매) + hover 툴팁" 방식으로 짜여 있었는데, 기획이 Docs/2.png로 확정되면서 상호작용 모델 자체가 바뀌었다. 항목 클릭 → 중앙에 설명 표시 → 하단 장착 버튼 하나로 확정하는 선택 모델로 전환 필요

확정한 설계 결정과 이유

결정내용이유
데이터 모델캐릭터당 파츠 1개 장착 유지, 세이브 구조 변경 없음기획 확인 결과 다중 장착 계획 없음. 2.png의 "장착중 3박스"는 슬롯 타입 표시용이지 3개 동시 장착이 아님
상호작용개별 버튼+툴팁 → 클릭 선택 모델2.png가 항목 클릭 후 중앙 패널에서 확인하고 장착하는 흐름을 확정 레이아웃으로 못 박음
설명 패널공용 위젯 1개(NSPartDetailWidget), 셋업 메서드만 2개로 분기카탈로그 후보(정의 정보: 비용/등급별 수치범위/리롤가능)와 장착중인 파츠(인스턴스 정보: 현재 등급/현재 수치)는 데이터 소스가 다르지만, 보여주는 자리(왼쪽/중앙)는 UI상 같은 종류의 패널이라 위젯 중복을 피함
카탈로그 분할바디/암/레그 3개 컨테이너로 라우팅FNSPartDefinitionRow::PartSlot 태그 하나로 구분 가능해서 별도 자료구조 없이 태그 매칭만으로 처리

구현 내용 (파일별 역할)

신규 — NSPartDetailWidget.h/.cpp
카탈로그 선택 파츠와 장착중 파츠 양쪽에서 쓰는 상시 설명 패널.

  • SetupFromDefinition(Row, Def) — 카탈로그 후보 표시 (비용/슬롯/리롤가능)
  • SetupFromEquipped(SaveData, Def) — 장착중인 파츠 표시 (현재 등급/현재 수치)
  • ClearDetail() — 선택/장착 없음 상태

수정 — NSPartCatalogEntryWidget.h/.cpp
개별 ActionButton(장착/구매)과 hover 툴팁(SetToolTip) 제거. 항목 전체를 감싸는 SelectButton 하나로 바뀌고, 클릭 시 Owner->SelectCatalogPart(Row) 호출. Icon/Name만 남음.

수정 — NSPartEquipWidget.h/.cpp

  • 카탈로그를 BodyListContainer/ArmListContainer/LegListContainer 3분할로 라우팅 (GetCatalogContainerForSlot)
  • 선택 상태(SelectedRow/bHasSelection) 보관, 카탈로그 클릭 시 중앙 설명·장착버튼 갱신 (SelectCatalogPart, RefreshEquipButton)
  • 장착 버튼 클릭 시 소유 여부에 따라 RequestEquipPart/RequestUnlockPart 분기 (OnEquipButtonClicked)
  • 왼쪽 장착중 3박스 클릭 시 해당 슬롯의 장착품 정보로 왼쪽 설명 갱신 (SelectEquippedSlot) — 클릭한 박스의 슬롯 태그와 실제 장착품의 슬롯이 일치할 때만 표시, 아니면 빈 상태

수정 — NSPartUtils.h/.cpp
TSoftObjectPtr<UNSPartDefinition>를 직접 받는 ResolvePartDefinition 오버로드 추가. 기존 FNSPartData 버전은 이 오버로드에 위임하도록 리팩터링.

  • 왜 추가했나: 카탈로그 선택(FNSPartDefinitionRow::Definition)과 장착중 조회(FNSPartSaveData::Definition) 둘 다 "소프트포인터에서 Definition을 캐시 경유로 안전하게 꺼내는" 동일한 로직이 필요했는데, 기존 함수는 FNSPartData 구조체 전용이라 재사용이 안 됐음. 중복 구현 대신 공용화.

삭제 — NSPartTooltipWidget.h/.cpp
hover 툴팁 방식 자체가 폐기되면서 참조가 완전히 사라짐. 다른 참조 없음을 확인 후 삭제.

에디터에서 남은 작업

  • WBP_PartTooltip.uasset 삭제 (C++ 부모 클래스 없어짐)
  • WBP_PartEquip.uasset 레이아웃 재구성 (3열 구조, 바인딩 이름 일치)
  • WBP_PartCatalogEntry.uassetSelectButton 구조로 재구성
  • 신규 WBP_PartDetail.uasset 생성 (부모: NSPartDetailWidget)

2. UMG 레이아웃 트러블슈팅 — Anchor와 Box Slot Fill 혼동

에디터 작업 중 위젯이 전부 왼쪽 위에 몰리는 문제 발생. 계층 구조(Hierarchy)는 올바르게 잡혀 있었는데도 발생한 원인:

  • UMG는 사이즈 규칙이 두 층으로 분리됨
    1. Canvas Panel Slot (Anchors + Offset) — Canvas Panel의 직계 자식에만 적용
    2. Box Slot (Size Rule: Auto/Fill) — Horizontal/Vertical Box의 자식에만 적용
  • 최상위 Horizontal Box(3열 컨테이너)가 Canvas Panel의 자식인데 1번 레이어의 Anchor를 "전체 스트레치"로 안 잡아서, 그 안에서 2번 레이어(Vertical Box Fill)를 아무리 조정해도 애초에 나눠 가질 공간 자체가 없었음.
  • 해결: 최상위 Horizontal Box의 Anchor를 전체 스트레치(4방향 Offset 0)로 지정 → 그 안의 Vertical Box 3개에 Box Slot Fill 비율 지정.

3. 파츠 데이터 로드 구조를 UDeveloperSettings에서 NSCommonDataConfig로 이관

문제 발견 경위

사용자가 프로젝트의 데이터 로드 아키텍처를 다시 확인하자고 제안: NSDataSubsystem은 공용(Common) / 아웃런(OutGame) / 인런(Run) 페이즈별로 데이터를 로드하고, 페이즈마다 전용 UPrimaryDataAsset(예: UNSCommonDataConfig)에 DT/에셋 참조를 몰아두는 구조로 이미 정리되어 있는데, 파츠 DT(DT_PartDefinition/DT_PartSlot)만 예외적으로 UNSDataSettings(UDeveloperSettings, 프로젝트 세팅 전역 싱글톤)에 붙어 있었음.

조사로 확인한 사실

  • Common 페이즈는 이미 UNSCommonDataConfig : UPrimaryDataAsset를 통해 AssetBundles="CommonData" 방식으로 DT를 로드하고 있었음 (예: AbilityBaseStatTable).
  • OutGame 페이즈용 동급 DA(HubAssetType/"NSHubData")는 코드에 상수만 선언돼 있고 실제 클래스도, AssetManager 스캔 등록도 없는 미완성 상태였음.
  • 파츠는 인런/아웃런 양쪽에서 다 필요한 데이터라, OutGame 전용 DA가 아니라 Common 설정에 들어가는 게 맞다고 결론.

변경 내용

  • NSCommonDataConfig.hPartsBaseStatTable, PartsSlotBaseStatTable 필드 추가 (AssetBundles="CommonData")
  • NSDataSubsystem.cpp:
    • OnOutGamePrimaryAssetsLoaded/OnOutGameAssetsLoaded에서 파츠 DT 로드·캐시빌드 로직 제거
    • OnCommonAssetsLoadedBuildPartRowCache()/BuildSlotRowCache() 호출 추가
    • 두 함수가 GetDefault<UNSDataSettings>() 대신 GetCommonDataConfig()를 조회하도록 변경
  • NSDataSettings.h/.cpp 삭제, Config/DefaultGame.ini[/Script/NeoSanctum.NSDataSettings] 섹션 삭제

왜 이렇게 했나

  • 페이즈 일관성: 다른 모든 데이터가 "페이즈 전용 PrimaryDataAsset에 소프트 참조를 몰아두고, 해당 페이즈 로드 시 AssetBundle로 같이 끌려오게" 하는 패턴을 쓰는데, 파츠만 UDeveloperSettings라는 별개 메커니즘(프로젝트 세팅, 페이즈 개념 없음)을 쓰고 있어 구조가 어긋나 있었음.
  • 로드 로직 단순화 부수효과: UDeveloperSettingsUPrimaryDataAsset이 아니라서 AssetBundle 시스템에 못 들어가고, 그래서 기존 코드는 PartDefinitionTable/PartSlotTable 경로를 수동으로 RequestAsyncLoad하는 별도 코드가 필요했음. NSCommonDataConfig로 옮기면서 AssetBundles="CommonData" 메타데이터 하나로 LoadPrimaryAssets(..., CommonBundles, ...) 호출에 자동으로 딸려 들어오게 되어, 수동 로드 분기 코드 자체가 통째로 삭제됨.
  • 미완성 OutGame DA(NSHubData)를 새로 만들지 않은 이유: 파츠가 인런에서도 필요한 이상 OutGame 전용 DA에 넣는 건 애초에 범위가 안 맞았음. 없는 클래스를 새로 만드는 것보다, 이미 있고 정상 작동 중인 Common 패턴에 얹는 게 더 적은 변경으로 올바른 구조에 도달하는 길이었음.

로드 → 캐시 흐름 (변경 후)

LoadCommonData()
  → StartLoadCommon(): CommonDataConfigAssetType를 CommonBundles로 LoadPrimaryAssets
      (PartsBaseStatTable/PartsSlotBaseStatTable가 AssetBundles="CommonData"라 자동으로 같이 로드됨)
  → OnCommonAssetsLoaded(): CacheLoaded() → BuildPartRowCache() → BuildSlotRowCache()
      → GetCommonDataConfig()로 DA를 찾고 DT를 꺼내 CachedPartRowsByDefId / CachedSlotRowsBySlot에 채움

에디터에서 남은 작업

Content/NeoSanctum/Data/Config/DA_CommonDataConfig.uasset을 열어 PartsBaseStatTableDT_PartDefinition, PartsSlotBaseStatTableDT_PartSlot 지정

0개의 댓글