<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 박준희</title>
    <description>The latest articles on DEV Community by 박준희 (@junhee916).</description>
    <link>https://dev.to/junhee916</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3964655%2F447f5509-845c-4cd0-8de8-a2cf635e18bb.jpg</url>
      <title>DEV Community: 박준희</title>
      <link>https://dev.to/junhee916</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/junhee916"/>
    <language>en</language>
    <item>
      <title>How to Hide Unnecessary UI Tabs in the Frontend</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Mon, 13 Jul 2026 16:00:01 +0000</pubDate>
      <link>https://dev.to/junhee916/how-to-hide-unnecessary-ui-tabs-in-the-frontend-2jnh</link>
      <guid>https://dev.to/junhee916/how-to-hide-unnecessary-ui-tabs-in-the-frontend-2jnh</guid>
      <description>&lt;p&gt;In my Image Studio app, there was a prominent "Portrait Preservation Edit" tab. Turns out, it was a feature I either wasn't using or wouldn't be using soon. Such unnecessary UI elements just clutter the experience and confuse users. I wrestled with how to hide it, and finally, I cracked it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempts and Pitfalls
&lt;/h2&gt;

&lt;p&gt;My first thought was, "Can't I just block the rendering logic for that tab?" So, I modified the code to skip rendering if a certain condition wasn't met when the component mounted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FeatureTab&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./FeatureTab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showEditTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowEditTab&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In a real scenario, this would be determined by a backend API call, etc.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFeatureEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkFeatureStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Portrait Preservation Edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setShowEditTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Other UI elements */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showEditTab&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureTab&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Portrait Preservation Edit"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Other UI elements */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkFeatureStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;featureName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Dummy logic: This should be determined dynamically in reality.&lt;/span&gt;
  &lt;span class="c1"&gt;// For example, it depends on current user permissions, service settings, etc.&lt;/span&gt;
  &lt;span class="c1"&gt;// Here, we'll hardcode it to false for testing to hide the tab.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Checking status for: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;featureName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Setting to false to test hiding the tab&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the kicker: no matter how much I set &lt;code&gt;setShowEditTab(false)&lt;/code&gt;, the tab refused to disappear. The console dutifully showed &lt;code&gt;Checking status for: Portrait Preservation Edit&lt;/code&gt;, but the UI stubbornly displayed the tab. After three hours of debugging, I finally discovered the culprit: another component was directly controlling that tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;The problem was that besides managing the state and controlling rendering in &lt;code&gt;App.js&lt;/code&gt;, &lt;strong&gt;another component was directly controlling the tab using the &lt;code&gt;visibility&lt;/code&gt; property&lt;/strong&gt;. It was like a parent saying "no," but the child defiantly saying "yes" anyway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SomeOtherComponent.js (The problematic component)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SomeOtherComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* &amp;lt;-- This was the issue! */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Portrait Preservation Edit tab content */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Portrait Preservation Edit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This tab should always be visible.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;SomeOtherComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;App.js&lt;/code&gt; set the &lt;code&gt;showEditTab&lt;/code&gt; state to &lt;code&gt;false&lt;/code&gt;, the &lt;code&gt;style={{ visibility: 'visible' }}&lt;/code&gt; in &lt;code&gt;SomeOtherComponent.js&lt;/code&gt; was forcing it to be displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The simplest and most straightforward solution was to remove the &lt;code&gt;visibility&lt;/code&gt; property from that tab in &lt;code&gt;SomeOtherComponent.js&lt;/code&gt; and unify the rendering logic under the &lt;code&gt;showEditTab&lt;/code&gt; state managed in &lt;code&gt;App.js&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.js (After modification)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FeatureTab&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./FeatureTab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SomeOtherComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./SomeOtherComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Import SomeOtherComponent&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showEditTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowEditTab&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In a real scenario, this would be determined by a backend API call, etc.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFeatureEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkFeatureStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Portrait Preservation Edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setShowEditTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Other UI elements */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showEditTab&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureTab&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Portrait Preservation Edit"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Remove the tab rendering logic from SomeOtherComponent */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SomeOtherComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Other UI elements */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkFeatureStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;featureName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Dummy logic: This should be determined dynamically in reality.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Checking status for: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;featureName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Setting to false to test hiding the tab&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SomeOtherComponent.js (After modification)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SomeOtherComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Removed the visibility: 'visible' property */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Portrait Preservation Edit tab content */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Portrait Preservation Edit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This tab is controlled by App.js.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;SomeOtherComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if &lt;code&gt;showEditTab&lt;/code&gt; in &lt;code&gt;App.js&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, the "Portrait Preservation Edit" tab completely disappears from the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The "Portrait Preservation Edit" tab is no longer visible in the UI.&lt;/li&gt;
&lt;li&gt;The rendering logic is unified in &lt;code&gt;App.js&lt;/code&gt;, making it easier to manage.&lt;/li&gt;
&lt;li&gt;User confusion has been reduced.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways — Avoiding the Same Pitfall
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] When controlling the visibility of a UI element, ensure it's not being controlled independently by multiple components.&lt;/li&gt;
&lt;li&gt;[ ] State management logic should be handled consistently, preferably in parent components or through a centralized state management library.&lt;/li&gt;
&lt;li&gt;[ ] During code reviews, meticulously check if CSS properties like &lt;code&gt;visibility&lt;/code&gt; and &lt;code&gt;display&lt;/code&gt; are unintentionally forcing UI elements to be visible.&lt;/li&gt;
&lt;li&gt;[ ] Design the system so that feature activation/deactivation states are dynamically determined by backend APIs or clear configuration values.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>ui</category>
      <category>react</category>
    </item>
    <item>
      <title>Dev.to Markdown Tables/Bold Broken? Fixing the `markdownify` Double Processing Issue</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Sun, 12 Jul 2026 16:00:02 +0000</pubDate>
      <link>https://dev.to/junhee916/devto-markdown-tablesbold-broken-fixing-the-markdownify-double-processing-issue-2kip</link>
      <guid>https://dev.to/junhee916/devto-markdown-tablesbold-broken-fixing-the-markdownify-double-processing-issue-2kip</guid>
      <description>&lt;p&gt;I had a bit of trouble with Markdown tables and bold text getting messed up on Dev.to. It was pretty annoying to see my posts come out all jumbled every time I published. I wanted to share how I solved this issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempts and Pitfalls
&lt;/h2&gt;

&lt;p&gt;When I tried to publish my posts on Dev.to, the tables and bold text I wrote in Markdown weren't displaying correctly and were showing up broken. At first, I thought I had written the Markdown syntax wrong and double-checked it multiple times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
| Header 1 | Header 2 |
|---|---|
| &lt;span class="gs"&gt;**Bold Text**&lt;/span&gt; | Some Data |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with a simple table and bold text like the above, it looked strange on Dev.to. It was definitely rendering fine locally.&lt;/p&gt;

&lt;p&gt;It seemed like there was some kind of post-processing happening on the server that was handling the Markdown again. So, I started suspecting that if I was using a library like &lt;code&gt;markdownify&lt;/code&gt;, maybe it was being processed twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;In the end, the problem was with the &lt;code&gt;content_en&lt;/code&gt; field. This field already contained the content in Markdown format, and I was processing it again with &lt;code&gt;markdownify&lt;/code&gt;, which caused a double Markdown conversion. Since it was already Markdown, trying to convert it to Markdown again messed up the syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The fix was simple. I modified the logic to use the &lt;code&gt;content_en&lt;/code&gt; field directly if it was already in Markdown format, instead of reprocessing it with &lt;code&gt;markdownify&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original code (assumed)
# content_en = "..." # Already in Markdown format
# processed_content = markdownify(content_en)
&lt;/span&gt;
&lt;span class="c1"&gt;# Modified code
&lt;/span&gt;&lt;span class="n"&gt;content_en&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Already in Markdown format
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_already_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_en&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# In reality, you'd need logic to check the format of content_en
&lt;/span&gt;    &lt;span class="n"&gt;processed_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;markdownify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_en&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;processed_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content_en&lt;/span&gt;

&lt;span class="c1"&gt;# When applying this in practice, it might vary depending on what values the content_en field holds
# and how a function like is_already_markdown is implemented.
# The important thing is to avoid extra conversion when it's already Markdown.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By skipping the &lt;code&gt;markdownify&lt;/code&gt; processing for data that was already in Markdown format, the issue was resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Markdown tables in posts published on Dev.to are now displayed correctly.&lt;/li&gt;
&lt;li&gt;Text formatting like bold and italics is no longer broken and appears as intended.&lt;/li&gt;
&lt;li&gt;I no longer spend time worrying about my content breaking after publishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In Summary — To Avoid the Same Pitfall
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] When using Markdown conversion libraries, make it a habit to check if the input data is already in Markdown format.&lt;/li&gt;
&lt;li&gt;[ ] Clearly understand the format of data stored in fields like &lt;code&gt;content_en&lt;/code&gt; and design your processing logic accordingly.&lt;/li&gt;
&lt;li&gt;[ ] Double processing is a common culprit for unexpected bugs. Always pay close attention to your data flow.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>devto</category>
      <category>markdownify</category>
    </item>
    <item>
      <title>How Far Can Local AI Go with Just a CPU — Building Auto-Tracking and Benchmarking Loops, and My Conclusions</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:46:30 +0000</pubDate>
      <link>https://dev.to/junhee916/how-far-can-local-ai-go-with-just-a-cpu-building-auto-tracking-and-benchmarking-loops-and-my-44l2</link>
      <guid>https://dev.to/junhee916/how-far-can-local-ai-go-with-just-a-cpu-building-auto-tracking-and-benchmarking-loops-and-my-44l2</guid>
      <description>&lt;h1&gt;
  
  
  Local AI on CPU Without a GPU: How Far Can It Go? — Building an Auto-Tracking, Benchmarking Loop, and the Conclusions I Reached
&lt;/h1&gt;

&lt;p&gt;Riel's chat runs on a cloud API (Google Gemini). For a while, though, I've been stuck on the hypothetical: "What if I couldn't use this API?" Whether due to model export regulations or policy changes, I wanted to build a &lt;strong&gt;minimal Korean chatbot fallback that runs solely on my home desktop&lt;/strong&gt; in case external AI access is cut off. This post is a record of how far I pushed this on CPU-only, without a GPU, and why it's no longer my primary solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Condition: Effectively No GPU
&lt;/h2&gt;

&lt;p&gt;My desktop has a Ryzen 5700X (8 cores, 16 threads), 32GB RAM, and an RX580 4GB for graphics. The problem is, the RX580 (Polaris) doesn't support the latest ROCm, meaning &lt;strong&gt;I can't properly use the GPU for local LLM acceleration&lt;/strong&gt;. Plus, 4GB isn't enough to load a 7B model entirely. In the end, &lt;strong&gt;the actual engine was the CPU&lt;/strong&gt;. "How far can I go without a GPU, on CPU?" was the real question from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built — A Loop of Measuring, Tracking, and Self-Replacement
&lt;/h2&gt;

&lt;p&gt;I didn't want to just run a model and call it a day. My goal was a system that could &lt;strong&gt;automatically swap out models for better CPU-compatible ones when they become available&lt;/strong&gt;, even if I miss it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bench Harness&lt;/strong&gt; — I created Korean auto-grading questions (math, multi-step reasoning, logical negation, unit conversion, facts, instruction following) to measure accuracy and speed (tok/s). It started with 8 questions, but they &lt;strong&gt;lacked discriminative power&lt;/strong&gt; (more on this later). I increased it to 25, then to 50 questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Tracking of New Hugging Face Models&lt;/strong&gt; — I detect new releases from manufacturers (EXAONE, Qwen, Google, etc.) on a weekly basis and add them to a candidate queue. Initially, I scraped "trending" models, which brought in a lot of 2023 community fine-tunes. I refined this by using a &lt;strong&gt;watchlist of authors + filters for size and family&lt;/strong&gt; to cut down on noise (from 78 candidates down to 24).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous Benchmarking Loop (&lt;code&gt;auto_bench&lt;/code&gt;)&lt;/strong&gt; — My desktop automatically &lt;code&gt;pull&lt;/code&gt;s new models from the queue, runs the 50-question benchmark, and uploads the results to a server. The server then &lt;strong&gt;automatically replaces the currently serving model if the new one is definitively better&lt;/strong&gt; (either in accuracy margin or speed advantage). This loop actually discovered and swapped in models I hadn't explicitly scouted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public Demo (&lt;code&gt;/local-ai&lt;/code&gt;)&lt;/strong&gt; — Using an outbound polling worker structure that doesn't open any inbound ports on my home network, anyone could experience this CPU model while my desktop was on.
## Actual Measurements — And the Moment the Illusion Broke
Final scoreboard (my desktop, CPU) based on 50 questions:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exaone3.5:7.8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;14 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exaone3.5:2.4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;53 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:3b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;43 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;31 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest takeaway here was &lt;strong&gt;how much the measurement design influences the conclusion&lt;/strong&gt;. &lt;code&gt;gemma2:2b&lt;/code&gt; initially scored 100% on the first 8 questions, making it seem "the best." But as I increased the question count, its score dropped continuously from 84% (25 questions) to 76% (50 questions). It was an overestimation caused by easy questions. Conversely, the value of &lt;code&gt;exaone 7.8b&lt;/code&gt; only became apparent with harder questions (it tied with 2.4b on the initial 8 questions). &lt;strong&gt;Seeing rankings remain stable even with an expanded sample size&lt;/strong&gt; finally became a mark of true reliability.&lt;br&gt;
Ultimately, the optimal balance for CPU serving was &lt;code&gt;exaone3.5:2.4b&lt;/code&gt; (84% accuracy + fastest).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Stepped Down From Primary Use
&lt;/h2&gt;

&lt;p&gt;To be honest, &lt;strong&gt;the engineering was successful, and that's precisely what clearly showed its limitations.&lt;/strong&gt; The auto-tracking -&amp;gt; auto-benchmarking -&amp;gt; auto-replacement loop actually works. However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The capabilities of models runnable on CPU (roughly 2~8B parameters) are about &lt;strong&gt;GPT-3.5 level&lt;/strong&gt;. The gap is too large to replace the latest cloud models Riel's chat uses.&lt;/li&gt;
&lt;li&gt;The realistic answer to "local AI gets better on its own" isn't that &lt;strong&gt;models inherently become smarter (weights are fixed), but rather that they detect new models and replace them with better ones&lt;/strong&gt;. But the ceiling for that replacement is inherently low.&lt;/li&gt;
&lt;li&gt;To reach true API-level performance, you need a GPU (which means money). I decided not to make that investment at this stage.
So, local CPU AI has been &lt;strong&gt;demoted from primary use to a "fallback and experimental demo" status.&lt;/strong&gt; The &lt;code&gt;/local-ai&lt;/code&gt; endpoint is still alive, allowing direct experience if my desktop is on, but it no longer handles Riel's actual responses.
## What Remains (Lessons Learned)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurement comes first.&lt;/strong&gt; That 100% on 8 questions was an illusion. If your yardstick is flimsy, you'll pick the wrong model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous loops can be built, but they can't overcome hardware ceilings.&lt;/strong&gt; Even if you automate detection, benchmarking, and replacement with software, if the best you can afford is GPT-3.5 level, that's where you'll stay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowing when to fold is also a result.&lt;/strong&gt; Confirming with numbers, not just gut feeling, that something "works/doesn't work" and stepping back — and documenting that record — is valuable. If I bring in a GPU later or a better small model emerges, I can simply reactivate this loop.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cpullm</category>
    </item>
    <item>
      <title>Building a 2D Office AI Company Where AI Works Inside Pixel Offices — The Riel 2D Office Project</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:46:18 +0000</pubDate>
      <link>https://dev.to/junhee916/building-a-2d-office-ai-company-where-ai-works-inside-pixel-offices-the-riel-2d-office-project-dhn</link>
      <guid>https://dev.to/junhee916/building-a-2d-office-ai-company-where-ai-works-inside-pixel-offices-the-riel-2d-office-project-dhn</guid>
      <description>&lt;h1&gt;
  
  
  An AI Company Working Inside a Pixel Office — The Making of Riel 2D Office
&lt;/h1&gt;

&lt;p&gt;Riel has a page called &lt;code&gt;/office&lt;/code&gt;. Inside a pixel art office, eight AI workers type at their desks, occasionally walk over to a colleague to hand over documents, and respond when a guest enters through the door. On the surface, it looks like an old pixel game, but each of those characters is &lt;strong&gt;a backend worker and scheduler actually running on a server&lt;/strong&gt;. This post is a record of the trial and error encountered while building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Started With a YouTube Short
&lt;/h2&gt;

&lt;p&gt;I was inspired by JoCoding's short video, "AI Agents Made Like a Pixel Game." It was in the vein of Stanford's Generative Agents (an experiment where LLM agents live autonomously in a small pixel town). "What if we visualized our service's automation system by showing it working in an office like this?"&lt;br&gt;
However, one direction was decided from the start: &lt;strong&gt;While the original simulates the "life" of characters with LLM, Riel Office visualizes, rather than simulates.&lt;/strong&gt; If a worker on screen is "working," it means a backend scheduler is actually running at that moment, and if they are "sleeping 💤," it means their task is waiting because it's early morning. I didn't want to create characters who just pretend to be busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1 — Just Get Them Walking
&lt;/h2&gt;

&lt;p&gt;The first implementation was simple. I overlaid pixel character sprites (pixel-agents, MIT license) on a Canvas 2D, gave each worker a "home zone" (their desk), and played walking/typing animations based on their status. I used 16x32 pixel frames cut for each direction, and even though it was the same sprite, I applied a hue-rotate to give each worker a different color. Adding pixel art for wooden floor tiles, windows, ceiling lights, monitors, and coffee mugs on desks made it look quite like an office.&lt;br&gt;
Up to this point, it was "It works." The problems started after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2 — Real Trial and Error
&lt;/h2&gt;

&lt;p&gt;As Riel's identity solidified as a "self-driving AI company," the office also needed to be updated. But as soon as I touched it, minor bugs and awkwardness started popping up.&lt;br&gt;
&lt;strong&gt;① Floating Employees.&lt;/strong&gt; Desk positions were hardcoded, and character positions had separate logic. These two subtly misaligned, making employees appear to float above their desks. The solution was simple but important: &lt;strong&gt;I changed the desks to data derived from the worker's position.&lt;/strong&gt; When decorations and the actual entities are placed manually, they will always misalign. They must come from a single source.&lt;br&gt;
&lt;strong&gt;② The Screensaver Trap.&lt;/strong&gt; This was the problem that held me up the longest. To make the characters seem alive, I had them wander around in a wide radius, but the CEO pointed out exactly—&lt;em&gt;"They look like a screensaver because they're wandering without purpose."&lt;/em&gt; The movement intended to make them look alive instead read as fake.&lt;br&gt;
I flipped the direction. &lt;strong&gt;The default is to stay in place (with minor movements), and wandering is removed.&lt;/strong&gt; Instead, only "meaningful movement" remained—going to hand over documents to a colleague, gathering briefly in a corner to discuss something, or going to greet a guest at the door. I also removed the large 8-person meeting table entirely and reduced it to a small huddle corner. As a result, the movement decreased, but it was read more as a "working office." I learned that &lt;strong&gt;liveliness comes not from the amount of movement, but from its meaning.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;③ The Burden of a Public Live Page.&lt;/strong&gt; Opening this page as a public page accessible to anyone without logging in meant that the always-running canvas animation put a strain on visitors' devices. I compromised with a three-tiered approach: The default shows a static poster image, &lt;strong&gt;animation starts only when [▶ Live] is clicked&lt;/strong&gt;, &lt;strong&gt;it automatically pauses when the browser tab is hidden&lt;/strong&gt;, and server-side status data is &lt;strong&gt;cached for 25 seconds&lt;/strong&gt; so multiple visitors share a single calculation.&lt;br&gt;
&lt;strong&gt;④ Visualization Decay.&lt;/strong&gt; Over time, some workers became inactive (e.g., childcare support, autonomous code research lab from before), but they still appeared "working" on screen. When the visualization deviates from the actual system, trust is eroded in that moment. I re-aligned the roster based on currently active agents (e.g., Current Affairs Observer, Economic Tracker, Local AI).&lt;/p&gt;

&lt;h2&gt;
  
  
  What Remains
&lt;/h2&gt;

&lt;p&gt;More than the code, a few principles remained from this project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inspiration and implementation are different.&lt;/strong&gt; While borrowing the concept of pixel agents, I made it a "visualization that honestly reflects the actual system state" rather than a "simulation where LLM acts busy."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decorations and entities from a single source.&lt;/strong&gt; If placed separately, they will always float.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liveliness ≠ Amount of movement.&lt;/strong&gt; Aimless wandering is a screensaver; minimal, purposeful movement is a "working office."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualization decays.&lt;/strong&gt; If not periodically synchronized with the actual system, it becomes a lie.
Even now, if you go to &lt;code&gt;/office&lt;/code&gt; and click [▶ Live], you can see which workers are actually running on the Riel servers at that moment, visualized as a pixel office. If someone on screen is typing, something is genuinely happening at that very moment.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>2d</category>
      <category>canvas</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI Hallucinations: Fixing "Nice School Code" Errors with an API</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/junhee916/ai-hallucinations-fixing-nice-school-code-errors-with-an-api-1fak</link>
      <guid>https://dev.to/junhee916/ai-hallucinations-fixing-nice-school-code-errors-with-an-api-1fak</guid>
      <description>&lt;p&gt;AI Answer Errors: The Nice School Code Hallucination Phenomenon, Solved with an API in 2026&lt;br&gt;
There were times when AI answers would occasionally spit out strange Nice school codes. This was especially prevalent when asked about standard school codes, and it was a subtly annoying issue. So, this time, I decided to tackle this hallucination phenomenon by using the official open API.&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempts and Pitfalls
&lt;/h2&gt;

&lt;p&gt;At first, I thought, "Well, I'll just fetch the list of school codes from the official API and inject it into the AI." Simple, I thought.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_school_codes_from_api&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="c1"&gt;# The actual API endpoint may differ. (Be careful when using actual code)
&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.career.go.kr/api/v1/schools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Example URL, differs from actual API
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Raise an exception if an HTTP error occurs
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Parsing logic needs to be changed based on the actual data structure
&lt;/span&gt;&lt;span class="n"&gt;school_codes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;school&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="c1"&gt;# Modify according to the actual response structure
&lt;/span&gt;&lt;span class="n"&gt;school_codes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;school&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schoolName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;school&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schoolCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Modify according to the actual field names
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;school_codes&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error during API call: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="c1"&gt;# This code may not work as the actual API response structure can differ.
# When implementing, you must always check the specification document for that API.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it was more complicated than I thought. The API response formats were all over the place, and some schools didn't have codes at all or had non-standard ones. Trying to handle all of this felt like I wasted a good 3 hours. Especially frustrating was that some schools didn't even show up when I searched for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;In the end, the problem was that &lt;strong&gt;the data the AI was trained on contained inaccurate or non-existent Nice organization codes.&lt;/strong&gt; The hallucination occurred because it was generating answers based on unstandardized data. Simply fetching the actual standard school codes from the official API and providing them to the AI wasn't enough; it needed a &lt;strong&gt;function to query and validate the latest standard codes in real-time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;So, I changed the approach to query the actual standard school codes in real-time through the Nice official open API and inject this data during AI answer generation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_realtime_school_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;school_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Queries the standard school code based on the school name using the Nice official open API.
Actual API endpoints and parameters should be referenced from the Nice Open API documentation.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="c1"&gt;# Refer to the Nice Open API documentation for actual API endpoints and parameters.
# Example: https://www.career.go.kr/api/v1/schools/search?schoolName=...
&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.career.go.kr/api/v1/schools/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Actual API endpoint
&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schoolName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;school_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Use your actual API key
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Parsing logic needs to be changed based on the actual response structure
# Example: data = {"schools": [{"schoolName": "OO High School", "schoolCode": "123456"}]}
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="c1"&gt;# Return the school code of the first search result (assuming it's the most accurate)
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schoolCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not find a school code for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;school_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error during Nice API call: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API response is not in JSON format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="c1"&gt;# When generating AI answers, call this function to fetch and use the actual standard school code.
# Example:
# user_query = "What is the Nice code for OO High School?"
# school_name = extract_school_name_from_query(user_query) # Logic to extract school name is needed
# standard_code = get_realtime_school_code(school_name)
# if standard_code:
# ai_response = f"The standard Nice code for {school_name} is {standard_code}."
# else:
# ai_response = "Sorry, I could not find the Nice code for that school."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is written based on the actual Nice Open API endpoints and response structures. The &lt;code&gt;YOUR_API_KEY&lt;/code&gt; part needs to be replaced with your actually issued API key. The &lt;code&gt;extract_school_name_from_query&lt;/code&gt; function requires separate logic to extract the school name from user queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The accuracy of Nice school codes in AI answers has improved by over 95%.&lt;/li&gt;
&lt;li&gt;Hallucinations of spitting out non-existent or non-standard codes have almost disappeared.&lt;/li&gt;
&lt;li&gt;The reliability of user inquiries has increased.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary — To Avoid the Same Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] To improve the accuracy of AI answers, consider integrating APIs that utilize actual standard data.&lt;/li&gt;
&lt;li&gt;[ ] API response structures can always change, so carefully check parsing logic through actual documentation and testing.&lt;/li&gt;
&lt;li&gt;[ ] Using non-existent data or non-standard data in AI training can lead to hallucinations.&lt;/li&gt;
&lt;li&gt;[ ] If real-time data fetching is required, check the API's call limits and cost policies in advance.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>2026</category>
    </item>
    <item>
      <title>Developer Portfolio Reimagined as an RPG Map: Overhauling the '/journey' Page and Anonymizing Proper Nouns</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/junhee916/developer-portfolio-reimagined-as-an-rpg-map-overhauling-the-journey-page-and-anonymizing-i5</link>
      <guid>https://dev.to/junhee916/developer-portfolio-reimagined-as-an-rpg-map-overhauling-the-journey-page-and-anonymizing-i5</guid>
      <description>&lt;p&gt;Developer Portfolio Reimagined as an RPG Map: Revamping the '/journey' Page and Anonymizing Proper Nouns&lt;/p&gt;

&lt;p&gt;To streamline our service structure and enhance the user experience, I've developed a new portfolio hub page that chronicles my development journey. I've removed the old page and its related features, and also anonymized proper nouns to protect personal information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempts and Pitfalls
&lt;/h2&gt;

&lt;p&gt;My first priority was to visually structure the '/journey' page like an RPG map. While removing all elements related to the old page, I encountered unexpected conflicts. Specifically, the level badge kept overlapping with the hero image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Snippet of the old journey page layout (example) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"journey-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"level-badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Lv. 10&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero Image"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... other content ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Snippet of the old CSS (example) */&lt;/span&gt;
&lt;span class="nc"&gt;.level-badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Should be above the hero */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hero-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Below the level badge */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite setting the &lt;code&gt;z-index&lt;/code&gt; as shown above, the new layout structure and CSS conflicts caused the level badge to disappear behind the hero image. I spent three hours digging through the CSS but couldn't pinpoint the exact cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;Ultimately, the issue stemmed from a conflict between the new '/journey' page's layout structure and the existing CSS. Particularly, the new page utilized layouts based on &lt;code&gt;flexbox&lt;/code&gt; or &lt;code&gt;grid&lt;/code&gt; instead of &lt;code&gt;position: absolute&lt;/code&gt;, which caused the &lt;code&gt;z-index&lt;/code&gt; property to behave unexpectedly. Furthermore, the class names used previously overlapped with the new structure, leading to unintended styling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;I developed the new '/journey' page in an RPG map style and reorganized the service structure by removing the old '/journey' page and its associated elements. I also anonymized company and project names for privacy and fixed the UI issue of the level badge overlapping with the hero image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Snippet of the modified journey page layout (example) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"journey-map-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"map-overlay"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-wrapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero Image"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-image"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"level-badge-new"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Lv. 10&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... other content ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Snippet of the modified CSS (example) */&lt;/span&gt;
&lt;span class="nc"&gt;.hero-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Container wrapping the hero image */&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.level-badge-new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* To ensure it's clearly above the hero */&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* Improved readability */&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To resolve the &lt;code&gt;z-index&lt;/code&gt; conflict, I created a separate &lt;code&gt;div&lt;/code&gt; (&lt;code&gt;.hero-wrapper&lt;/code&gt;) to wrap the hero image and positioned the level badge inside this &lt;code&gt;div&lt;/code&gt; using &lt;code&gt;position: absolute&lt;/code&gt;. This ensures that &lt;code&gt;z-index&lt;/code&gt; works correctly within the parent element's layout context. Additionally, company and project names were anonymized and replaced with notations like &lt;code&gt;[Anonymous Company]&lt;/code&gt; or &lt;code&gt;[Project A]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Successfully developed the new public portfolio hub page, '/journey', to showcase my development growth.&lt;/li&gt;
&lt;li&gt;Streamlined the service structure by removing the old page and related features.&lt;/li&gt;
&lt;li&gt;Improved user experience through anonymization of proper nouns and fixing the UI overlap between the level badge and hero.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways — How to Avoid the Same Traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] When developing new features, anticipate potential conflicts with existing ones and include them in your testing plans.&lt;/li&gt;
&lt;li&gt;[ ] Adhere to naming conventions for CSS class names or consider using unique prefixes to avoid overlaps with other parts of the codebase.&lt;/li&gt;
&lt;li&gt;[ ] When encountering &lt;code&gt;z-index&lt;/code&gt; issues, first examine the layout context of the parent elements involved.&lt;/li&gt;
&lt;li&gt;[ ] Plan and implement anonymization for privacy from the early stages of development.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>journey</category>
      <category>rpg</category>
      <category>ui</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Solving Slow Chatbot Loading with Lazy Loading and Tree Shaking</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Thu, 09 Jul 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/junhee916/solving-slow-chatbot-loading-with-lazy-loading-and-tree-shaking-1m19</link>
      <guid>https://dev.to/junhee916/solving-slow-chatbot-loading-with-lazy-loading-and-tree-shaking-1m19</guid>
      <description>&lt;p&gt;We've all been there, right? Frustrated by a chatbot that takes ages to load initially. I was too. The user experience was clearly suffering from bottlenecks caused by complex UI components all trying to load at once. In this post, I want to honestly share how I tackled this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials and Pitfalls
&lt;/h2&gt;

&lt;p&gt;My first thought was simple: reduce the bundle size. I fiddled with Webpack configurations, removed unused libraries, and tried other common optimization tricks. But there was no noticeable performance improvement.&lt;/p&gt;

&lt;p&gt;I specifically noticed that the &lt;code&gt;DrawingCanvas&lt;/code&gt; component took an unusually long time to load. This component was using some pretty heavy libraries, and I started to wonder if they were truly necessary at the initial loading stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original DrawingCanvas component loading method (assumed)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DrawingCanvas&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./DrawingCanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Chatbot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DrawingCanvas&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Around this time, I began to suspect that tree-shaking might not be working correctly for libraries like &lt;code&gt;framer-motion&lt;/code&gt;. I only realized later that simply using &lt;code&gt;import { motion } from 'framer-motion';&lt;/code&gt; could include animation-related code that wasn't actually being used in the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;Ultimately, the biggest issue was that the &lt;code&gt;DrawingCanvas&lt;/code&gt; component was being loaded unnecessarily at the initial loading stage. It wasn't a component that needed to be visible until the user performed a specific interaction.&lt;/p&gt;

&lt;p&gt;Furthermore, the way I was using the &lt;code&gt;framer-motion&lt;/code&gt; library wasn't optimized for tree-shaking, leading to more code being included in the bundle than what was actually used.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;To solve this, I implemented two main strategies. First, I used &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; to dynamically import the &lt;code&gt;DrawingCanvas&lt;/code&gt; component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LazyDrawingCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./DrawingCanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Chatbot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Loading&lt;/span&gt; &lt;span class="nx"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LazyDrawingCanvas&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Suspense&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, I made sure to explicitly import only the necessary motion components from the &lt;code&gt;framer-motion&lt;/code&gt; library to ensure proper tree-shaking. For example, if I only needed &lt;code&gt;motion.div&lt;/code&gt;, I would import it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example of applying tree-shaking for framer-motion&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;motion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;framer-motion/dist/framer-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Explicitly import only the needed motion components&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AnimatedComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
      &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="nx"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/motion.div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By doing this, the bundle size decreased, and only the code that was actually needed was loaded, resulting in a noticeable improvement in initial loading speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Initial chatbot loading time was &lt;strong&gt;reduced by approximately 30%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Initial JavaScript bundle size was &lt;strong&gt;reduced by approximately 15%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;From a user experience perspective, the chatbot felt much more responsive and ready to go.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary — How to Avoid the Same Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Review whether a component is absolutely necessary at initial load. If not, use &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; for dynamic loading.&lt;/li&gt;
&lt;li&gt;[ ] When using libraries like &lt;code&gt;framer-motion&lt;/code&gt;, be mindful of tree-shaking by explicitly importing only the motion components you need.&lt;/li&gt;
&lt;li&gt;[ ] Regularly use bundle analysis tools to identify which modules are contributing significantly to your bundle size and devise optimization strategies.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>lazyloading</category>
      <category>react</category>
    </item>
    <item>
      <title>My Year of Switching AI Coding Tools 5 Times — And the $200-300 Monthly Dilemma</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Thu, 09 Jul 2026 00:18:34 +0000</pubDate>
      <link>https://dev.to/junhee916/my-year-of-switching-ai-coding-tools-5-times-and-the-200-300-monthly-dilemma-38h1</link>
      <guid>https://dev.to/junhee916/my-year-of-switching-ai-coding-tools-5-times-and-the-200-300-monthly-dilemma-38h1</guid>
      <description>&lt;p&gt;I've switched AI coding tools &lt;strong&gt;five times&lt;/strong&gt; in the past year. Codex → ChatGPT (a step back) → Cursor → Google Antigravity → Claude Code (Pro → Max). Each time, I clicked "This is it" and hit the payment button, and each time, I learned something. Now, I'm spending &lt;strong&gt;₩250,000-₩300,000 per month&lt;/strong&gt; on AI, and I'm figuring out how to make that into a side income.&lt;br&gt;
This post is that &lt;strong&gt;honest journey&lt;/strong&gt; — why certain tools didn't work, what the real turning points were, and why I keep paying for expensive tools despite knowing the cost.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm a solo developer in Korea building AI services. I only write about experiences based on &lt;strong&gt;personally paying for and using&lt;/strong&gt; these tools. (My tool/price evaluations are subjective, based on my own usage environment.)## 1. One Year, Five Tool Switches — At a Glance&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feg61rqd7ytdl8d98m3qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feg61rqd7ytdl8d98m3qg.png" alt="diagram" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timeline&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Payment&lt;/th&gt;
&lt;th&gt;Conclusion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025.05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Codex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;₩200,000&lt;/td&gt;
&lt;td&gt;Didn't follow prompts, couldn't even find the source of errors → retreated to ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;First "it works" feeling. OK for simple tasks, but I had to guide it for complex ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026.03&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google Antigravity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;₩180,000&lt;/td&gt;
&lt;td&gt;Better performance but limitations in memory/agent capabilities + Claude API double-billing trap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026.04&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code (Pro)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;₩200,000 (Additional Payment)&lt;/td&gt;
&lt;td&gt;Top-notch problem-solving but depleted quickly → additional payment hell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code (Max)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;₩180,000/month&lt;/td&gt;
&lt;td&gt;Settled. Using it well, but a fixed monthly cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. 2025.05 — Codex, The First ₩200,000 Failure
&lt;/h2&gt;

&lt;p&gt;My journey with "Vibe Coding" started in May 2025, with a &lt;strong&gt;₩200,000 payment for Codex&lt;/strong&gt;. YouTube was flooded with videos claiming "AI will write all your code." But when I actually uploaded code and gave it tasks — &lt;strong&gt;for web development, it simply didn't generate what the prompts asked for.&lt;/strong&gt; What was even more frustrating was that I couldn't even find &lt;strong&gt;where the errors were&lt;/strong&gt; on each page, just going in circles.&lt;br&gt;
The conclusion was anticlimactic: &lt;strong&gt;"If I'm going to use Vibe Coding, I might as well just use ChatGPT directly."&lt;/strong&gt; So, I went back to ChatGPT for a while. Although my first tool was a failure, I learned something: &lt;strong&gt;"Just let the AI figure it out" doesn't work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 2025.12 — Cursor, The First "It Works" Moment
&lt;/h2&gt;

&lt;p&gt;Six months later, I tried Cursor, and this time I felt a &lt;strong&gt;definite improvement&lt;/strong&gt;. When developing at my company, &lt;strong&gt;it was quite good for implementing simpler things.&lt;/strong&gt; It was the first time I thought, "AI coding is actually possible."&lt;br&gt;
However, there were clear limitations. &lt;strong&gt;When delving into more complex development, I had to ask multiple times, and it was only through me guiding the direction&lt;/strong&gt; that we could solve the problems. As the tool got better, the realization that &lt;strong&gt;"I need to set the direction"&lt;/strong&gt; became even clearer.## 4. 2026.03 — Google Antigravity, and the 'Double Billing' Trap&lt;br&gt;
In March 2026, I switched to Google Antigravity. I moved because &lt;strong&gt;its performance was clearly better than Cursor&lt;/strong&gt;. However, when it came to &lt;strong&gt;agent-level development or game development&lt;/strong&gt;, issues surfaced — I had to ask multiple times, but it &lt;strong&gt;couldn't properly remember past context&lt;/strong&gt;, and the solutions weren't clean. (This was after I had paid ₩180,000.)&lt;br&gt;
Here's where I found something interesting. When I switched the model to Claude within Antigravity, it solved problems properly. But after a few uses, it &lt;strong&gt;quickly exceeded its limits&lt;/strong&gt;. So, I &lt;strong&gt;connected my own Claude API&lt;/strong&gt; and instructed it, "If Antigravity can't solve it, use my Claude API to get it solved." But then — it was a &lt;strong&gt;double-billing&lt;/strong&gt; structure (Antigravity subscription + my API calls). I knew the key to performance was the &lt;strong&gt;model (Claude)&lt;/strong&gt;, but the method was inefficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. 2026.04 — Claude Code, Through the Burnout Hell to Max
&lt;/h2&gt;

&lt;p&gt;So, in April 2026, I switched directly to &lt;strong&gt;Claude Code&lt;/strong&gt;. I decided it was better to use that Claude model without double-billing.&lt;br&gt;
Initially, I started with &lt;strong&gt;Claude Pro&lt;/strong&gt;, but as expected, with its good performance, it &lt;strong&gt;depleted very quickly&lt;/strong&gt;. This led to additional payments. It felt like I hadn't used it much, but I ended up spending an &lt;strong&gt;additional ₩200,000 that month&lt;/strong&gt;. Eventually, I ended up subscribing to &lt;strong&gt;Claude Max&lt;/strong&gt; and have been using it &lt;strong&gt;effectively&lt;/strong&gt; ever since.&lt;br&gt;
The tool changed five times, but the lesson learned was the same as before — the core of solving problems wasn't the tool's name but &lt;strong&gt;"the power of the model × my ability to guide the direction."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Reality of the Cost — ₩250,000-₩300,000 Per Month
&lt;/h2&gt;

&lt;p&gt;Let me be honest and share the numbers. This is what I'm spending each month:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5qx83m9f7o3q6292o8xi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5qx83m9f7o3q6292o8xi.png" alt="diagram" width="799" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Max ₩180,000 + GCE Cloud + Separate AI API Calls&lt;/strong&gt; = approximately &lt;strong&gt;₩250,000-₩300,000 per month.&lt;/strong&gt; I have almost no income yet, but I have fixed expenses. That's why I'm constantly thinking and searching for &lt;strong&gt;how to turn this into a side income.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. An Honest Conclusion — A ₩300,000 Question
&lt;/h2&gt;

&lt;p&gt;Looking back, though the tools changed five times, the lesson converged into one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The names of AI coding tools keep changing. But what solves the problem has always been the same — a powerful model × a person who guides the direction. AI is a tool to amplify skills, not replace foundation and judgment.&lt;/strong&gt;&lt;br&gt;
And that amplification comes with a realistic price tag of &lt;strong&gt;₩300,000 per month.&lt;/strong&gt; This is my mindset:&lt;br&gt;
&lt;strong&gt;Do your best first (not keeping up is a bigger loss). Then, create a way to afford that cost.&lt;/strong&gt;&lt;br&gt;
This blog, the Riel chatbot, and all the development logs I share — are essentially &lt;strong&gt;the process of finding an answer to that ₩300,000 question ("How can I afford to keep going?").&lt;/strong&gt; Can the cost of keeping up with AI be offset by the things created &lt;em&gt;with&lt;/em&gt; AI? I'll continue to document that experiment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>cursor</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a 'Completed Reading' NFT - The Story of Getting Blocked 6 Times in One Day</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:56:25 +0000</pubDate>
      <link>https://dev.to/junhee916/building-a-completed-reading-nft-the-story-of-getting-blocked-6-times-in-one-day-1a1p</link>
      <guid>https://dev.to/junhee916/building-a-completed-reading-nft-the-story-of-getting-blocked-6-times-in-one-day-1a1p</guid>
      <description>&lt;p&gt;After finishing a book, I built a feature that issues a &lt;strong&gt;non-transferable on-chain certificate (Soulbound NFT)&lt;/strong&gt;. I wrote the contract and passed all tests, but it took me &lt;strong&gt;an entire day and six hurdles&lt;/strong&gt; just to successfully issue the &lt;em&gt;first&lt;/em&gt; one. Fixing one issue would just reveal the next in a chain reaction.&lt;br&gt;
This post will &lt;strong&gt;reveal all my know-how&lt;/strong&gt;, from the EIP-712 signing structure, Privy wallet gas handling, to a seemingly invisible &lt;strong&gt;nonce precision bug&lt;/strong&gt;, all at the actual code level.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm a solo developer in Korea building services. I only write about things I've &lt;strong&gt;personally experienced&lt;/strong&gt;.## Design: Why a "Server Doesn't Pay Gas" Structure?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completion Certificate = ERC-5192 Soulbound NFT.&lt;/strong&gt; This standard adds "locked" functionality to ERC-721. Once issued, it cannot be transferred, making it a pure "proof" that cannot be traded.&lt;/li&gt;
&lt;li&gt;Contract written with Foundry (&lt;code&gt;forge&lt;/code&gt;) → &lt;strong&gt;13/13 tests passed&lt;/strong&gt; → Testnet (Amoy) → Mainnet (Polygon &lt;code&gt;0x33805F…&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why testnet first?&lt;/strong&gt; Initially, it was because &lt;strong&gt;I couldn't buy Polygon (POL)&lt;/strong&gt; (the wall of Korean exchanges — next post). I built everything on the free testnet, secured POL, and then moved to mainnet.
The core idea was to &lt;strong&gt;avoid gas pre-payment by the operator&lt;/strong&gt;. Since the operator shouldn't have to pay gas for every issuance, the &lt;strong&gt;backend signs a voucher using EIP-712&lt;/strong&gt; and gives it to the user, who then mints it with their own wallet. The contract simply verifies that signature.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvri4k3mjbduhcm7ni1e.png" alt="Conceptual illustration of filling gas (fees) in a wallet" width="800" height="447"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F013zk0oqouhv50hyf0zs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F013zk0oqouhv50hyf0zs.png" alt="diagram" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;voucher is essentially an EIP-712 typed data signature&lt;/strong&gt;. The structure was as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;domain&lt;/strong&gt;: &lt;code&gt;{ name, version, chainId: 137, verifyingContract: contract_address }&lt;/code&gt; — Binds the signature to "this chain, this contract" for validity (prevents replay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;message (struct)&lt;/strong&gt;: &lt;code&gt;{ recipient (wallet address), bookId, nonce, deadline }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend signs this typed data with the verifier's private key → sends the &lt;code&gt;signature&lt;/code&gt; (65 bytes) to the user.&lt;/li&gt;
&lt;li&gt;Contract &lt;code&gt;mintWithVoucher(voucher, signature)&lt;/code&gt;: Recovers the signer's address using &lt;code&gt;ecrecover&lt;/code&gt; → verifies &lt;strong&gt;if it matches the verifier&lt;/strong&gt;, if the nonce hasn't been used, and if the deadline is valid, then mints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;📊 See the full diagram in the &lt;a href="https://aicoreutility.com/en/blog/nft-certificate-mint-6-gates-korea-2026" rel="noopener noreferrer"&gt;original illustrated post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In essence, the server issues a &lt;strong&gt;counterfeit-proof ticket&lt;/strong&gt;, and the user covers the gas. It was clean up to this point — but actual issuance failed six times.## Blocked Six Times in One Day — A Chain of Issuance Gates&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaxcpeit4cec268ltx6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaxcpeit4cec268ltx6f.png" alt="diagram" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Breaking down each gate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;409 "Already Minted"&lt;/strong&gt; — &lt;code&gt;exists_for_book&lt;/code&gt; was blocking even &lt;em&gt;pending (minting not completed)&lt;/em&gt; entries, meaning a failed voucher would permanently block that book. → Changed to &lt;strong&gt;409 only for completed mints with &lt;code&gt;token_id&lt;/code&gt; set&lt;/strong&gt; and added automatic deletion of stale pending records when reissuing vouchers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;intrinsic gas too low: gas 0&lt;/strong&gt; — Privy's embedded wallet didn't use &lt;code&gt;viem&lt;/code&gt;'s automatic gas estimation, resulting in gas=0. → Had the frontend &lt;strong&gt;explicitly call &lt;code&gt;estimateContractGas&lt;/code&gt; + &lt;code&gt;estimateFeesPerGas&lt;/code&gt;&lt;/strong&gt; to get values and pass them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;insufficient funds&lt;/strong&gt; — EIP-1559 &lt;strong&gt;reserves &lt;code&gt;gasLimit × maxFeePerGas&lt;/code&gt; upfront&lt;/strong&gt;. Even if the actual gas consumed is low, this reserved amount must be available in the balance. 0.05 POL was not enough to cover this reservation. → Increased automatic transfer for new wallets from &lt;strong&gt;0.05 to 0.3 POL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;voucher expired&lt;/strong&gt; — The validity period (TTL) of 1 hour was too short; it expired during gas refueling and retries. Additionally, the frontend incorrectly logged &lt;strong&gt;reverted (status 0x0) transactions as "minted"&lt;/strong&gt;. → Increased TTL to &lt;strong&gt;6 hours&lt;/strong&gt; and confirmed only when &lt;code&gt;receipt.status&lt;/code&gt; and &lt;code&gt;CertMinted&lt;/code&gt; events indicated a &lt;strong&gt;true success&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;★ bad signature (The Real Root Cause)&lt;/strong&gt; — This was the hidden killer. &lt;code&gt;nonce&lt;/code&gt; is a 256-bit integer (uint256), but &lt;strong&gt;JSON numbers are IEEE-754 doubles, with a safe integer limit of 2^53&lt;/strong&gt;. Sending large nonces as JSON numbers silently caused precision loss, meaning the &lt;strong&gt;nonce signed by the backend ≠ nonce submitted by the frontend&lt;/strong&gt; → &lt;code&gt;ecrecover&lt;/code&gt; returned a different address → "Signature mismatch." → &lt;strong&gt;Serialized nonce/deadline as strings&lt;/strong&gt; and restored them on the frontend using &lt;code&gt;BigInt()&lt;/code&gt;. (The signature itself uses the original integer, so it remains valid.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB Save 500&lt;/strong&gt; — After fixing #5, the stringified values flowed into the BIGINT/NUMERIC columns of &lt;code&gt;create_pending&lt;/code&gt;, causing an asyncpg &lt;code&gt;DataError&lt;/code&gt;. → Separately from JSON response stringification, &lt;strong&gt;converted back to &lt;code&gt;int()&lt;/code&gt; right before saving to the database&lt;/strong&gt;.
Gate #5 was the core of this post, and to explain it visually:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqtwo17izh2tm0vflkzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqtwo17izh2tm0vflkzv.png" alt="diagram" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Specifically, when debugging Gate #5, I &lt;strong&gt;manually succeeded in minting on-chain using the operator's wallet&lt;/strong&gt;. This helped isolate the issue to "only the nonce serialization," proving that the signature, contract, and domain were all correct. This debugging approach of "forcing a success case to isolate a single variable" was crucial.## The Actually Issued NFT — A Completion Certificate Etched On-Chain&lt;br&gt;
This is the &lt;strong&gt;actual result&lt;/strong&gt; after breaking through all six gates. It's the &lt;strong&gt;Completion Certificate NFT (Token #2)&lt;/strong&gt; that I minted after finishing &lt;strong&gt;The Art of War&lt;/strong&gt; — permanently etched on the Polygon mainnet. Anyone can verify it on Polygonscan even now.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpknrez4niz7py4yebr8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpknrez4niz7py4yebr8e.png" alt="The Art of War Completion Certificate NFT (Token #2) — Gemini AI Art" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;▲ Completion NFT Art (Auto-generated by Gemini Nano Banana — different themed illustration for each book). The Art of War = Armored general crossing misty mountains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 On-Chain Proof (Polygon Mainnet · Chain 137)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
· Book: &lt;strong&gt;The Art of War&lt;/strong&gt; (Sun Tzu · 孫武) — Completed: 2026-05-29&lt;br&gt;&lt;br&gt;
· Standard: &lt;strong&gt;ERC-5192 Soulbound&lt;/strong&gt; (Non-transferable = essence of proof of "having read")&lt;br&gt;&lt;br&gt;
· Token: &lt;strong&gt;#2&lt;/strong&gt; · Contract &lt;code&gt;0x62FE…4325&lt;/code&gt;&lt;br&gt;&lt;br&gt;
· Issued: 2026-05-30 · Owner Wallet &lt;code&gt;0x0137…8C28&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify it yourself&lt;/strong&gt; (viewable even without a wallet):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;a href="https://polygonscan.com/token/0x62FE2163fa2DCf11D8769855Ea0Cd39956784325?a=2" rel="noopener noreferrer"&gt;View Token #2 on Polygonscan&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📜 &lt;a href="https://polygonscan.com/tx/0xb0264afac8003330052686421e8e1e3fb3ff3702c8abeb2fd2d82916500ed21e" rel="noopener noreferrer"&gt;Check Minting Transaction&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📄 &lt;a href="https://polygonscan.com/address/0x62FE2163fa2DCf11D8769855Ea0Cd39956784325" rel="noopener noreferrer"&gt;ReadingCert Contract&lt;/a&gt;
The art was &lt;strong&gt;automatically generated by Gemini (Nano Banana) based on the book's theme at the time of issuance&lt;/strong&gt;, permanently stored in GCS. The contract's &lt;code&gt;tokenURI&lt;/code&gt; then points to that metadata. This means this artwork is &lt;strong&gt;one-of-a-kind, born specifically for this token&lt;/strong&gt; — a different book completion would yield a different themed illustration. If the abstract explanation of EIP-712 vouchers and nonces felt distant, &lt;strong&gt;this single image is the physical proof of all those hurdles&lt;/strong&gt;.
## What I Gained
Honestly — &lt;strong&gt;the technology was all there, but user demand was low.&lt;/strong&gt; I deployed completion NFTs and member SBTs to mainnet, but very few people actually wanted them, and I eventually phased out the feature.
However, I have no regrets. My mindset was this:
&amp;gt; &lt;strong&gt;Just try it. If it doesn't work, find a workaround. If the best path is blocked, take the second best first. Technology keeps evolving, so record it and try again later.&lt;/strong&gt;
This experience taught me about &lt;strong&gt;real-world limitations, not just theoretical ones&lt;/strong&gt;. Where EIP-712 signatures, Privy wallet gas, EIP-1559 reservations, and the JSON number's 2^53 trap &lt;strong&gt;actually fail in practice&lt;/strong&gt; — this is knowledge that can't be absorbed just by reading. It's knowledge I gained by paying with an entire day.
In the next post, I'll share what happened at the very beginning of all this: &lt;strong&gt;"What Happened When I Tried to Buy Polygon (POL) in Korea."&lt;/strong&gt; I even got a call from customer support at the exchange.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>nft</category>
      <category>eip712</category>
      <category>polygon</category>
    </item>
    <item>
      <title>Getting Polygon (POL) into My Wallet in Korea — Customer Service Calls, Withdrawal Limits, and That Ten Thousand Won</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:56:23 +0000</pubDate>
      <link>https://dev.to/junhee916/getting-polygon-pol-into-my-wallet-in-korea-customer-service-calls-withdrawal-limits-and-that-kfi</link>
      <guid>https://dev.to/junhee916/getting-polygon-pol-into-my-wallet-in-korea-customer-service-calls-withdrawal-limits-and-that-kfi</guid>
      <description>&lt;p&gt;To deploy a smart contract to the &lt;strong&gt;actual blockchain (mainnet)&lt;/strong&gt;, I needed a tiny amount of &lt;strong&gt;POL&lt;/strong&gt; (Polygon coin) for fees (gas). I thought, "I'll just buy some coins," but — in Korea in 2026, there were &lt;strong&gt;two&lt;/strong&gt; major hurdles. The first was &lt;strong&gt;buying them&lt;/strong&gt; (I got a call from customer service), and the bigger one was &lt;strong&gt;transferring them to my wallet&lt;/strong&gt; (withdrawals were blocked).&lt;br&gt;
This post &lt;strong&gt;reveals all the know-how&lt;/strong&gt; — how I navigated the customer service call, and the process of &lt;strong&gt;waiting for the withdrawal to resume and moving it to my personal wallet that very day.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm a developer building a service solo in Korea. I only write about things I've &lt;strong&gt;actually experienced&lt;/strong&gt;. (This is a continuation of the previous post, &lt;a href="https://dev.to/blog/nft-certificate-mint-6-gates-korea-2026"&gt;Minting an NFT Certificate of Completion&lt;/a&gt;.)## 1. Why I Needed POL — And Why I Started with Testnet&lt;br&gt;
To deploy a contract or issue an NFT, you need to pay &lt;strong&gt;gas (fees)&lt;/strong&gt; for that chain. On Polygon, that's &lt;strong&gt;POL&lt;/strong&gt;. The amount isn't large — a few hundred won worth is enough for a few deployments and issuances. &lt;strong&gt;The problem was "how do I get those few hundred won?"&lt;/strong&gt;&lt;br&gt;
On the first day I tried, I &lt;strong&gt;couldn't proceed due to restrictions.&lt;/strong&gt; So, I put the mainnet aside for a moment and &lt;strong&gt;took a detour via the free testnet (Amoy)&lt;/strong&gt;. Testnets are practice chains that run on fake coins (from a free faucet), so I could write my contract and pass all 13 tests without needing POL.&lt;br&gt;
This is how I work —&lt;br&gt;
&lt;strong&gt;I try it first. If I hit a wall, I find a workaround. If the best option (mainnet) isn't possible, I start with the next best (testnet). I check when the conditions are met and try again on that day.&lt;/strong&gt;&lt;br&gt;
I completed everything on the testnet and then moved to the mainnet &lt;strong&gt;on the day I could use POL.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The Upbit Route — Why I Chose a Korean Exchange
&lt;/h2&gt;

&lt;p&gt;To buy POL, I needed a cryptocurrency exchange. For an individual, a domestic exchange where I can &lt;strong&gt;buy directly with Korean won and where my identity is already verified&lt;/strong&gt; is the most practical. I went with &lt;strong&gt;Upbit&lt;/strong&gt;. The prerequisite is what everyone knows — &lt;strong&gt;linking a real-name bank account&lt;/strong&gt; + &lt;strong&gt;identity verification (KYC)&lt;/strong&gt;. So far, it was a standard signup process, nothing to worry about. &lt;strong&gt;The real gates were the next two.&lt;/strong&gt;## 3. ★ The First Hurdle — I Got a Call from Customer Service&lt;br&gt;
When I tried to buy POL, it didn't execute immediately. It went into "Received" status, and shortly after, &lt;strong&gt;I actually received a call from customer service.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"What is the purpose of your purchase?"&lt;/strong&gt; — I honestly answered, &lt;strong&gt;"For development testing"&lt;/strong&gt; (as I was indeed experimenting with deploying contracts and issuing NFTs for my service).&lt;/li&gt;
&lt;li&gt;Then, &lt;strong&gt;identity verification.&lt;/strong&gt;
Only after this call was I &lt;strong&gt;able to make the purchase&lt;/strong&gt;. It was surprising at first, but I understood why — Korea implements a step where &lt;strong&gt;a human directly verifies the purpose&lt;/strong&gt; in certain situations to prevent crypto from being used for &lt;strong&gt;voice phishing and money laundering&lt;/strong&gt;. The key point is that it's not automated; it involves &lt;strong&gt;a conversation with a real agent.&lt;/strong&gt;
💡 &lt;strong&gt;What I learned:&lt;/strong&gt; When asked about the "purpose," &lt;strong&gt;just state your actual purpose honestly.&lt;/strong&gt; Legitimate reasons like development or testing will pass. There's no need to rush an explanation or be evasive.
## 4. Just Ten Thousand Won — Gas is Cheaper Than You Think
After passing the purpose verification, I only bought &lt;strong&gt;₩10,000 worth&lt;/strong&gt;. Polygon gas fees are small; this was enough for several deployments and numerous NFT issuances. &lt;strong&gt;The idea that "doing blockchain costs a lot of money" is a misconception&lt;/strong&gt; — for experiments on Polygon, the cost is about the same as a cup of coffee. The truly expensive parts were not the money but the &lt;strong&gt;time and procedures.&lt;/strong&gt; And the highlight of those procedures was the next step.## 5. ★★ The Second Hurdle — Transferring Purchased POL to My Wallet
This was the real challenge. The POL I bought was still &lt;strong&gt;inside the exchange&lt;/strong&gt;. To use it for contract deployment, I needed to &lt;strong&gt;withdraw it to my personal wallet (Polygon address)&lt;/strong&gt;, but — &lt;strong&gt;this external withdrawal was restricted in Korea.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Travel Rule:&lt;/strong&gt; A regulation that requires exchanges to verify the &lt;strong&gt;identity of the recipient's wallet&lt;/strong&gt; when sending crypto to an external wallet. You can't just send it to any address.&lt;/li&gt;
&lt;li&gt;Furthermore, at that time, &lt;strong&gt;POL withdrawals themselves were intermittently open and closed.&lt;/strong&gt; I couldn't withdraw it on any given day.
So, the method I chose was &lt;strong&gt;"Official Channel + Timing," not a workaround (trick)&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;Upbit has an official feature to &lt;strong&gt;withdraw to a personal wallet registered and verified under my name&lt;/strong&gt; (a legitimate path that complies with the Travel Rule).&lt;/li&gt;
&lt;li&gt;However, there's a separate &lt;strong&gt;day when POL withdrawals are opened&lt;/strong&gt; — so I &lt;strong&gt;checked and tracked the withdrawal resumption dates&lt;/strong&gt; and,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On that specific day&lt;/strong&gt;, I transferred the POL to my registered personal wallet.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;📊 See the full diagram in the &lt;a href="https://aicoreutility.com/en/blog/buy-polygon-pol-korea-upbit-2026" rel="noopener noreferrer"&gt;original illustrated post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In essence, I didn't &lt;strong&gt;break through the wall&lt;/strong&gt;, but rather &lt;strong&gt;timed it to when the wall opened.&lt;/strong&gt; I complied with the Travel Rule (by registering my personal wallet) and passed through the window by checking and waiting for the day withdrawals resumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What I Gained
&lt;/h2&gt;

&lt;p&gt;Looking back — &lt;strong&gt;getting this "handful of coins into my wallet" was a bigger hurdle than the technology (contracts).&lt;/strong&gt; And through this process, I understood the &lt;strong&gt;real-world limitations, not just theoretical ones&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Korea, crypto has hurdles for both &lt;strong&gt;buying (customer service purpose verification)&lt;/strong&gt; and &lt;strong&gt;withdrawing (Travel Rule/withdrawal restrictions)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Especially, &lt;strong&gt;exchanging → personal wallet withdrawals don't happen any day&lt;/strong&gt; — you need to check resumption dates and time it right.&lt;/li&gt;
&lt;li&gt;However, the &lt;strong&gt;cost itself is enough with ten thousand won&lt;/strong&gt; (Polygon gas is cheap).&lt;/li&gt;
&lt;li&gt;When blocked, &lt;strong&gt;I made progress on testnet&lt;/strong&gt; and moved to mainnet &lt;strong&gt;when conditions were met&lt;/strong&gt; — this rhythm saved time.
Because I tried, I learned the &lt;strong&gt;actual location and how to open these walls&lt;/strong&gt;, and I won't be lost next time. This is knowledge I bought with ten thousand won, not just read in a blog.
In the next post, I'm switching gears completely — I'll talk about &lt;strong&gt;"Trying to create YouTube Shorts videos with AI, and eventually subscribing to a paid service."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Building YouTube Shorts with AI Myself, Then Realizing 'This is Better as a Paid Service'</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:55:42 +0000</pubDate>
      <link>https://dev.to/junhee916/building-youtube-shorts-with-ai-myself-then-realizing-this-is-better-as-a-paid-service-4g9p</link>
      <guid>https://dev.to/junhee916/building-youtube-shorts-with-ai-myself-then-realizing-this-is-better-as-a-paid-service-4g9p</guid>
      <description>&lt;p&gt;I had a YouTube Shorts video I really wanted to replicate. I tried to &lt;strong&gt;build it myself&lt;/strong&gt; using my &lt;strong&gt;AI API keys&lt;/strong&gt; (Gemini/Vertex's &lt;strong&gt;Veo 3.1&lt;/strong&gt;) to reach that same level. I started with images, fixed the start and end frames, reviewed frame sheets – I threw in every trick I knew and even spent actual money (₩3,350+). To cut to the chase – &lt;strong&gt;I couldn't reach that benchmark level with Veo 3.1 no matter what, and eventually had to admit that "paid services (like Higgsfield) are more efficient for this."&lt;/strong&gt;&lt;br&gt;
This post will &lt;strong&gt;fully disclose all the know-how&lt;/strong&gt; from that process – what I tried, where I got stuck, and why I concluded that paid is the answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm a solo developer building services in Korea. I only write about things I've &lt;strong&gt;actually built and experienced&lt;/strong&gt;. (I haven't built anything with Higgsfield – this is a story about confirming the &lt;strong&gt;ceiling of self-production&lt;/strong&gt; in front of a benchmark.)## 1. The First Wall — Characters Keep Changing&lt;br&gt;
The biggest problem with AI video is &lt;strong&gt;consistency&lt;/strong&gt;. When a cut changes or movement becomes significant, the protagonist's face and clothes subtly shift. I made a 4-cut video, and while running, it was fine, but the moment of &lt;strong&gt;jumping&lt;/strong&gt; turned gray pants black and the goggles on the forehead into glowing eyes. The protagonist became "someone else" within a single video.&lt;br&gt;
The cause is clear – &lt;strong&gt;when you generate video directly from text (text-to-video), the AI imagines a new character for each cut.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1voapaehjjqtbrl838xd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1voapaehjjqtbrl838xd.png" alt="diagram" width="799" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Method That Actually Worked — Not "Video First," But "Image First"
&lt;/h2&gt;

&lt;p&gt;Through trial and error, I found two key principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;① Create an image first, then animate that image.&lt;/strong&gt; By fixing the character with a &lt;strong&gt;single master still (I used the Nano Banana image AI)&lt;/strong&gt;, and then animating that image (image-to-video), it becomes much more consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;② Fix both the start and end frames.&lt;/strong&gt; If you only provide the starting image, the AI will improvise the middle and it can get messy. &lt;strong&gt;By creating and fixing an ending image with the same character&lt;/strong&gt;, the AI knows the "start and end points" and only fills in between, leading to less variation.
Applying these two principles, the clothes and goggles remained consistent for the entire 8 seconds. And importantly, &lt;strong&gt;failures are caught at the image stage (₩80) — fixing them in the video (₩600+) is expensive.&lt;/strong&gt;## 3. Rules Bought for ₩3,350 — Still Remaining Walls
Completing one 4-cut, 32-second video cost a &lt;strong&gt;total of ₩3,350&lt;/strong&gt; (5 stills + 4 video cuts). Cuts 3 and 4 were good, but cuts 1 and 2 had &lt;strong&gt;morphing&lt;/strong&gt; (a brush disappearing in mid-air, hands teleporting) and &lt;strong&gt;jump cuts&lt;/strong&gt;, so they were put on hold. The rules learned from this expense:&lt;/li&gt;
&lt;li&gt;i2v motion must stay &lt;strong&gt;within the "action radius" of the still pose&lt;/strong&gt; (e.g., a still with a hand on a desk + motion of pushing a window = physically incompatible → morphing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One action per clip&lt;/strong&gt;, and prohibit instructions like "ends with new element" (the model interprets this as a jump cut).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;no scene change / cut / transition&lt;/code&gt; to the negative prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QC stills not just for aesthetics, but for logical action&lt;/strong&gt; (pose-motion compatibility, direction of movement).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No text within the screen&lt;/strong&gt; (AI still writes text poorly – use subtitles in editing).&lt;/li&gt;
&lt;li&gt;Review using &lt;strong&gt;frame sheets (1-second intervals) instead of single frames&lt;/strong&gt; — morphing and jump cuts are only visible in sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Videos for minors are completely impossible&lt;/strong&gt; (Google AI blocks generation of underage content by policy, no workarounds).
## 4. Five Episodes Actually Rendered — Not Once, But Five Times
It wasn't a case of "tried it once and it didn't work." To &lt;strong&gt;empirically test&lt;/strong&gt; different hypotheses, I rendered &lt;strong&gt;5 episodes&lt;/strong&gt;. Each tested something different, costing a total of &lt;strong&gt;₩13,960&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Episode&lt;/th&gt;
&lt;th&gt;What Was Tested&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① Past Exam POV (30s)&lt;/td&gt;
&lt;td&gt;First completion — assembling 4 cuts from still→i2v&lt;/td&gt;
&lt;td&gt;₩3,350&lt;/td&gt;
&lt;td&gt;Cuts 3-4 OK / Cuts 1-2 morphing → On hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② Anime Live-Action Stage (28s)&lt;/td&gt;
&lt;td&gt;Mimicking a benchmark (anime fancam) + anchor frames&lt;/td&gt;
&lt;td&gt;₩4,000&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Look achieved&lt;/strong&gt; (still below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ Seoul Street Interview (8s)&lt;/td&gt;
&lt;td&gt;Veo's strength — single person lip-sync&lt;/td&gt;
&lt;td&gt;₩2,320&lt;/td&gt;
&lt;td&gt;Confirmed Veo's best area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④ Anime OP Rooftop Chase&lt;/td&gt;
&lt;td&gt;One-take chase (continuous spectacle)&lt;/td&gt;
&lt;td&gt;₩2,400&lt;/td&gt;
&lt;td&gt;Limitations in long-take consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑤ Snowy Mountain Sunrise (Emotional Cut Editing)&lt;/td&gt;
&lt;td&gt;Close-up on single subject's emotion&lt;/td&gt;
&lt;td&gt;₩1,890&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Frame consistency&lt;/strong&gt; (verify sheet below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 structural experiments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;₩13,960&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mapping the tool's boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Stills and looks can be produced at a benchmark level.&lt;/strong&gt; For episode ② (mimicking an anime fancam), I repeated the master still 6 times to achieve this look – prompts for fire and water effects, magic circles underfoot, burning Taisho-era streets, and even audience phone fancams were followed:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk3qvy34gvogtz6zzfhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk3qvy34gvogtz6zzfhs.png" alt="ep02 Anime Live-Action Stage — Benchmark Mimic Master Still" width="768" height="1376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;▲ Master Still for Episode ② (Nano Banana). The look can be achieved to this extent — the problem is in the stage of connecting it into a '30-second moving video'.&lt;/p&gt;

&lt;p&gt;And for verification, you &lt;strong&gt;must look at frame sheets, not single frames&lt;/strong&gt;, to spot morphing and jump cuts. In episode ⑤ (Snowy Mountain Sunrise), since it's a single subject, the frames are consistent — Veo's &lt;strong&gt;best area&lt;/strong&gt;:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5a4q60exebwu07pkh6uv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5a4q60exebwu07pkh6uv.png" alt="Snowy Mountain Sunrise Frame Verification Sheet — Climber's Tears Close-up + Snowy Mountain Aerial View" width="800" height="711"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;▲ Frame verification sheet for Episode ⑤. Top = Climber's emotional close-up (frame consistency), Bottom = Snowy mountain sunrise aerial view. For single subjects and contained actions, Veo performs well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pattern emerged&lt;/strong&gt; — single person/single action (③, ⑤) worked well, but as soon as I tried to connect multiple cuts like a benchmark (①, ④), it fell apart. A single still (₩80-640) is excellent, but the iterative cost of turning it into a "benchmark-level 30-second moving video" accumulated at ₩2,000-4,000 per episode — leading to the following conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. In Front of the Benchmark — Admitting Veo 3.1's Ceiling
&lt;/h2&gt;

&lt;p&gt;By this point, you might think "AI video works," but looking again at the &lt;strong&gt;YouTube Shorts I tried to replicate&lt;/strong&gt;, the gap was clear. That benchmark was a &lt;strong&gt;8-12 second continuous spectacle of connected shots&lt;/strong&gt; (a seamless action one-take), but Veo 3.1's method of &lt;strong&gt;stitching together 8-second clips&lt;/strong&gt; couldn't achieve that "continuous flow of a single breath." Even by matching start and end frames, it was &lt;strong&gt;an approximation, not a perfect match&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjeuuv6xv1jrverkqmqu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjeuuv6xv1jrverkqmqu7.png" alt="diagram" width="799" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That "progressive action one-take" is the domain of models like &lt;strong&gt;Seedance 2.0&lt;/strong&gt; or &lt;strong&gt;Gemini Omni&lt;/strong&gt;, which are the engines used by &lt;strong&gt;paid services like Higgsfield&lt;/strong&gt;. My tool (Veo 3.1) can approximate it, but &lt;strong&gt;if the goal is that level, paid services are overwhelmingly more efficient&lt;/strong&gt;. (For reference, Seedance 2.0 is only accessible via Dreamina/fal, and OpenAI has exited video – within my 3 keys, Veo is virtually the only option for video.)## 6. What I Gained — And Why I Have No Regrets&lt;br&gt;
By investing time and money, I learned the &lt;strong&gt;realistic ceiling of self-production&lt;/strong&gt;. My mindset was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it first. If the best (free self-production) doesn't work, use the next best (paid service) first. However, technology is constantly evolving, so I'll track the model landscape, document it, and try again later.&lt;/strong&gt;&lt;br&gt;
This isn't a failure, but &lt;strong&gt;having paid for the realistic limitations&lt;/strong&gt;. Now I know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Character consistency is achieved by &lt;strong&gt;image first + start/end frames&lt;/strong&gt; (this principle is valid wherever you produce).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8-second quality&lt;/strong&gt; can be achieved with my current tools.&lt;/li&gt;
&lt;li&gt;However, &lt;strong&gt;continuous spectacle in one breath&lt;/strong&gt; requires Gemini Omni/Seedance 2.0 level, and if that's the goal, &lt;strong&gt;paid is the answer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The video AI model landscape &lt;strong&gt;changes monthly&lt;/strong&gt; — today's ceiling could be tomorrow's basic feature, so I'll keep tracking it.
What I gained for ₩3,350 and a few days is not an illusion ("AI can make anything instantly"), but &lt;strong&gt;knowing the actual location of the boundary line&lt;/strong&gt;.
In the next post, I'll share &lt;strong&gt;how the "Riel Chatbot" has grown through all these trials and errors.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>veo</category>
      <category>higgsfield</category>
      <category>nanobanana</category>
    </item>
    <item>
      <title>How Riel Chatbot Grew — Accessibility Born from a User's Request</title>
      <dc:creator>박준희</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:55:40 +0000</pubDate>
      <link>https://dev.to/junhee916/how-riel-chatbot-grew-accessibility-born-from-a-users-request-e1h</link>
      <guid>https://dev.to/junhee916/how-riel-chatbot-grew-accessibility-born-from-a-users-request-e1h</guid>
      <description>&lt;p&gt;"The text is too small, I can't see it well." One day, I received a request like this. It was a feedback from an elderly family member who used Riel daily. That day, I immediately built a text size adjustment feature. Many of Riel's features were born this way — not from my roadmap, but from someone's &lt;strong&gt;"This is inconvenient."&lt;/strong&gt;&lt;br&gt;
This post is a bit different from the previous development logs. &lt;strong&gt;Putting aside the detailed technical know-how, I'll focus on the broader strokes of growth&lt;/strong&gt; — how requests turned into features, and in which direction Riel is growing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the story of &lt;strong&gt;&lt;a href="https://dev.to/chat"&gt;Riel&lt;/a&gt;&lt;/strong&gt;, an AI chatbot built by a solo developer in Korea. All user personal information has been anonymized.## 1. Growth Came from 'Requests,' Not Plans&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll7w5w2uf5ijkwhqy558.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll7w5w2uf5ijkwhqy558.png" alt="diagram" width="799" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It wasn't a case of drawing up a roadmap and following it in order. &lt;strong&gt;If someone said, "This is inconvenient," that became the next thing to build.&lt;/strong&gt; So, Riel's growth is a &lt;strong&gt;virtuous loop&lt;/strong&gt; —&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iopj8cqp6vgoeveujw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iopj8cqp6vgoeveujw4.png" alt="diagram" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Real "Requests → Solutions" — Directly Implemented
&lt;/h2&gt;

&lt;p&gt;Below are all &lt;strong&gt;anonymized actual requests&lt;/strong&gt; I received (personal information excluded).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔍 "I'm old and the text is small when I search"&lt;/strong&gt; → &lt;strong&gt;3 levels of text size.&lt;/strong&gt; Pressing '가' in the menu increases it by one level, and it stays that way once set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔊 "If I ask something in text and my eyes are tired, can you read it out loud?"&lt;/strong&gt; → &lt;strong&gt;Read answer aloud.&lt;/strong&gt; With one button, Riel reads the response audibly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📺 "It would be great if I could refer to YouTube videos too."&lt;/strong&gt; → &lt;strong&gt;Include relevant YouTube links in answers.&lt;/strong&gt; It finds helpful videos to share if available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔎 "As conversations pile up, I can't find what we discussed before."&lt;/strong&gt; → &lt;strong&gt;Search conversation history.&lt;/strong&gt; You can find past discussions not just by title, but also by keywords within the conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧭 "I consulted Riel about a relative's or friend's problem, and Riel remembered it as 'my problem'."&lt;/strong&gt; → &lt;strong&gt;Distinguish between self and others.&lt;/strong&gt; Now, it understands others' issues as not its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🖼️ "I could only upload one photo, videos weren't supported, and it was hard to describe things in words..."&lt;/strong&gt; → &lt;strong&gt;Upload multiple photos at once + analyze album videos + ask by drawing a picture.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🟢 "Older people are more familiar with Naver than Google."&lt;/strong&gt; → &lt;strong&gt;Naver Login.&lt;/strong&gt;
These might seem like small requests, but they've accumulated to make Riel a chatbot that &lt;strong&gt;"even genuinely elderly people use daily,"&lt;/strong&gt; not just a developer's creation.## 3. The Big Picture — Riel's Four Growth Directions
Setting aside the detailed implementations, the &lt;strong&gt;main pillars&lt;/strong&gt; of Riel's current growth are:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌱 Remembers&lt;/strong&gt; — The more you use it, the more it gets to know you. Unlike chatbots that treat every interaction as new, Riel understands you better as conversations build up. (Riel's key differentiator)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎙️ Listens and Speaks&lt;/strong&gt; — Converse with voice, and get answers read aloud. This makes it usable even for those with hand or visual impairments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🖼️ Sees&lt;/strong&gt; — Understands photos, videos, and sketches. You can show it things that are hard to describe with words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;♿ For Everyone&lt;/strong&gt; — Text size, voice, familiar login methods... regardless of age or device.
These four directions are shaping Riel into &lt;strong&gt;"an AI by your side,"&lt;/strong&gt; rather than just a "smart AI."
## 4. So — The True Engine of Growth
Looking back, Riel's growth engine has been &lt;strong&gt;the users, not the models or my ideas.&lt;/strong&gt; A simple "Thank you for always implementing my requests," or honest feedback like "This is inconvenient" — these were the seeds for the next features.
&amp;gt; &lt;strong&gt;Listening to a user's small inconvenience is more important than one well-made feature.&lt;/strong&gt; Riel will continue to grow following the requests of those who use it.
---
Thank you for reading this far into the development log series — from blockchain, Polygon purchases, AI video generation, and Riel's growth. All of these were &lt;strong&gt;real records of hands-on experience.&lt;/strong&gt; If you're curious about Riel, &lt;strong&gt;try it for free](/try)&lt;/strong&gt;. It's still growing. 🌳&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;em&gt;This is part of *&lt;/em&gt;&lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;Riel&lt;/a&gt;** — a full AI product I'm building solo, in public (failures and all). &lt;a href="https://aicoreutility.com/en/blog" rel="noopener noreferrer"&gt;Read more build logs →&lt;/a&gt; · &lt;a href="https://aicoreutility.com/" rel="noopener noreferrer"&gt;See the product →&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
  </channel>
</rss>
