<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>washble2 님의 작업공간</title>
    <link>https://washble2.tistory.com/</link>
    <description>코딩하면서 배웠던 것들을 기록하는 나만의 공간</description>
    <language>ko</language>
    <pubDate>Fri, 12 Jun 2026 15:03:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>washble2</managingEditor>
    <item>
      <title>ComfyUI + ComfyUI-Manager 설치</title>
      <link>https://washble2.tistory.com/29</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. ComfyUI설치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1-1) ComfyUI Github에서 ComfyUI다운&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/comfy-org/comfyui&quot;&gt;https://github.com/comfy-org/comfyui&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아래로 내리다 보면 window설치 관련된 부분이 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 해당하는 것으로 설치해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(저는 Nvidia GPUs with pytorch cuda 12.6 and python 3.12로 했습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;779&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nzyn9/dJMcahR6pzr/zPEP5I2ISJzg6UQkOJtlB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nzyn9/dJMcahR6pzr/zPEP5I2ISJzg6UQkOJtlB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nzyn9/dJMcahR6pzr/zPEP5I2ISJzg6UQkOJtlB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNzyn9%2FdJMcahR6pzr%2FzPEP5I2ISJzg6UQkOJtlB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;859&quot; height=&quot;779&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;779&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1-2) 다운받은 comfyUI 압축풀고 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운받은 comfyUI의 압축을 풀면 아래와 같이 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cY0z16/dJMcadWtSRI/JiI3fNM0CSo3meHUVuREdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cY0z16/dJMcadWtSRI/JiI3fNM0CSo3meHUVuREdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cY0z16/dJMcadWtSRI/JiI3fNM0CSo3meHUVuREdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcY0z16%2FdJMcadWtSRI%2FJiI3fNM0CSo3meHUVuREdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;246&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ComfyUI_windows_portable폴더 안에 들어가서 보면 아래와 같이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 실행은 run_nvidia_gpu_fast_fp16_accumulation.bat같은 bat파일을 실행해주면 웹창으로 열립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKCWTI/dJMcaiwJQlE/fxs2pIawknXknrsjtI5t1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKCWTI/dJMcaiwJQlE/fxs2pIawknXknrsjtI5t1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKCWTI/dJMcaiwJQlE/fxs2pIawknXknrsjtI5t1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKCWTI%2FdJMcaiwJQlE%2Ffxs2pIawknXknrsjtI5t1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;381&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9G10z/dJMcaf7OSdZ/GwQlCPHdJ9X07w144rQhek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9G10z/dJMcaf7OSdZ/GwQlCPHdJ9X07w144rQhek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9G10z/dJMcaf7OSdZ/GwQlCPHdJ9X07w144rQhek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9G10z%2FdJMcaf7OSdZ%2FGwQlCPHdJ9X07w144rQhek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1270&quot; height=&quot;943&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;portable버전이기에 python을 python_embeded에 있는것을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 추가적인 것을 설치할 때는 저기 python을 사용해주어야합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y9THR/dJMcaicuqAb/UvqMTlQX8NNFqkKIUg8aj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y9THR/dJMcaicuqAb/UvqMTlQX8NNFqkKIUg8aj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y9THR/dJMcaicuqAb/UvqMTlQX8NNFqkKIUg8aj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy9THR%2FdJMcaicuqAb%2FUvqMTlQX8NNFqkKIUg8aj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;346&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. ComfyUI-Manager 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적인 모델들을 custom하기 위해서 필요로 할 경우 설치 관리를 위한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-1) ComfyUI-Manager git 링크로 가줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Comfy-Org/ComfyUI-Manager&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Comfy-Org/ComfyUI-Manager&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-2) 설치하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 링크의 페이지를 아래로 내리다 보면 설치방법이 적혀있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmRwyS/dJMcadWtS0G/ZXZ8tpinD5628kxJjHFIxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmRwyS/dJMcadWtS0G/ZXZ8tpinD5628kxJjHFIxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmRwyS/dJMcadWtS0G/ZXZ8tpinD5628kxJjHFIxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmRwyS%2FdJMcadWtS0G%2FZXZ8tpinD5628kxJjHFIxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;917&quot; height=&quot;899&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;899&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 여기서 설치1번으로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 폴더 위치로 이동 후 ComfyUI-Manager를 깃으로 clone 후 run_nvidia_gpu_fast_fp16_accumulation.bat파일을 시작해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMFrZ5/dJMcahklUCh/8kkPCH1ojkcPdBHMFThyTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMFrZ5/dJMcahklUCh/8kkPCH1ojkcPdBHMFThyTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMFrZ5/dJMcahklUCh/8kkPCH1ojkcPdBHMFThyTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMFrZ5%2FdJMcahklUCh%2F8kkPCH1ojkcPdBHMFThyTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;264&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 때 cmd를 보면 fetch라고하면서 작업을 하고 창이 열립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 때 꼭 열려있는 ComfyUI창이 있으면 끄고 다시 bat로 열어야 합니다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;1016&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be2p1n/dJMb990UtiE/7qYbLjnqD4yBeCLlyQnBrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be2p1n/dJMb990UtiE/7qYbLjnqD4yBeCLlyQnBrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be2p1n/dJMb990UtiE/7qYbLjnqD4yBeCLlyQnBrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe2p1n%2FdJMb990UtiE%2F7qYbLjnqD4yBeCLlyQnBrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1270&quot; height=&quot;1016&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;1016&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ComfyUI</category>
      <category>ai</category>
      <category>comfyui</category>
      <category>comfyui manager</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/29</guid>
      <comments>https://washble2.tistory.com/29#entry29comment</comments>
      <pubDate>Sat, 30 May 2026 15:47:52 +0900</pubDate>
    </item>
    <item>
      <title>Unity App Check at Android, Apple With ASP.NET (Unity, ASP.NET에서 App Check)</title>
      <link>https://washble2.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;구글 스토어에서 APK를 빼가서 다른곳에서 사용하여 접속하는 경우가 있었기에 이를 막기위한 방법으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Check라는 것이 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android, iOS각각 설정하는 방법도 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase를 사용하여 동시에 해결하는 방법도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unity는 아래의 방법에서 설정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/app-check/unity/default-providers?hl=ko&amp;amp;authuser=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://firebase.google.com/docs/app-check/unity/default-providers?hl=ko&amp;amp;authuser=1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Firebase설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Firebase에 해당 프로젝트의 App Check에 들어가줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(보안 탭 &amp;gt; App Check &amp;gt; 시작하기 클릭)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjgKvI/dJMcahK260r/b5hjhyoqwf1EKtl3oUsHg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjgKvI/dJMcahK260r/b5hjhyoqwf1EKtl3oUsHg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjgKvI/dJMcahK260r/b5hjhyoqwf1EKtl3oUsHg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjgKvI%2FdJMcahK260r%2Fb5hjhyoqwf1EKtl3oUsHg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;662&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 앱탭을 선택한 후 세부정보 표시에서 각 플랫폼에 맞게 설정해줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android나 iOS모두 앱 출시를 하시는 과정을 거치면 해당 필요한 부분이 어디인지 쉽게 하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckK6BR/dJMcaaLUYpY/SV0BrLu90P0jkkJIMgkzZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckK6BR/dJMcaaLUYpY/SV0BrLu90P0jkkJIMgkzZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckK6BR/dJMcaaLUYpY/SV0BrLu90P0jkkJIMgkzZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckK6BR%2FdJMcaaLUYpY%2FSV0BrLu90P0jkkJIMgkzZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1433&quot; height=&quot;485&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 앱이 없다면 프로젝트 개요에서 앱을 추가해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIKxw/dJMcahdcg7n/8Izj2571ODzS6eKg2W7ru0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIKxw/dJMcahdcg7n/8Izj2571ODzS6eKg2W7ru0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIKxw/dJMcahdcg7n/8Izj2571ODzS6eKg2W7ru0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIKxw%2FdJMcahdcg7n%2F8Izj2571ODzS6eKg2W7ru0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1076&quot; height=&quot;670&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Unity 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/unity/setup?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://firebase.google.com/docs/unity/setup?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777540893467&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Unity 프로젝트에 Firebase 추가 &amp;nbsp;|&amp;nbsp; Firebase for Unity&quot; data-og-description=&quot;Firebase 프로젝트를 만들고, 앱을 등록하고, Firebase Unity SDK를 추가하는 방법을 비롯하여 Unity 프로젝트에 Firebase를 추가하는 방법을 안내합니다.&quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/unity/setup?hl=ko&quot; data-og-url=&quot;https://firebase.google.com/docs/unity/setup?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/unity/setup?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/unity/setup?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Unity 프로젝트에 Firebase 추가 &amp;nbsp;|&amp;nbsp; Firebase for Unity&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Firebase 프로젝트를 만들고, 앱을 등록하고, Firebase Unity SDK를 추가하는 방법을 비롯하여 Unity 프로젝트에 Firebase를 추가하는 방법을 안내합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Unity에 firebase_unity_sdk의 FirebaseAppCheck.unitypackage를 넣어줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/firebase/firebase-unity-sdk/releases&quot;&gt;https://github.com/firebase/firebase-unity-sdk/releases&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 해당 SDK를 넣은 후에는 의존성 주입을 해야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Android의 경우 Force Resolve&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;713&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lSDIk/dJMcajhJKu9/DQ7UbqmKdaKg480Ma8krQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lSDIk/dJMcajhJKu9/DQ7UbqmKdaKg480Ma8krQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lSDIk/dJMcajhJKu9/DQ7UbqmKdaKg480Ma8krQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlSDIk%2FdJMcajhJKu9%2FDQ7UbqmKdaKg480Ma8krQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;849&quot; height=&quot;713&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;713&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- iOS의 경우 install Cocoapods&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A4hg1/dJMcafzG79B/WsIFcGYvgaSWtJBiusPqaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A4hg1/dJMcafzG79B/WsIFcGYvgaSWtJBiusPqaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A4hg1/dJMcafzG79B/WsIFcGYvgaSWtJBiusPqaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA4hg1%2FdJMcafzG79B%2FWsIFcGYvgaSWtJBiusPqaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;818&quot; height=&quot;699&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Unity Firebase App Check 초기화 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 반드시 Firebase설정 전에 App Check를 설정이 되도록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아래 사진은 공식에서 해당 부분을 캡쳐해두었습니다)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OASLb/dJMcacbS6Jd/iQQqkJqF4EYb2BmakzF5Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OASLb/dJMcacbS6Jd/iQQqkJqF4EYb2BmakzF5Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OASLb/dJMcacbS6Jd/iQQqkJqF4EYb2BmakzF5Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOASLb%2FdJMcacbS6Jd%2FiQQqkJqF4EYb2BmakzF5Y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;432&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777543220908&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Firebase App Check
#if UNITY_EDITOR
                    DebugAppCheckProviderFactory.Instance.SetDebugToken(&quot;F11EB149-BD3F-4781-9952-3A334D2DD8BE&quot;);
                    FirebaseAppCheck.SetAppCheckProviderFactory(DebugAppCheckProviderFactory.Instance);
#else

#if UNITY_ANDROID
                    FirebaseAppCheck.SetAppCheckProviderFactory(PlayIntegrityProviderFactory.Instance);
#elif UNITY_IOS
                    FirebaseAppCheck.SetAppCheckProviderFactory(AppAttestProviderFactory.Instance);
#endif
                    
#endif
                    
                    DependencyStatus dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync()
                        .AsUniTask()
                        .AttachExternalCancellation(destroyCancellationToken);

                    if (dependencyStatus == DependencyStatus.Available)
                    {
                        Auth = FirebaseAuth.DefaultInstance;
                        FirebaseSharedState.SharedInitState = FirebaseSharedState.InitState.Success;
                        
                        // Get AppCheck Token
                        FirebaseSharedState.AppCheckToken = await FetchAppCheckTokenAsync();
                        appCheckResultEvent.Invoke(FirebaseSharedState.AppCheckToken is not null);
                        
                        UnityEngine.Debug.Log($&quot;Firebase Auth initialized successfully.&quot;);
                    }
                    else
                    {
                        UnityEngine.Debug.LogError($&quot;Could not resolve Firebase dependencies: {dependencyStatus}&quot;);
                        FirebaseSharedState.SharedInitState = FirebaseSharedState.InitState.Failed;
                        return;
                    }

                    // Wait one frame to ensure the Firebase authentication system is fully initialized
                    await UniTask.Yield(destroyCancellationToken);
                    UserAuth();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #f3c000; text-align: start;&quot;&gt;iOS는 이대로만하면 문제가 되기 때문에 따로 plugin을 수정하여 Firebase초기화 보다 AppCheck가 되도록 설정해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(iOS의 native가 unity보다 먼저 실행되어 firebase초기화가 먼저되고 app check 설정이 늦게되어서 app check가 먹통이 되었습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 iOS에서 Firebase 초기화 보다 App Check를 먼저할 수 있게 설정한 스크립트입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 때 자동으로 Library를 수정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 firebase_unity_sdk_12.8.0을 사용했을 때 올바르게 작동했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777544313611&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#if UNITY_EDITOR
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

public sealed class IOSFirebaseEarlyInitPatchPostprocess : IOSPostProcessBase
{
    private const string FirebaseInitRelativePath = &quot;Libraries/Plugins/iOS/FirebaseInit.mm&quot;;
    private const string GoogleSignInAppControllerRelativePath = &quot;Libraries/Plugins/iOS/GoogleSignIn/GoogleSignInAppController.mm&quot;;
    private const int FirebasePatchPostProcessOrder = PostProcessOrder + 1;

    private static readonly Regex FirebaseImportRegex = new Regex(
        @&quot;^\s*#import &amp;lt;Firebase\.h&amp;gt;\r?\n&quot;,
        RegexOptions.Multiline | RegexOptions.Compiled);

    private static readonly Regex FirebaseForceInitRegex = new Regex(
        @&quot;\r?\n\+ \(void\)load\s*\r?\n\{\r?\n\s*if \(\[FIRApp defaultApp\] == nil\)\r?\n\s*\{\r?\n\s*\[FIRApp configure\];\r?\n\s*NSLog\(@&quot;&quot;Firebase configured \(force init\)&quot;&quot;\);\r?\n\s*\}\r?\n\}\r?\n&quot;,
        RegexOptions.Compiled);

    private static readonly Regex GoogleSignInForceInitRegex = new Regex(
        @&quot;\r?\n\s*if \(\[FIRApp defaultApp\] == nil\) \{\r?\n\s*\[FIRApp configure\];\r?\n\s*\}\r?\n&quot;,
        RegexOptions.Compiled);

    [PostProcessBuild(FirebasePatchPostProcessOrder)]
    public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
    {
        if (!IsIOSBuild(target)) { return; }

        PatchFirebaseInit(pathToBuiltProject);
        PatchGoogleSignInAppController(pathToBuiltProject);
    }

    private static void PatchFirebaseInit(string pathToBuiltProject)
    {
        string filePath = GetBuildFilePath(pathToBuiltProject, FirebaseInitRelativePath);
        string original = ReadRequiredFile(filePath);
        string patched = FirebaseImportRegex.Replace(original, string.Empty);
        patched = FirebaseForceInitRegex.Replace(patched, &quot;\n&quot;);

        WriteIfChanged(filePath, original, patched, &quot;FirebaseInit.mm&quot;);
    }

    private static void PatchGoogleSignInAppController(string pathToBuiltProject)
    {
        string filePath = GetBuildFilePath(pathToBuiltProject, GoogleSignInAppControllerRelativePath);
        string original = ReadRequiredFile(filePath);
        string patched = FirebaseImportRegex.Replace(original, string.Empty);
        patched = GoogleSignInForceInitRegex.Replace(patched, &quot;\n&quot;);

        WriteIfChanged(filePath, original, patched, &quot;GoogleSignInAppController.mm&quot;);
    }

    private static string ReadRequiredFile(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($&quot;Missing iOS plugin file to patch: {filePath}&quot;, filePath);
        }

        return File.ReadAllText(filePath);
    }

    private static void WriteIfChanged(string filePath, string original, string patched, string displayName)
    {
        if (patched == original)
        {
            Debug.Log($&quot;[iOS Firebase Patch] No early-init patch needed for {displayName}.&quot;);
            return;
        }

        File.WriteAllText(filePath, patched);
        Debug.Log($&quot;[iOS Firebase Patch] Patched {displayName} to remove early FIRApp.configure().&quot;);
    }
}
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그리고 iOS는 Firebase Console의 앱에서 설정한 DeviceCheck또는 App Attest는 골라서 사용하면 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(※ App Attest는 Apple Xcode의 Signing &amp;amp; Capabilities의 Capability에서 App Attest를 넣어줘야 한다고 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeviceCheck의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirebaseAppCheck.SetAppCheckProviderFactory(DeviceCheckProviderFactory.Instance);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Attest의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirebaseAppCheck.SetAppCheckProviderFactory(AppAttestProviderFactory.Instance);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Unity App Check 토큰 가져오기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Check로 인증이 제대로 됐는지 토큰 가져오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(백엔드 서버에서 검증을 위해 가져옵니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/app-check/unity/custom-resource?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://firebase.google.com/docs/app-check/unity/custom-resource?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777569337870&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 공식 문서 버전
void CallApiExample() {
    FirebaseAppCheck.DefaultInstance.GetAppCheckToken(false).
      ContinueWithOnMainThread(task =&amp;gt; {
        if (!task.IsFaulted) {
            // Got a valid App Check token. Include it in your own http calls.
            string appCheckToken = task.Result.Token;
        }
    });
}


// UniTask 버전
private async UniTask&amp;lt;string&amp;gt; FetchAppCheckTokenAsync(bool forceRefresh = true)
{
    try
    {
        AppCheckToken token = await FirebaseAppCheck.DefaultInstance
            .GetAppCheckTokenAsync(forceRefresh)
            .AsUniTask()
            .AttachExternalCancellation(destroyCancellationToken);
        return token.Token;
    }
    catch (Exception e)
    {
        UnityEngine.Debug.LogError($&quot;App Check failed: {e.Message}&quot;);
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 서버에서 토큰 검증&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 토큰을 가지고 검증해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko#node.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko#node.js&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777569582519&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;커스텀 백엔드에서 앱 체크 토큰 확인 &amp;nbsp;|&amp;nbsp; Firebase App Check&quot; data-og-description=&quot;Google 외 백엔드 리소스를 보호하기 위해 커스텀 백엔드에서 앱 체크 토큰을 확인하는 방법을 안내합니다.&quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko#node.js&quot; data-og-url=&quot;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko#node.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/app-check/custom-resource-backend?hl=ko#node.js&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;커스텀 백엔드에서 앱 체크 토큰 확인 &amp;nbsp;|&amp;nbsp; Firebase App Check&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google 외 백엔드 리소스를 보호하기 위해 커스텀 백엔드에서 앱 체크 토큰을 확인하는 방법을 안내합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 예시는 ASP.NET서버 사용 했을 때입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Logger설정, IHttpClientFactory설정, appsetting.json에 값 넣기등은 직접 하셔야합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;projectNumber, projectId는 firebase console에 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgy3Of/dJMcadhyZb1/NyHkSU9BoGJW1ZHSqNDpy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgy3Of/dJMcadhyZb1/NyHkSU9BoGJW1ZHSqNDpy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgy3Of/dJMcadhyZb1/NyHkSU9BoGJW1ZHSqNDpy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgy3Of%2FdJMcadhyZb1%2FNyHkSU9BoGJW1ZHSqNDpy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1055&quot; height=&quot;613&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VerifyAsync에서 검증 결과를 반환해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777569458699&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;

public class AppCheckVerification
{
    private static ILogger&amp;lt;AppCheckVerification&amp;gt; _logger = null!;
    private static IHttpClientFactory _httpClientFactory = null!;
    private static string _validIssuer = null!;
    private const string JwksUrl = &quot;https://firebaseappcheck.googleapis.com/v1/jwks&quot;;
    
    private static string[] _validAudiences = null!;
    
    public static void Initialize(ILogger&amp;lt;AppCheckVerification&amp;gt; logger, 
        string projectId, string projectNumber, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;

        _validIssuer = $&quot;https://firebaseappcheck.googleapis.com/{projectNumber}&quot;;
        _validAudiences = new[] { $&quot;projects/{projectNumber}&quot;, $&quot;projects/{projectId}&quot; };
        
        _logger.LogInformation(&quot;AppCheck Verification initialized successfully.&quot;);
    }

    public static async Task&amp;lt;bool&amp;gt; VerifyAsync(string? appCheckToken)
    {
        if (string.IsNullOrEmpty(appCheckToken)) return false;

        try
        {
            // Debug: Uncomment to inspect token claims (aud, iss) when troubleshooting validation failures
            // JwtSecurityTokenHandler debugHandler = new JwtSecurityTokenHandler();
            // JwtSecurityToken jwtToken = debugHandler.ReadJwtToken(appCheckToken);
            // _logger.LogInformation(&quot;Audiences: {aud}&quot;, string.Join(&quot;, &quot;, jwtToken.Audiences));
            // _logger.LogInformation(&quot;Issuer: {iss}&quot;, jwtToken.Issuer);
            
            HttpClient client = _httpClientFactory.CreateClient();
            JsonWebKeySet? jwks = await client.GetFromJsonAsync&amp;lt;JsonWebKeySet&amp;gt;(JwksUrl);

            JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
            handler.ValidateToken(appCheckToken, new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = _validIssuer,
                ValidateAudience = true,
                ValidAudiences = _validAudiences,
                ValidateLifetime = true,
                IssuerSigningKeys = jwks!.Keys,
            }, out _);

            _logger.LogInformation(&quot;AppCheck token verified successfully.&quot;);
            return true;
        }
        catch (SecurityTokenException ex)
        {
            _logger.LogError(ex, &quot;AppCheck token validation failed.&quot;);
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Unexpected error during AppCheck token verification.&quot;);
            return false;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. Firebase Authentication(파이어베이스 로그인 인증)에 App Check 적용 &lt;/b&gt;&lt;b&gt;(적용 클릭 시 App Check 강제함)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- authentication 사용하기 설정 후&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btPyrH/dJMcagrOWyV/7yDI5VLZh73uFgGB5LKEXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btPyrH/dJMcagrOWyV/7yDI5VLZh73uFgGB5LKEXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btPyrH/dJMcagrOWyV/7yDI5VLZh73uFgGB5LKEXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtPyrH%2FdJMcagrOWyV%2F7yDI5VLZh73uFgGB5LKEXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;418&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp; 실제 적용할 때는 Firebase Console에서 Authentication에서 적용을 누르면 Firebase Login 인증에 AppCheck를 강제합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3vzhw/dJMb99TKY4K/hUbEOp3WayA16iKaUpopWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3vzhw/dJMb99TKY4K/hUbEOp3WayA16iKaUpopWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3vzhw/dJMb99TKY4K/hUbEOp3WayA16iKaUpopWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3vzhw%2FdJMb99TKY4K%2FhUbEOp3WayA16iKaUpopWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;933&quot; height=&quot;1186&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unity</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/28</guid>
      <comments>https://washble2.tistory.com/28#entry28comment</comments>
      <pubDate>Fri, 1 May 2026 02:25:20 +0900</pubDate>
    </item>
    <item>
      <title>Unity AR World와 3D World 같이 쓸 때 주의사항</title>
      <link>https://washble2.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AR을 껐다 켰다하며 유니티 3D World를 같이 쓸 때 주의할 점 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AR을 껐다 켜는 스크립트 참고는 아래 링크에서 하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.naver.com/washble2/223958898798&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/washble2/223958898798&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. AR월드의 Position과 3D World의 Position차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AR월드의 Position과 3D World의 Position이 다르기 때문에 이를 보간하는 작업을 해줘야 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 링크를 참고하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.naver.com/washble2/224085744801&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/washble2/224085744801&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777056272114&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;AR Camera(AR Face) 와 Main Camera 같이 쓸 때 위치 변화&quot; data-og-description=&quot;Unity 6000.0.59f2버전을 사용했습니다 프로젝트에서 Main Camera와 AR Camera를 같이 바꿔가며 써야...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/washble2/224085744801&quot; data-og-url=&quot;https://blog.naver.com/washble2/224085744801&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dfFX5Q/dJMb82MFJls/BUQStX1yzqDSpPDWxsg1Ak/img.png?width=260&amp;amp;height=117&amp;amp;face=0_0_260_117&quot;&gt;&lt;a href=&quot;https://blog.naver.com/washble2/224085744801&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/washble2/224085744801&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dfFX5Q/dJMb82MFJls/BUQStX1yzqDSpPDWxsg1Ak/img.png?width=260&amp;amp;height=117&amp;amp;face=0_0_260_117');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;AR Camera(AR Face) 와 Main Camera 같이 쓸 때 위치 변화&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Unity 6000.0.59f2버전을 사용했습니다 프로젝트에서 Main Camera와 AR Camera를 같이 바꿔가며 써야...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. AR을 실행시킬 때 Camera 세팅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 AR Session과 3D Object들을 분리해두는것으로는 해결이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서는 별 문제 없이 작동합니다만 iOS에서는 AR World와 3D World가 따로놀기 시작합니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 3D World의 것들을 보는 OverlayCamera를 준비하고 여기서 culling mask에서 AR관련 것들을 빼버립니다(Layer사용)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t29jl/dJMcaad0UAj/8XxO4f3Rvd5B0kSgfLBqW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t29jl/dJMcaad0UAj/8XxO4f3Rvd5B0kSgfLBqW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t29jl/dJMcaad0UAj/8XxO4f3Rvd5B0kSgfLBqW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft29jl%2FdJMcaad0UAj%2F8XxO4f3Rvd5B0kSgfLBqW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;560&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- XR Origin 하위의 AR Camera는&amp;nbsp; Culling mask에서 AR관련된 것들만 볼 수 있도록 해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xfdvD/dJMcaakLhGB/hoa8EyceXIcVQQVw0Nh071/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xfdvD/dJMcaakLhGB/hoa8EyceXIcVQQVw0Nh071/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xfdvD/dJMcaakLhGB/hoa8EyceXIcVQQVw0Nh071/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxfdvD%2FdJMcaakLhGB%2Fhoa8EyceXIcVQQVw0Nh071%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;448&quot; height=&quot;684&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Culling Mask작업이 끝났으면 AR Camera Stack에 Overlay Camera를 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 Stack은 URP버전에 있습니다 Built-in버전에는 없어요)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rS7gl/dJMcafTVkAL/FVvg6ZkuVEgA5G1JWkKInk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rS7gl/dJMcafTVkAL/FVvg6ZkuVEgA5G1JWkKInk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rS7gl/dJMcafTVkAL/FVvg6ZkuVEgA5G1JWkKInk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrS7gl%2FdJMcafTVkAL%2FFVvg6ZkuVEgA5G1JWkKInk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;143&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AR을 사용하게 될 때 AR Camera와, Overlay Camera를 활성화 해주면 AR과 3D World가 함께 잘 나오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. iOS에서 AR을 껐다 켤 때 처음 킨 AR의 위치로만 계속 작동 오류 해결&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 처음 AR실행시킬 때의 위치값을 Reset시켜줘야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 AR Start 시킬 때 arSession.Reset();함수를 작동시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수를 작동 시키면 AR을 켤 때마다 다시 위치정보를 가져와서 제대로 위치값을 가져옵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777057275018&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private void SetActiveARSession(bool value)
    {
        arSession.transform.parent.gameObject.SetActive(value);
        arSession.enabled = value;
        ARStarted = value;
        
        if(value) { arSession.Reset(); }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unity</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/27</guid>
      <comments>https://washble2.tistory.com/27#entry27comment</comments>
      <pubDate>Sat, 25 Apr 2026 04:02:26 +0900</pubDate>
    </item>
    <item>
      <title>LightSail 서버 시간 바꾸기</title>
      <link>https://washble2.tistory.com/26</link>
      <description>&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;1. 현재 서버 시간 확인&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;먼저 지금 서버 시간이 어떻게 설정되어 있는지 확인합니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8y--i5OqTAxUAAAAAHQAAAAAQhgs&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;$ date
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;2. 타임존을 서울(KST)로 변경&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;timedatectl 명령어를 사용하면 복잡한 설정 없이 바로 바뀝니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8y--i5OqTAxUAAAAAHQAAAAAQhws&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;$ sudo timedatectl set-timezone Asia/Seoul
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;3. 변경 결과 확인&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;다시 date를 입력해봅니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8y--i5OqTAxUAAAAAHQAAAAAQiAs&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;$ date
# 결과 예시: Tue Apr 14 20:29:24 KST 2026&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개발</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/26</guid>
      <comments>https://washble2.tistory.com/26#entry26comment</comments>
      <pubDate>Tue, 14 Apr 2026 20:32:25 +0900</pubDate>
    </item>
    <item>
      <title>ASP.Net서버에 Serilog를 이용한 File Logging</title>
      <link>https://washble2.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Serilog 패키지 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776063848206&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Serilog Package 설치
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Program.cs 상단에 Serilog 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Log설정이 우선 되어야 이후 에러들이 파일이 남습니다)&lt;/p&gt;
&lt;pre id=&quot;code_1776064004589&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Serilog configuration
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()
    .WriteTo.File(&quot;logs/myapp-.txt&quot;, rollingInterval: RollingInterval.Day)
    .CreateLogger();

// Replace default logger with Serilog
builder.Host.UseSerilog();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Log의존성 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776064823143&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace MyProject.Models.Dao;

public class Dao : IDao
{
    private readonly ServerDbContext _context;
    private readonly ILogger&amp;lt;PremiumGemDao&amp;gt; _logger; // Logger 필드 선언

    // 생성자를 통해 DI(종속성 주입) 받기
    public Dao(ILogger&amp;lt;Dao&amp;gt; logger, ServerDbContext context)
    {
    	_logger = logger;
        _context = context;
    }

    public async Task ProcessPurchase(string userId, string itemId, decimal price)
    {
        // 로그 작성
        _logger.LogInformation(&quot;Purchase started: UserID={UserId}, ItemID={ItemId}, Price={Price}&quot;, 
            userId, itemId, price);

        try 
        {
            // DB logic here...
        }
        catch (Exception ex)
        {
            // 에러 발생 시 Exception 객체와 함께 로그 기록
            _logger.LogError(ex, &quot;Database error during purchase: UserID={UserId}&quot;, userId);
            throw;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. LogLevel 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776066125336&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// appsettings.json
{
  &quot;ConnectionStrings&quot;: {
    &quot;Db&quot; : &quot;server=localhost;port=3306;database=my_project;uid=root;password=my_password&quot;
  },
  &quot;Serilog&quot;: {
    &quot;MinimumLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Override&quot;: {
        &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;,
        &quot;Microsoft.EntityFrameworkCore&quot;: &quot;Warning&quot;
      }
    }
  }
  &quot;AllowedHosts&quot;: &quot;*&quot;
}


// Program.cs
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Serilog configuration
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()
    .WriteTo.File(&quot;logs/eatlivestream-.txt&quot;, rollingInterval: RollingInterval.Day)
    .ReadFrom.Configuration(builder.Configuration) // 여기 부분 추가
    .CreateLogger();

// Replace default logger with Serilog
builder.Host.UseSerilog();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;로그 레벨 6단계 (낮은 순서 -&amp;gt; 높은 순서)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;2&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;레벨&lt;/td&gt;
&lt;td&gt;명칭&lt;/td&gt;
&lt;td&gt;용도 및 설명&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,0,0&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,1,0&quot;&gt;Trace&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,2,0&quot;&gt;가장 상세한 정보. 주로 개발 중 로직의 흐름을 한 줄 한 줄 추적할 때 사용합니다. (매우 양이 많음)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,0,0&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,1,0&quot;&gt;Debug&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,2,0&quot;&gt;개발 단계에서 디버깅 목적으로 사용합니다. 운영 환경에서는 보통 끕니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,0,0&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,1,0&quot;&gt;Information&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,2,0&quot;&gt;(일반적 기본값) 앱의 정상적인 흐름(서비스 시작, 결제 시도 등)을 기록합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,4,0,0&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,4,1,0&quot;&gt;Warning&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,4,2,0&quot;&gt;에러는 아니지만 주의가 필요한 상황. (예: 결제 재시도, API 응답 지연, 비정상적인 입력값)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,5,0,0&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,5,1,0&quot;&gt;Error&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,5,2,0&quot;&gt;현재 작업이 실패한 상태. 예외(Exception)가 발생했을 때 주로 사용하며, 앱은 계속 작동합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,6,0,0&quot;&gt;5&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,6,1,0&quot;&gt;Critical&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,6,2,0&quot;&gt;아주 치명적인 실패. 시스템 전체가 멈추거나 DB 연결이 완전히 끊기는 등 즉시 조치가 필요한 상황입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;범주 (Override)&lt;/td&gt;
&lt;td&gt;추천 레벨&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,1,0,0&quot;&gt;Microsoft.AspNetCore&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,1,1,0&quot;&gt;Warning&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,1,2,0&quot;&gt;HTTP 요청 시작/종료, 미들웨어 동작 등 아주 많은 로그를 줄여줍니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,2,0,0&quot;&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,2,1,0&quot;&gt;Information&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,2,2,0&quot;&gt;(개발 시) 실행되는 SQL문을 보고 싶을 때 사용하세요.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,3,0,0&quot;&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,3,1,0&quot;&gt;Warning&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;13,3,2,0&quot;&gt;(운영 시) SQL 로그가 너무 많아 파일 용량이 커질 때 사용하세요.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. Log작성 팁&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 구조적 로깅(Structured Logging) 필수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 나쁜 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$&quot;User {id} login&quot; &amp;rarr; 문자열로 합치면 나중에 &quot;특정 유저&quot;로 검색이 안 된다 합니다.&lt;br /&gt;- 옳은 예시&lt;br /&gt;&quot;User {UserId} login&quot;, id &amp;rarr; 속성 이름(UserId)을 지정해야 로그 분석 툴(ELK, Seq 등)에서 클릭 한 번으로 필터링이 됩니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. 민감 정보 제외 (Security)&lt;/b&gt;&lt;br /&gt;- 비밀번호, 주민번호, 카드번호, 계좌비밀번호 등은 절대 로그에 남기지 않습니다. (법적 문제 발생 가능) &lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. 로그 수준(Level) 지키기&lt;/b&gt;&lt;br /&gt;- 사용자 클릭 하나하나를 Error로 남기면 실제 중요한 에러를 놓칩니다. 일반적인 흐름은 Information, 문제는 Error로 명확히 구분합니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4. 영어 권장&lt;/b&gt;&lt;br /&gt;- 운영 환경의 인코딩 문제나 글로벌 협업, 로그 분석 툴과의 호환성을 위해 메시지는 가급적 영어로 작성하는 것이 관례입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ASP.NET</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/25</guid>
      <comments>https://washble2.tistory.com/25#entry25comment</comments>
      <pubDate>Mon, 13 Apr 2026 16:48:17 +0900</pubDate>
    </item>
    <item>
      <title>Apple IAP 서버 검증 in ASP.NET Server</title>
      <link>https://washble2.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 일단 Apple에서 .p8키 파일을 받아야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://appstoreconnect.apple.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://appstoreconnect.apple.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773985937087&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://appstoreconnect.apple.com/&quot; data-og-description=&quot;&quot; data-og-host=&quot;appstoreconnect.apple.com&quot; data-og-source-url=&quot;https://appstoreconnect.apple.com/&quot; data-og-url=&quot;https://appstoreconnect.apple.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://appstoreconnect.apple.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://appstoreconnect.apple.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://appstoreconnect.apple.com/&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;appstoreconnect.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 주소로 가서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 및 액세스 &amp;gt; 통합(개인 일 경우 여기로 가야함) &amp;gt; App Store Connect API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액세스 요청&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnxCwD/dJMcabXPEaN/5QMXMl7pKPVKNVaKDgTuN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnxCwD/dJMcabXPEaN/5QMXMl7pKPVKNVaKDgTuN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnxCwD/dJMcabXPEaN/5QMXMl7pKPVKNVaKDgTuN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnxCwD%2FdJMcabXPEaN%2F5QMXMl7pKPVKNVaKDgTuN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;894&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 보안을 위해 전체 API키 생성보다는 보안상 IAP 전용키를 만드는것이 좋다고합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 앱 내 구매 탭으로 이동해 여기서 key을 생성해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I0lfm/dJMcacJdWiU/Xk8o5D5UDOd8gmO6yCDZ5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I0lfm/dJMcacJdWiU/Xk8o5D5UDOd8gmO6yCDZ5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I0lfm/dJMcacJdWiU/Xk8o5D5UDOd8gmO6yCDZ5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI0lfm%2FdJMcacJdWiU%2FXk8o5D5UDOd8gmO6yCDZ5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1230&quot; height=&quot;728&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;파일 다운로드 전 체크!&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;Issuer ID&lt;/b&gt;: 액세스 요청 후 화면 상단에 나타나는 값입니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;Key ID&lt;/b&gt;: 키를 생성하면 목록에 나타나는 10자리 값입니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;다운로드&lt;/b&gt;: .p8 파일은 &lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;7,2,0&quot;&gt;한 번만&lt;/b&gt; 다운로드 가능하니 꼭 PC에 잘 보관해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duPFoN/dJMcac3tzdL/oVoxeKfS2ggksupCtYPeu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duPFoN/dJMcac3tzdL/oVoxeKfS2ggksupCtYPeu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duPFoN/dJMcac3tzdL/oVoxeKfS2ggksupCtYPeu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduPFoN%2FdJMcac3tzdL%2FoVoxeKfS2ggksupCtYPeu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;731&quot; height=&quot;246&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 프로젝트 준비 (NuGet 패키지)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773986167524&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 최신버전
dotnet add package System.IdentityModel.Tokens.Jwt

// 오래된 레거시
dotnet add package Microsoft.IdentityModel.JsonWebTokens&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 설정(csproj)에서 보시면 설치된것을 확인할 수있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773986349716&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &amp;lt;ItemGroup&amp;gt;
      &amp;lt;PackageReference Include=&quot;FirebaseAdmin&quot; Version=&quot;3.1.0&quot; /&amp;gt;
      &amp;lt;PackageReference Include=&quot;Google.Apis.AndroidPublisher.v3&quot; Version=&quot;1.73.0.4052&quot; /&amp;gt;
      &amp;lt;PackageReference Include=&quot;Pomelo.EntityFrameworkCore.MySql&quot; Version=&quot;8.0.3&quot; /&amp;gt;
      &amp;lt;PackageReference Include=&quot;System.IdentityModel.Tokens.Jwt&quot; Version=&quot;8.16.0&quot; /&amp;gt;
    &amp;lt;/ItemGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 애플 IAP 인증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(AppleIapVerification.cs에서 애플 인증만 관리, IapVerificationFacade.cs에서 인증결과를 통해 중복체크)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;코드 사용흐름&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;DAO &amp;rarr; IapVerificationFacade.cs (Apple 인증 요청) &amp;rarr; AppleIapVerification.cs (Apple 인증) &amp;rarr; IapVerificationFacade.cs (결과 확인 후 중복체크) &amp;rarr; DAO (Apple 인증 결과받기)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; AppleIapVerification.cs &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774239591327&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

namespace EatLiveStream.Configurations;

public class AppleIapVerification
{
    private static IHttpClientFactory _httpClientFactory = null!;
    
    private static string _privateKeyContent = null!;
    private const string KeyId = &quot;your keyId&quot;;
    private const string IssuerId = &quot;your issuerId&quot;;
    private const string BundleId = &quot;your bundleId&quot;;
    private static string _baseUrl = null!;

    private const string Bearer = &quot;Bearer&quot;;
    private const string ProductId = &quot;productId&quot;;
    private const string PurchaseDate = &quot;purchaseDate&quot;;
    private const string TransactionId = &quot;transactionId&quot;;
    private const string RevocationDate = &quot;revocationDate&quot;;
    private const string SignedTransactionInfo = &quot;signedTransactionInfo&quot;;

    private const char LeftBrace = '{';

    public AppleIapVerification(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public static void Initialize(string serviceConfigPath, string baseUrl, IHttpClientFactory httpClientFactory)
    {
        _privateKeyContent = File.ReadAllText(serviceConfigPath);
        _baseUrl = baseUrl;
        _httpClientFactory = httpClientFactory;
        
        Console.WriteLine(&quot;Apple IAP SDK initialized successfully.&quot;);
    }

    /// &amp;lt;summary&amp;gt;
    /// Apple API 호출을 위한 JWT 토큰 생성
    /// &amp;lt;/summary&amp;gt;
    private static string GenerateAppleAppStoreJwt()
    {
        // ECDsa 객체는 사용 후 처리가 필요하므로 using 사용
        using ECDsa ecdsa = ECDsa.Create();
        ecdsa.ImportFromPem(_privateKeyContent);

        ECDsaSecurityKey securityKey = new ECDsaSecurityKey(ecdsa);
        SigningCredentials credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256) { CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false } };

        JwtHeader header = new JwtHeader(credentials)
        {
            [&quot;kid&quot;] = KeyId
        };

        JwtPayload payload = new JwtPayload
        {
            { &quot;iss&quot;, IssuerId },
            { &quot;iat&quot;, DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
            { &quot;exp&quot;, DateTimeOffset.UtcNow.AddMinutes(20).ToUnixTimeSeconds() },
            { &quot;aud&quot;, &quot;appstoreconnect-v1&quot; },
            { &quot;bid&quot;, BundleId }
        };

        JwtSecurityToken token = new JwtSecurityToken(header, payload);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public static async Task&amp;lt;(bool, string)&amp;gt; VerifyProductsPurchaseAsync(string transactionId)
    {
        string? signedData = await GetTransactionInfoAsync(transactionId);
        if (string.IsNullOrEmpty(signedData))
        {
            Console.WriteLine(&quot;Failed to get data from Apple.&quot;);
            return (false, string.Empty);
        }
        
        (string productId, string transactionId) result = ProcessTransactionInfo(signedData);

        if (!string.IsNullOrEmpty(result.transactionId))
        {
            return (true, result.transactionId);
        }
        
        return (false, string.Empty);
    }

    /// &amp;lt;summary&amp;gt;
    /// 트랜잭션 정보 조회 (Sandbox/Production 구분)
    /// &amp;lt;/summary&amp;gt;
    private static async Task&amp;lt;string?&amp;gt; GetTransactionInfoAsync(string transactionId)
    {
        string jwtToken = GenerateAppleAppStoreJwt();
        
        HttpClient client = _httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(Bearer, jwtToken);

        // Sandbox 여부에 따른 URL 분기
        string url = $&quot;{_baseUrl}/inApps/v1/transactions/{transactionId}&quot;;
        
        try 
        {
            HttpResponseMessage response = await client.GetAsync(url);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            
            // 에러 로깅이 필요할 경우 여기서 처리 (ex: 401 Unauthorized 시 키/ID 확인 필요)
            return null;
        }
        catch (Exception e)
        {
            Console.WriteLine($&quot;Error in GetTransactionInfoAsync: {e}&quot;);
            return null;
        }
    }
    
    private static (string, string) ProcessTransactionInfo(string signedTransactionInfo)
    {
        string jwsToken = signedTransactionInfo;

        // If the response is in JSON object form ({}), extract only the 'signedTransactionInfo' value.
        try
        {
            // If the input is not in JSON format, JsonException will be thrown here and handled by the catch block.
            using JsonDocument doc = JsonDocument.Parse(signedTransactionInfo);

            if (doc.RootElement.TryGetProperty(SignedTransactionInfo, out JsonElement signedElement))
            {
                jwsToken = signedElement.GetString() ?? signedTransactionInfo;
            }
        }
        catch (JsonException) { /* Use the original value if parsing fails */ }

        // Parse the raw token string using the JWT handler.
        JsonWebTokenHandler handler = new JsonWebTokenHandler();
        JsonWebToken jwtToken = handler.ReadJsonWebToken(jwsToken.Trim());

        // Extract payment information from the Payload (using explicit types).
        // If TryGetPayloadValue is not supported in your version, use GetPayloadValue instead.
        string productId = jwtToken.GetPayloadValue&amp;lt;string&amp;gt;(ProductId);
        string transactionId = jwtToken.GetPayloadValue&amp;lt;string&amp;gt;(TransactionId);
        
        // Additional information can be extracted as shown below.
        // long purchaseDate = jwtToken.GetPayloadValue&amp;lt;long&amp;gt;(PurchaseDate);
        // long? revocationDate = jwtToken.GetPayloadValue&amp;lt;long?&amp;gt;(RevocationDate);

        return (productId, transactionId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; IapVerificationFacade.cs &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774239628463&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using EatLiveStream.Configurations;
using Microsoft.EntityFrameworkCore;

namespace EatLiveStream.Services.IAP;

public class IapVerificationFacade
{
    private readonly ServerDbContext _dbContext;

    public IapVerificationFacade(ServerDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    
    public async Task&amp;lt;(bool, string)&amp;gt; IapVerification
        (IapPlatform iapPlatform, string productId, string receiptIdentity, string dbTable)
    {
        // Google: orderId
        // Apple: originalTransactionId (https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)
        (bool isVerified, string orderId) = (false, null!);
        switch (iapPlatform)
        {
            case IapPlatform.Google:
                (isVerified, orderId) = await GoogleIapVerification.VerifyProductsPurchaseAsync(productId, receiptIdentity);
                break;
            case IapPlatform.IOS:
                (isVerified, orderId) = await AppleIapVerification.VerifyProductsPurchaseAsync(receiptIdentity);
                break;
        }

        if(!isVerified) { return (false, null!); }
        string sql = $&quot;SELECT EXISTS (SELECT 1 FROM {dbTable} WHERE orderId = @p0) AS Value&quot;;
        int exists = await _dbContext.Database.SqlQueryRaw&amp;lt;int&amp;gt;(sql, orderId).SingleAsync();
        
        // If it exists in the DB, the order has already been applied
        if(exists &amp;gt; 0) { return (false, null!); }
        
        return (true, orderId);
    }
    
    public async Task&amp;lt;bool&amp;gt; IapConsume(IapPlatform iapPlatform, string productId, string verificationToken)
    {
        // Google: orderId
        // Apple: originalTransactionId (https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)
        switch (iapPlatform)
        {
            case IapPlatform.Google:
                return await GoogleIapVerification.ConsumeProductsAsync(productId, verificationToken!);
            case IapPlatform.IOS:
                return true;
            default:
                return false;
        }
    }
    
    public async Task&amp;lt;bool&amp;gt; IapAcknowledge(IapPlatform iapPlatform, string productId, string verificationToken)
    {
        // Google: orderId
        // Apple: originalTransactionId (https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)
        switch (iapPlatform)
        {
            case IapPlatform.Google:
                return await GoogleIapVerification.AcknowledgeNonConsumeAsync(productId, verificationToken);
            case IapPlatform.IOS:
                return true;
            default:
                return false;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 인증을 사용하기 위한 appsetting.json 및 Program.cs설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;appsettings.json&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774239769191&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
```

  &quot;Iap&quot;: {
    &quot;GoogleIapServiceConfigPath&quot;: &quot;yummytteokbokki-4c5eb-032e9efbd9ba.json&quot;,
    &quot;AppleIapServiceConfigPath&quot;: &quot;SubscriptionKey_D89VVVKTA8.p8&quot;,
    &quot;AppleBaseUrl&quot;: &quot;https://api.storekit.itunes.apple.com&quot;
    // Sandbox일경우
    // &quot;AppleBaseUrl&quot;: &quot;https://api.storekit-sandbox.itunes.apple.com&quot;
  },
 
 ```
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Program.cs&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Apple 서버와 통신을 위한 HttpClientFactory가 필요합니다)&lt;/p&gt;
&lt;pre id=&quot;code_1774239909303&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);

```

// Apple IAP
string? appleIapConfigPath = builder.Configuration[&quot;Iap:AppleIapServiceConfigPath&quot;];
if (string.IsNullOrEmpty(appleIapConfigPath))
{
    throw new InvalidOperationException(&quot;Apple Iap service account path is not configured. Please add 'Iap:AppleIapServiceConfigPath' to your configuration.&quot;);
}
string? appleIapBaseUrl = builder.Configuration[&quot;Iap:AppleBaseUrl&quot;];
if (string.IsNullOrEmpty(appleIapBaseUrl))
{
    throw new InvalidOperationException(&quot;Apple Iap service base url path is not configured. Please add 'Iap:AppleBaseUrl' to your configuration.&quot;);
}
string absoluteAppleIapConfigPath = Path.Combine(builder.Environment.ContentRootPath, appleIapConfigPath);
// --- End of IAP SDK Configuration Path Setup ---

// Add services to the container.
// Http Client Setting
builder.Services.AddHttpClient&amp;lt;AppleIapVerification&amp;gt;(client =&amp;gt; {
    // Set a timeout of 10 seconds for requests
    client.Timeout = TimeSpan.FromSeconds(10);
});

```


builder.Services.AddControllers();

var app = builder.Build();



```

// Static SDK Initialize Google &amp;amp; Apple
try
{
    // Google IAP Initialize
    GoogleIapVerification.Initialize(absoluteGoogleIapConfigPath);
    // Apple IAP Initialize
    using IServiceScope scope = app.Services.CreateScope();
    IHttpClientFactory httpClientFactory = scope.ServiceProvider.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;();
    AppleIapVerification.Initialize(absoluteAppleIapConfigPath, appleIapBaseUrl, httpClientFactory);
}
catch (Exception e)
{
    throw new InvalidOperationException($&quot;Failed to initialize IAP SDKs: {e.Message}&quot;, e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ASP.NET</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/24</guid>
      <comments>https://washble2.tistory.com/24#entry24comment</comments>
      <pubDate>Mon, 23 Mar 2026 13:31:49 +0900</pubDate>
    </item>
    <item>
      <title>You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' WHERE orderId = '') AS Value</title>
      <link>https://washble2.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;You&amp;nbsp;have&amp;nbsp;an&amp;nbsp;error&amp;nbsp;in&amp;nbsp;your&amp;nbsp;SQL&amp;nbsp;syntax;&amp;nbsp;check&amp;nbsp;the&amp;nbsp;manual&amp;nbsp;that&amp;nbsp;corresponds&amp;nbsp;to&amp;nbsp;your&amp;nbsp;MariaDB&amp;nbsp;server&amp;nbsp;version&amp;nbsp;for&amp;nbsp;the&amp;nbsp;right&amp;nbsp;syntax&amp;nbsp;to&amp;nbsp;use&amp;nbsp;near&amp;nbsp;'''&amp;nbsp;WHERE&amp;nbsp;orderId&amp;nbsp;=&amp;nbsp;'')&amp;nbsp;AS&amp;nbsp;Value&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 오류가 아래의 코드를 했을 때 일어났습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773915251296&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FormattableString sql = $&quot;SELECT EXISTS (SELECT 1 FROM {dbTable} WHERE orderId = {productId}) AS Value&quot;;
int exists = await _dbContext.Database.SqlQuery&amp;lt;int&amp;gt;(sql).SingleAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 dbTable을 변수로 넣으려고 해서 그렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서는 dbTable을 명시적으로 넣어야하는데 dbTable&amp;nbsp; 매번 바껴야 한다면 아래와 같이 변경하면 작동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773915478471&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @p를 사용해서 값 넣기
string sql = $&quot;SELECT EXISTS (SELECT 1 FROM {dbTable} WHERE orderId = @p0) AS Value&quot;;
int exists = await _dbContext.Database.SqlQueryRaw&amp;lt;int&amp;gt;(sql, orderId).SingleAsync();

// 변수가 여러개라면
string sql = $&quot;SELECT EXISTS (SELECT 1 FROM {dbTable} WHERE orderId = @p0 AND updateDate = @p1) AS Value&quot;;
int exists = await _dbContext.Database.SqlQueryRaw&amp;lt;int&amp;gt;(sql, orderId, updateDate).SingleAsync();&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ASP.NET</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/23</guid>
      <comments>https://washble2.tistory.com/23#entry23comment</comments>
      <pubDate>Fri, 20 Mar 2026 00:18:21 +0900</pubDate>
    </item>
    <item>
      <title>The query uses the 'First'/'FirstOrDefault' operator without 'OrderBy' and filter operators.</title>
      <link>https://washble2.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;warn:&amp;nbsp;Microsoft.EntityFrameworkCore.Query[10103] &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;The&amp;nbsp;query&amp;nbsp;uses&amp;nbsp;the&amp;nbsp;'First'/'FirstOrDefault'&amp;nbsp;operator&amp;nbsp;without&amp;nbsp;'OrderBy'&amp;nbsp;and&amp;nbsp;filter&amp;nbsp;operators.&amp;nbsp;This&amp;nbsp;may&amp;nbsp;lead&amp;nbsp;to&amp;nbsp;unpredictable&amp;nbsp;results.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 에러가 아래의 코드에서 나왔는데 해결방법은 OrderBy를 넣거나 Single 결과가 나온다고 명시해주는 것 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773914870735&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FormattableString sql = $&quot;SELECT EXISTS (SELECT 1 FROM dbTable WHERE orderId = {productId}) AS Value&quot;;
int exists = await _dbContext.Database.SqlQuery&amp;lt;int&amp;gt;(sql).FirstOrDefaultAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법&lt;/p&gt;
&lt;pre id=&quot;code_1773915019287&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OrderBy 사용
FormattableString sql = $&quot;SELECT EXISTS (SELECT 1 FROM dbTable WHERE orderId = {productId}) AS Value&quot;;
int exists = await _dbContext.Database.SqlQuery&amp;lt;int&amp;gt;(sql).OrderBy(v =&amp;gt; v).FirstOrDefaultAsync();

// SingleAsync 사용(결과가 1개라는 명시)
FormattableString sql = $&quot;SELECT EXISTS (SELECT 1 FROM dbTable WHERE orderId = {productId}) AS Value&quot;;
int exists = await _dbContext.Database.SqlQuery&amp;lt;int&amp;gt;(sql).SingleAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ASP.NET</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/22</guid>
      <comments>https://washble2.tistory.com/22#entry22comment</comments>
      <pubDate>Thu, 19 Mar 2026 19:11:05 +0900</pubDate>
    </item>
    <item>
      <title>OperationException : Failed to initialize localization, could not preload asset tables</title>
      <link>https://washble2.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;[Error]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;OperationException&amp;nbsp;:&amp;nbsp;Failed&amp;nbsp;to&amp;nbsp;initialize&amp;nbsp;localization,&amp;nbsp;could&amp;nbsp;not&amp;nbsp;preload&amp;nbsp;asset&amp;nbsp;tables&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Localization을 쓸 때 preload를 적용했는데 자꾸 앱 시작 초반에 위와 같은 에러가 떠서 해결하기 위해서 찾아보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래와 같은 링크의 해결법을 찾을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://discussions.unity.com/t/locales-preloadoperation-has-not-been-initialized-in-standalone-build/863013/2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://discussions.unity.com/t/locales-preloadoperation-has-not-been-initialized-in-standalone-build/863013/2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- Localization이 초기화 될 때 까지 기다리게 하는 코드입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;다만 사용하면 초기화 될 때까지 기다리게 하는 것이라 멈춤이 보일 수 있기에 초기 로딩화면에서 작동시켜면 될 듯 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773385753095&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LocalizationSettings.InitializationOperation.WaitForCompletion();&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Unity</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/21</guid>
      <comments>https://washble2.tistory.com/21#entry21comment</comments>
      <pubDate>Fri, 13 Mar 2026 16:12:04 +0900</pubDate>
    </item>
    <item>
      <title>Google IAP 정보 호출을 제대로 구현했지만 Google에 설정한 정보가 제대로 불려오지 않을 때</title>
      <link>https://washble2.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일단 정보 호출코드를 제대로 구현한 상태에서 설정정보대로 값이 넘어오지 않을 때 해봐야할 조치를 적습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우는 분명 ProductMetadatat를 제대로 가져오는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 안에 값이 제대로 표출 안되고 default값으로만 출력이 되어서 고민하던 중에 해결했던 경험입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구글 플레이 스토어 앱 '데이터 삭제'&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰에 있는 구글 플레이 스토어 앱이 예전의 빈 데이터를 기억(캐싱)하고 있어서 업데이트된 불러오는것일 수 있다고 합니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;해결방법:&amp;nbsp;스마트폰&amp;nbsp;설정&amp;nbsp;&amp;gt;&amp;nbsp;애플리케이션&amp;nbsp;&amp;gt;&amp;nbsp;Google&amp;nbsp;Play&amp;nbsp;스토어&amp;nbsp;&amp;gt;&amp;nbsp;저장공간&amp;nbsp;&amp;gt;&amp;nbsp;데이터&amp;nbsp;삭제&amp;nbsp;(캐시&amp;nbsp;삭제만으로는&amp;nbsp;부족할&amp;nbsp;수&amp;nbsp;있습니다).&lt;/span&gt; &lt;br /&gt;&lt;br /&gt;그 후 앱을 다시 실행하면 구글 서버에서 최신 데이터를 다시 가져옵니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>washble2</author>
      <guid isPermaLink="true">https://washble2.tistory.com/20</guid>
      <comments>https://washble2.tistory.com/20#entry20comment</comments>
      <pubDate>Thu, 12 Mar 2026 16:25:38 +0900</pubDate>
    </item>
  </channel>
</rss>