<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>s_kim__.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 07 Apr 2026 12:26:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>s_kim__.log</title>
            <url>https://velog.velcdn.com/images/s_kim__/profile/413ff5f8-2db4-4feb-85bf-1ae90aba3e5f/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. s_kim__.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/s_kim__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL]멀티플레이 개발 이슈 & 게임성 방향 고민]]></title>
            <link>https://velog.io/@s_kim__/TIL%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%88%EC%9D%B4-%EA%B0%9C%EB%B0%9C-%EC%9D%B4%EC%8A%88-%EA%B2%8C%EC%9E%84%EC%84%B1-%EB%B0%A9%ED%96%A5-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@s_kim__/TIL%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%88%EC%9D%B4-%EA%B0%9C%EB%B0%9C-%EC%9D%B4%EC%8A%88-%EA%B2%8C%EC%9E%84%EC%84%B1-%EB%B0%A9%ED%96%A5-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Tue, 07 Apr 2026 12:26:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/s_kim__/post/fe462a6e-1c87-4835-b31d-272caddf30d5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Gameplay Cue OnRemove에서 CastChecked 크래시]]></title>
            <link>https://velog.io/@s_kim__/UE5-GAS-Gameplay-Cue-OnRemove%EC%97%90%EC%84%9C-CastChecked-%ED%81%AC%EB%9E%98%EC%8B%9C</link>
            <guid>https://velog.io/@s_kim__/UE5-GAS-Gameplay-Cue-OnRemove%EC%97%90%EC%84%9C-CastChecked-%ED%81%AC%EB%9E%98%EC%8B%9C</guid>
            <pubDate>Tue, 07 Apr 2026 12:13:06 GMT</pubDate>
            <description><![CDATA[<p><strong>분류:</strong> Unreal Engine 5.7 / GAS / Gameplay Cue<br><strong>발생 시점:</strong> 액터 파괴(Destroy) 시<br><strong>증상:</strong> <code>ApplyOriginalMaterial</code> 내부에서 크래시 발생, 로그상 &quot;MyTarget is null&quot;처럼 보이지만 실제 원인은 다른 곳</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>레벨을 정리할 때 어노말리 액터를 파괴하는 로직</p>
<p><code>AGameplayCueNotify_Actor</code> 기반의 머티리얼 교체 Cue를 구현하였다.<br><code>OnRemove</code>에서 <code>MyTarget</code>과 <code>CachedMaterialCueData</code> 유효성을 모두 체크한 뒤 원본 머티리얼 복원 함수를 호출했음에도, 액터가 파괴될 때 크래시가 발생하였다.</p>
<pre><code class="language-cpp">bool ADZGCN_AnomalyChangeMaterial::OnRemove_Implementation(
    AActor* MyTarget, const FGameplayCueParameters&amp; Parameters)
{
    if (!MyTarget) return false;                     // ← 통과
    if (CachedMaterialCueData.Num() == 0) return false; // ← 통과

    for (auto&amp; MaterialData : CachedMaterialCueData)
    {
        if (IsValid(MyTarget))
            ApplyOriginalMaterial(MyTarget, MaterialData); // ← 여기서 크래시
    }
    return true;
}</code></pre>
<pre><code class="language-cpp">void ADZGCN_AnomalyChangeMaterial::ApplyOriginalMaterial(
    AActor* MyTarget, FDZMaterialCueData&amp; CueData)
{
    UStaticMeshComponent* TargetMesh = CastChecked&lt;UStaticMeshComponent&gt;(
        MyTarget-&gt;FindComponentByTag(..., CueData.TargetMeshTag)); // ← 실제 크래시 지점
    ...
}</code></pre>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<p><code>OnRemove</code>에서의 <code>IsValid(MyTarget)</code> 체크는 통과하였다.<br>그러나 액터가 <strong>파괴 진행 중(BeginDestroy 이후)</strong> 상태일 경우, 액터 자체는 아직 유효한 포인터를 가지고 있더라도 <strong>내부 컴포넌트는 이미 정리된 상태</strong>일 수 있다.</p>
<p>이 상황에서 <code>FindComponentByTag</code>는 <code>nullptr</code>을 반환하고, 이 <code>nullptr</code>을 <code>CastChecked</code>에 넘기면 <strong>즉시 assert 크래시</strong>가 발생한다.</p>
<pre><code>OnRemove 진입
    ↓
IsValid(MyTarget) → true  (액터 포인터는 아직 유효)
    ↓
FindComponentByTag → nullptr  (컴포넌트는 이미 정리됨)
    ↓
CastChecked&lt;UStaticMeshComponent&gt;(nullptr) → 💥 크래시</code></pre><blockquote>
<p><code>CastChecked</code>는 내부적으로 입력값이 절대 <code>null</code>이 아님을 전제로 동작한다.<br>외부 조회 결과처럼 <code>null</code> 가능성이 있는 값에는 사용하면 안 된다.</p>
</blockquote>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p><code>CastChecked</code> → <code>Cast</code> 로 교체하고, 결과에 대해 명시적인 <code>IsValid</code> 체크를 추가하였다.</p>
<pre><code class="language-cpp">void ADZGCN_AnomalyChangeMaterial::ApplyOriginalMaterial(
    AActor* MyTarget, FDZMaterialCueData&amp; CueData)
{
    UStaticMeshComponent* TargetMesh = Cast&lt;UStaticMeshComponent&gt;(
        MyTarget-&gt;FindComponentByTag(UStaticMeshComponent::StaticClass(), CueData.TargetMeshTag));

    if (!IsValid(TargetMesh))
    {
        UE_LOG(LogTemp, Warning,
            TEXT(&quot;ApplyOriginalMaterial: TargetMesh not found. Tag: %s&quot;),
            *CueData.TargetMeshTag.ToString());
        return;
    }

    for (auto&amp; SlotOverride : CueData.SlotOverrides)
    {
        TargetMesh-&gt;SetMaterial(SlotOverride.SlotIndex, SlotOverride.OriginalMaterial);
    }
}</code></pre>
<p><code>OnRemove</code>에서도 <code>!MyTarget</code> 대신 <code>!IsValid(MyTarget)</code>으로 교체하고, 중복 체크를 제거하였다.</p>
<pre><code class="language-cpp">bool ADZGCN_AnomalyChangeMaterial::OnRemove_Implementation(
    AActor* MyTarget, const FGameplayCueParameters&amp; Parameters)
{
    if (!IsValid(MyTarget)) return false;
    if (CachedMaterialCueData.Num() == 0) return false;

    for (auto&amp; MaterialData : CachedMaterialCueData)
    {
        ApplyOriginalMaterial(MyTarget, MaterialData);
    }
    return true;
}</code></pre>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><code>CastChecked</code></td>
<td>입력이 절대 <code>null</code>이 아님을 <strong>개발자가 보장</strong>할 수 있을 때만 사용</td>
</tr>
<tr>
<td><code>Cast</code> + <code>IsValid</code></td>
<td><code>FindComponentByTag</code>, <code>GetComponentByClass</code> 등 <strong>외부 조회 결과</strong>에는 반드시 이 조합 사용</td>
</tr>
<tr>
<td><code>!MyTarget</code> vs <code>!IsValid(MyTarget)</code></td>
<td>raw null 체크만으로는 GC 수거 중인 객체를 잡지 못함. <code>IsValid</code> 사용이 안전</td>
</tr>
<tr>
<td>액터 파괴 순서</td>
<td><code>OnRemove</code>가 호출되는 시점에 액터 포인터는 유효해도, 내부 컴포넌트는 이미 정리되어 있을 수 있음</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[DungeonZero 어노말리 어빌리티 구현 회고]]></title>
            <link>https://velog.io/@s_kim__/DungeonZero-%EC%96%B4%EB%85%B8%EB%A7%90%EB%A6%AC-%EC%96%B4%EB%B9%8C%EB%A6%AC%ED%8B%B0-%EA%B5%AC%ED%98%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@s_kim__/DungeonZero-%EC%96%B4%EB%85%B8%EB%A7%90%EB%A6%AC-%EC%96%B4%EB%B9%8C%EB%A6%AC%ED%8B%B0-%EA%B5%AC%ED%98%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 01 Apr 2026 06:52:54 GMT</pubDate>
            <description><![CDATA[<h2 id="배경--왜-이-어빌리티들을-만들었는가">배경 — 왜 이 어빌리티들을 만들었는가</h2>
<p>DungeonZero는 &quot;8번출구&quot; 류의 <strong>이상현상 탐지 게임</strong>이다. 플레이어는 복도를 반복해서 걷다가 뭔가 달라진 것을 발견해야 한다. 이때 &quot;달라진 것&quot;을 만들어내는 주체가 <strong>어노말리 어빌리티</strong>다.</p>
<p>4인 멀티플레이를 전제로 하기 때문에 이상현상은 <strong>서버에서만 결정</strong>되어야 하고, 그 결과는 <strong>모든 클라이언트에 동일하게 보여야</strong> 한다. 또한 어노말리의 종류는 앞으로 계속 늘어날 것이므로 <strong>확장성</strong>도 요구된다.</p>
<p>이 두 조건(멀티플레이 복제 + 확장성)을 동시에 충족하기 위해 Unreal의 <strong>Gameplay Ability System(GAS)</strong> 을 채택했고, 각 이상현상 행동을 독립적인 <code>UGameplayAbility</code> 서브클래스로 구현했다.</p>
<p>현재 구현된 어빌리티는 다음 세 가지다.</p>
<table>
<thead>
<tr>
<th>클래스</th>
<th>이상현상 유형</th>
</tr>
</thead>
<tbody><tr>
<td><code>UDZGA_AnomalyScale</code></td>
<td>오브젝트 크기가 달라짐</td>
</tr>
<tr>
<td><code>UDZGA_AnomalyRelocate</code></td>
<td>오브젝트 위치가 바뀜</td>
</tr>
<tr>
<td><code>UDZGA_AnomalyPatrol</code></td>
<td>오브젝트가 스스로 이동함</td>
</tr>
</tbody></table>
<hr>
<h2 id="설계-원칙--메시에-종속되지-않는-범용-어빌리티-와-그-한계">설계 원칙 — 메시에 종속되지 않는 범용 어빌리티 (와 그 한계)</h2>
<p>이상현상 오브젝트는 의자, 표지판, 소화기 등 <strong>메시가 전부 다르다.</strong> 어빌리티가 특정 메시나 액터 타입에 종속되면 어노말리를 추가할 때마다 코드를 수정해야 한다.</p>
<p>이를 피하기 위해 두 가지 원칙을 지켰다.</p>
<p><strong>① 어빌리티는 가능한 한 <code>AActor*</code>만 알고 있다</strong>
어빌리티 내부 로직은 <code>GetAvatarActorFromActorInfo()</code>로 얻은 <code>AActor*</code>에만 의존하는 것을 원칙으로 한다. <code>AnomalyRelocate</code>와 <code>AnomalyPatrol</code>은 이 원칙을 완전히 따르므로, 어떤 오브젝트에든 부여하면 동작한다.</p>
<p>단, <strong><code>AnomalyScale</code>은 예외</strong>다. 스케일 변경은 <code>SetActorScale3D</code>를 호출해도 <code>SetReplicatingMovement</code>만으로는 클라이언트에 복제되지 않는다. 이를 해결하려면 <code>UPROPERTY(ReplicatedUsing = OnRep_...)</code> 전용 변수와 세터가 필요하고, 이 구현은 불가피하게 베이스 액터 클래스(<code>ADZAnomalyActorBase</code>)에 위치할 수밖에 없었다. 결과적으로 <code>AnomalyScale</code>은 <code>ADZAnomalyActorBase</code>를 캐스트해 <code>SetAnomalyScale</code>을 호출하는 형태로 구현되었다.</p>
<pre><code class="language-cpp">// DZGA_AnomalyScale.cpp 발췌
if (ADZAnomalyActorBase* AnomalyActor = Cast&lt;ADZAnomalyActorBase&gt;(TargetActor))
{
    AnomalyActor-&gt;SetAnomalyScale(NewScale);
}</code></pre>
<p>이는 &quot;멀티플레이 복제 요구사항&quot;과 &quot;범용성&quot; 사이의 트레이드오프이며, 복제 정확성을 우선한 결과다.</p>
<table>
<thead>
<tr>
<th>어빌리티</th>
<th>액터 종속성</th>
</tr>
</thead>
<tbody><tr>
<td><code>AnomalyRelocate</code></td>
<td><code>AActor*</code>만 사용, 어떤 액터든 동작</td>
</tr>
<tr>
<td><code>AnomalyPatrol</code></td>
<td><code>AActor*</code>만 사용, 어떤 액터든 동작</td>
</tr>
<tr>
<td><code>AnomalyScale</code></td>
<td><code>ADZAnomalyActorBase</code> 캐스트 필요 (복제 요구사항)</td>
</tr>
</tbody></table>
<p><strong>② 변화량은 <code>EditDefaultsOnly</code>로 노출한다</strong>
크기 범위, 이동 반경, 이동 속도 같은 수치를 블루프린트에서 오버라이드할 수 있게 열어두었다. 덕분에 같은 어빌리티 클래스를 상속한 블루프린트 서브클래스를 만들기만 하면, 의자용·소화기용 등 오브젝트별로 다른 수치를 코드 변경 없이 적용할 수 있다.</p>
<pre><code class="language-cpp">// DZGA_AnomalyPatrol.h 발췌
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = &quot;DZ | Anomaly | Patrol&quot;)
float MinPatrolRadius = 100.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = &quot;DZ | Anomaly | Patrol&quot;)
float MaxPatrolRadius = 300.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = &quot;DZ | Anomaly | Patrol&quot;)
float PatrolSpeed = 100.f;</code></pre>
<hr>
<h2 id="구현-포인트-1--getrandomreachablepointinradius-대신-각도·거리-직접-계산">구현 포인트 1 — <code>GetRandomReachablePointInRadius</code> 대신 각도·거리 직접 계산</h2>
<p><strong>문제</strong>: <code>UNavigationSystemV1::GetRandomReachablePointInRadius</code>는 최소 이동 거리를 보장하지 않는다. 반환된 위치가 원점 바로 옆일 수 있어, 이상현상으로서의 시각적 변화가 없는 경우가 생긴다.</p>
<p><strong>해결</strong>: 랜덤 각도(0<del>360°)와 랜덤 반경(`Min</del>Max<code>)을 직접 계산해 후보 위치를 생성한 뒤,</code>ProjectPointToNavigation`으로 NavMesh 위 유효 위치인지 검증하는 루프를 최대 30회 시도한다.</p>
<pre><code class="language-cpp">float RandomAngle  = FMath::FRandRange(0.f, 360.f);
float RandomRadius = FMath::FRandRange(MinRelocateRadius, MaxRelocateRadius);
FVector RandomOffset = FVector(
    RandomRadius * FMath::Cos(FMath::DegreesToRadians(RandomAngle)),
    RandomRadius * FMath::Sin(FMath::DegreesToRadians(RandomAngle)),
    0.f
);
FVector TestLocation = OriginLocation + RandomOffset;

FNavLocation NavLocation;
if (NavSystem-&gt;ProjectPointToNavigation(TestLocation, NavLocation)) { ... }</code></pre>
<p><strong>추가 검증 (Relocate 한정)</strong>: <code>ProjectPointToNavigation</code>은 NavMesh 경계 밖의 위치도 가장 가까운 NavMesh 위치로 스냅한다. 이 경우 결과 위치가 원점 근처로 끌려와 최소 거리 보장이 깨진다. XY 거리를 한 번 더 검증해 이를 방지한다.</p>
<pre><code class="language-cpp">const float ActualDist2D = FVector::Dist2D(OriginLocation, NavLocation.Location);
if (ActualDist2D &lt; MinRelocateRadius) { continue; }</code></pre>
<hr>
<h2 id="구현-포인트-2--z값-보정-피벗이-바닥-중심이-아닌-액터-대응">구현 포인트 2 — Z값 보정 (피벗이 바닥 중심이 아닌 액터 대응)</h2>
<p><strong>문제</strong>: NavMesh 위의 점은 &quot;바닥 표면&quot; 좌표를 반환한다. 그러나 액터의 피벗이 오브젝트 중심에 있는 경우, 이 값을 그대로 <code>SetActorLocation</code>에 넘기면 액터가 바닥을 뚫거나 허공에 떠 있게 된다.</p>
<p><strong>해결</strong>: 이동 전에 현재 위치와 바운딩 박스 최솟값의 Z 차이(<code>PivotToBottom</code>)를 미리 계산해두고, NavMesh 결과에 더해준다. 피벗 위치에 관계없이 &quot;액터 바닥면이 NavMesh 표면에 닿도록&quot; 보정한다. Relocate와 Patrol 양쪽에 모두 적용했다.</p>
<pre><code class="language-cpp">const FBox  ActorBox      = TargetActor-&gt;GetComponentsBoundingBox();
const float PivotToBottom = OriginLocation.Z - ActorBox.Min.Z;

// NavMesh 위치 계산 후
ResultLocation.Z += PivotToBottom;</code></pre>
<hr>
<h2 id="구현-포인트-3--minscaledelta-스케일-최소-변화량-보장">구현 포인트 3 — <code>MinScaleDelta</code> (스케일 최소 변화량 보장)</h2>
<p><strong>문제</strong>: <code>FMath::RandRange(MinScale, MaxScale)</code>로 랜덤 스케일을 뽑으면 결과가 1.0(원래 크기)에 가까울 수 있고, 플레이어가 변화를 인지하지 못한다. 이상현상 게임에서 &quot;변화가 있었지만 눈에 안 띄는&quot; 상황은 게임 경험을 망친다.</p>
<p><strong>해결</strong>: 1.0 기준으로 <code>[MinScale, 1 - MinScaleDelta]</code>(축소) 또는 <code>[1 + MinScaleDelta, MaxScale]</code>(확대) 두 구간으로 나눠 랜덤 선택한다. 두 구간이 모두 유효하면 <code>RandBool()</code>로 방향을 먼저 결정한 뒤 각 범위에서 뽑는다.</p>
<pre><code class="language-cpp">const bool bCanShrink = MinScale &lt;= 1.f - MinScaleDelta;
const bool bCanGrow   = MaxScale &gt;= 1.f + MinScaleDelta;

if      (bCanShrink &amp;&amp; bCanGrow) NewScale = FMath::RandBool()
    ? FMath::RandRange(MinScale,            1.f - MinScaleDelta)
    : FMath::RandRange(1.f + MinScaleDelta, MaxScale);
else if (bCanShrink)             NewScale = FMath::RandRange(MinScale, 1.f - MinScaleDelta);
else if (bCanGrow)               NewScale = FMath::RandRange(1.f + MinScaleDelta, MaxScale);
else                             /* 범위 오류 → 어빌리티 종료 */;</code></pre>
<p><code>MinScale</code>, <code>MaxScale</code>, <code>MinScaleDelta</code> 세 값을 <code>EditDefaultsOnly</code>로 노출해 블루프린트 서브클래스마다 다른 변화 폭을 설정할 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>문제</th>
<th>원인</th>
<th>해결책</th>
</tr>
</thead>
<tbody><tr>
<td>최소 이동 거리 미보장</td>
<td><code>GetRandomReachablePoint</code>의 거리 하한 없음</td>
<td>각도·반경 직접 계산 + 루프 검증</td>
</tr>
<tr>
<td>NavMesh 스냅 후 거리 붕괴</td>
<td><code>ProjectToNavigation</code>의 경계 스냅</td>
<td>XY 거리 재검증으로 2차 필터</td>
</tr>
<tr>
<td>액터가 허공에 뜨거나 땅속에 묻힘</td>
<td>NavMesh 점 ≠ 액터 피벗 위치</td>
<td><code>PivotToBottom</code> 오프셋 보정</td>
</tr>
<tr>
<td>스케일 변화가 눈에 안 띔</td>
<td>랜덤 결과가 1.0 근처로 수렴 가능</td>
<td><code>MinScaleDelta</code>로 데드존 제거</td>
</tr>
<tr>
<td>메시마다 코드 수정 필요</td>
<td>어빌리티가 특정 액터 타입에 종속</td>
<td><code>AActor*</code> 기반 + <code>EditDefaultsOnly</code> 노출</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 내배캠 수료날]]></title>
            <link>https://velog.io/@s_kim__/TIL-%EB%82%B4%EB%B0%B0%EC%BA%A0-%EC%88%98%EB%A3%8C%EB%82%A0</link>
            <guid>https://velog.io/@s_kim__/TIL-%EB%82%B4%EB%B0%B0%EC%BA%A0-%EC%88%98%EB%A3%8C%EB%82%A0</guid>
            <pubDate>Wed, 14 Jan 2026 13:04:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/s_kim__/post/e1fd8b51-2071-434c-9334-f21740d35e91/image.png" alt="">
<img src="https://velog.velcdn.com/images/s_kim__/post/f81cb1bd-84a6-45cd-b1fe-479f7db0c35e/image.png" alt="">
8개월간 9to9으로 정말 열심히 달려왔습니다. 마지막까지 함께한 소인 연구소 팀원들 다들 너무 고생했고 취업까지 잘 되길 바랍니다 ㅠㅠ
항상 체크하러 와주시고 조언해주신 튜터님도 잊지 못할 것 같습니다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251218]]></title>
            <link>https://velog.io/@s_kim__/TIL-251218</link>
            <guid>https://velog.io/@s_kim__/TIL-251218</guid>
            <pubDate>Thu, 18 Dec 2025 12:15:18 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-12-18</code></p>
<ul>
<li><input disabled="" type="checkbox"> 타이머로 주기적인 연료 소모 시스템 구현</li>
<li><input disabled="" type="checkbox"> 델리게이트로 UI와 로직 분리</li>
<li><input disabled="" type="checkbox"> 특정 아이템만 받는 커스텀 인벤토리 컴포넌트</li>
<li><input disabled="" type="checkbox"> StartTime 저장으로 클라이언트 프로그레스바 구현</li>
</ul>
<hr>
<h2 id="타이머로-주기적인-연료-소모-시스템-구현">타이머로 주기적인 연료 소모 시스템 구현</h2>
<p>연료를 넣으면 불을 켤 수 있는 가로등 액터를 만들었다. 핵심은 <strong>서버에서 타이머로 연료를 주기적으로 소모</strong>하고, <strong>연료가 없으면 자동으로 불을 끄는 구조</strong>다.</p>
<h3 id="구현-방식">구현 방식</h3>
<pre><code class="language-cpp">void ATSStreetLamp::SetFuelTimer()
{
    if (bIsFueling) return;

    GetWorldTimerManager().SetTimer(
        FuelTimerHandle, 
        this, 
        &amp;ATSStreetLamp::UseFuel, 
        MaintenanceInterval,  // 초당 소모 간격
        true,   // 반복
        0.f     // 즉시 시작
    );
    bIsFueling = true;
}

void ATSStreetLamp::UseFuel()
{
    if (LampInventory-&gt;GetItemCount(MaintenanceCostID) &gt; 0)
    {
        // 연료 소모 &amp; 불 켜기
        LampInventory-&gt;ConsumeItem(MaintenanceCostID, 1);
        ChangeLightScaleByErosion(ErosionSubSystem-&gt;GetCurrentErosion());
        StartTime = GetWorld()-&gt;GetTimeSeconds();  // 🔑 현재 시간 저장
    }
    else
    {
        // 연료 없으면 불 끄기 &amp; 타이머 중지
        SetLightScale(0);
        GetWorldTimerManager().ClearTimer(FuelTimerHandle);
        bIsFueling = false;
    }
    OnFuelModeChanged.Broadcast(bIsFueling);
}</code></pre>
<p><strong>핵심 포인트</strong></p>
<ul>
<li><code>MaintenanceInterval</code>마다 <code>UseFuel()</code> 호출로 연료 1개씩 소모</li>
<li>연료가 있으면 계속 타이머 반복, 없으면 <code>ClearTimer()</code>로 중지</li>
<li><code>StartTime</code>을 <code>Replicated</code>로 저장해서 <strong>클라이언트도 경과 시간 계산 가능</strong>하게 함</li>
<li><code>bIsFueling</code> 상태로 현재 가동 중인지 추적</li>
</ul>
<hr>
<h2 id="델리게이트로-ui와-로직-분리">델리게이트로 UI와 로직 분리</h2>
<p>UI 업데이트를 위해 <code>FOnFuelModeChanged</code> 델리게이트를 만들었다. <strong>연료 상태가 바뀔 때마다 브로드캐스트</strong>해서 위젯에서 구독하도록 했다.</p>
<h3 id="델리게이트-선언--사용">델리게이트 선언 &amp; 사용</h3>
<pre><code class="language-cpp">// .h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFuelModeChanged, bool, bIsFueling);

UPROPERTY(BlueprintAssignable, Category = &quot;Lamp|Events&quot;)
FOnFuelModeChanged OnFuelModeChanged;

// .cpp
UPROPERTY(ReplicatedUsing = OnRep_IsFueling)
bool bIsFueling = false;

void ATSStreetLamp::OnRep_IsFueling()
{
    OnFuelModeChanged.Broadcast(bIsFueling);
}</code></pre>
<p><strong>위젯 측 구현</strong></p>
<ul>
<li>위젯에서 <code>OnFuelModeChanged</code>를 구독</li>
<li><code>bIsFueling</code>이 <code>false</code>면 프로그레스바 업데이트 타이머 <strong>pause</strong></li>
<li><code>true</code>면 타이머 <strong>재개</strong>해서 0.2초마다 남은 시간 계산</li>
</ul>
<p>이렇게 하면 <strong>액터 로직과 UI 업데이트가 완전히 분리</strong>되고, 위젯은 상태 변화에만 반응하면 된다.</p>
<hr>
<h2 id="특정-아이템만-받는-커스텀-인벤토리-컴포넌트">특정 아이템만 받는 커스텀 인벤토리 컴포넌트</h2>
<p>램프는 <strong>연료 아이템만 받을 수 있어야</strong> 한다. <code>UTSLampInventory</code>를 만들어서 <code>CanPlaceItemInSlot()</code>을 오버라이드했다.</p>
<pre><code class="language-cpp">bool UTSLampInventory::CanPlaceItemInSlot(int32 StaticDataID, EInventoryType InventoryType, int32 SlotIndex)
{
    if (Super::CanPlaceItemInSlot(StaticDataID, InventoryType, SlotIndex))
    {
        if (StaticDataID != MaintenanceCostID)  // 연료 아이템만 허용
        {
            return false;
        }
    }
    OnFuelTransferred.Broadcast();  // 연료 추가됨 알림
    return true;
}

void UTSLampInventory::ClearSlot(FSlotStructMaster&amp; Slot)
{
    Super::ClearSlot(Slot);
    Slot.ItemData.StaticDataID = MaintenanceCostID;  // 빈 슬롯도 연료 ID로
    Slot.MaxStackSize = MaintenanceCostQty;
}</code></pre>
<p><strong>추가 구현</strong></p>
<ul>
<li><code>Internal_TransferItem()</code>에서도 <code>MaintenanceCostID</code> 체크</li>
<li>연료가 추가되면 <code>OnFuelTransferred</code> 델리게이트 브로드캐스트</li>
<li>램프 액터에서 이걸 구독해서 <code>SetFuelTimer()</code> 호출</li>
</ul>
<pre><code class="language-cpp">// BeginPlay에서 델리게이트 바인딩
LampInventory-&gt;OnFuelTransferred.AddDynamic(this, &amp;ATSStreetLamp::SetFuelTimer);</code></pre>
<p>이렇게 하면 <strong>플레이어가 연료를 넣는 순간 자동으로 타이머가 시작</strong>된다.</p>
<hr>
<h2 id="starttime-저장으로-클라이언트-프로그레스바-구현">StartTime 저장으로 클라이언트 프로그레스바 구현</h2>
<p>서버에서만 타이머가 돌지만, <strong>클라이언트도 프로그레스바를 보여줘야</strong> 한다. 이를 위해 <code>StartTime</code>을 <code>Replicated</code> 변수로 만들었다.</p>
<pre><code class="language-cpp">UPROPERTY(Replicated)
float StartTime = 0.f;

// UseFuel()에서 연료 소모할 때마다 갱신
StartTime = GetWorld()-&gt;GetTimeSeconds();</code></pre>
<p><strong>위젯에서 계산</strong></p>
<pre><code class="language-cpp">// 위젯의 0.2초 타이머에서
float OperatingTime = CurrentTime - StartTime;
float Progress = 1.0f - (OperatingTime / MaintenanceInterval);</code></pre>
<p>이렇게 하면:</p>
<ol>
<li>서버가 연료 소모할 때마다 <code>StartTime</code> 업데이트</li>
<li>클라이언트는 <code>StartTime</code>과 현재 시간 차이로 경과 시간 계산</li>
<li><code>MaintenanceInterval</code>로 나눠서 진행률 계산</li>
</ol>
<p>서버 타이머 없이도 <strong>클라이언트가 독립적으로 프로그레스바 업데이트</strong> 가능!</p>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>Tick을 사용하지 않고 부하를 줄이려고 노력했다. 연료를 소비하거나 불을 끄는 시점만 델리게이트로 확실하게 동기화하고 나머지는 클라이언트에서 예측하여 남은 연료 가동시간을 UI에 나타냈다. 이때도 Tick보다는 타이머를 사용해 램프가 가동되는 순간에만 타이머를 활성화하는 방식으로 부하를 줄였다.</p>
<p>기존의 인벤토리를 커스텀하여 특정 아이템만 넣을 수 있고 최대로 넣을 수 있는 연료 아이템의 수도 제한하도록 <code>CanPlaceItemInSlot()</code>를 오버라이드했다. 기존에는 아이템 데이터에서 MaxStack을 가져와 슬롯에 적용하는 방식이었는데 이번 로직을 구현하기 위해 스택할 때 슬롯의 MaxStack을 가지고 오는 방식으로 수정했다. 기존 인벤토리 컴포넌트를 꽤 확장성 높게 만든 것 같아 뿌듯하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251211]]></title>
            <link>https://velog.io/@s_kim__/TIL-251211</link>
            <guid>https://velog.io/@s_kim__/TIL-251211</guid>
            <pubDate>Thu, 11 Dec 2025 12:45:09 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-12-11</code></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> Physical Material을 활용한 재질별 발소리 시스템 구현</li>
<li><input checked="" disabled="" type="checkbox"> AnimNotify 기반 사운드 트리거링</li>
<li><input checked="" disabled="" type="checkbox"> 멀티플레이어 환경에서의 로컬 사운드 재생</li>
</ul>
<hr>
<h2 id="physical-material을-활용한-재질별-발소리-시스템-구현">Physical Material을 활용한 재질별 발소리 시스템 구현</h2>
<h3 id="🎯-구현-목표">🎯 구현 목표</h3>
<p>바닥 재질(콘크리트, 나무, 금속 등)에 따라 다른 발소리를 재생하는 시스템을 구현. 걷기/달리기/착지/웅크리기/벽타기 각각에 대해 재질별 사운드 지원.</p>
<h3 id="📐-시스템-구조">📐 시스템 구조</h3>
<h4 id="1-footstepcomponent-설계">1. FootstepComponent 설계</h4>
<pre><code class="language-cpp">// 재질별 사운드를 담는 구조체
USTRUCT()
struct FFootstepSound
{
    GENERATED_BODY()
    TObjectPtr&lt;USoundBase&gt; Left;      // 왼발
    TObjectPtr&lt;USoundBase&gt; Right;     // 오른발  
    TObjectPtr&lt;USoundBase&gt; Climbing;  // 벽타기
};

// Physical Surface Type별로 사운드 매핑
TMap&lt;TEnumAsByte&lt;EPhysicalSurface&gt;, FFootstepSound&gt; FootstepSounds;</code></pre>
<p><strong>설계 의도:</strong></p>
<ul>
<li>TMap을 사용해 Surface Type을 키로 사운드 검색 → O(1) 성능</li>
<li>구조체로 묶어 좌/우/등반 사운드를 하나로 관리</li>
<li>TObjectPtr로 안전한 포인터 관리</li>
</ul>
<h4 id="2-재질-감지-linetrace--physical-material">2. 재질 감지: LineTrace + Physical Material</h4>
<pre><code class="language-cpp">void PlayFootstepSound(const FVector&amp; Location, bool&amp; IsLeft)
{
    FCollisionQueryParams Params;
    Params.bReturnPhysicalMaterial = true; // ⭐ 핵심!

    GetWorld()-&gt;LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility, Params);

    EPhysicalSurface SurfaceType = EPhysicalSurface::SurfaceType_Default;
    if (HitResult.PhysMaterial.IsValid())
    {
        SurfaceType = HitResult.PhysMaterial-&gt;SurfaceType;
    }

    // TMap에서 사운드 찾기
    if (USoundBase* Sound = IsLeft 
        ? FootstepSounds.FindRef(SurfaceType).Left 
        : FootstepSounds.FindRef(SurfaceType).Right)
    {
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), Sound, Location);
    }
}</code></pre>
<p><strong>핵심 포인트:</strong></p>
<ul>
<li><code>bReturnPhysicalMaterial = true</code> 없으면 PhysMaterial 정보를 받을 수 없음</li>
<li><code>FindRef</code>는 키가 없어도 안전 (기본값 반환)</li>
<li>Physical Material 미할당 시 → <code>SurfaceType_Default</code> 반환</li>
</ul>
<h3 id="🎬-animnotify-기반-트리거링">🎬 AnimNotify 기반 트리거링</h3>
<h4 id="animnotify-구현">AnimNotify 구현</h4>
<pre><code class="language-cpp">// AN_Footstep.h
UCLASS()
class UAN_Footstep : public UAnimNotify
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere)
    FName SocketName = TEXT(&quot;foot_l&quot;);

    UPROPERTY(EditAnywhere)
    bool IsLeft = true;

    virtual void Notify(USkeletalMeshComponent* MeshComp, 
                       UAnimSequenceBase* Animation, 
                       const FAnimNotifyEventReference&amp; EventReference) override;
};</code></pre>
<p><strong>주의사항:</strong></p>
<ul>
<li><code>Super::Notify()</code> 호출 시 Blueprint 이벤트와 충돌 가능 → 호출하지 않는 것을 권장</li>
<li><code>Received_Notify</code>는 BlueprintImplementableEvent → C++에서 override 불가</li>
<li>UE 5.0+ 이후 시그니처에 <code>FAnimNotifyEventReference</code> 파라미터 추가됨</li>
</ul>
<h3 id="🎯-특수-케이스-처리">🎯 특수 케이스 처리</h3>
<h4 id="1-착지-소리">1. 착지 소리</h4>
<pre><code class="language-cpp">// TSCharacter.cpp
void ATSCharacter::Landed(const FHitResult&amp; Hit)
{
    Super::Landed(Hit);
    FootstepComponent-&gt;PlayFootstepSoundFromHit(Hit);
}</code></pre>
<p><strong>문제:</strong> <code>Landed()</code>의 Hit는 <code>PhysMaterial</code> 정보가 없음</p>
<ul>
<li>CharacterMovementComponent의 내부 Sweep이 Physical Material을 요청하지 않음</li>
</ul>
<p><strong>해결:</strong> Hit 위치에서 다시 짧은 LineTrace 수행</p>
<pre><code class="language-cpp">void PlayFootstepSoundFromHit(const FHitResult&amp; Hit)
{
    // Hit 위치에서 재질 감지를 위한 재 LineTrace
    FVector Start = Hit.ImpactPoint;
    FVector End = Hit.ImpactPoint - FVector(0, 0, 50);

    FCollisionQueryParams Params;
    Params.bReturnPhysicalMaterial = true; // 필수!

    GetWorld()-&gt;LineTraceSingleByChannel(...);
}</code></pre>
<h4 id="2-벽타기-소리">2. 벽타기 소리</h4>
<pre><code class="language-cpp">void PlayClimbingSound(const FVector&amp; Location)
{
    // 캐릭터 앞 방향으로 LineTrace
    FVector Forward = GetOwner()-&gt;GetActorForwardVector(); // 정규화됨 (크기=1)
    FVector End = Location + Forward * 100.f;

    // 벽 재질 감지 후 Climbing 사운드 재생
}</code></pre>
<p><strong>학습:</strong></p>
<ul>
<li><code>GetActorForwardVector()</code>는 항상 정규화된 벡터 반환 (크기 = 1)</li>
<li>벡터 연산 후 크기 변경 시 <code>GetSafeNormal()</code> 재정규화 필요</li>
</ul>
<h3 id="🌐-멀티플레이어-고려사항">🌐 멀티플레이어 고려사항</h3>
<h4 id="복제-원리">복제 원리</h4>
<pre><code>애니메이션 복제됨 (Character Movement 자동)
  ↓
모든 클라이언트에서 애니메이션 재생
  ↓
각 클라이언트에서 AnimNotify 실행
  ↓
각 클라이언트에서 로컬 사운드 재생 (RPC 불필요!)</code></pre><p><strong>핵심:</strong></p>
<ul>
<li>RPC를 사용하지 않음 → 네트워크 부하 없음</li>
<li>애니메이션이 복제되므로 자연스럽게 동기화</li>
<li>&quot;애니메이션이 보이면 소리도 들린다&quot; 원칙 성립</li>
</ul>
<p><strong>주의:</strong></p>
<pre><code class="language-cpp">// ❌ 이렇게 하면 안 됨
if (Owner-&gt;IsLocallyControlled()) 
{
    PlaySound(); // 자기 캐릭터만 소리 남
}

// ✅ 조건 없이 모든 클라이언트에서 재생
PlaySound();</code></pre>
<h3 id="🛠️-최적화--안정성">🛠️ 최적화 &amp; 안정성</h3>
<h4 id="1-gc-방지">1. GC 방지</h4>
<pre><code class="language-cpp">void BeginPlay()
{
    // 자주 사용하는 사운드를 GC로부터 보호
    for (const auto&amp; Pair : FootstepSounds)
    {
        if (Pair.Value.Left) Pair.Value.Left-&gt;AddToRoot();
        if (Pair.Value.Right) Pair.Value.Right-&gt;AddToRoot();
        if (Pair.Value.Climbing) Pair.Value.Climbing-&gt;AddToRoot();
    }
}</code></pre>
<h4 id="2-buildcs-설정">2. Build.cs 설정</h4>
<pre><code class="language-csharp">PublicDependencyModuleNames.AddRange(new string[] 
{ 
    &quot;PhysicsCore&quot;  // Physical Material 사용 시 필수!
});</code></pre>
<ul>
<li>누락 시 링크 에러: <code>Z_Construct_UEnum_PhysicsCore_EPhysicalSurface</code></li>
</ul>
<h4 id="3-project-settings">3. Project Settings</h4>
<ul>
<li><strong>Physics &gt; Physical Surface</strong>: SurfaceType1~3 정의</li>
<li><strong>Physics &gt; Default Physical Material</strong>: 미할당 재질의 기본값 설정</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p><strong>Physical Material 시스템</strong>
엔진 표준 기능을 활용하니 파티클, 데칼 등 다른 시스템과도 자연스럽게 연동되는 것이 인상적이었다. 단순히 발소리만이 아니라 발자국 파티클, 재질별 마찰음 등으로 확장 가능한 설계라는 점이 좋았다.</p>
<p><strong>멀티플레이어 사운드 복제</strong>
RPC 없이도 애니메이션 복제만으로 사운드가 동기화되는 구조가 매우 편리했다. 각 클라이언트가 로컬에서 재생하므로 네트워크 부하도 없고, 타이밍도 정확하다. Listen Server 구조에서 특히 효율적이다.</p>
<p><strong><code>Landed()</code>의 PhysMaterial 미제공 이슈</strong>
CharacterMovementComponent의 내부 로직이 Physical Material을 요청하지 않는다는 점이 까다로웠다. Hit 위치에서 재 LineTrace를 해야 한다는 해결책을 찾기까지 시간이 걸렸다. 엔진의 내부 동작을 완전히 이해하는 것이 얼마나 중요한지 다시 깨달았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251209]]></title>
            <link>https://velog.io/@s_kim__/TIL-251209</link>
            <guid>https://velog.io/@s_kim__/TIL-251209</guid>
            <pubDate>Tue, 09 Dec 2025 12:57:21 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p>📅 <code>2025-12-09</code></p>
<ul>
<li><input disabled="" type="checkbox"> 인터페이스 래퍼 + 팩토리 패턴으로 카테고리별 스탯 표시 로직 분리</li>
</ul>
<hr>
<h2 id="인터페이스-래퍼--팩토리-패턴으로-카테고리별-스탯-표시-로직-분리">인터페이스 래퍼 + 팩토리 패턴으로 카테고리별 스탯 표시 로직 분리</h2>
<h3 id="📌-문제-상황">📌 문제 상황</h3>
<p>빌딩/아이템 스탯 표시 시스템에서:</p>
<ul>
<li><code>FItemData</code>와 <code>FBuildingData</code>는 완전히 다른 구조체 → 함수를 따로 만들어야 함</li>
<li>각 카테고리(TOOL, WEAPON, ARMOR, CONSUMABLE, WALL, FLOOR...)마다 표시할 스탯이 다름</li>
<li>Switch-case로 처리하니 코드가 길어지고, 새 카테고리 추가 시 기존 코드 수정 필요</li>
</ul>
<pre><code class="language-cpp">// Before: 너무 길고 유지보수 어려움
void UpdateCraftingItemStatView(const FItemData&amp; ItemData)
{
    switch (ItemData.Category)
    {
    case EItemCategory::TOOL:
        // 채취 유형, 공격력, 내구도... 50줄
        break;
    case EItemCategory::WEAPON:
        // 공격력, 사거리, 내구도... 50줄
        break;
    // ...
    }
}

void UpdateBuildingItemStatView(const FBuildingData&amp; BuildingData)
{
    switch (BuildingData.Type) { /* ... */ }
}</code></pre>
<h3 id="🎯-해결-방법">🎯 해결 방법</h3>
<p><strong>인터페이스 래퍼 + 팩토리 패턴</strong> 조합으로 해결</p>
<h4 id="1-인터페이스-래퍼로-데이터-통합">1. 인터페이스 래퍼로 데이터 통합</h4>
<pre><code class="language-cpp">// IDisplayDataProvider (순수 C++ 인터페이스)
class IDisplayDataProvider
{
public:
    virtual FGameplayTag GetCategoryTag() const = 0;
    virtual float GetStatValue(const FGameplayTag&amp; StatTag) const = 0;
    // ...
};

// 각 데이터 타입을 래핑
class FItemDataProvider : public IDisplayDataProvider { /* ... */ };
class FBuildingDataProvider : public IDisplayDataProvider { /* ... */ };</code></pre>
<h4 id="2-팩토리-패턴으로-카테고리별-viewer-생성">2. 팩토리 패턴으로 카테고리별 Viewer 생성</h4>
<pre><code class="language-cpp">// 추상 클래스
UCLASS(Abstract)
class UStatViewer : public UObject
{
    virtual void ShowStats(
        UListView* ListView,
        const IDisplayDataProvider&amp; DataProvider,
        UGameplayTagDisplaySubsystem* DisplaySystem) PURE_VIRTUAL();
};

// 카테고리별 구현
UCLASS() class UToolStatViewer : public UStatViewer { /* 도구 스탯 표시 */ };
UCLASS() class UWeaponStatViewer : public UStatViewer { /* 무기 스탯 표시 */ };
UCLASS() class UWallStatViewer : public UStatViewer { /* 벽 스탯 표시 */ };

// 팩토리가 적절한 Viewer 반환
UCLASS()
class UStatViewerFactory : public UObject
{
    UStatViewer* GetViewer(const FGameplayTag&amp; CategoryTag)
    {
        if (CategoryTag == TAG_Category_Tool)
            return CachedToolViewer;
        // ...
    }
};</code></pre>
<h4 id="3-위젯에서-통합-사용">3. 위젯에서 통합 사용</h4>
<pre><code class="language-cpp">// After: 5줄로 축약, 모든 타입 통합!
void UItemInfo::UpdateStatViewInternal(const IDisplayDataProvider&amp; DataProvider)
{
    FGameplayTag CategoryTag = DataProvider.GetCategoryTag();
    UStatViewer* Viewer = ViewerFactory-&gt;GetViewer(CategoryTag);
    Viewer-&gt;ShowStats(ItemStatView, DataProvider, TagDisplaySubsystem);
}

void UpdateCraftingItemStatView(const FItemData&amp; ItemData)
{
    FItemDataProvider Provider(ItemData);
    UpdateStatViewInternal(Provider);
}

void UpdateBuildingItemStatView(const FBuildingData&amp; BuildingData)
{
    FBuildingDataProvider Provider(BuildingData);
    UpdateStatViewInternal(Provider);
}</code></pre>
<h3 id="🔑-핵심-포인트">🔑 핵심 포인트</h3>
<ol>
<li><p><strong>인터페이스 래퍼 (IDisplayDataProvider)</strong></p>
<ul>
<li>서로 다른 구조체를 통일된 방식으로 접근</li>
<li>순수 C++ 인터페이스로 가볍게 구현</li>
<li>스택에 생성 가능 (임시 래퍼)</li>
</ul>
</li>
<li><p><strong>팩토리 패턴 (UStatViewerFactory)</strong></p>
<ul>
<li>카테고리 태그로 적절한 Viewer 반환</li>
<li>Viewer들을 캐싱해서 재사용 (성능)</li>
<li>새 카테고리 추가 시 Factory에 한 줄만 추가</li>
</ul>
</li>
<li><p><strong>Strategy 패턴 (UStatViewer)</strong></p>
<ul>
<li>각 카테고리별 스탯 표시를 독립 클래스로</li>
<li>각 Viewer가 자기 카테고리 스탯만 담당</li>
<li>OCP 준수 (Open-Closed Principle)</li>
</ul>
</li>
</ol>
<h3 id="📂-폴더-구조">📂 폴더 구조</h3>
<pre><code>UI/StatDisplay/
├── DisplayDataProvider.h           # 인터페이스
├── Providers/
│   ├── ItemDataProvider.h/cpp      # 아이템 래퍼
│   └── BuildingDataProvider.h/cpp  # 빌딩 래퍼
├── Viewers/
│   ├── StatViewer.h/cpp            # 추상 클래스
│   ├── Items/
│   │   ├── ToolStatViewer.h/cpp
│   │   ├── WeaponStatViewer.h/cpp
│   │   └── ...
│   └── Buildings/
│       ├── CraftingStatViewer.h/cpp
│       └── ...
└── StatViewerFactory.h/cpp         # 팩토리</code></pre><h3 id="✅-장점">✅ 장점</h3>
<ul>
<li>서로 다른 데이터 타입을 하나의 함수로 처리</li>
<li>각 카테고리 로직이 독립적 (유지보수 쉬움)</li>
<li>새 카테고리 추가 시 기존 코드 수정 불필요</li>
<li>각 Viewer별 단위 테스트 가능</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>처음엔 switch-case가 간단해 보였는데 카테고리가 늘어나면서 함수가 너무 길어졌다. 인터페이스 래퍼로 데이터를 추상화하고, 팩토리 패턴으로 각 로직을 분리하니까 코드가 훨씬 깔끔해졌다. 특히 FItemData와 FBuildingData를 전혀 건드리지 않고 Provider로만 감싸서 해결할 수 있는게 신기했다.</p>
<p>팩토리 패턴이 처음엔 오버엔지니어링 같았는데, 이후 빌딩의 특정 카테고리의 내용만 수정할 때 훨씬 간단해 편했다. 그리고 각 Viewer 클래스가 30~50줄 정도로 짧아서 읽기도 편하고 테스트하기도 좋았다.</p>
<p>UCLASS로 구현한 Viewer 클래스들이 순수 C++ 인터페이스(IDisplayDataProvider)를 파라미터로 받아서 쓸 수 있다는 게 새로웠다. UObject 계열과 순수 C++을 조합해서 쓸 수 있다는 걸 몰랐는데, 스택에 생성한 Provider를 UObject인 Viewer가 참조로 받아서 사용하는 구조가 깔끔했다. 언리얼에서도 순수 C++과 디자인 패턴을 실전에 제대로 적용해본 경험이었다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251205]]></title>
            <link>https://velog.io/@s_kim__/TIL-251205</link>
            <guid>https://velog.io/@s_kim__/TIL-251205</guid>
            <pubDate>Fri, 05 Dec 2025 13:07:57 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p>📅 <code>2025-12-05</code></p>
<ul>
<li><input disabled="" type="checkbox"> 아이템 상세데이터 표시 정보 관리를 위한 GameInstanceSubsystem 구현</li>
</ul>
<hr>
<h2 id="아이템-상세데이터-표시-정보-관리를-위한-gameinstancesubsystem-구현">아이템 상세데이터 표시 정보 관리를 위한 GameInstanceSubsystem 구현</h2>
<h3 id="📌-문제-상황">📌 문제 상황</h3>
<p>빌딩/크래프팅 위젯의 결과물 상세페이지에서 스탯을 표시할 때 각 스탯과 효과마다 표시 용어가 다름</p>
<ul>
<li>&quot;체력 회복&quot;, &quot;Health Recovery&quot; 같은 다국어 표시명</li>
<li>&quot;초&quot;, &quot;%&quot;, &quot;개&quot; 같은 단위</li>
<li>아이템 카테고리(TOOL, WEAPON, ARMOR, CONSUMABLE)마다 보여줄 스탯이 다름</li>
<li>하드코딩하면 관리가 어렵고 확장성이 떨어짐</li>
</ul>
<h3 id="🎯-해결-방법">🎯 해결 방법</h3>
<p><strong>DeveloperSettings + GameInstanceSubsystem + DataTable</strong> 조합으로 중앙 관리 시스템 구축</p>
<h4 id="1-developersettings로-dt-경로-관리">1. DeveloperSettings로 DT 경로 관리</h4>
<pre><code class="language-cpp">// GameplayTagDisplaySettings.h
UCLASS(Config=Game, DefaultConfig)
class UGameplayTagDisplaySettings : public UDeveloperSettings
{
    UPROPERTY(Config, EditAnywhere)
    TSoftObjectPtr&lt;UDataTable&gt; GameplayTagDisplayDataTable;
};</code></pre>
<ul>
<li>Project Settings에서 DT 경로를 설정 가능</li>
<li>하드코딩 없이 에디터에서 변경 가능</li>
</ul>
<h4 id="2-gameinstancesubsystem에서-초기화--캐싱">2. GameInstanceSubsystem에서 초기화 &amp; 캐싱</h4>
<pre><code class="language-cpp">void UGameplayTagDisplaySubsystem::Initialize(FSubsystemCollectionBase&amp; Collection)
{
    const UGameplayTagDisplaySettings* Settings = GetDefault&lt;UGameplayTagDisplaySettings&gt;();
    GameplayTagDisplayTable = Settings-&gt;GameplayTagDisplayDataTable.LoadSynchronous();

    // TMap으로 캐싱 (O(1) 조회)
    TArray&lt;FGameplayTagDisplayData*&gt; Rows;
    DataTable-&gt;GetAllRows&lt;FGameplayTagDisplayData&gt;(TEXT(&quot;LoadDataTable&quot;), Rows);
    for (FGameplayTagDisplayData* Row : Rows)
    {
        CachedDisplayData.Add(Row-&gt;Tag, *Row);
    }
}</code></pre>
<h4 id="3-위젯에서-실제-사용">3. 위젯에서 실제 사용</h4>
<pre><code class="language-cpp">void UItemInfo::UpdateCraftingItemStatView(const FItemData&amp; ItemData)
{
    TagDisplaySubsystem = UGameplayTagDisplaySubsystem::Get(this);
    ItemStatView-&gt;ClearListItems();

    switch (ItemData.Category)
    {
    case EItemCategory::TOOL:
        // 채취 유형 - 여러 태그를 쉼표로 연결
        UStatDataObject* StatData = NewObject&lt;UStatDataObject&gt;();
        StatData-&gt;StatName = TagDisplaySubsystem-&gt;GetDisplayName_KR(TAG_Display_Stat_HarvestTarget);
        StatData-&gt;StatValue = TagDisplaySubsystem-&gt;GetDisplayNamesFromContainer_KR(
            ItemData.ToolData.HarvestTargetTag);
        ItemStatView-&gt;AddItem(StatData);

        // 공격력 - 값 + 단위 포맷팅
        GetStatData_KR(*StatData, TAG_Display_Stat_AttackDamage, ItemData.ToolData.DamageValue);
        ItemStatView-&gt;AddItem(StatData);
        break;
    }
}

void UItemInfo::GetStatData_KR(UStatDataObject&amp; OutStatData, const FGameplayTag&amp; StatTag, const float&amp; StatValue)
{
    FGameplayTagDisplayData DisplayData;
    if (TagDisplaySubsystem-&gt;GetDisplayData(StatTag, DisplayData))
    {
        OutStatData.StatName = DisplayData.DisplayName_KR;
        OutStatData.StatValue = FText::AsNumber(StatValue);
        OutStatData.StatUnit = TagDisplaySubsystem-&gt;GetUnit(StatTag); // &quot;초&quot;, &quot;%&quot;, &quot;개&quot; 등
    }
}</code></pre>
<h3 id="🔑-핵심-포인트">🔑 핵심 포인트</h3>
<ol>
<li><strong>DeveloperSettings 활용</strong>: 에디터에서 직접 DT 경로 설정, 코드 수정 없이 변경 가능</li>
<li><strong>Initialize에서 캐싱</strong>: 런타임마다 DT를 순회하지 않고 TMap으로 O(1) 조회</li>
<li><strong>Static Get() 헬퍼</strong>: WorldContextObject만 있으면 어디서든 접근 가능</li>
<li><strong>유틸리티 함수 제공</strong>: <ul>
<li><code>GetDisplayName_KR/EN</code> - 단일 태그 표시명</li>
<li><code>GetDisplayNamesFromContainer_KR</code> - 여러 태그를 쉼표로 연결</li>
<li><code>GetUnit</code> - 단위만 가져오기</li>
<li><code>GetDisplayData</code> - 전체 데이터 한 번에</li>
</ul>
</li>
<li><strong>ListView와의 조합</strong>: UStatDataObject를 동적으로 생성해서 AddItem → 카테고리별로 다른 스탯 표시</li>
</ol>
<h3 id="📂-구조">📂 구조</h3>
<pre><code>Settings (에디터) → Subsystem (캐싱) → Widget (사용)
     ↓                    ↓                ↓
  DT 경로 설정      Initialize에서      Get()으로 접근
                  TMap에 캐싱         GetDisplayName_KR()</code></pre><hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>처음엔 효과 태그, 스탯 별로 출력할 이름과 단위를 매핑하는 함수를 만들다가 효과가 추가되거나 이름을 변경하고 싶을 때 불편함이 커질것 같아 데이터 테이블로 관리하는 방식을 선택했다. 그리고 데이터 테이블에서 데이터를 가져오기 위해 게임인스턴스 서브시스템을 사용했다.</p>
<p>그리고 위젯에서 아이템 카테고리별로 분기 처리하는 부분이 좀 지저분해 보이긴 하는데 이건 나중에 팩토리 패턴으로 개선해봐야겠다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드카타] 섬 연결하기]]></title>
            <link>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EC%84%AC-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EC%84%AC-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 Dec 2025 02:18:10 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42861">문제링크</a></p>
<h2 id="틀린-풀이">틀린 풀이</h2>
<p>0번 섬에서 시작하여 모든 섬을 한번씩 방문하는 최소 비용을 찾는 로직이라 틀림</p>
<pre><code class="language-cpp">#include &lt;vector&gt;
#include &lt;unordered_map&gt;

using namespace std;
struct bridge
{
    int island;
    int cost;
};
void dfs(unordered_map&lt;int, vector&lt;bridge&gt;&gt;&amp; myMap, vector&lt;bool&gt;&amp; visited, int visitedCount, int island, int cost, int&amp; minCost)
{
    if(visitedCount == visited.size())
    {
        minCost = minCost&lt;cost? minCost : cost;
        return;
    }
    for(const auto&amp; m:myMap[island])
    {
        if(visited[m.island]) continue;
        visited[m.island] = true;
        dfs(myMap, visited, visitedCount+1, m.island, cost+m.cost, minCost);
        visited[m.island] = false;
    }
}
int solution(int n, vector&lt;vector&lt;int&gt;&gt; costs) {
    int answer = 1e9;
    unordered_map&lt;int, vector&lt;bridge&gt;&gt; myMap;
    for(auto&amp; v:costs)
    {
        myMap[v[0]].push_back({v[1], v[2]});
        myMap[v[1]].push_back({v[0], v[2]});
    }
    vector&lt;bool&gt; visited(n,false);
    visited[0] = true;
    dfs(myMap, visited, 1, 0, 0, answer);
    return answer;
}</code></pre>
<h2 id="최종---kruskalunion-find">최종 - Kruskal(Union-Find)</h2>
<p>최소 신장 트리(MST)의 개념으로 접근하여 해결했다. 
최소 신장 트리: 모든 노드를 연결하며 사이클이 없는 부분 그래프(신장 트리) 중 가중치의 합이 최소가 되는 트리</p>
<pre><code class="language-cpp">#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;

int parent[100];

int find(int x)
{
    // 경로 압축
    if(parent[x] == x) return x;
    return parent[x] = find(parent[x]);
}

void unite(int a, int b)
{
    a = find(a);
    b = find(b);
    parent[a] = b;
}

int solution(int n, vector&lt;vector&lt;int&gt;&gt; costs) {
    int answer = 0;

    // parent 초기화
    for(int i=0; i&lt;n; i++)
    {
        parent[i] = i;
    }

    sort(costs.begin(), costs.end(),
        [](const auto&amp; a, const auto&amp; b)
         {
             return a[2]&lt;b[2];
         });
    int edgeCount = 0;
    for(const auto&amp; cost:costs)
    {
        int from = cost[0];
        int to = cost[1];
        int weight = cost[2];

        // 최상위 부모가 같지 않으면 선택
        if(find(from)!=find(to))
        {
            unite(from, to);
            answer += weight;
            ++edgeCount;

            // n-1개의 간선을 선택하면 종료
            if(edgeCount == n-1)
            {
                break;
            }
        }
    }
    return answer;
}</code></pre>
<h1 id="최소-신장-트리mst-찾는-기법">최소 신장 트리(MST) 찾는 기법</h1>
<h2 id="union---find"><code>Union - Find</code></h2>
<h3 id="개념">개념</h3>
<p>서로소 집합(Disjoint Set)을 표현하는 자료구조. &quot;두 원소가 같은 집합에 속하는지&quot; 빠르게 판단하고, 두 집합을 합칠 수 있음.</p>
<h3 id="핵심-연산">핵심 연산</h3>
<ol>
<li><strong>Find</strong>: 원소가 속한 집합의 대표(루트) 찾기</li>
<li><strong>Union</strong>: 두 집합을 하나로 합치기</li>
</ol>
<h3 id="구현">구현</h3>
<pre><code class="language-cpp">int parent[MAX];

// 초기화: 각자가 자신의 부모
for(int i = 0; i &lt; n; i++)
    parent[i] = i;

// Find: 루트 찾기 + 경로 압축
int find(int x) {
    if(parent[x] == x) return x;
    return parent[x] = find(parent[x]);  // 경로 압축
}

// Union: 두 집합 합치기
void union(int a, int b) {
    a = find(a);
    b = find(b);
    if(a != b) parent[a] = b;
}

// 같은 집합인지 확인
if(find(a) == find(b)) // 같은 집합</code></pre>
<h3 id="경로-압축-path-compression">경로 압축 (Path Compression)</h3>
<p><code>return parent[x] = find(parent[x]);</code></p>
<ul>
<li>find 호출 시 경로상 모든 노드를 루트에 직접 연결</li>
<li>트리 높이를 줄여 다음 find를 O(1)로 만듦</li>
</ul>
<h3 id="시간-복잡도">시간 복잡도</h3>
<ul>
<li>경로 압축 사용 시: <strong>O(α(n))</strong> ≈ O(1) (아커만 역함수, 실질적 상수)</li>
<li>경로 압축 없으면: O(n)</li>
</ul>
<h3 id="활용">활용</h3>
<ul>
<li><strong>최소 신장 트리(MST)</strong>: Kruskal 알고리즘에서 사이클 검사</li>
<li><strong>네트워크 연결성</strong>: 두 노드가 연결되어 있는지 판단</li>
<li><strong>동적 연결성</strong>: 간선 추가하면서 실시간으로 연결 여부 확인</li>
</ul>
<h3 id="kruskal에서의-역할">Kruskal에서의 역할</h3>
<pre><code class="language-cpp">sort(edges);  // 간선을 비용순 정렬
for(간선 in edges) {
    if(find(u) != find(v)) {  // 사이클 안 생기면
        union(u, v);           // 연결
        answer += cost;
    }
}</code></pre>
<p><strong>핵심</strong>: Union-Find로 &quot;이 간선을 추가하면 사이클이 생기는가?&quot;를 O(1)에 판단한다.</p>
<h2 id="kruskal-알고리즘"><code>Kruskal</code> 알고리즘</h2>
<h3 id="kruskal--union-find--그리디">Kruskal = Union-Find + 그리디</h3>
<pre><code>Union-Find: &quot;사이클 검사 도구&quot;
     +
그리디: &quot;비용이 작은 간선부터 선택하는 전략&quot;
     =
Kruskal 알고리즘</code></pre><h3 id="알고리즘-순서">알고리즘 순서</h3>
<pre><code class="language-cpp">// 1. 간선을 비용 기준 오름차순 정렬 ⭐ (그리디)
sort(edges, 비용순);

// 2. 작은 비용부터 하나씩 확인
for(간선 in edges) {
    // 3. Union-Find로 사이클 체크
    if(find(u) != find(v)) {  // 다른 집합이면
        union(u, v);           // 합치기
        answer += cost;
        edgeCount++;

        // 4. n-1개 선택하면 완료
        if(edgeCount == n-1) break;
    }
}</code></pre>
<h3 id="핵심-아이디어">핵심 아이디어</h3>
<table>
<thead>
<tr>
<th>요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>정렬 (그리디)</strong></td>
<td>비용 작은 간선부터 고려</td>
</tr>
<tr>
<td><strong>Union-Find</strong></td>
<td>사이클 안 생기는지 체크</td>
</tr>
<tr>
<td><strong>n-1개 선택</strong></td>
<td>트리는 n개 노드에 n-1개 간선</td>
</tr>
</tbody></table>
<p><strong>왜 이게 최소 신장 트리?</strong></p>
<ol>
<li><strong>비용이 작은 것부터</strong> 선택 (그리디)</li>
<li><strong>사이클만 안 만들면</strong> 무조건 선택</li>
<li>결과적으로 <strong>최소 비용으로 모든 노드 연결</strong></li>
</ol>
<h3 id="시간-복잡도-1">시간 복잡도</h3>
<ul>
<li>정렬: <strong>O(E log E)</strong></li>
<li>Union-Find: O(E α(V)) ≈ O(E)</li>
<li><strong>전체: O(E log E)</strong> (정렬이 지배)</li>
</ul>
<p><strong>정리</strong>: Kruskal은 Union-Find를 사이클 체크 도구로 사용하면서, <strong>&quot;비용 작은 간선부터 선택&quot;</strong>이라는 그리디 전략을 추가한 MST 알고리즘</p>
<h2 id="prim-알고리즘"><code>Prim</code> 알고리즘</h2>
<h3 id="kruskal-vs-prim-핵심-차이">Kruskal vs Prim 핵심 차이</h3>
<table>
<thead>
<tr>
<th></th>
<th>Kruskal</th>
<th>Prim</th>
</tr>
</thead>
<tbody><tr>
<td><strong>접근</strong></td>
<td>간선 중심</td>
<td>정점 중심</td>
</tr>
<tr>
<td><strong>방식</strong></td>
<td>모든 간선 정렬 후 선택</td>
<td>한 정점에서 시작해 확장</td>
</tr>
<tr>
<td><strong>자료구조</strong></td>
<td>Union-Find</td>
<td>우선순위 큐</td>
</tr>
<tr>
<td><strong>느낌</strong></td>
<td>정렬 + 그리디</td>
<td>BFS의 변형</td>
</tr>
</tbody></table>
<h3 id="알고리즘-순서-1">알고리즘 순서</h3>
<pre><code class="language-cpp">// 1. 시작 정점 선택 (보통 0번)
visited[0] = true;

// 2. 시작 정점의 모든 간선을 우선순위 큐에 추가
for(간선 in graph[0])
    pq.push(간선);

// 3. 비용 작은 간선부터 꺼내기
while(!pq.empty()) {
    edge = pq.top(); pq.pop();

    // 이미 방문한 정점이면 스킵
    if(visited[edge.to]) continue;

    // 새 정점 연결
    visited[edge.to] = true;
    answer += edge.cost;

    // 새로 연결된 정점의 간선들 추가
    for(간선 in graph[edge.to])
        if(!visited[간선.to])
            pq.push(간선);
}</code></pre>
<h3 id="핵심-아이디어-1">핵심 아이디어</h3>
<p><strong>&quot;현재 트리에 연결 가능한 간선 중 가장 비용이 작은 것 선택&quot;</strong></p>
<ul>
<li>트리를 <strong>점진적으로 확장</strong></li>
<li>항상 <strong>연결된 상태 유지</strong></li>
<li>우선순위 큐로 <strong>최소 비용 간선</strong> 선택</li>
</ul>
<h3 id="시각화">시각화</h3>
<pre><code>Kruskal (간선 관점):
모든 간선 보고 → 작은 것부터 선택 → 사이클만 피하기

Prim (정점 관점):
시작점 → 인접 정점 중 가장 가까운 곳 → 계속 확장</code></pre><h3 id="시간-복잡도-2">시간 복잡도</h3>
<ul>
<li>우선순위 큐: <strong>O(E log E)</strong></li>
<li><strong>전체: O(E log E)</strong> 또는 O(E log V)</li>
</ul>
<h3 id="언제-뭘-쓸까">언제 뭘 쓸까?</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 알고리즘</th>
</tr>
</thead>
<tbody><tr>
<td>간선이 정렬되어 있음</td>
<td>Kruskal</td>
</tr>
<tr>
<td>간선 수가 적음 (희소 그래프)</td>
<td>Kruskal</td>
</tr>
<tr>
<td>간선 수가 많음 (밀집 그래프)</td>
<td>Prim</td>
</tr>
<tr>
<td>특정 정점에서 시작</td>
<td>Prim</td>
</tr>
</tbody></table>
<p><strong>둘 다 정확도와 시간복잡도는 같다.</strong> 구현 편의나 그래프 특성에 따라 선택.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251203]]></title>
            <link>https://velog.io/@s_kim__/TIL-251203</link>
            <guid>https://velog.io/@s_kim__/TIL-251203</guid>
            <pubDate>Wed, 03 Dec 2025 12:24:56 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-12-03</code></p>
<ul>
<li><input disabled="" type="checkbox"> <strong>빌딩 컴포넌트</strong></li>
<li><input disabled="" type="checkbox"> 건축 시스템 설치 구역 검증 로직</li>
<li><input disabled="" type="checkbox"> LightSource 범위 체크로 설치 가능 영역 제한</li>
<li><input disabled="" type="checkbox"> 서버 권한 기반 레시피 재료 검증 및 소비</li>
</ul>
<hr>
<h1 id="빌딩-컴포넌트">빌딩 컴포넌트</h1>
<p>상자나 제작대 등 각각 다른 상호작용이 필요한 액터를 제작 및 설치하는 빌딩 컴포넌트를 구현했다.
<img src="https://velog.velcdn.com/images/s_kim__/post/ce8f9c8f-fcef-4e98-8905-972fd945d1fd/image.gif" alt=""></p>
<h2 id="설치-구역-검증-시스템">설치 구역 검증 시스템</h2>
<h3 id="validateplacement---3단계-검증">ValidatePlacement - 3단계 검증</h3>
<pre><code class="language-cpp">bool UTSBuildingComponent::ValidatePlacement(FHitResult HitResult)
{
    // 1. 지면 검사 (경사도 체크)
    const float DotProduct = FVector::DotProduct(HitResult.Normal, FVector::UpVector);
    if (DotProduct &lt; 0.7f) return false;

    // 2. 충돌 체크
    if (!CheckOverlap(HitResult.Location, CheckExtent)) return false;

    // 3. LightSource 범위 체크
    if (!IsInLightSourceRange(HitResult.Location)) return false;

    return true;
}</code></pre>
<p><strong>검증 순서의 의미</strong></p>
<ul>
<li>연산 비용이 낮은 순서로 배치 (DotProduct → BoxOverlap → SphereOverlap)</li>
<li>Early exit 패턴으로 불필요한 검사 스킵</li>
<li>각 단계가 독립적으로 실패 가능</li>
</ul>
<h3 id="isinlightsourcerange---광원-범위-검사">IsInLightSourceRange - 광원 범위 검사</h3>
<pre><code class="language-cpp">bool UTSBuildingComponent::IsInLightSourceRange(const FVector&amp; Location) const
{
    TArray&lt;AActor*&gt; FoundActors;
    UKismetSystemLibrary::SphereOverlapActors(
        GetWorld(), Location, LightSourceDetectionRadius,
        ObjectTypes, nullptr, IgnoreActors, FoundActors);

    for (AActor* Actor : FoundActors)
    {
        AErosionLightSourceSubActor* LightSource = Cast&lt;AErosionLightSourceSubActor&gt;(Actor);
        if (LightSource &amp;&amp; LightSource-&gt;GetLightscale() &gt; 0.f)
        {
            return true;
        }
    }
    return false;
}</code></pre>
<p><strong>핵심 로직</strong></p>
<ul>
<li>5000.f 반경 내 LightSource 탐색</li>
<li><code>GetLightscale() &gt; 0.f</code> 체크로 활성화된 광원만 인정</li>
<li>게임 디자인: 빛의 영역 내에서만 건설 가능</li>
</ul>
<h3 id="checkoverlap---충돌-필터링">CheckOverlap - 충돌 필터링</h3>
<pre><code class="language-cpp">bool UTSBuildingComponent::CheckOverlap(const FVector&amp; Location, const FVector&amp; Extent)
{
    // BoxOverlap으로 Pawn, WorldDynamic, WorldStatic 체크
    bool bHasOverlap = UKismetSystemLibrary::BoxOverlapActors(...);
    if (!bHasOverlap) return true;

    for (AActor* OverlappedActor : OutActors)
    {
        if (OverlappedActor-&gt;IsA(APawn::StaticClass())) return false;
        if (OverlappedActor-&gt;ActorHasTag(FName(&quot;BlockBuilding&quot;))) return false;
    }
    return true;
}</code></pre>
<p><strong>Extent 계산</strong></p>
<pre><code class="language-cpp">FBoxSphereBounds Bounds = PreviewMeshComp-&gt;GetStaticMesh()-&gt;GetBounds();
CheckExtent = Bounds.BoxExtent * 0.9f; // 90% 크기로 체크</code></pre>
<ul>
<li>0.9배로 줄여서 경계선 false positive 방지</li>
<li>플레이어에게 더 관대한 설치 경험</li>
</ul>
<h3 id="실시간-피드백---프리뷰-색상">실시간 피드백 - 프리뷰 색상</h3>
<pre><code class="language-cpp">void UTSBuildingComponent::UpdatePreviewMesh(float DeltaTime)
{
    bCanPlace = ValidatePlacement(HitResult);

    // 상태 변경 시에만 머티리얼 업데이트
    if (bCanPlace != bLastCanPlace &amp;&amp; CachedDynamicMaterial)
    {
        CachedDynamicMaterial-&gt;SetVectorParameterValue(FName(&quot;Color&quot;),
            bCanPlace ? FLinearColor::Green : FLinearColor::Red);
        bLastCanPlace = bCanPlace;
    }
}</code></pre>
<ul>
<li><code>bLastCanPlace</code> 플래그로 불필요한 업데이트 방지</li>
<li>매 프레임 검증하지만 색상은 변경 시에만</li>
</ul>
<h2 id="레시피-기반-재료-검증">레시피 기반 재료 검증</h2>
<pre><code class="language-cpp">bool UTSBuildingComponent::CanBuild(int32 RecipeID, int32&amp; OutResultID)
{
    if (!GetOwner()-&gt;HasAuthority()) return false; // 서버에서만

    FBuildingRecipeData RecipeData;
    if (!BuildingRecipeDataSub-&gt;GetBuildingRecipeDataSafe(RecipeID, RecipeData))
        return false;

    OutResultID = RecipeData.ResultItemID;

    // 인벤토리 검증
    for (const FBuildingIngredientData&amp; Ingredient : RecipeData.Ingredients)
    {
        int32 ItemCount = PlayerInventoryComp-&gt;GetItemCount(Ingredient.MaterialID);
        if (ItemCount &lt; Ingredient.Count) return false;
    }
    return true;
}</code></pre>
<ul>
<li>서버 권한 체크 필수</li>
<li>레시피 데이터와 인벤토리 동시 검증</li>
<li><code>ConsumeIngredients</code>에서 실제 재료 소비</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>검증 파이프라인의 순서가 생각보다 중요했다. 비용이 낮은 검사부터 수행해서 불필요한 연산을 줄이는 게 성능과 직결된다는 걸 체감했다. </p>
<p><code>ActorHasTag</code>를 활용한 블랙리스트 방식이 간단하면서도 확장성이 좋다. 복잡한 타입 체크 대신 태그 하나로 의미를 명확하게 전달할 수 있어서 편했다.</p>
<p>매 틱마다 오버랩으로 LightSource를 검증하는 부분이 비효율적인것 같다. 어떤 방식으로 최적화해야 피드백도 늦지 않고 비용을 줄일 수 있을지 더 찾아봐야겠다. </p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드카타] 가장 먼 노드]]></title>
            <link>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EA%B0%80%EC%9E%A5-%EB%A8%BC-%EB%85%B8%EB%93%9C</link>
            <guid>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EA%B0%80%EC%9E%A5-%EB%A8%BC-%EB%85%B8%EB%93%9C</guid>
            <pubDate>Tue, 02 Dec 2025 02:32:26 GMT</pubDate>
            <description><![CDATA[<h1 id="프로그래머스---가장-먼-노드">프로그래머스 - 가장 먼 노드</h1>
<p><a href="!https://school.programmers.co.kr/learn/courses/30/lessons/49189/solution_groups?language=cpp&amp;type=my">문제링크</a></p>
<ol>
<li>BFS를 이용해 1과 각 노드의 거리를 구한다.</li>
<li>더이상 탐색할 하위 노드가 없는 노드들을 저장한다.</li>
<li>저장한 노드를 dist의 내림차순으로 정렬한다.</li>
<li>최대 거리값과 같은 노드의 개수를 구한다.<pre><code class="language-cpp">#include &lt;unordered_map&gt;
#include &lt;unordered_set&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;
#include &lt;queue&gt;
</code></pre>
</li>
</ol>
<p>using namespace std;
struct point
{
    int node;
    int dist;
};
int solution(int n, vector&lt;vector<int>&gt; edge) {
    int answer = 0;
    unordered_map&lt;int, unordered_set<int>&gt; nodes;
    vector<bool> visited(n+1, false);
    queue<point> q;
    vector<point> points;</p>
<pre><code>q.push({1,0});
visited[1] = true;

for(auto v:edge)
{
    nodes[v[0]].insert(v[1]);
    nodes[v[1]].insert(v[0]);
}

while(!q.empty())
{
    point temp = q.front();
    q.pop();
    bool isLeafNode = true;
    for(int i:nodes[temp.node])
    {
        if(!visited[i])
        {
            isLeafNode = false;
            visited[i] = true;
            q.push({i,temp.dist+1});
        }
    }
    if(isLeafNode)
    {
        points.push_back(temp);
    }
}
sort(points.begin(), points.end(), 
    [](const auto&amp; a, const auto&amp; b)
     {
         return a.dist&gt;b.dist;
     });
int maxDist = points[0].dist;
for(const point&amp; p:points)
{
    if(p.dist&lt;maxDist)
    {
        break;
    }
    answer++;
}
return answer;</code></pre><p>}</p>
<h2 id="">```</h2>
<pre><code>출처: 프로그래머스 - 가장 먼 노드</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251201]]></title>
            <link>https://velog.io/@s_kim__/TIL-251201</link>
            <guid>https://velog.io/@s_kim__/TIL-251201</guid>
            <pubDate>Mon, 01 Dec 2025 12:32:36 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p>📅 <code>2025-12-01</code></p>
<ul>
<li><input disabled="" type="checkbox"> 플레이어 컨트롤러를 중재자로 활용한 제작 시스템 구현</li>
</ul>
<hr>
<h2 id="플레이어-컨트롤러를-중재자로-활용한-제작-시스템-구현">플레이어 컨트롤러를 중재자로 활용한 제작 시스템 구현</h2>
<p>오너가 없는 제작대 액터와 플레이어 간의 네트워크 통신을 구현할 때, 플레이어 컨트롤러를 중재자(Mediator) 패턴으로 활용했다.</p>
<h3 id="문제-상황-오너-없는-액터의-네트워크-통신">문제 상황: 오너 없는 액터의 네트워크 통신</h3>
<p>제작대(<code>ATSCraftingTable</code>)는 월드에 배치된 액터로, 특정 플레이어의 소유가 아니다. 언리얼의 RPC는 기본적으로 Owner를 통해 라우팅되기 때문에, 오너가 없는 액터에서 직접 클라이언트 RPC를 호출하면 어느 클라이언트로 보내야 할지 알 수 없다.</p>
<h3 id="해결책-플레이어-컨트롤러-중재자-패턴">해결책: 플레이어 컨트롤러 중재자 패턴</h3>
<p><strong>클라이언트 → 플레이어 컨트롤러 → 제작대</strong></p>
<pre><code class="language-cpp">// PlayerController.h
UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = &quot;Crafting&quot;)
void ServerRequestCraft(ATSCraftingTable* CraftingTable, int32 RecipeID);</code></pre>
<pre><code class="language-cpp">// PlayerController.cpp
void ATSPlayerController::ServerRequestCraft_Implementation(
    ATSCraftingTable* CraftingTable, int32 RecipeID)
{
    if (!CraftingTable) return;

    ATSCharacter* TSCharacter = Cast&lt;ATSCharacter&gt;(GetPawn());
    if (!TSCharacter) return;

    // 제작대의 함수를 대신 호출
    CraftingTable-&gt;ServerRequestCraft(RecipeID, TSCharacter);
}</code></pre>
<p>클라이언트 UI에서 제작 버튼을 누르면 → 플레이어 컨트롤러의 <code>ServerRequestCraft</code> RPC 호출 → 서버에서 제작대의 실제 제작 로직 실행</p>
<p><strong>제작대 → 플레이어 컨트롤러 → 클라이언트</strong></p>
<pre><code class="language-cpp">// CraftingTable.cpp - 제작 완료 후
void ATSCraftingTable::StartCrafting(int32 RecipeID, ATSCharacter* InstigatorCharacter)
{
    // ... 제작 로직 ...

    ATSPlayerController* PC = Cast&lt;ATSPlayerController&gt;(InstigatorCharacter-&gt;GetController());
    if (!PC) return;

    int32 SlotIndex = CraftingInventory-&gt;PlaceCraftResult(PC, RecipeData.ResultItemID, RemainingQuantity);

    // 플레이어 컨트롤러를 통해 클라이언트에 알림
    PC-&gt;ClientNotifyCraftResult(SlotIndex);
}</code></pre>
<pre><code class="language-cpp">// PlayerController.cpp
void ATSPlayerController::ClientNotifyCraftResult_Implementation(int32 SlotIndex)
{
    // 델리게이트 브로드캐스트로 UI 업데이트
    OnCraftComplete.Broadcast(SlotIndex);
}</code></pre>
<p>제작 완료 → 플레이어 컨트롤러의 <code>ClientNotifyCraftResult</code> RPC로 결과 전달 → <code>OnCraftComplete</code> 델리게이트 브로드캐스트 → UI가 슬롯 업데이트</p>
<h3 id="중재자-패턴의-장점">중재자 패턴의 장점</h3>
<p><strong>1. 네트워크 라우팅 문제 해결</strong></p>
<ul>
<li>플레이어 컨트롤러는 항상 명확한 Owner 관계를 가짐</li>
<li>서버와 클라이언트 간 안정적인 RPC 통신 보장</li>
</ul>
<p><strong>2. 권한 검증 중앙화</strong></p>
<pre><code class="language-cpp">bool ATSPlayerController::ServerRequestCraft_Validate(
    ATSCraftingTable* CraftingTable, int32 RecipeID)
{
    return CraftingTable &amp;&amp; RecipeID &gt; 0;
}</code></pre>
<p>플레이어 컨트롤러에서 한 번만 검증하면 됨</p>
<p><strong>3. UI 통신 간소화</strong></p>
<ul>
<li>UI 위젯 → 델리게이트 바인딩 → 플레이어 컨트롤러만 알면 됨</li>
<li>제작대 액터를 직접 참조할 필요 없음</li>
</ul>
<h3 id="제작대-종료-처리">제작대 종료 처리</h3>
<pre><code class="language-cpp">// PlayerController.h
UFUNCTION(Server, Reliable, BlueprintCallable, Category = &quot;Crafting&quot;)
void ServerNotifyCraftingTableClosed(UTSCraftingTableInventory* CraftingInventory);

// PlayerController.cpp
void ATSPlayerController::ServerNotifyCraftingTableClosed_Implementation(
    UTSCraftingTableInventory* CraftingInventory)
{
    if (CraftingInventory)
    {
        CraftingInventory-&gt;OnPlayerClosedUI(this);
    }
}</code></pre>
<p>클라이언트가 제작대 UI를 닫으면 → 서버에 알림 → 제작대 인벤토리 정리. 이것도 플레이어 컨트롤러를 통해 처리.</p>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>처음엔 &quot;제작대에서 직접 RPC 날리면 되지 않나?&quot;라고 생각했는데, Owner가 없는 액터는 어느 클라이언트한테 보낼지 몰라서 안 된다는 걸 깨달았다. 플레이어 컨트롤러가 중간에서 전달하는 중재자 역할을 하니까 깔끔하게 해결됐다.</p>
<p>이 패턴의 핵심은 &quot;통신 경로를 명확하게 만드는 것&quot;인 것 같다. UI → PlayerController → CraftingTable → PlayerController → UI 이런 식으로 항상 플레이어 컨트롤러를 거치니까 누가 누구한테 말하는지 헷갈릴 일이 없다.</p>
<p>근데 이렇게 하면 플레이어 컨트롤러가 너무 많은 걸 알게 되는 건 아닌가 싶기도 하다. 제작대도 알고, 인벤토리도 알고, UI도 알고... 나중에 시스템이 더 복잡해지면 플레이어 컨트롤러가 God Object가 될 수도 있을 것 같은데, 그때 가서 리팩토링 고민해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251126]]></title>
            <link>https://velog.io/@s_kim__/TIL-251126</link>
            <guid>https://velog.io/@s_kim__/TIL-251126</guid>
            <pubDate>Wed, 26 Nov 2025 12:34:38 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p>📅 <code>2025-11-26</code></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 인벤토리 컴포넌트 편의 함수 구현 (GetItemCount, ConsumeItem, AddItem 오버로드)</li>
<li><input checked="" disabled="" type="checkbox"> 제작 시스템과 인벤토리 연동을 위한 인터페이스 완성</li>
</ul>
<hr>
<h2 id="인벤토리-컴포넌트-편의-함수-구현">인벤토리 컴포넌트 편의 함수 구현</h2>
<h3 id="📌-구현-내용">📌 구현 내용</h3>
<p>제작 시스템에서 필요로 하는 인벤토리 조작 함수들을 추가로 구현했다:</p>
<h4 id="1-getitemcount---특정-아이템-총-개수-조회">1. <strong>GetItemCount - 특정 아이템 총 개수 조회</strong></h4>
<pre><code class="language-cpp">int32 UTSInventoryMasterComponent::GetItemCount(int32 StaticDataID) const
{
    int32 ResultCount = 0;

    // 핫키 인벤토리 탐색
    for (const FSlotStructMaster&amp; Slot : HotkeyInventory.InventorySlotContainer)
    {
        if (Slot.ItemData.StaticDataID == StaticDataID)
        {
            ResultCount += Slot.CurrentStackSize;
        }
    }

    // 가방 인벤토리 탐색 (가방이 활성화된 경우만)
    if (GetCurrentBagSlotCount() == 0)
    {
        return ResultCount;
    }
    for (const FSlotStructMaster&amp; Slot : BagInventory.InventorySlotContainer)
    {
        if (Slot.ItemData.StaticDataID == StaticDataID)
        {
            ResultCount += Slot.CurrentStackSize;
        }
    }

    return ResultCount;
}</code></pre>
<p><strong>특징:</strong></p>
<ul>
<li><code>const</code> 함수로 읽기 전용 보장</li>
<li>핫키 + 가방 인벤토리의 모든 슬롯을 순회하며 집계</li>
<li>가방이 비활성화된 경우 조기 반환으로 성능 최적화</li>
</ul>
<h4 id="2-consumeitem---특정-아이템-소비">2. <strong>ConsumeItem - 특정 아이템 소비</strong></h4>
<pre><code class="language-cpp">void UTSInventoryMasterComponent::ConsumeItem(int32 StaticDataID, int32 Quantity)
{
    // 핫키 인벤토리 탐색
    for (int32 i = 0; i &lt; HotkeyInventory.InventorySlotContainer.Num(); ++i)
    {
        FSlotStructMaster&amp; Slot = HotkeyInventory.InventorySlotContainer[i];
        if (Slot.ItemData.StaticDataID == StaticDataID)
        {
            int32 ToRemove = FMath::Min(Quantity, Slot.CurrentStackSize);
            RemoveItem(EInventoryType::HotKey, i, ToRemove);
            Quantity -= ToRemove;
            if (ActiveHotkeyIndex == i)
            {
                HandleActiveHotkeyIndexChanged();
            }
            if (Quantity &lt;= 0)
            {
                break;
            }
        }
    }
    // 가방 인벤토리 탐색
    if (GetCurrentBagSlotCount() &gt; 0)
    {
        for (int32 i = 0; i &lt; BagInventory.InventorySlotContainer.Num(); ++i)
        {
            FSlotStructMaster&amp; Slot = BagInventory.InventorySlotContainer[i];
            if (Slot.ItemData.StaticDataID == StaticDataID)
            {
                int32 ToRemove = FMath::Min(Quantity, Slot.CurrentStackSize);
                RemoveItem(EInventoryType::BackPack, i, ToRemove);
                Quantity -= ToRemove;
                if (Quantity &lt;= 0)
                {
                    break;
                }
            }
        }
    }
    HandleInventoryChanged();
}</code></pre>
<p><strong>설계 의도:</strong></p>
<ul>
<li>핫키 인벤토리 → 가방 인벤토리 순서로 소비 (접근성 높은 순서)</li>
<li>활성 핫키 슬롯의 아이템이 소비되면 장착 상태 갱신</li>
<li>전체 소비 완료 후 한 번만 <code>HandleInventoryChanged()</code> 호출</li>
</ul>
<h4 id="3-additem-오버로드---int32-버전-추가">3. <strong>AddItem 오버로드 - int32 버전 추가</strong></h4>
<pre><code class="language-cpp">bool UTSInventoryMasterComponent::AddItem(const int32 StaticDataID, int32 Quantity, int32&amp; OutRemainingQuantity)
{
    FItemInstance ItemData = FItemInstance(StaticDataID, GetWorld()-&gt;GetTimeSeconds());
    return AddItem(ItemData, Quantity, OutRemainingQuantity);
}</code></pre>
<p><strong>편의성:</strong></p>
<ul>
<li>제작 시스템에서 <code>StaticDataID</code>만으로 아이템 추가 가능</li>
<li>서버 시간 자동 설정으로 부패 시스템 연동</li>
<li>기존 <code>FItemInstance</code> 버전 AddItem을 재사용</li>
</ul>
<h3 id="🎯-제작-시스템과의-연동-완성">🎯 제작 시스템과의 연동 완성</h3>
<p>이제 제작대에서 다음과 같이 사용 가능:</p>
<pre><code class="language-cpp">// 제작 가능 여부 확인
bool ATSCraftingTable::CanCraft(int32 RecipeID, ATSCharacter* InstigatorCharacter)
{
    for (const FIngredientData&amp; Ingredient : RecipeData.Ingredients)
    {
        int32 ItemCount = PlayerInventoryComp-&gt;GetItemCount(Ingredient.MaterialID);
        if (ItemCount &lt; Ingredient.Count)
        {
            return false;  // 재료 부족
        }
    }
    return true;
}

// 제작 실행
void ATSCraftingTable::StartCrafting(int32 RecipeID, ATSCharacter* InstigatorCharacter)
{
    // 재료 소비
    for (const FIngredientData&amp; Ingredient : RecipeData.Ingredients)
    {
        PlayerInventoryComp-&gt;ConsumeItem(Ingredient.MaterialID, Ingredient.Count);
    }

    // 결과물 생성
    int32 RemainingQuantity;
    InventoryComp-&gt;AddItem(RecipeData.ResultItemID, RecipeData.ResultCount, RemainingQuantity);
}</code></pre>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>오버로드 함수의 편의성을 체감했다. <code>AddItem(int32, ...)</code> 버전 하나 추가했을 뿐인데, 제작 시스템 코드가 훨씬 간결해졌다. <code>FItemInstance</code>를 매번 생성하지 않아도 되니 코드 가독성도 좋아졌다.</p>
<p>위젯쪽에서 현재 부족한 재료 아이템의 경우에는 한눈에 볼 수 있도록 표시를 하거나 &quot;제작 실패 → 사용자 피드백&quot;까지 이어지도록 반환값으로 성공 여부를 전달하거나, 에러 델리게이트를 추가하는 방식을 고려해봐야겠다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드카타] - 빛의 경로 사이클]]></title>
            <link>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EB%B9%9B%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EC%82%AC%EC%9D%B4%ED%81%B4</link>
            <guid>https://velog.io/@s_kim__/%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EB%B9%9B%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EC%82%AC%EC%9D%B4%ED%81%B4</guid>
            <pubDate>Mon, 24 Nov 2025 02:33:02 GMT</pubDate>
            <description><![CDATA[<h1 id="프로그래머스---빛의-경로-사이클">프로그래머스 - 빛의 경로 사이클</h1>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/86052">문제링크</a></p>
<h2 id="처음-풀이">처음 풀이</h2>
<p>사이클 중복을 방지할 수 없어서 실패</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
// n행 m열
// 상 [0,1], 하 [0,-1], 좌 [-1,0], 우[1,0]

int dx[]={1,0,-1,0};
int dy[]={0,1,0,-1};
int findPath(const vector&lt;string&gt;&amp; grid, const vector&lt;int&gt; startPoint, int n, int m , 
             int direction, int result)
{
    if(result&gt;0 &amp;&amp; startPoint[0]==n &amp;&amp; startPoint[1]==m &amp;&amp; startPoint[2]==direction) return result;

    if(grid[n][m]==&#39;L&#39;)
    {
        //1,0 -&gt; 0,1 -&gt; -1,0 -&gt; 0,-1 -&gt;1,0
        direction = (direction+1)%4;
    }
    else if(grid[n][m] == &#39;R&#39;)
    {
        //1,0 -&gt; 0,-1 -&gt; -1,0 -&gt; 0,1 -&gt;1,0
        direction = (direction+3)%4;
    }
    n=n+dx[direction];
    m=m+dy[direction];
    if(n&lt;0) n=grid.size()-1;
    else if(n&gt;=grid.size()) n=0;
    if(m&lt;0) m=grid[0].size()-1;
    else if(m&gt;=grid[0].size()) m=0;

    return findPath(grid,startPoint, n, m, direction, result+1);
}
vector&lt;int&gt; solution(vector&lt;string&gt; grid) {
    vector&lt;int&gt; answer;

    for(int n=0; n&lt;grid.size(); ++n)
    {
        for(int m=0; m&lt;grid[0].size(); ++m)
        {
            for(int i=0; i&lt;4; ++i)
            {
                int temp = findPath(grid,{n,m,i}, n, m, i, 0);
                if(temp&gt;0) answer.push_back(temp);
            }
        }
    }
    return answer;
}</code></pre>
<h2 id="최종-풀이">최종 풀이</h2>
<p>지나간 경로인지 체크하여 중복 방지하여 해결</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
// n행 m열
// 상 [0,1], 하 [0,-1], 좌 [-1,0], 우[1,0]

int dx[]={1,0,-1,0};
int dy[]={0,1,0,-1};
int findPath(const vector&lt;string&gt;&amp; grid, const vector&lt;int&gt; startPoint,
             vector&lt;vector&lt;vector&lt;bool&gt;&gt;&gt;&amp; visited,
             int n, int m , 
             int direction, int result)
{
    if(visited[n][m][direction] &amp;&amp; 
       startPoint[0]==n &amp;&amp; startPoint[1]==m &amp;&amp; startPoint[2]==direction) 
        return result;

    if(visited[n][m][direction]) return -1;
    visited[n][m][direction]=true;
    if(grid[n][m]==&#39;L&#39;)
    {
        //1,0 -&gt; 0,1 -&gt; -1,0 -&gt; 0,-1 -&gt;1,0
        direction = (direction+1)%4;
    }
    else if(grid[n][m] == &#39;R&#39;)
    {
        //1,0 -&gt; 0,-1 -&gt; -1,0 -&gt; 0,1 -&gt;1,0
        direction = (direction+3)%4;
    }
    n=n+dx[direction];
    m=m+dy[direction];
    if(n&lt;0) n=grid.size()-1;
    else if(n&gt;=grid.size()) n=0;
    if(m&lt;0) m=grid[0].size()-1;
    else if(m&gt;=grid[0].size()) m=0;

    return findPath(grid,startPoint,visited, n, m, direction, result+1);
}
vector&lt;int&gt; solution(vector&lt;string&gt; grid) {
    vector&lt;int&gt; answer;
    vector&lt;vector&lt;vector&lt;bool&gt;&gt;&gt; visited(grid.size(), 
                                            vector&lt;vector&lt;bool&gt;&gt;(grid[0].size(),
                                                                 vector&lt;bool&gt;(4,false)));
    for(int n=0; n&lt;grid.size(); ++n)
    {
        for(int m=0; m&lt;grid[0].size(); ++m)
        {
            for(int i=0; i&lt;4; ++i)
            {
                int temp = findPath(grid,{n,m,i}, visited,n, m, i, 0);
                if(temp&gt;0) answer.push_back(temp);
            }
        }
    }
    sort(answer.begin(), answer.end());
    return answer;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251121]]></title>
            <link>https://velog.io/@s_kim__/TIL-251121</link>
            <guid>https://velog.io/@s_kim__/TIL-251121</guid>
            <pubDate>Fri, 21 Nov 2025 12:42:10 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-21</code></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 인터페이스 기반 상호작용 위젯 표시 시스템 추가</li>
<li><input checked="" disabled="" type="checkbox"> 픽업 아이템 액터 구현 및 AddItem 테스트</li>
<li><input checked="" disabled="" type="checkbox"> 컨테이너 액터 구현</li>
<li><input checked="" disabled="" type="checkbox"> 서버/클라이언트 실행 분기 처리 (RunOnServer 패턴)</li>
</ul>
<hr>
<h2 id="인터페이스-기반-상호작용-위젯-표시-시스템">인터페이스 기반 상호작용 위젯 표시 시스템</h2>
<h3 id="기존-코드-팀원-작업">기존 코드 (팀원 작업)</h3>
<ul>
<li>캐릭터에서 라인트레이스로 <code>CurrentHitActor</code>, <code>LastHitActor</code> 저장</li>
<li>액터 전환 감지 기능</li>
</ul>
<h3 id="추가-구현-내용">추가 구현 내용</h3>
<p><strong>1. IInteractableInterface 확인 후 위젯 표시</strong></p>
<pre><code class="language-cpp">// 이전 액터 처리
    if (IsValid(LastHitActor.Get()))
    {
        if (LastHitActor-&gt;Implements&lt;UIInteraction&gt;())
        {
            IIInteraction* InteractionInterface = Cast&lt;IIInteraction&gt;(LastHitActor);
            if (InteractionInterface)
            {
                InteractionInterface-&gt;HideInteractionWidget();
            }
        }
    }

    // 현재 액터 처리
    if (IsValid(CurrentHitActor.Get()))
    {
        if (CurrentHitActor-&gt;Implements&lt;UIInteraction&gt;())
        {
            IIInteraction* InteractionInterface = Cast&lt;IIInteraction&gt;(CurrentHitActor);
            if (InteractionInterface &amp;&amp; InteractionInterface-&gt;CanInteract(this))
            {
                InteractionInterface-&gt;ShowInteractionWidget(this);
            }
        }
    }</code></pre>
<p><strong>2. 픽업 아이템 액터 구현</strong></p>
<ul>
<li><code>FItemInstance</code> 데이터 보유 (StaticDataID, CreationServerTime)</li>
<li>Widget Component로 &quot;줍기 [E]&quot; 프롬프트 표시</li>
<li><code>Interact()</code> 호출 시 <code>UTSInventoryMasterComponent::AddItem()</code> 연동</li>
<li>AddItem 성공 시 액터 Destroy &lt;- 추후 풀로 반환하도록 수정 예정</li>
<li>멀티플레이어 테스트 완료</li>
</ul>
<p><strong>3. 컨테이너 액터 구현</strong></p>
<ul>
<li><code>UTSInventoryMasterComponent</code> 보유</li>
<li>&quot;열기 [E]&quot; 프롬프트 표시</li>
<li><code>Interact()</code> 호출 시 PlayerController의 HUD에서 <code>ToggleContainer()</code> 실행</li>
</ul>
<hr>
<h2 id="runonserver-패턴으로-서버클라이언트-실행-분기-처리">RunOnServer 패턴으로 서버/클라이언트 실행 분기 처리</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>E키 상호작용 시 실행 위치가 액터마다 달라야 하는 문제:</p>
<ul>
<li><strong>픽업 아이템</strong>: <code>AddItem()</code>은 인벤토리 데이터 수정이므로 <strong>서버에서 실행</strong> 필요</li>
<li><strong>컨테이너</strong>: HUD의 <code>ToggleContainer()</code>는 UI 조작이므로 <strong>클라이언트에서 실행</strong> 필요</li>
</ul>
<p>기존 방식으로는 모든 상호작용을 <code>ServerInteract</code> RPC로 보내면 컨테이너에서 HUD를 찾을 수 없는 문제 발생</p>
<h3 id="해결-방법-runonserver-인터페이스-함수-추가">해결 방법: RunOnServer() 인터페이스 함수 추가</h3>
<p><strong>1. 인터페이스에 실행 위치 판별 함수 추가</strong></p>
<pre><code class="language-cpp">// IInteractableInterface
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
bool RunOnServer();</code></pre>
<p><strong>2. 각 액터에서 실행 위치 반환</strong></p>
<pre><code class="language-cpp">// ATSPickupItem - 서버에서 실행해야 함
bool ATSPickupItem::RunOnServer_Implementation()
{
    return true;  // AddItem()은 서버에서 실행
}

// ATSContainerActor - 클라이언트에서 실행해야 함
bool ATSContainerActor::RunOnServer_Implementation()
{
    return false;  // ToggleContainer()는 클라이언트에서 실행
}</code></pre>
<p><strong>3. 캐릭터에서 실행 위치 분기</strong></p>
<pre><code class="language-cpp">void ATSCharacter::OnInteract(const struct FInputActionValue&amp; Value)
{
    UE_LOG(LogTemp, Log, TEXT(&quot;e pressed&quot;));

    if (!IsValid(CurrentHitActor.Get()))
    {
        return;
    }
    if (CurrentHitActor-&gt;Implements&lt;UIInteraction&gt;())
    {
        IIInteraction* InteractionInterface = Cast&lt;IIInteraction&gt;(CurrentHitActor);
        if (InteractionInterface &amp;&amp; InteractionInterface-&gt;CanInteract(this))
        {
            if (InteractionInterface-&gt;RunOnServer())
            {
                ServerInteract(CurrentHitActor.Get());
            }
            else
            {
                InteractionInterface-&gt;Interact(this);
            }
        }
    }
}</code></pre>
<h3 id="패턴의-장점">패턴의 장점</h3>
<ul>
<li>✅ <strong>확장성</strong>: 새로운 액터 추가 시 <code>RunOnServer()</code>만 구현하면 자동으로 올바른 위치에서 실행</li>
<li>✅ <strong>명확성</strong>: 각 액터가 자신의 실행 위치를 명시적으로 선언</li>
<li>✅ <strong>유지보수성</strong>: 캐릭터 코드 수정 없이 액터별 동작 변경 가능</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p><strong>1. 인터페이스 설계의 유연성</strong></p>
<ul>
<li><code>RunOnServer()</code> 함수 하나 추가로 서버/클라이언트 실행 분기를 깔끔하게 처리할 수 있었다.</li>
<li>각 액터가 자신의 실행 위치를 결정하는 방식이 객체지향 설계 원칙에 부합하고 확장성도 좋은 것 같다.</li>
</ul>
<p><strong>2. 멀티플레이어 아키텍처의 복잡성</strong></p>
<ul>
<li>같은 E키 입력이라도 액터 종류에 따라 서버/클라이언트 중 어디서 실행해야 하는지가 다르다는 점이 흥미로웠다.</li>
<li>데이터 수정은 서버, UI 조작은 클라이언트라는 기본 원칙을 명확히 이해하게 되었다.</li>
</ul>
<p><strong>3. 팀 작업의 효율성</strong></p>
<ul>
<li>팀원이 구현한 라인트레이스 시스템 위에 인터페이스 기반 기능을 추가하는 방식으로 협업이 잘 이루어졌다.</li>
<li>기존 코드를 최대한 수정하지 않고 기능을 확장할 수 있어서 협업 시 충돌을 최소화할 수 있었다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251120]]></title>
            <link>https://velog.io/@s_kim__/TIL-251120</link>
            <guid>https://velog.io/@s_kim__/TIL-251120</guid>
            <pubDate>Thu, 20 Nov 2025 11:14:47 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: \#fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-20</code></p>
<ul>
<li><input disabled="" type="checkbox"> 언리얼 GAS: 클라이언트에서 실행되지 않는 패시브 어빌리티 문제 해결</li>
</ul>
<hr>
<h2 id="언리얼-gas-클라이언트에서-실행되지-않는-패시브-어빌리티-문제-해결">언리얼 GAS: 클라이언트에서 실행되지 않는 패시브 어빌리티 문제 해결</h2>
<h3 id="1-🎯-내가-하려던-것-goal">1. 🎯 내가 하려던 것 (Goal)</h3>
<ul>
<li><strong>목표:</strong> 언리얼 엔진의 Gameplay Ability System (GAS)을 사용하여 <strong>리슨 서버 멀티플레이 환경</strong>에서 <strong>상태 유지형 패시브 어빌리티</strong>를 구현하고, 클라이언트의 특정 입력(예: 숫자키 <code>1</code>)이 있을 때 이 어빌리티가 이벤트를 받아 특정 동작을 수행하도록 만들고자 했다.</li>
<li><strong>구현 방식:</strong><ol>
<li>어빌리티 부여 시, 능력이 종료되지 않고 <strong>활성화 상태</strong>를 유지.</li>
<li>활성화된 능력 내에서 <strong><code>Wait Gameplay Event</code></strong> 노드를 사용하여 특정 태그를 기다림.</li>
<li>클라이언트가 키를 누르면 <strong><code>SendGameplayEventToActor</code></strong>를 호출하여 이벤트를 보냄.</li>
</ol>
</li>
</ul>
<h3 id="2-🤯-직면한-문제-problem">2. 🤯 직면한 문제 (Problem)</h3>
<ul>
<li><p><strong>현상:</strong> 어빌리티가 <strong>호스트(서버 겸 클라이언트)에서는 정상적으로 동작</strong>했으나, <strong>일반 클라이언트에서는 어빌리티가 활성화되지 않거나</strong>, 활성화되더라도 키 입력에 따른 <strong>후속 동작이 실행되지 않음</strong>.</p>
</li>
<li><p><strong>오류 코드:</strong></p>
<pre><code class="language-cpp">UGA_Hotkey::UGA_Hotkey() {
    // ...
    NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalOnly; // 문제의 원인 1
}

void UGA_Hotkey::OnGiveAbility(...) {
    // ...
    if (ActorInfo-&gt;IsLocallyControlled() &amp;&amp; !Spec.IsActive()) { // 문제의 원인 2
       // ... TryActivateAbility 호출
    }
}</code></pre>
</li>
</ul>
<h3 id="3-💡-문제의-원인-및-해결-방법-cause--solution">3. 💡 문제의 원인 및 해결 방법 (Cause &amp; Solution)</h3>
<p><strong>1. 초기 활성화 권한 문제</strong></p>
<p>능력의 <code>NetExecutionPolicy</code>가 <strong><code>LocalOnly</code></strong>로 설정되어 있어 서버의 권한이 있는 상태에서 활성화되지 않음. 또한 <code>OnGiveAbility</code>에서 <strong><code>IsLocallyControlled()</code></strong> 조건만 확인하여, 클라이언트의 로컬 복사본에서만 활성화가 시도되어 서버에는 상태가 복제되지 않음.</p>
<p><strong>해결: 서버 권한으로 초기 활성화</strong> </p>
<ol>
<li><code>NetExecutionPolicy</code>를 <strong><code>EGameplayAbilityNetExecutionPolicy::ServerOnly</code></strong>로 변경하여 어빌리티의 실행 권한을 서버로 위임.</li>
<li><code>OnGiveAbility</code>에서 <strong><code>ActorInfo-&gt;IsNetAuthority()</code></strong> (서버)일 때만 <code>TryActivateAbility</code>를 호출하여 능력이 <strong>서버에서 권한 있게</strong> 활성화되도록 수정.</li>
</ol>
<p><strong>2. 이벤트 전송 문제</strong></p>
<p>능력이 <code>ServerOnly</code>로 서버에서 활성화되어 <code>Wait Gameplay Event</code> 노드가 <strong>서버에서만</strong> 실행됨. 그런데 클라이언트에서 호출된 <strong><code>SendGameplayEventToActor</code></strong>는 로컬 함수 호출일 뿐, <strong>네트워크를 통해 서버에 전달되지 않아</strong> 서버의 대기 중인 능력이 이벤트를 받지 못함.</p>
<p><strong>해결: Server RPC를 통한 이벤트 전달</strong></p>
<ol>
<li>클라이언트의 키 입력 처리 부분에서 <strong><code>SendGameplayEventToActor</code>를 직접 호출하는 대신</strong>, 서버로 전송되는 <strong>Server RPC</strong> 함수를 호출.</li>
<li>서버 RPC 함수 안에서 <strong><code>UAbilitySystemComponent::HandleGameplayEvent</code></strong> 또는 <strong><code>SendGameplayEventToActor</code></strong>를 호출하여 서버의 능력에게 이벤트를 전달.</li>
</ol>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>멀티플레이 환경에서 <strong>Gameplay Ability System (GAS)</strong>을 사용할 때는, 단순히 코드를 작성하는 것 이상으로 <strong>&quot;이 코드가 지금 서버에서 실행되는가, 클라이언트에서 실행되는가?&quot;</strong>에 대한 <strong>네트워크 권한(Authority)</strong> 이해가 가장 중요하다는 것을 다시 한번 깨달았다. 특히 <strong>패시브 어빌리티</strong>라도 서버에서 권한을 가지고 활성화해야 하며, 클라이언트 입력이 서버의 상태를 변경할 때는 반드시 <strong>Server RPC</strong>를 사용해 안전하게 권한을 넘겨야 예상치 못한 싱크(Sync) 문제를 방지할 수 있다. <code>LocalOnly</code> 정책은 신중하게 사용해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251119]]></title>
            <link>https://velog.io/@s_kim__/TIL-251119</link>
            <guid>https://velog.io/@s_kim__/TIL-251119</guid>
            <pubDate>Wed, 19 Nov 2025 12:18:19 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-19</code></p>
<ul>
<li><input disabled="" type="checkbox"> 위젯 스위처를 활용한 UI 상태 관리</li>
<li><input disabled="" type="checkbox"> Toggle 패턴으로 UI Open/Close 통합</li>
<li><input disabled="" type="checkbox"> ESC 키 우선순위 기반 UI 닫기 로직</li>
<li><input disabled="" type="checkbox"> UInterface를 통한 블루프린트-C++ 데이터 전달</li>
<li><input disabled="" type="checkbox"> 방어적 프로그래밍으로 UI 상태 관리</li>
</ul>
<hr>
<h2 id="위젯-스위처를-활용한-ui-상태-관리">위젯 스위처를 활용한 UI 상태 관리</h2>
<h3 id="핵심-개념">핵심 개념</h3>
<p>Visibility를 직접 바꾸지 말고 <code>SetActiveWidgetIndex()</code>만 사용해서 UI 전환</p>
<h3 id="구조-설계">구조 설계</h3>
<pre><code class="language-cpp">// WidgetSwitcher_Backpack
enum EBackpackWidgetIndex {
    Empty_Backpack = 0,
    PlayerInventory = 1
};

// WidgetSwitcher_Content  
enum EContentWidgetIndex {
    Empty_Content = 0,
    Container = 1,
    BuildingMode = 2,
    CraftingMode = 3,
    Settings = 4
};</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>코드가 간결해짐 (Visibility 제어 불필요)</li>
<li>enum으로 가독성 향상</li>
<li>각 스위처는 독립적으로 동작</li>
</ul>
<hr>
<h2 id="toggle-패턴으로-ui-openclose-통합">Toggle 패턴으로 UI Open/Close 통합</h2>
<p>사용자 입력(E키)키 하나로 UI Open/Close 통합 관리</p>
<pre><code class="language-cpp">void ToggleContainer(AContainerActor* Container) {
    // 같은 상자면 닫기
    if (CurrentContainer == Container) {
        CloseCurrentContainer();
        return;
    }
    // 다른 상자면 교체
    // ...
}</code></pre>
<hr>
<h2 id="esc-키-우선순위-기반-ui-닫기-로직">ESC 키 우선순위 기반 UI 닫기 로직</h2>
<h3 id="요구사항">요구사항</h3>
<ul>
<li>ESC로 UI 단계적으로 닫기</li>
<li>아무것도 없으면 환경설정 열기</li>
</ul>
<h3 id="구현">구현</h3>
<pre><code class="language-cpp">void HandleEscapeKey() {
    // 1순위: 환경설정 닫기
    if (bIsSettingsOpen) {
        CloseSettings();
        return;
    }

    // 2순위: 게임 UI 모두 닫기
    if (IsAnyUIOpen()) {
        CloseAllGameUI();
        return;
    }

    // 3순위: 환경설정 열기
    OpenSettings();
}</code></pre>
<hr>
<h2 id="uinterface를-통한-블루프린트-c-데이터-전달">UInterface를 통한 블루프린트-C++ 데이터 전달</h2>
<h3 id="문제">문제</h3>
<p>블루프린트로 만든 위젯에 C++의 컨테이너 인벤토리 데이터를 어떻게 전달할까?</p>
<h3 id="해결-uinterface-사용">해결: UInterface 사용</h3>
<pre><code class="language-cpp">// C++ 인터페이스 정의
UINTERFACE(Blueprintable)
class UIInventoryWidget : public UInterface {
    GENERATED_BODY()
};

class IIInventoryWidget {
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
    void SetInventoryData(UTSInventoryMasterComponent* Inventory);
};</code></pre>
<pre><code class="language-cpp">// C++에서 호출
if (ContainerWidget-&gt;Implements&lt;UIInventoryWidget&gt;()) {
    IIInventoryWidget::Execute_SetInventoryData(
        ContainerWidget, 
        ContainerInventory
    );
}</code></pre>
<h3 id="블루프린트-연결">블루프린트 연결</h3>
<ol>
<li>위젯 블루프린트 Class Settings에서 인터페이스 추가</li>
<li>Event Graph에서 <code>SetInventoryData</code> 이벤트 구현</li>
<li>전달받은 데이터로 슬롯 UI 생성</li>
</ol>
<hr>
<h2 id="방어적-프로그래밍으로-ui-상태-관리">방어적 프로그래밍으로 UI 상태 관리</h2>
<h3 id="발견한-버그">발견한 버그</h3>
<pre><code class="language-cpp">// 문제 시나리오
1. 상자 열림 (타이머 실행 중)
2. 환경설정 버튼 클릭
3. 환경설정 닫음
4. CurrentContainer가 남아있고 타이머도 실행 중!</code></pre>
<h3 id="해결">해결</h3>
<pre><code class="language-cpp">void CloseSettings() {
    bIsSettingsOpen = false;

    // ✅ 혹시 남은 상태 정리
    CloseCurrentContainer();  // nullptr이면 바로 return

    UpdateInputMode();
}

void OpenSettings() {
    // ✅ 열기 전에 게임 UI 정리
    if (IsAnyUIOpen()) {
        CloseAllGameUI();
    }
    // ...
}</code></pre>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>UI 시스템을 설계하면서 &quot;사용자가 어떤 순서로 버튼을 누를지 모른다&quot;는 걸 체감했다. 처음엔 정상적인 경로만 생각했는데, 상자 열고 → 설정 열고 → 설정 닫으면 상자 타이머가 남아있는 버그를 발견했다. </p>
<p>방어적 프로그래밍의 중요성을 깨달았다. 특히 UI처럼 상태가 복잡한 시스템에서는 각 함수가 &quot;들어올 때 상태 확인, 나갈 때 정리&quot;를 철저히 해야 한다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251117]]></title>
            <link>https://velog.io/@s_kim__/TIL-251117</link>
            <guid>https://velog.io/@s_kim__/TIL-251117</guid>
            <pubDate>Mon, 17 Nov 2025 12:09:56 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-17</code></p>
<ul>
<li><input disabled="" type="checkbox"> OnPreviewMouseButtonDown vs OnMouseButtonDown 차이점</li>
<li><input disabled="" type="checkbox"> 드래그앤드롭에서 좌클릭/우클릭 분기 처리</li>
<li><input disabled="" type="checkbox"> DragDropOperation에 데이터 저장하는 패턴</li>
<li><input disabled="" type="checkbox"> 언리얼 이벤트 버블링 순서 (Tunneling vs Bubbling)</li>
</ul>
<hr>
<h2 id="onpreviewmousebuttondown-vs-onmousebuttondown-차이점">OnPreviewMouseButtonDown vs OnMouseButtonDown 차이점</h2>
<p>언리얼 엔진의 마우스 이벤트는 <strong>두 단계</strong>로 처리된다:</p>
<h3 id="1-tunneling-단계-preview">1. Tunneling 단계 (Preview)</h3>
<pre><code>OnPreviewMouseButtonDown
- 부모 → 자식 방향으로 전파 ⬇️
- 원본 이벤트 정보를 그대로 받음
- Get Effecting Button이 정확하게 동작함</code></pre><h3 id="2-bubbling-단계-일반">2. Bubbling 단계 (일반)</h3>
<pre><code>OnMouseButtonDown
- 자식 → 부모 방향으로 전파 ⬆️
- 자식 위젯이 이벤트를 먼저 처리
- 이벤트 정보가 변형되거나 손상될 수 있음</code></pre><h3 id="실제-문제-상황">실제 문제 상황</h3>
<pre><code>Button (또는 Canvas Panel)
├─ Image
└─ TextBlock</code></pre><ul>
<li><strong>OnMouseButtonDown 사용</strong>: Image/TextBlock이 먼저 이벤트를 받아 변형 → 부모가 받을 때는 버튼 정보 손상</li>
<li><strong>OnPreviewMouseButtonDown 사용</strong>: 부모가 먼저 원본 이벤트 받음 → 버튼 정보 정확함 ✅</li>
</ul>
<p><strong>결론</strong>: 드래그앤드롭 구현 시 <strong>OnPreviewMouseButtonDown을 사용</strong>해야 한다. 위젯 타입(Button, Canvas Panel 등)과 무관하게 자식 위젯이 있으면 Preview를 써야 한다.</p>
<hr>
<h2 id="드래그앤드롭에서-좌클릭우클릭-분기-처리">드래그앤드롭에서 좌클릭/우클릭 분기 처리</h2>
<h3 id="요구사항">요구사항</h3>
<ul>
<li>좌클릭 드래그: 아이템 전체 이동 (bIsFullStack = true)</li>
<li>우클릭 드래그: 아이템 1개만 이동 (bIsFullStack = false)</li>
</ul>
<h3 id="잘못된-접근-❌">잘못된 접근 ❌</h3>
<pre><code>OnMouseButtonDown의 반환값(true/false)이
→ OnDragDetected나 OnDrop으로 전달된다고 생각</code></pre><p>→ <strong>전달되지 않는다!</strong> 각 이벤트의 FPointerEvent 파라미터에서 독립적으로 확인해야 함.</p>
<h3 id="올바른-접근-✅">올바른 접근 ✅</h3>
<h4 id="1단계-버튼-정보-캐싱">1단계: 버튼 정보 캐싱</h4>
<pre><code>OnPreviewMouseButtonDown:
  Mouse Event → Get Effecting Button
    → Set CachedMouseButton (위젯 변수에 저장)
    → Branch (Left or Right?)
       True: Detect Drag 반환
       False: Unhandled 반환</code></pre><h4 id="2단계-dragoperation에-정보-저장">2단계: DragOperation에 정보 저장</h4>
<pre><code>OnDragDetected:
  CachedMouseButton 사용
    → Branch (Left?)
       True: 
         - bIsFullStack = true
         - TransferQuantity = CurrentStackSize
       False:
         - bIsFullStack = false
         - TransferQuantity = 1
    → DragOperation 생성 및 반환</code></pre><h4 id="3단계-ondrop에서-사용">3단계: OnDrop에서 사용</h4>
<pre><code>OnDrop:
  DragOperation → bIsFullStack 읽기
    → ServerTransferItem(..., bIsFullStack)</code></pre><hr>
<h2 id="dragdropoperation에-데이터-저장하는-패턴">DragDropOperation에 데이터 저장하는 패턴</h2>
<h3 id="핵심-원칙">핵심 원칙</h3>
<p><strong>버튼 확인은 한 번만, 저장해서 재사용</strong></p>
<h3 id="구조">구조</h3>
<pre><code class="language-cpp">UCLASS()
class UMyDragDropOperation : public UDragDropOperation
{
    UPROPERTY(BlueprintReadWrite)
    bool bIsFullStack;

    UPROPERTY(BlueprintReadWrite)
    int32 TransferQuantity;
};</code></pre>
<h3 id="장점">장점</h3>
<ol>
<li><strong>중복 확인 제거</strong>: OnDragDetected에서 한 번만 확인</li>
<li><strong>드래그 비주얼</strong>: TransferQuantity로 개수 표시 가능</li>
<li><strong>OnDrop 간소화</strong>: 저장된 값만 읽으면 됨</li>
</ol>
<h3 id="흐름">흐름</h3>
<pre><code>OnPreviewMouseButtonDown (버튼 확인 및 캐싱)
    ↓
OnDragDetected (DragOperation에 데이터 저장)
    ↓
OnDrop (저장된 데이터 사용)</code></pre><hr>
<h2 id="추가-학습-내용">추가 학습 내용</h2>
<h3 id="onmousebuttondown-반환값의-의미">OnMouseButtonDown 반환값의 의미</h3>
<ul>
<li><strong>Detect Drag</strong>: 드래그 감지 시작 (가장 일반적)</li>
<li><strong>Handled</strong>: 이벤트만 소비, 드래그 안 함</li>
<li><strong>Unhandled</strong>: 이벤트 무시, 부모로 전파</li>
</ul>
<h3 id="ondragdetected의-버그">OnDragDetected의 버그</h3>
<p><strong>OnDragDetected의 Pointer Event에서 Get Effecting Button → None 반환</strong></p>
<ul>
<li>드래그 감지 후 시점이라 원본 버튼 정보 손실</li>
<li><strong>해결책</strong>: OnPreviewMouseButtonDown에서 미리 캐싱</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>언리얼의 이벤트 시스템이 생각보다 복잡하다는 걸 깨달았다. 처음에는 단순히 OnMouseButtonDown에서 반환값을 조작하면 되는 줄 알았는데, 실제로는:</p>
<ol>
<li><strong>이벤트 전파 순서</strong> (Preview → Normal)</li>
<li><strong>자식/부모 위젯 간 이벤트 흐름</strong></li>
<li><strong>각 이벤트 함수의 Pointer Event 독립성</strong></li>
</ol>
<p>이 모든 것을 이해해야 제대로 동작시킬 수 있었다.</p>
<p>특히 <strong>OnPreviewMouseButtonDown의 존재</strong>를 몰랐다면 계속 삽질했을 것 같다. 위젯 안에 Image나 TextBlock 같은 자식 위젯이 있으면 이들이 이벤트를 먼저 받아서 버튼 정보를 망가뜨린다는 것도 직접 겪지 않았으면 상상도 못했을 문제다.</p>
<p>결국 프레임워크의 내부 동작 원리를 이해하는 것이 중요하다는 걸 다시 한번 배웠다. 표면적인 사용법만 아는 것과 내부 메커니즘을 이해하는 것의 차이를 체감했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251114]]></title>
            <link>https://velog.io/@s_kim__/TIL-251114</link>
            <guid>https://velog.io/@s_kim__/TIL-251114</guid>
            <pubDate>Fri, 14 Nov 2025 12:19:58 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-14</code></p>
<ul>
<li><input disabled="" type="checkbox"> 2주차 작업 정리</li>
</ul>
<hr>
<h2 id="2주차-작업-정리">2주차 작업 정리</h2>
<h3 id="🎯-핵심-구현-내용">🎯 핵심 구현 내용</h3>
<h4 id="1-부패도-시스템-연동">1. 부패도 시스템 연동</h4>
<ul>
<li><strong>인벤토리 컴포넌트에서 부패도 관련 구현</strong><ul>
<li>부패도 매니저 델리게이트 구독 (<code>OnDecayTick</code> 바인딩)</li>
<li><code>OnDecayTick()</code> 콜백 함수 구현 - 1초마다 호출</li>
<li><code>ConvertToDecayedItem()</code> 함수로 만료된 아이템을 부패물로 변환</li>
<li><code>UpdateExpirationTime()</code> 함수로 아이템 스택 시 만료 시각 평균값 계산</li>
<li>부패물 아이템 정보 캐싱 (<code>CachedDecayedItemInfo</code>)</li>
</ul>
</li>
</ul>
<h4 id="2-인벤토리-시스템-리팩토링">2. 인벤토리 시스템 리팩토링</h4>
<ul>
<li><p><strong>데이터 구조 개선</strong></p>
<ul>
<li>기존 슬롯 구조를 <code>FItemInstance</code> 구조체 기반으로 전면 수정</li>
<li>만료 시간(<code>ExpirationTime</code>) 필드 추가 및 활용</li>
<li>오래된 중복 프로퍼티 제거</li>
</ul>
</li>
<li><p><strong>핵심 기능 구현</strong></p>
<ul>
<li><code>AddItem</code> 테스트 및 검증 완료</li>
<li><code>TransferItem</code> 기능 테스트</li>
<li>소모품 아이템 사용 시 GameplayAbility 활성화 연동</li>
<li>서버 네트워크 갱신 (ForceNetUpdate 주석 처리 - 테스트 중)</li>
</ul>
</li>
</ul>
<h4 id="3-ui-시스템-개선">3. UI 시스템 개선</h4>
<ul>
<li><p><strong>HUD 초기화 및 동기화</strong></p>
<ul>
<li>컨트롤러에서 HUD 초기화 구현</li>
<li><strong>클라이언트 HUD 업데이트 문제 해결</strong> ✅</li>
</ul>
</li>
<li><p><strong>인벤토리 UI 기능</strong></p>
<ul>
<li><code>ToggleInventory</code> 함수 구현</li>
<li>가방 사이즈 확장 시 동적 슬롯 추가 UI 구현</li>
</ul>
</li>
<li><p><strong>드래그 앤 드롭 개선</strong></p>
<ul>
<li><code>FSlotStructMaster</code> 관련 위젯 변경사항 적용</li>
<li>슬롯 아이콘 이미지 업데이트 기능 추가</li>
</ul>
</li>
</ul>
<h4 id="4-최적화-및-리팩토링">4. 최적화 및 리팩토링</h4>
<ul>
<li>델리게이트 처리 간소화 (<code>HandleInventoryChanged()</code>)</li>
<li><code>OnInventoryInitialized</code> 델리게이트 추가</li>
<li>부패도 매니저 캐싱으로 성능 최적화</li>
</ul>
<hr>
<h2 id="tsinventorymastercomponent-완성도-평가">TSInventoryMasterComponent 완성도 평가</h2>
<h3 id="📊-전체-완성도-약-73">📊 전체 완성도: <strong>약 73%</strong></h3>
<h3 id="세부-분석">세부 분석:</h3>
<h4 id="✅-완전-구현된-기능-약-65">✅ <strong>완전 구현된 기능</strong> (약 65%)</h4>
<ol>
<li><p><strong>네트워크 리플리케이션</strong> (100%)</p>
<ul>
<li>DOREPLIFETIME 설정 완료</li>
<li>OnRep 콜백 구현</li>
<li>Server RPC (Validate + Implementation) 완벽 구현</li>
</ul>
</li>
<li><p><strong>인벤토리 관리</strong> (95%)</p>
<ul>
<li>핫키/장비/가방 인벤토리 초기화 ✅</li>
<li>AddItem (스택 처리, 우선순위 로직 포함) ✅</li>
<li>RemoveItem ✅</li>
<li>슬롯 조회/검증 함수 ✅</li>
</ul>
</li>
<li><p><strong>아이템 이동 시스템</strong> (100%)</p>
<ul>
<li>Internal_TransferItem (스택 처리, 교환) ✅</li>
<li>타입 검증 (CanPlaceItemInSlot) ✅</li>
<li>드래그 앤 드롭 지원 ✅</li>
</ul>
</li>
<li><p><strong>가방 확장 시스템</strong> (100%)</p>
<ul>
<li>ExpandBagInventory ✅</li>
<li>가방 아이템 사용 로직 ✅</li>
<li>OnBagSizeChanged 델리게이트 ✅</li>
</ul>
</li>
<li><p><strong>부패도 시스템 연동</strong> (100%)</p>
<ul>
<li>OnDecayTick 콜백 ✅</li>
<li>ConvertToDecayedItem ✅</li>
<li>UpdateExpirationTime ✅</li>
<li>부패물 캐싱 ✅</li>
</ul>
</li>
<li><p><strong>아이템 사용</strong> (90%)</p>
<ul>
<li>Internal_UseItem ✅</li>
<li>GAS 연동 (Ability 활성화) ✅</li>
<li>가방 아이템 특수 처리 ✅</li>
</ul>
</li>
</ol>
<h4 id="⚠️-부분-구현todo-항목-약-8">⚠️ <strong>부분 구현/TODO 항목</strong> (약 8%)</h4>
<ol start="7">
<li><p><strong>장비 시스템</strong> (30%)</p>
<ul>
<li>Equipment 인벤토리 구조 ✅</li>
<li>슬롯 타입 검증 ✅</li>
<li>❌ 실제 장착/해제 로직 (TODO 주석만 존재)</li>
<li>❌ 장비 효과 적용 (스탯, 능력치 변화 등)</li>
</ul>
</li>
<li><p><strong>월드 아이템 드롭</strong> (20%)</p>
<ul>
<li>ServerDropItemToWorld RPC 구조 ✅</li>
<li>❌ 스폰 매니저 연동 (TODO 주석)</li>
</ul>
</li>
<li><p><strong>네트워크 최적화</strong> (50%)</p>
<ul>
<li>⚠️ ForceNetUpdate() 전부 주석 처리</li>
<li>의도적인지 테스트 중인지 불명확</li>
</ul>
</li>
</ol>
<h4 id="❌-미구현-기능-약-27">❌ <strong>미구현 기능</strong> (약 27%)</h4>
<ol start="10">
<li><p><strong>컨테이너/상자 시스템</strong> (0%)</p>
<ul>
<li>스크럼 메모에서 &quot;다음 할 일&quot;로 언급</li>
<li>현재 플레이어 인벤토리만 구현됨</li>
<li>컨테이너와의 상호작용 로직 필요</li>
</ul>
</li>
<li><p><strong>내구도 시스템</strong> (0%)</p>
<ul>
<li>ItemData.h에 데이터는 존재</li>
<li>인벤토리 컴포넌트에서 내구도 감소/복구 로직 없음</li>
<li>장비/도구 사용 시 내구도 처리 필요</li>
</ul>
</li>
<li><p><strong>에러 처리 및 클라이언트 피드백</strong> (40%)</p>
<ul>
<li>서버 측 검증은 잘 되어 있음</li>
<li>❌ 클라이언트로의 실패 피드백 메커니즘 부족</li>
<li>❌ 인벤토리 가득 참, 이동 불가 등의 UI 알림 없음</li>
</ul>
</li>
<li><p><strong>추가 필요 기능</strong></p>
<ul>
<li>❌ 아이템 정렬 기능</li>
<li>❌ 빠른 이동 (더블클릭 등)</li>
<li>❌ 아이템 분할 UI</li>
</ul>
</li>
</ol>
<hr>
<h3 id="💡-종합-평가">💡 종합 평가</h3>
<p><strong>현재 상태: 약 73% 완성</strong></p>
<ul>
<li><strong>핵심 플레이어 인벤토리</strong>: 95% 완성 (매우 잘 구현됨)</li>
<li><strong>멀티플레이어 네트워크</strong>: 90% 완성 (ForceNetUpdate 주석 처리 확인 필요)</li>
<li><strong>확장 시스템</strong>: 30% 완성 (구조만 있거나 TODO)</li>
</ul>
<p><strong>다음 단계 권장사항:</strong></p>
<ol>
<li>🔴 <strong>우선</strong>: 컨테이너/상자 인벤토리 구현 (계획된 작업)</li>
<li>🟡 <strong>중요</strong>: 장비 시스템 완성 (TODO 해결)</li>
<li>🟡 <strong>중요</strong>: 월드 드롭 스폰 매니저 연동</li>
<li>🟢 <strong>개선</strong>: ForceNetUpdate 주석 처리 의도 확인</li>
<li>🟢 <strong>장기</strong>: 내구도 시스템, 에러 피드백 UI</li>
</ol>
<p><strong>강점:</strong></p>
<ul>
<li>코드 구조가 깔끔하고 모듈화가 잘 되어 있음</li>
<li>네트워크 리플리케이션 구조가 탄탄함</li>
<li>부패도 같은 복잡한 시스템도 잘 통합됨</li>
</ul>
<p><strong>개선점:</strong></p>
<ul>
<li>TODO 항목들 해결 필요</li>
<li>클라이언트 UX 관련 피드백 메커니즘 추가 필요</li>
</ul>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>코드랑 이번주 스크럼 내용으로 AI를 이용해 완성도를 평가해봤다.
2주차까지 한 작업을 정리해보니 어느 부분을 빠르게 처리해야할지 한 눈에 보인다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 251113]]></title>
            <link>https://velog.io/@s_kim__/TIL-251113</link>
            <guid>https://velog.io/@s_kim__/TIL-251113</guid>
            <pubDate>Thu, 13 Nov 2025 12:17:27 GMT</pubDate>
            <description><![CDATA[<h1 id="span-stylebackground-color-fbdea2✍️today-i-learnedspan"><span style="background-color: #fbdea2;">✍️Today I Learned</span></h1>
<p> 📅 <code>2025-11-13</code></p>
<ul>
<li><input disabled="" type="checkbox"> 언리얼 네트워크 복제: Actor 이름은 월드별로 독립적이다</li>
<li><input disabled="" type="checkbox"> 구조체 내부 값 변경 시 자동 복제 조건</li>
</ul>
<hr>
<h2 id="언리얼-네트워크-복제-actor-이름은-월드별로-독립적이다">언리얼 네트워크 복제: Actor 이름은 월드별로 독립적이다</h2>
<h3 id="🤔-문제-상황">🤔 문제 상황</h3>
<p>멀티플레이어 환경에서 Client가 <code>BP_TSCharacter_C_0</code>을 참조하는 것을 보고 &quot;어? Player 1인데 왜 0번 캐릭터를 참조하지?&quot;라고 착각했다.</p>
<h3 id="💡-핵심-개념">💡 핵심 개념</h3>
<p><strong>각 월드(Server, Client)는 자신만의 Actor 인스턴스를 가지며, 이름은 각 월드에서 독립적으로 생성된다.</strong></p>
<pre><code>Server 월드:
├─ BP_TSCharacter_C_0 (Server Player)
└─ BP_TSCharacter_C_1 (Client Player)

Client 월드:
├─ BP_TSCharacter_C_0 (자신 = AutonomousProxy) ← 서버의 C_1과 동기화!
└─ BP_TSCharacter_C_1 (서버 복제본 = SimulatedProxy)</code></pre><p><strong>Actor 이름이 아니라 Role 값으로 판단해야 한다:</strong></p>
<ul>
<li><code>Role: ROLE_AutonomousProxy (2)</code> = 내가 제어하는 캐릭터</li>
<li><code>Role: ROLE_SimulatedProxy (1)</code> = 다른 플레이어의 복제본</li>
<li><code>Role: ROLE_Authority (3)</code> = 서버</li>
</ul>
<h3 id="✅-결론">✅ 결론</h3>
<ul>
<li>Actor 이름(C_0, C_1)은 각 월드에서 독립적으로 생성됨</li>
<li>&quot;같은 Player&quot;라도 Server 월드와 Client 월드에서 이름이 다를 수 있음</li>
<li><strong>Role 값이 진실!</strong> 이름만 보고 판단하면 안 됨</li>
</ul>
<hr>
<h2 id="구조체-내부-값-변경-시-자동-복제-조건">구조체 내부 값 변경 시 자동 복제 조건</h2>
<h3 id="🤔-기존-지식">🤔 기존 지식</h3>
<p>&quot;구조체 안에 구조체가 있으면 내부 값만 바꿨을 때 복제 안 되니까 <code>ForceNetUpdate()</code> 써야 한다&quot;</p>
<h3 id="💡-실제로는">💡 실제로는?</h3>
<p><strong>최상위 변수에 <code>ReplicatedUsing</code>을 선언하고, 내부 값들을 <code>UPROPERTY</code>로 정의하면 언리얼이 알아서 직렬화해서 복제해준다!</strong></p>
<pre><code class="language-cpp">// 구조체 정의
USTRUCT(BlueprintType)
struct FSlotStructMaster
{
    GENERATED_BODY()

    UPROPERTY()  // ✅ UPROPERTY만 있으면 됨!
    int32 CurrentStackSize;

    UPROPERTY()
    FItemInstance ItemData;  // 구조체 안의 구조체도 OK
};

// 컴포넌트
UPROPERTY(ReplicatedUsing = OnRep_HotkeyInventory)  // ✅ 최상위만 선언
FInventoryStructMaster HotkeyInventory;</code></pre>
<p><strong>내부 값을 바꾸면:</strong></p>
<pre><code class="language-cpp">HotkeyInventory.InventorySlotContainer[0].CurrentStackSize = 10;
// ✅ 자동으로 OnRep_HotkeyInventory() 호출됨!
// ForceNetUpdate() 불필요!</code></pre>
<h3 id="✅-조건">✅ 조건</h3>
<ol>
<li>최상위 변수에 <code>ReplicatedUsing</code> 선언</li>
<li>구조체의 모든 멤버 변수에 <code>UPROPERTY</code> 매크로</li>
<li><code>GetLifetimeReplicatedProps</code>에 등록</li>
</ol>
<p>→ 언리얼이 자동으로 변경 감지 및 직렬화!</p>
<hr>
<h2 id="💡-느낀-점-what-i-felt">💡 느낀 점 (What I Felt)</h2>
<p>네트워크 복제는 &quot;눈에 보이는 것&quot;과 &quot;실제 동작&quot;이 다를 수 있다는 걸 배웠다. Actor 이름 같은 표면적인 정보에 속지 말고, Role이나 NetMode 같은 본질적인 값을 봐야 한다. </p>
<p>그리고 언리얼의 복제 시스템은 생각보다 똑똑해서, UPROPERTY만 제대로 선언하면 복잡한 구조체도 알아서 직렬화해준다. 불필요한 ForceNetUpdate() 남발을 줄일 수 있을 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>