<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>sudoSoooooo</title>
    <link>https://soobysu.tistory.com/</link>
    <description>imSoo TecBlog </description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 12:46:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>imSoo</managingEditor>
    <image>
      <title>sudoSoooooo</title>
      <url>https://tistory1.daumcdn.net/tistory/5328955/attach/409cb2d239bd4ce193a1757203caccde</url>
      <link>https://soobysu.tistory.com</link>
    </image>
    <item>
      <title>[오류노트] 샤오미 온습도계(LYWSD03MMC) 2세대 지그비로 바꾸기</title>
      <link>https://soobysu.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;problem&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤오미 블루투스 2세대는 블루투스 연동으로 샤오미 미 홈 앱으로 원격 모니터링 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 스마트 싱스를 이용해서 자동화를 하기 위해서는 블루투스를 Zigbee 로 바꿔 스마트 싱스에 연결 시킬 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 곳 저곳 팁이 많지만 몇번의 시도를 하다가 벽돌되서 버렸던 경험이 있어서 글로 남겨 봅니다 ㅎㅎ...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;solution&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비물&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 블루투스 연결 가능한 PC - 맥북은 M1 은 안되고 M4 에서는 동작 확인 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 샤오미 온습도계 2세대(LYWSD03MMC)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 핸드폰&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온습도계 토큰 추출&lt;/h3&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; 온습도 센서 -&amp;gt; Mi 블루투스 온습도계2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(가끔 바로 잡히지 않아 여러번 해야 잡히는 경우가 있었습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기기 등록 완료 후, PC 로 갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774853630972&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - PiotrMachowski/Xiaomi-cloud-tokens-extractor: This tool retrieves tokens for all devices connected to Xiaomi cloud and &quot; data-og-description=&quot;This tool retrieves tokens for all devices connected to Xiaomi cloud and encryption keys for BLE devices. - PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot; data-og-url=&quot;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bQWEkc/dJMb87f6u8E/rI9V1zLX9gnqJIHuFFFuKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dQhl7C/dJMb83ksXrU/EK2CfGJKLYAPZjxP07WyLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bQWEkc/dJMb87f6u8E/rI9V1zLX9gnqJIHuFFFuKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dQhl7C/dJMb83ksXrU/EK2CfGJKLYAPZjxP07WyLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - PiotrMachowski/Xiaomi-cloud-tokens-extractor: This tool retrieves tokens for all devices connected to Xiaomi cloud and&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This tool retrieves tokens for all devices connected to Xiaomi cloud and encryption keys for BLE devices. - PiotrMachowski/Xiaomi-cloud-tokens-extractor&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 들어가서 토큰 추출 할 exe 또는 파이썬 파일을 받아줍니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우는 exe 파일 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Mac OS 용 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널을 엽니다. (homebrew 가 설치 되어 있어야 합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄 한줄 복사해서 명령을 실행 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774853842531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# homebrew 로 wget 설치
brew install wget
# homebrew 로 파이썬 설치
brew install python
# wget 으로 파일 다운로드
wget https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/releases/latest/download/token_extractor.zip\nunzip token_extractor.zip -d token_extractor\ncd token_extractor
# 다운로드된 파일로 이동
cd token_extractor
# 파이썬으로 인스톨
pip3 install -r requirements.txt\npython3 token_extractor.py
pip3 install -r requirements.txt --break-system-packages
python3 token_extractor.py&lt;/code&gt;&lt;/pre&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-filename=&quot;blob&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XpDBa/dJMcadIccB8/K4iHfDWwYVacCJwqCcztn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XpDBa/dJMcadIccB8/K4iHfDWwYVacCJwqCcztn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XpDBa/dJMcadIccB8/K4iHfDWwYVacCJwqCcztn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXpDBa%2FdJMcadIccB8%2FK4iHfDWwYVacCJwqCcztn1%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;588&quot; height=&quot;481&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. p 를 입력해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 샤오미 로그인 Email, 비밀번호를 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 캡차 인증 (대소문자 구별)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 이메일 2단계인증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. server 를 선택하라고 나오는데, 무시를 합시다 (엔터)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이 나오면, 우리가 필요한건 ID, Token, BLDXXX 이 세개를 어딘가에 복사해둡니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지그비 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;브라우저 블루투스 기능 활성화&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;크롬&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;chrome://flags/#enable-experimental-web-platform-features&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오페라&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Opera://flags/#enable-experimental-web-platform-features&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엣지&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;edge://flags/#enable-experimental-web-platform-features&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;웨일&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;whale://flags/#enable-experimental-web-platform-features&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창에 검색하면 각 브라우저에 맞게 setting 을 변경 할 수 있는 창이 뜹니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-30 16.10.43.png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byiJVr/dJMcaaLwoAL/dbZCXBFgYQASPjsyjgK1lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byiJVr/dJMcaaLwoAL/dbZCXBFgYQASPjsyjgK1lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byiJVr/dJMcaaLwoAL/dbZCXBFgYQASPjsyjgK1lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyiJVr%2FdJMcaaLwoAL%2FdbZCXBFgYQASPjsyjgK1lK%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;536&quot; height=&quot;108&quot; data-filename=&quot;스크린샷 2026-03-30 16.10.43.png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬기준 위 사진을 &quot;사용 가능&quot; 으로 변경 후 새로고침을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html&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;Filter 에 LYWDS03 을 입력 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connect 버튼을 누르면 검색을 시작 합니다.&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-filename=&quot;403111641-a50ac3c7-87b5-49f5-b5f5-18d6cd2eed89.png&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2AMtP/dJMcagdUrZf/SNE1igTtkzA05KlDlW2k6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2AMtP/dJMcagdUrZf/SNE1igTtkzA05KlDlW2k6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2AMtP/dJMcagdUrZf/SNE1igTtkzA05KlDlW2k6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2AMtP%2FdJMcagdUrZf%2FSNE1igTtkzA05KlDlW2k6K%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;372&quot; height=&quot;318&quot; data-filename=&quot;403111641-a50ac3c7-87b5-49f5-b5f5-18d6cd2eed89.png&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&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;차례대로 디바이스 ID, 토큰, BLDXX 는 bind Key 에 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Login 버튼을 누르고 연결이 되면, 5번을 누르고 Start Flashing 버튼을 눌러줍니다.&lt;/p&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;403111766-9098e68b-f6da-4750-b71f-70079804816a.png&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpF1Kt/dJMcac3BIlp/dT2a2mEWf0KFP7LQyJ7wy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpF1Kt/dJMcac3BIlp/dT2a2mEWf0KFP7LQyJ7wy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpF1Kt/dJMcac3BIlp/dT2a2mEWf0KFP7LQyJ7wy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpF1Kt%2FdJMcac3BIlp%2FdT2a2mEWf0KFP7LQyJ7wy0%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;513&quot; height=&quot;140&quot; data-filename=&quot;403111766-9098e68b-f6da-4750-b71f-70079804816a.png&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 커넥트 해줄땐 filter 에 있는 LYWDS03 를 지우고 ATC 로 변경 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 검색을 하면 ATCXXX 로 이름이 바뀌어 검색 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 8번을 누른 후 다시 Start Flashing 을 눌러주면 끝이 납니다.&lt;/p&gt;</description>
      <category>일-상/오류노트</category>
      <category>LYWSD03MMC 블루투스 지그비</category>
      <category>LYWSD03MMC 지그비</category>
      <category>샤오미 온습도계 지그비</category>
      <category>샤오미 온습도계 지그비 변경</category>
      <category>샤오미 온습도계2</category>
      <author>imSoo</author>
      <guid isPermaLink="true">https://soobysu.tistory.com/256</guid>
      <comments>https://soobysu.tistory.com/256#entry256comment</comments>
      <pubDate>Mon, 30 Mar 2026 16:19:29 +0900</pubDate>
    </item>
    <item>
      <title>[Spring/Kotlin] 애드몹 광고 보상 - 서버 검증 SSV</title>
      <link>https://soobysu.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Google 애드몹 광고단위에는 배너형 보상형 등.. 광고들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그것들 중 보상형 광고 (광고영상, 광고게임) 후, 이것을 서버측에서 검증 하는 방법을 적어보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;부탁해&quot; 에서는 광고 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;마카다미아 2배속&quot;, &quot;돼지 먹이주기&quot; 보상을 제공 하는데, 이를 사용자가 실제로 광고를 정상적으로 시청 했는지 검증하는 도구가 필요 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애드몹에서는 이를 위해 SSV 기능을 제공한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solution&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/admob/android/ssv?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/admob/android/ssv?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773966876703&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;서버 측 인증 (SSV) 콜백 확인 &amp;nbsp;|&amp;nbsp; Android &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;Android용 Google 모바일 광고 SDK를 사용하여 보상형 광고의 서버 측 확인 (SSV) 콜백을 확인합니다.&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/admob/android/ssv?hl=ko&quot; data-og-url=&quot;https://developers.google.com/admob/android/ssv?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0QS7b/dJMb89ycHrB/3Ke4LblPYsToDHJ7DnVpO0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://developers.google.com/admob/android/ssv?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/admob/android/ssv?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0QS7b/dJMb89ycHrB/3Ke4LblPYsToDHJ7DnVpO0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&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;서버 측 인증 (SSV) 콜백 확인 &amp;nbsp;|&amp;nbsp; Android &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android용 Google 모바일 광고 SDK를 사용하여 보상형 광고의 서버 측 확인 (SSV) 콜백을 확인합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Tink 라이브러리를 사용해서 검증을 할 수 있다.&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자 광고 시청 -&amp;gt; 광고시청완료 -&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클라이언트가 애드몹 서버로 (광고 시청 완료 됨 - 커스텀키 포함) 요청 -&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 애드몹이 my서버로 (광고 시청 완료됨 - 커스텀키 포함 ) 요청 -&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 클라이언트 - 서버로 광고 완료 요청(커스텀키 포함) -&amp;gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 2번에서 보낸 커스텀 키를 서버에서 검증 -&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 보상지급&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보자&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 추가 ㄱㄱ&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(필자는 Kotlin 을 사용 중..)&lt;/p&gt;
&lt;pre id=&quot;code_1773966997873&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;com.google.crypto.tink:apps-rewardedads:1.14.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애드몹 -&amp;gt; my서버로 들어오는 요청 (3번)&lt;/h3&gt;
&lt;pre id=&quot;code_1773968569432&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val adDoneKey = &quot;ad:ssv:done:&quot;
 
fun processReward(fullUrl: String, queryParam: String): Boolean {
    try {
    //쿼리 스트링 해체
        val params = parseQueryString(queryParam)
        // 클라이언트에서 애드몹 서버로 보낸 customData 가 여기로 들어옵니다.
        val sessionId = params[&quot;custom_data&quot;] ?: return false
        // url 검증
        verifier.verify(fullUrl)
        
        // 중복된 SessionId 가 있다면 그냥 true
        val ssvKey = &quot;$adDoneKey$sessionId&quot;
        val first = redisTemplate.opsForValue().setIfAbsent(ssvKey, &quot;1&quot;, Duration.ofMinutes(5))
        if (first != true) {
            return true
        }

        // Redis 를 이용하여 클라이언트에서 애드몹으로 보낸 Session Key 를 저장 해줍니다.
        val tempKey = &quot;$adsRewardKey$sessionId&quot;
        redisTemplate.opsForValue().set(tempKey, &quot;1&quot;, Duration.ofMinutes(30))
        
        return true
    } catch (e: Exception) {
        log.error(&quot;SSV Verification Failed&quot;, e)
        return false
    }
}
    
// 쿼리 스트링 해체 작업
private fun parseQueryString(query: String): Map&amp;lt;String, String&amp;gt; {
    return query.split(&quot;&amp;amp;&quot;).associate {
        val (key, value) = it.split(&quot;=&quot;).let { pair -&amp;gt;
            pair[0] to (pair.getOrNull(1) ?: &quot;&quot;)
        }
        key to java.net.URLDecoder.decode(value, &quot;UTF-8&quot;)
    }
}

// RewardedAdsVerifier 는 tink 라이브러리로 검증 할 수 있다.
    private val verifier = RewardedAdsVerifier.Builder()
        .fetchVerifyingPublicKeysWith(RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
        .build()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 -&amp;gt; my 서버로 요청 (4번 단계)&lt;/h3&gt;
&lt;pre id=&quot;code_1773967495181&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class RewardRequest(
    val sessionId: String
)

@PostMapping
fun pig(@RequestBody dto: RewardRequest) {
        service.pigHistoryRegisterV2(dto, getAuthReq())
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773967746598&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  fun pigHistoryRegisterV2(dto: RewardRequest): Int {
        verificationManager.claimUserReward(dto.sessionId, customerId)
        ....
        //리워드 지급 로직
        return amount
    }
    
 private val adsRewardKey = &quot;ad:reward:temp:&quot;
 
 fun claimUserReward(sessionId: String): Boolean {
        val key = &quot;$adsRewardKey$sessionId&quot;
        // Redis 에 저장된 키를 삭제 해준다. 없으면 false 로 유효하지 않은 Key
        val deleted = redisTemplate.delete(key)

        if (deleted != true) {
            throw NoSuchElementException(&quot;유효하지 않거나 이미 사용된 보상입니다.&quot;)
        }

        // 광고 지급 저장 로직...

        return true
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개-발/Java + Spring + Kotlin</category>
      <category>ssv</category>
      <category>광고 검증</category>
      <category>구글 애드몹 SSV</category>
      <category>애드몹 Spring</category>
      <category>애드몹 서버</category>
      <category>애드몹 서버 검증</category>
      <author>imSoo</author>
      <guid isPermaLink="true">https://soobysu.tistory.com/255</guid>
      <comments>https://soobysu.tistory.com/255#entry255comment</comments>
      <pubDate>Fri, 20 Mar 2026 10:09:34 +0900</pubDate>
    </item>
    <item>
      <title>[오류노트] Apple 소셜로그인</title>
      <link>https://soobysu.tistory.com/254</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 아래 url 로 code 인증을 보낼때 invalid_client 에러가 떴다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;/auth/token&quot;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;solution&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. apple_key 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.client_id 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. jwt sign 에 사용하는 p8 키가 위 apple key 와 같은 파일인지 확인&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-filename=&quot;blob&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byFFLB/dJMcagc2w16/LI4bxusxBdD5hVneq0HSWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byFFLB/dJMcagc2w16/LI4bxusxBdD5hVneq0HSWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byFFLB/dJMcagc2w16/LI4bxusxBdD5hVneq0HSWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyFFLB%2FdJMcagc2w16%2FLI4bxusxBdD5hVneq0HSWK%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;533&quot; height=&quot;123&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;identifier 을 client_id 에 잘 넣어주었는지 확인하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key 를 새로 발급받아서 p8 키를 아예 새로 발급받아서 다시 넣어주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pDRUl/dJMcadAyBXH/DsMngKeXkYbHukKMHrsEKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pDRUl/dJMcadAyBXH/DsMngKeXkYbHukKMHrsEKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pDRUl/dJMcadAyBXH/DsMngKeXkYbHukKMHrsEKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpDRUl%2FdJMcadAyBXH%2FDsMngKeXkYbHukKMHrsEKK%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;522&quot; height=&quot;94&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;235&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;</description>
      <category>일-상/오류노트</category>
      <category>invalid_client</category>
      <category>애플 invalid_client</category>
      <category>애플 소셜로그인 invalid_client</category>
      <author>imSoo</author>
      <guid isPermaLink="true">https://soobysu.tistory.com/254</guid>
      <comments>https://soobysu.tistory.com/254#entry254comment</comments>
      <pubDate>Mon, 22 Dec 2025 19:11:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Apple 소셜 로그인 구현 (f.Expo)</title>
      <link>https://soobysu.tistory.com/253</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;pre id=&quot;code_1765935420298&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class AppleKeyLocator(
    private val appleKeyClient: AppleKeyClient
) {
    fun genaratePublicK(token: String): PublicKey {
        val headerPart = token.substringBefore('.')

        val header = ObjectMapper().readValue(
            String(Base64.getUrlDecoder().decode(headerPart), Charsets.UTF_8),
            object : TypeReference&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt;() {}
        )

        require(header[&quot;alg&quot;] == &quot;RS256&quot;) {
            &quot;Unsupported JWT algorithm: ${header[&quot;alg&quot;]}&quot;
        }

        val publicKeys = appleKeyClient.getPublicKeys()

        val key = publicKeys.keys.firstOrNull {
            it.kid == header[&quot;kid&quot;] &amp;amp;&amp;amp; it.alg == &quot;RS256&quot;
        } ?: throw IllegalArgumentException(&quot;Matching Apple Public Key not found&quot;)

        val modulus = BigInteger(1, Base64.getUrlDecoder().decode(key.n))
        val exponent = BigInteger(1, Base64.getUrlDecoder().decode(key.e))

        return KeyFactory.getInstance(&quot;RSA&quot;)
            .generatePublic(RSAPublicKeySpec(modulus, exponent))
    }
}​&lt;/code&gt;&lt;/pre&gt;
이 글은 사용자가 Front 에서 인증 완료 후 토큰을 받아 Backend 에서 Apple 로 검증 하는 방법을 설명한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native(Expo) -&amp;gt; Expo go (x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;프론트에서 받아 올 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple 인증을 받으려면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;identityToken , authorizationCode 를 받아와야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 인증서 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 09.49.02.png&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IF7Hd/dJMcahbTQ6r/n8RSfvwszLHXQhspPbkPv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IF7Hd/dJMcahbTQ6r/n8RSfvwszLHXQhspPbkPv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IF7Hd/dJMcahbTQ6r/n8RSfvwszLHXQhspPbkPv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIF7Hd%2FdJMcahbTQ6r%2Fn8RSfvwszLHXQhspPbkPv1%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;1061&quot; height=&quot;695&quot; data-filename=&quot;스크린샷 2025-12-17 09.49.02.png&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서,ID 및 프로파일 -&amp;gt; 인증서 탭을 누르면 아래 화면이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 09.51.59.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQ1M3/dJMcahCXVdN/kNFnkBrG912vtjxHuezi9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQ1M3/dJMcahCXVdN/kNFnkBrG912vtjxHuezi9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQ1M3/dJMcahCXVdN/kNFnkBrG912vtjxHuezi9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQ1M3%2FdJMcahCXVdN%2FkNFnkBrG912vtjxHuezi9k%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;1280&quot; height=&quot;176&quot; data-filename=&quot;스크린샷 2025-12-17 09.51.59.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Identifiers 의 + 버튼을 누르고&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 09.52.47.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vp7A6/dJMcacVXMda/QBQhEv82kM8ZxyxdWsQYsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vp7A6/dJMcacVXMda/QBQhEv82kM8ZxyxdWsQYsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vp7A6/dJMcacVXMda/QBQhEv82kM8ZxyxdWsQYsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVp7A6%2FdJMcacVXMda%2FQBQhEv82kM8ZxyxdWsQYsK%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;688&quot; height=&quot;156&quot; data-filename=&quot;스크린샷 2025-12-17 09.52.47.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Front 의 환경이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App -&amp;gt; APP IDs&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web -&amp;gt; Services IDs&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;필자는 앱 환경이기 때문에 App IDs 를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 09.54.21.png&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vTcuh/dJMcabW3FIB/Oy4jNQCE3YeqPo9xDM1bT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vTcuh/dJMcabW3FIB/Oy4jNQCE3YeqPo9xDM1bT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vTcuh/dJMcabW3FIB/Oy4jNQCE3YeqPo9xDM1bT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvTcuh%2FdJMcabW3FIB%2FOy4jNQCE3YeqPo9xDM1bT1%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;682&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2025-12-17 09.54.21.png&quot; data-origin-width=&quot;1223&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bundle ID 를 입력 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 앱 패키지 ID랑은 별개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 패키지 명(com.exam.myapp)을 사용 하는데,&amp;nbsp; Services ID 와 App ID 를 구분짓기 위해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;appID.com.exam.myapp&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;servicesID.com.exam.myapp&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 만들기도 한다. 번들 ID 는 중복이 안됨.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gf5bN/dJMcabJxi2f/zbL2SxVEH0KjxLNutbnDNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gf5bN/dJMcabJxi2f/zbL2SxVEH0KjxLNutbnDNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gf5bN/dJMcabJxi2f/zbL2SxVEH0KjxLNutbnDNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGf5bN%2FdJMcabJxi2f%2FzbL2SxVEH0KjxLNutbnDNk%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;697&quot; height=&quot;296&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에 sign In with Apple 을 추가 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Edit 창에 들어가서 아래 Primary App 을 선택 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkpZxT/dJMcaaKB6sF/i9Pza1LLY4YVs2WmlhrNtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkpZxT/dJMcaaKB6sF/i9Pza1LLY4YVs2WmlhrNtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkpZxT/dJMcaaKB6sF/i9Pza1LLY4YVs2WmlhrNtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkpZxT%2FdJMcaaKB6sF%2Fi9Pza1LLY4YVs2WmlhrNtk%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;539&quot; height=&quot;304&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F3omK/dJMcag48TJK/OT6RvMhpFg3KvqVyTnd4T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F3omK/dJMcag48TJK/OT6RvMhpFg3KvqVyTnd4T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F3omK/dJMcag48TJK/OT6RvMhpFg3KvqVyTnd4T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF3omK%2FdJMcag48TJK%2FOT6RvMhpFg3KvqVyTnd4T0%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;575&quot; height=&quot;137&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;289&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;Team ID, Bundle ID 를 기록 해두자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Key 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 10.17.54.png&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrXsS/dJMcad1ACst/enXdy43SzMit2uw5uDSLok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrXsS/dJMcad1ACst/enXdy43SzMit2uw5uDSLok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrXsS/dJMcad1ACst/enXdy43SzMit2uw5uDSLok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrXsS%2FdJMcad1ACst%2FenXdy43SzMit2uw5uDSLok%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;161&quot; height=&quot;261&quot; data-filename=&quot;스크린샷 2025-12-17 10.17.54.png&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에 Keys 탭에 + 버튼을 눌러 키를 생성해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 10.16.56.png&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9SJg/dJMcag48TS6/CYQ6QYmk0t0XRT9jrN2Fgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9SJg/dJMcag48TS6/CYQ6QYmk0t0XRT9jrN2Fgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9SJg/dJMcag48TS6/CYQ6QYmk0t0XRT9jrN2Fgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9SJg%2FdJMcag48TS6%2FCYQ6QYmk0t0XRT9jrN2Fgk%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;680&quot; height=&quot;369&quot; data-filename=&quot;스크린샷 2025-12-17 10.16.56.png&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sign in with apple 을 선택하고 Configure 를 눌러 앱을 선택 해주자.&lt;span style=&quot;background-color: #ffffff; color: #0070c9; text-align: center;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키를 만들고 Key ID 를 메모 해두자.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1765933689477&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//gradle
implementation(&quot;io.jsonwebtoken:jjwt-api:0.12.6&quot;)
implementation(&quot;io.jsonwebtoken:jjwt-impl:0.12.6&quot;)
implementation(&quot;io.jsonwebtoken:jjwt-jackson:0.12.6&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple 은 인증 수단으로 JWT 토큰을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 토큰을 만들어서 Apple 로 인증 받는 과정이 있음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체코드&lt;/h3&gt;
&lt;pre id=&quot;code_1765934484296&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Request Dto
class OauthSignUpRequestDto(
    val token: String,
    val appleCode: String?,
)

// 전체코드
fun getEmailAndValidateToken(request: OauthSignUpRequestDto): String {
        val jwt = appleJwtUtil.generateAuthJwt()
        val code = request.appleCode ?: throw IllegalArgumentException(&quot;애플 코드가 존재하지 않습니다.&quot;)

        try {
            val response = webClient.post()
                .uri { uriBuilder -&amp;gt;
                    uriBuilder.path(&quot;/auth/token&quot;)
                        .queryParam(&quot;grant_type&quot;, &quot;authorization_code&quot;)
                        .queryParam(&quot;client_id&quot;, clientId)
                        .queryParam(&quot;client_secret&quot;, jwt)
                        .queryParam(&quot;code&quot;, code)
                        .build()
                }
                .retrieve()
                .bodyToMono(AppleTokenResponseDto::class.java)
                .block() ?: throw ExternalApiErrorException(&quot;Apple token response was null&quot;)
            val claims = appleJwtUtil.extractAuthClaims(response.idToken!!)
            val identifier = claims.get(&quot;sub&quot;, String::class.java)
            return identifier

        } catch (e: WebClientResponseException) {
            log.error(&quot;[애플 로그인 실패]: ${e.responseBodyAsString}&quot;, e)
            throw e
        } catch (e: Exception) {
            log.error(&quot;[애플 로그인 실패 - 예상치 못한 오류]: ${e.message}&quot;, e)
            throw e
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Jwt Token 생성 (apple 인증용)&lt;/h3&gt;
&lt;pre id=&quot;code_1765934565954&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; @Value(&quot;\${apple_identifier_bundle_id}&quot;)
    private lateinit var bundleId: String

    @Value(&quot;\${apple_team_id}&quot;)
    private lateinit var teamId: String

    @Value(&quot;\${apple_key_id}&quot;)
    private lateinit var keyId: String

    fun generateAuthJwt(): String {
        val now = System.currentTimeMillis()
        val iat = Date(now)
        val exp = Date(now + 20 * 60 * 1000)
        val privateKey = keyManager.loadPrivateKeyForApple(AppleKeyType.AUTH)

        return Jwts.builder()
            .subject(bundleId) 
            .issuer(teamId)
            .issuedAt(iat)
            .expiration(exp)
            .audience()
            .add(&quot;https://appleid.apple.com&quot;)
            .and()
            .header()
            .keyId(keyId)
            .and()
            .signWith(privateKey, Jwts.SIG.ES256)
            .compact()
    }&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;BundleId, TeamId, KeyId 를 환경변수에 넣어&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jwt 토큰을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 인증&lt;/h3&gt;
&lt;pre id=&quot;code_1765934736057&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        
        
        val jwt = appleJwtUtil.generateAuthJwt()
        val code = request.appleCode ?: throw IllegalArgumentException(&quot;애플 코드가 존재하지 않습니다.&quot;)

        try {
            val response = webClient.post()
                .uri { uriBuilder -&amp;gt;
                    uriBuilder.path(&quot;/auth/token&quot;)
                        .queryParam(&quot;grant_type&quot;, &quot;authorization_code&quot;)
                        //ClientId 는 위에서 사용한 Bundle Id가 들어간다.
                        .queryParam(&quot;client_id&quot;, clientId)
                        // 위에서 생성한 JWT 토큰
                        .queryParam(&quot;client_secret&quot;, jwt)
                        // Front 에서 받아온 authorizationCode
                        .queryParam(&quot;code&quot;, code)
                        .build()
                }
                .retrieve()
                .bodyToMono(AppleTokenResponseDto::class.java)
                .block() ?: throw ExternalApiErrorException(&quot;Apple token response was null&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1765934859367&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// APPLE 응답 Dto
@JsonIgnoreProperties(ignoreUnknown = true)
data class AppleTokenResponseDto(
    @JsonProperty(&quot;access_token&quot;)
    val accessToken: String?,

    @JsonProperty(&quot;token_type&quot;)
    val tokenType: String?,

    @JsonProperty(&quot;expires_in&quot;)
    val expiresIn: String?,

    @JsonProperty(&quot;refresh_token&quot;)
    val refreshToken: String?,

    @JsonProperty(&quot;id_token&quot;)
    val idToken: String?
)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 토큰이 실제 하는지 인증을 하면 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;APPLE 은 사용자가 이메일을 가려서 가입 할 수 있는데, 이때 사용자를 식별할만한 무언가를 만들어주어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토큰 정보 추출&lt;/h3&gt;
&lt;pre id=&quot;code_1765935331887&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   fun extractAuthClaims(token: String): Claims {
        try {
            val key = appleKeyClient.genaratePublicK(token)
            return Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(idToken)
                .payload
        } catch (e: ExpiredJwtException) {
            throw ExpiredJwtException(e.header, e.claims, &quot;Apple JWT 토큰이 만료되었습니다.&quot;)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받아온 토큰으로 public Key 를 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1765935430300&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class AppleKey(
    private val appleKeyClient: AppleKeyClient
) {
    fun genaratePublicK(token: String): PublicKey {
        val headerPart = token.substringBefore('.')

        val header = ObjectMapper().readValue(
            String(Base64.getUrlDecoder().decode(headerPart), Charsets.UTF_8),
            object : TypeReference&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt;() {}
        )

        require(header[&quot;alg&quot;] == &quot;RS256&quot;) {
            &quot;Unsupported JWT algorithm: ${header[&quot;alg&quot;]}&quot;
        }
		
        // !!! 애플에서 Key 를 받아와야 함. 캐시 처리를 해주자
        val publicKeys = appleKeyClient.getPublicKeys()

        val key = publicKeys.keys.firstOrNull {
            it.kid == header[&quot;kid&quot;] &amp;amp;&amp;amp; it.alg == &quot;RS256&quot;
        } ?: throw IllegalArgumentException(&quot;Matching Apple Public Key not found&quot;)

        val modulus = BigInteger(1, Base64.getUrlDecoder().decode(key.n))
        val exponent = BigInteger(1, Base64.getUrlDecoder().decode(key.e))

        return KeyFactory.getInstance(&quot;RSA&quot;)
            .generatePublic(RSAPublicKeySpec(modulus, exponent))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1765935590698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Openfeign 을 사용하였다 webClient 를 사용해도됨
@FeignClient(name = &quot;apple-auth&quot;, url = &quot;https://appleid.apple.com/auth&quot;)
interface AppleKeyClient {

    @GetMapping(&quot;/keys&quot;)
    fun getPublicKeys(): ApplePublicKeysResponse
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1765935627982&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class ApplePublicKeysResponse(
    val keys: List&amp;lt;ApplePublicKeyResponseDto&amp;gt;
) {
    data class ApplePublicKeyResponseDto(
        val kty: String, // Key Type (예: RSA)
        val kid: String, // Key ID
        val use: String,
        val alg: String,
        val n: String,   // Modulus (Base64 URL-safe)
        val e: String    // Public Exponent (Base64 URL-safe)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받아오면 키 조합들이 응답으로 오는데, 이 조합으로 온 키 중 RS256 을 사용하여&amp;nbsp; Public Key 를 만들어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1gFf7/dJMcaiaOrOl/2ObEPqvqK8ILskGmW9HcO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1gFf7/dJMcaiaOrOl/2ObEPqvqK8ILskGmW9HcO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1gFf7/dJMcaiaOrOl/2ObEPqvqK8ILskGmW9HcO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1gFf7%2FdJMcaiaOrOl%2F2ObEPqvqK8ILskGmW9HcO0%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;487&quot; height=&quot;290&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claims 를 까보면 위와 같이 되어 있는데, 이 중&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Sub&quot; 로 유저의 고유의 값을 처리 해주면 된다.&amp;nbsp;&lt;/p&gt;</description>
      <category>개-발/Java + Spring + Kotlin</category>
      <category>react-native apple 소셜로그인</category>
      <category>spring boot 소셜 로그인</category>
      <category>spring boot 애플</category>
      <category>spring boot 애플 소셜로그인 구현</category>
      <category>spring 애플 소셜 로그인</category>
      <author>imSoo</author>
      <guid isPermaLink="true">https://soobysu.tistory.com/253</guid>
      <comments>https://soobysu.tistory.com/253#entry253comment</comments>
      <pubDate>Wed, 17 Dec 2025 10:44:47 +0900</pubDate>
    </item>
    <item>
      <title>[오류노트] nextJS 취약점 업데이트 (f.expo)</title>
      <link>https://soobysu.tistory.com/252</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-10 09.56.03.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ySYUz/dJMcahpppBJ/mbqqMWqiz0Nlkg89njdOk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ySYUz/dJMcahpppBJ/mbqqMWqiz0Nlkg89njdOk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ySYUz/dJMcahpppBJ/mbqqMWqiz0Nlkg89njdOk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FySYUz%2FdJMcahpppBJ%2FmbqqMWqiz0Nlkg89njdOk1%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;698&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2025-12-10 09.56.03.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next 취약점 이슈로 인해 react 버전을 업데이트 해주어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 프로젝트가 모노레포로 되어 있어서 react 를 19.0.1 버전으로 업데이트를 해야 하는 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;업데이트 진행 도중 react 와 react-native-renderer 버전이 맞지 않다고 오류가 떴다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;solution&lt;/h3&gt;
&lt;pre id=&quot;code_1765328306067&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//app.json

&quot;expo&quot;:{
  &quot;experiments&quot;: {
      &quot;reactCanary&quot;: true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;expo 블록에 위 코드를 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 Expo 에서 React의 Canary 실험용 최신버전 기능을 강제로 사용할 수 있도록 열어두는 플래그이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765330248031&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;Mitigating the Critical Security Vulnerability in React Server Components - Expo Changelog&quot; data-og-description=&quot;Check out new updates and improvements to Expo and EAS from the Expo team.&quot; data-og-host=&quot;expo.dev&quot; data-og-source-url=&quot;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&quot; data-og-url=&quot;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/G8D2m/hyZONGHSUs/xnJrAH5KC80BMSPRcIMqi0/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788,https://scrap.kakaocdn.net/dn/b9hP0T/hyZPc0vuhP/50EoExOz2wKbwPIDec5kEk/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788,https://scrap.kakaocdn.net/dn/nclky/hyZOGguk78/m08JgKUptwEyJjtZxp8c51/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788&quot;&gt;&lt;a href=&quot;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/G8D2m/hyZONGHSUs/xnJrAH5KC80BMSPRcIMqi0/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788,https://scrap.kakaocdn.net/dn/b9hP0T/hyZPc0vuhP/50EoExOz2wKbwPIDec5kEk/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788,https://scrap.kakaocdn.net/dn/nclky/hyZOGguk78/m08JgKUptwEyJjtZxp8c51/img.png?width=1500&amp;amp;height=788&amp;amp;face=0_0_1500_788');&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;Mitigating the Critical Security Vulnerability in React Server Components - Expo Changelog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Check out new updates and improvements to Expo and EAS from the Expo team.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;expo.dev&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;</description>
      <category>개-발/App</category>
      <category>EXPO</category>
      <category>nextjs</category>
      <author>imSoo</author>
      <guid isPermaLink="true">https://soobysu.tistory.com/252</guid>
      <comments>https://soobysu.tistory.com/252#entry252comment</comments>
      <pubDate>Wed, 10 Dec 2025 10:33:19 +0900</pubDate>
    </item>
  </channel>
</rss>